Files
Siro/siro_service/lib/views/home/complaints/complaint_resolve_page.dart
T

372 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/mainController/complaints/complaint_resolve_controller.dart';
/// شاشة تسوية شكوى — أداة الموظف لإغلاق قضية.
///
/// ترتيب الأقسام مقصود: الوقائع أولاً، ثم التحليل المحفوظ، ثم القرار.
/// الموظف يقرأ قبل أن يقرّر، لا العكس.
class ComplaintResolvePage extends StatelessWidget {
const ComplaintResolvePage({super.key, required this.complaint});
final Map<String, dynamic> complaint;
@override
Widget build(BuildContext context) {
final c = Get.put(
ComplaintResolveController(complaint),
tag: complaint['id'].toString(),
);
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(title: Text('شكوى #${complaint['id']}')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_Facts(complaint: complaint),
const SizedBox(height: 16),
_SavedAnalysis(c: c),
const SizedBox(height: 16),
_SuggestionCard(c: c),
const SizedBox(height: 16),
_Decision(c: c),
const SizedBox(height: 24),
Obx(() => SizedBox(
height: 48,
child: ElevatedButton.icon(
onPressed: c.isSaving.value
? null
: () async {
if (await c.save()) Get.back(result: true);
},
icon: c.isSaving.value
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.check),
label: const Text('حفظ التسوية'),
),
)),
const SizedBox(height: 24),
],
),
),
);
}
}
// ── الوقائع ────────────────────────────────────────────────
class _Facts extends StatelessWidget {
const _Facts({required this.complaint});
final Map<String, dynamic> complaint;
@override
Widget build(BuildContext context) {
return _Section(
title: 'الوقائع',
icon: Icons.description_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_row('الرحلة', complaint['ride_id']),
_row('النوع', complaint['complaint_type']),
_row('تاريخ التقديم', complaint['date_filed']),
_row('الحالة الحالية', complaint['statusComplaint']),
const Divider(height: 20),
Text(
complaint['description']?.toString().trim().isNotEmpty == true
? complaint['description'].toString()
: 'لا وصف',
style: const TextStyle(height: 1.5),
),
],
),
);
}
Widget _row(String label, dynamic value) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 110,
child: Text(label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
),
Expanded(child: Text('${value ?? '—'}', style: const TextStyle(fontSize: 13))),
],
),
);
}
// ── التحليل المحفوظ ────────────────────────────────────────
class _SavedAnalysis extends StatelessWidget {
const _SavedAnalysis({required this.c});
final ComplaintResolveController c;
@override
Widget build(BuildContext context) {
if (!c.hasSavedAnalysis) return const SizedBox.shrink();
return _Section(
// ‏هذا التحليل أنتجه Gemini وقت تقديم الشكوى وحُفظ في الجدول.
// ‏عرضه لا يكلّف نداءً ولا رصيداً — لذلك يظهر دائماً بلا زر.
title: 'تحليل وقت التقديم',
icon: Icons.auto_awesome_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (c.savedFault.isNotEmpty || c.savedNature.isNotEmpty)
Wrap(spacing: 8, runSpacing: 8, children: [
if (c.savedFault.isNotEmpty) _Chip('المخطئ: ${c.savedFault}'),
if (c.savedNature.isNotEmpty) _Chip('الطبيعة: ${c.savedNature}'),
]),
if (c.savedSolutions.isNotEmpty) ...[
const SizedBox(height: 12),
const Text('حلول مقترحة:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(c.savedSolutions, style: const TextStyle(height: 1.5)),
],
if (c.savedPassengerReport.isNotEmpty) ...[
const SizedBox(height: 12),
const Text('ما أُبلغ به الراكب:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(c.savedPassengerReport, style: const TextStyle(height: 1.5)),
],
if (c.savedDriverReport.isNotEmpty) ...[
const SizedBox(height: 12),
const Text('ما أُبلغ به السائق:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(c.savedDriverReport, style: const TextStyle(height: 1.5)),
],
],
),
);
}
}
// ── توصية المبلغ ───────────────────────────────────────────
class _SuggestionCard extends StatelessWidget {
const _SuggestionCard({required this.c});
final ComplaintResolveController c;
@override
Widget build(BuildContext context) {
return Obx(() {
final s = c.suggestion.value;
return _Section(
title: 'توصية التسوية',
icon: Icons.psychology_outlined,
child: s == null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'يحلّل سجلّ الطرفين وتقييماتهما وتعليقات من تعاملوا معهما،'
' ثم يقترح مبلغاً ورداً للعميل.',
style: TextStyle(fontSize: 13, height: 1.5),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: c.isSuggesting.value ? null : c.requestSuggestion,
icon: c.isSuggesting.value
? const SizedBox(
width: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.lightbulb_outline),
label: Text(c.isSuggesting.value ? 'جارٍ التحليل…' : 'اقترح تسوية'),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(spacing: 8, runSpacing: 8, children: [
_Chip('المخطئ: ${ComplaintResolveController.faults[s['fault_determination']] ?? s['fault_determination']}'),
_Chip('الثقة: ${((s['confidence'] ?? 0) * 100).round()}%'),
if ((s['compensation']?['amount'] ?? 0) > 0)
_Chip('التعويض: ${s['compensation']['amount']} ${s['compensation']['currency']}')
else
const _Chip('لا تعويض'),
]),
if ((s['customer_reply'] ?? '').toString().isNotEmpty) ...[
const SizedBox(height: 12),
const Text('ردّ مقترَح:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(s['customer_reply'].toString(),
style: const TextStyle(height: 1.5)),
],
if ((s['internal_note'] ?? '').toString().isNotEmpty) ...[
const SizedBox(height: 8),
Text('ملاحظة داخلية: ${s['internal_note']}',
style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
const SizedBox(height: 12),
// ‏نسخ التوصية بفعل صريح لا تلقائياً: التعبئة الصامتة تجعل
// ‏الضغط على "حفظ" موافقةً ضمنية على رقم لم يقرأه أحد.
OutlinedButton.icon(
onPressed: c.applySuggestion,
icon: const Icon(Icons.arrow_downward),
label: const Text('انسخ إلى الحقول'),
),
],
),
);
});
}
}
// ── القرار ─────────────────────────────────────────────────
class _Decision extends StatelessWidget {
const _Decision({required this.c});
final ComplaintResolveController c;
@override
Widget build(BuildContext context) {
return _Section(
title: 'القرار',
icon: Icons.gavel_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() => DropdownButtonFormField<String>(
value: c.status.value,
decoration: const InputDecoration(labelText: 'الحالة'),
items: ComplaintResolveController.statuses
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (v) => c.status.value = v ?? c.status.value,
)),
const SizedBox(height: 12),
Obx(() => DropdownButtonFormField<String>(
value: c.reasonCode.value,
decoration: const InputDecoration(labelText: 'تصنيف السبب'),
items: ComplaintResolveController.reasons.entries
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
.toList(),
onChanged: (v) => c.reasonCode.value = v,
)),
const SizedBox(height: 12),
Obx(() => DropdownButtonFormField<String>(
value: c.faultOn.value,
decoration: const InputDecoration(labelText: 'تحديد المسؤولية'),
items: ComplaintResolveController.faults.entries
.map((e) => DropdownMenuItem(value: e.key, child: Text(e.value)))
.toList(),
onChanged: (v) => c.faultOn.value = v,
)),
const SizedBox(height: 12),
TextField(
controller: c.resolutionCtrl,
maxLines: 4,
decoration: const InputDecoration(
labelText: 'نص القرار / الرد على العميل',
alignLabelWithHint: true,
),
),
const Divider(height: 28),
Obx(() => SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('صرف تعويض'),
subtitle: c.hardCap.value > 0
? Text('الحد الأقصى ${c.hardCap.value} ${c.currency.value}')
: const Text('اضغط "اقترح تسوية" لمعرفة الحد الأقصى'),
value: c.compEnabled.value,
onChanged: (v) => c.compEnabled.value = v,
)),
Obx(() => c.compEnabled.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: c.compAmountCtrl,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: 'المبلغ',
suffixText: c.currency.value,
),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: c.compKind.value,
decoration: const InputDecoration(labelText: 'النوع'),
items: const [
DropdownMenuItem(
value: 'refund', child: Text('استرجاع (لا يتجاوز قيمة الرحلة)')),
DropdownMenuItem(
value: 'goodwill', child: Text('رصيد اعتذار')),
],
onChanged: (v) => c.compKind.value = v ?? 'goodwill',
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: c.compBeneficiary.value,
decoration: const InputDecoration(labelText: 'المستفيد'),
items: const [
DropdownMenuItem(value: 'passenger', child: Text('الراكب')),
DropdownMenuItem(value: 'driver', child: Text('السائق')),
],
onChanged: (v) => c.compBeneficiary.value = v ?? 'passenger',
),
const SizedBox(height: 12),
TextField(
controller: c.compNoteCtrl,
decoration: const InputDecoration(labelText: 'سبب التعويض (اختياري)'),
),
],
)
: const SizedBox.shrink()),
],
),
);
}
}
// ── عناصر مشتركة ───────────────────────────────────────────
class _Section extends StatelessWidget {
const _Section({required this.title, required this.icon, required this.child});
final String title;
final IconData icon;
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
]),
const SizedBox(height: 12),
child,
],
),
),
);
}
}
class _Chip extends StatelessWidget {
const _Chip(this.text);
final String text;
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(20),
),
child: Text(text, style: const TextStyle(fontSize: 12)),
);
}