import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../constant/links.dart'; import '../../../views/widgets/mycircular.dart'; import '../../functions/crud.dart'; /// كنترولر شاشة تسوية الشكوى. /// /// يفصل بين شيئين متعمّداً: /// • **التحليل المحفوظ** — أنتجه Gemini وقت تقديم الشكوى وحُفظ في /// الجدول (cs_solutions / passenger_report / driver_report). يُعرض /// مجاناً بلا أي نداء خارجي. /// • **توصية المبلغ** — نداء جديد بضغطة زر. مدفوع، فلا يُطلق تلقائياً /// عند فتح كل شكوى. class ComplaintResolveController extends GetxController { ComplaintResolveController(this.complaint); /// صف الشكوى كما جاء من getComplaintAllData.php final Map complaint; final CRUD _crud = CRUD(); // ── حالة الشاشة ── final isSaving = false.obs; final isSuggesting = false.obs; // ── حقول التسوية ── final status = 'In Progress'.obs; final reasonCode = RxnString(); final faultOn = RxnString(); final resolutionCtrl = TextEditingController(); // ── التعويض ── final compEnabled = false.obs; final compKind = 'goodwill'.obs; final compBeneficiary = 'passenger'.obs; final compAmountCtrl = TextEditingController(); final compNoteCtrl = TextEditingController(); /// السقف الصلب بعملة الدولة — يصل من نداء التوصية. صفر يعني "غير معروف /// بعد"، فلا نفرض حداً في الواجهة ونترك الخادم يرفض إن تجاوز. final hardCap = 0.0.obs; final currency = ''.obs; /// توصية الذكاء الاصطناعي بعد الضغط على الزر. فارغة = لم تُطلب بعد. final suggestion = Rxn>(); static const statuses = ['Open', 'In Progress', 'Resolved']; /// نفس القائمة المغلقة في serviceapp/resolve_complaint.php. أي تعديل /// هناك يجب أن ينعكس هنا، وإلا رُفض الطلب بـ Invalid reason_code. static const reasons = { 'driver_behavior': 'سلوك السائق', 'passenger_behavior': 'سلوك الراكب', 'overcharge': 'سعر أعلى من المتوقع', 'route_issue': 'مسار خاطئ أو أطول', 'no_show': 'عدم حضور', 'vehicle_condition': 'حالة المركبة', 'app_issue': 'عطل تقني', 'payment_issue': 'مشكلة دفع', 'safety': 'سلامة', 'other': 'أخرى', }; static const faults = { 'driver': 'السائق', 'passenger': 'الراكب', 'company': 'الشركة', 'unclear': 'غير واضح', }; // ── التحليل المحفوظ (بلا أي نداء) ── String get savedSolutions => _s('cs_solutions'); String get savedPassengerReport => _s('passenger_report'); String get savedDriverReport => _s('driver_report'); String get savedNature => _s('complaint_nature'); String get savedFault => _s('fault_determination'); bool get hasSavedAnalysis => savedSolutions.isNotEmpty || savedPassengerReport.isNotEmpty || savedDriverReport.isNotEmpty; String _s(String key) => complaint[key]?.toString().trim() ?? ''; @override void onInit() { super.onInit(); final current = _s('statusComplaint'); if (statuses.contains(current)) status.value = current; if (reasons.containsKey(_s('reason_code'))) reasonCode.value = _s('reason_code'); resolutionCtrl.text = _s('resolution'); } @override void onClose() { resolutionCtrl.dispose(); compAmountCtrl.dispose(); compNoteCtrl.dispose(); super.onClose(); } /// يطلب توصية المبلغ. لا يملأ الحقول تلقائياً — يعرضها ليقرّر الموظف، /// فالتعبئة الصامتة تجعل الضغط على "حفظ" موافقةً ضمنية على رقم لم يُقرأ. Future requestSuggestion() async { if (isSuggesting.value) return; isSuggesting.value = true; try { final res = await _crud.post( link: AppLink.complaintAiSuggest, payload: {'complaint_id': complaint['id'].toString()}, ); final decoded = res is String ? jsonDecode(res) : res; final data = decoded is Map ? (decoded['message'] ?? decoded['data']) : null; if (data is! Map || data['available'] != true) { mySnackbarError('تعذّر الحصول على توصية الآن'); return; } suggestion.value = Map.from(data); final comp = data['compensation']; if (comp is Map) { hardCap.value = double.tryParse('${comp['hard_cap']}') ?? 0; currency.value = comp['currency']?.toString() ?? ''; } } catch (e) { mySnackbarError('تعذّر الاتصال بخدمة التوصية'); } finally { isSuggesting.value = false; } } /// ينسخ توصية الذكاء الاصطناعي إلى الحقول — بفعل صريح من الموظف. void applySuggestion() { final s = suggestion.value; if (s == null) return; if (reasons.containsKey(s['reason_code'])) reasonCode.value = s['reason_code']; if (faults.containsKey(s['fault_determination'])) { faultOn.value = s['fault_determination']; } if (statuses.contains(s['suggested_status'])) status.value = s['suggested_status']; final reply = s['customer_reply']?.toString() ?? ''; if (reply.isNotEmpty) resolutionCtrl.text = reply; final comp = s['compensation']; if (comp is Map) { final amount = double.tryParse('${comp['amount']}') ?? 0; if (amount > 0) { compEnabled.value = true; compAmountCtrl.text = amount.toString(); compKind.value = comp['kind']?.toString() ?? 'goodwill'; compBeneficiary.value = comp['beneficiary']?.toString() ?? 'passenger'; } } update(); } /// يحفظ التسوية. يرجع true عند النجاح ليغلق المُنادي الشاشة. Future save() async { if (isSaving.value) return false; final payload = { 'complaint_id': complaint['id'].toString(), 'status': status.value, if (reasonCode.value != null) 'reason_code': reasonCode.value!, if (faultOn.value != null) 'fault_determination': faultOn.value!, 'resolution': resolutionCtrl.text.trim(), }; if (compEnabled.value) { final amount = double.tryParse(compAmountCtrl.text.trim()) ?? 0; if (amount <= 0) { mySnackbarError('أدخل مبلغ تعويض صحيحاً'); return false; } // ‏فحص محلي للراحة فقط — الخادم هو من يفرض السقف فعلاً. if (hardCap.value > 0 && amount > hardCap.value) { mySnackbarError('المبلغ يتجاوز الحد ${hardCap.value} ${currency.value}'); return false; } payload['compensation_amount'] = amount.toString(); payload['compensation_kind'] = compKind.value; payload['compensation_beneficiary'] = compBeneficiary.value; if (compNoteCtrl.text.trim().isNotEmpty) { payload['compensation_note'] = compNoteCtrl.text.trim(); } } isSaving.value = true; try { final res = await _crud.post(link: AppLink.resolveComplaint, payload: payload); final decoded = res is String ? jsonDecode(res) : res; if (decoded is Map && decoded['status'] == 'success') { // ‏التحويل قد يفشل بينما تُحفظ القضية. نُظهر ذلك صراحةً بدل رسالة // ‏نجاح عامة تُخفي أن العميل لم يستلم ما وُعد به. final data = decoded['message'] ?? decoded['data']; final comp = (data is Map) ? data['compensation'] : null; if (comp is Map && comp['status'] == 'failed') { mySnackbarError('حُفظت القضية، لكن التعويض لم يصل — يحتاج تسوية يدوية'); } else { mySnackbarSuccess('تم حفظ التسوية'); } return true; } final msg = (decoded is Map ? decoded['message'] : null)?.toString(); mySnackbarError(msg?.isNotEmpty == true ? msg! : 'فشل حفظ التسوية'); return false; } catch (e) { mySnackbarError('تعذّر الاتصال بالخادم'); return false; } finally { isSaving.value = false; } } }