Update: 2026-08-07 05:03:39

This commit is contained in:
Hamza-Ayed
2026-08-07 05:03:39 +03:00
parent 8e7c4a7104
commit 977d238c84
18 changed files with 1830 additions and 128 deletions
+10
View File
@@ -179,6 +179,16 @@ class AppLink {
static String addDriverWantWork = "$serviceApp/work/addDriverWantWork.php";
static String addCarWantWork = "$serviceApp/work/addCarWantWork.php";
static String getComplaintAllData = "$serviceApp/getComplaintAllData.php";
/// أداة التسوية: تحديث القضية + صرف تعويض في نداء واحد.
static String resolveComplaint = "$serviceApp/resolve_complaint.php";
/// توصية الذكاء الاصطناعي بمبلغ التعويض. تُستدعى بضغطة زر لا تلقائياً —
/// التحليل المحفوظ وقت تقديم الشكوى يُعرض مجاناً، وهذا يضيف المبلغ فقط.
static String complaintAiSuggest = "$serviceApp/complaint_ai_suggest.php";
/// مؤشرات دورة الشكوى: زمن أول رد، زمن الإغلاق، المفتوح الآن.
static String complaintSlaStats = "$serviceApp/complaint_sla_stats.php";
static String getComplaintAllDataForDriver =
"$serviceApp/getComplaintAllDataForDriver.php";
static String rejectDriver = "$serviceApp/rejectDriver.php";
@@ -0,0 +1,76 @@
import 'dart:convert';
import 'package:get/get.dart';
import '../../../constant/links.dart';
import '../../../views/widgets/mycircular.dart';
import '../../functions/crud.dart';
/// قائمة الشكاوى لموظف خدمة العملاء.
///
/// الترتيب: غير المحلولة أولاً ثم الأحدث. الموظف يحتاج ما لم يُغلق بعد،
/// لا أحدث ما وصل.
class ComplaintListController extends GetxController {
final CRUD _crud = CRUD();
final complaints = <Map<String, dynamic>>[].obs;
final isLoading = false.obs;
/// فلتر الحالة. فارغ = الكل.
final statusFilter = ''.obs;
List<Map<String, dynamic>> get visible {
final f = statusFilter.value;
if (f.isEmpty) return complaints;
return complaints.where((c) => c['statusComplaint'] == f).toList();
}
/// شكاوى مفتوحة مضى عليها أكثر من أسبوع — الرقم الذي يجب ألا يكبر.
int get staleCount {
final weekAgo = DateTime.now().subtract(const Duration(days: 7));
return complaints.where((c) {
if (c['statusComplaint'] == 'Resolved') return false;
final d = DateTime.tryParse('${c['date_filed']}');
return d != null && d.isBefore(weekAgo);
}).length;
}
@override
void onInit() {
super.onInit();
fetch();
}
Future<void> fetch() async {
isLoading.value = true;
try {
final res = await _crud.get(link: AppLink.getComplaintAllData);
if (res == null || res == 'failure' || res == 'token_expired') {
complaints.clear();
return;
}
final decoded = res is String ? jsonDecode(res) : res;
if (decoded is! Map || decoded['status'] != 'success') {
complaints.clear();
return;
}
final list = (decoded['message'] as List?) ?? [];
final mapped =
list.map((e) => Map<String, dynamic>.from(e as Map)).toList();
// ‏غير المحلول أولاً، ثم الأحدث داخل كل مجموعة.
mapped.sort((a, b) {
final ar = a['statusComplaint'] == 'Resolved' ? 1 : 0;
final br = b['statusComplaint'] == 'Resolved' ? 1 : 0;
if (ar != br) return ar - br;
return '${b['date_filed']}'.compareTo('${a['date_filed']}');
});
complaints.assignAll(mapped);
} catch (e) {
mySnackbarError('فشل جلب الشكاوى');
} finally {
isLoading.value = false;
}
}
}
@@ -0,0 +1,222 @@
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<String, dynamic> 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<Map<String, dynamic>>();
static const statuses = ['Open', 'In Progress', 'Resolved'];
/// نفس القائمة المغلقة في serviceapp/resolve_complaint.php. أي تعديل
/// هناك يجب أن ينعكس هنا، وإلا رُفض الطلب بـ Invalid reason_code.
static const reasons = <String, String>{
'driver_behavior': 'سلوك السائق',
'passenger_behavior': 'سلوك الراكب',
'overcharge': 'سعر أعلى من المتوقع',
'route_issue': 'مسار خاطئ أو أطول',
'no_show': 'عدم حضور',
'vehicle_condition': 'حالة المركبة',
'app_issue': 'عطل تقني',
'payment_issue': 'مشكلة دفع',
'safety': 'سلامة',
'other': 'أخرى',
};
static const faults = <String, String>{
'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<void> 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<String, dynamic>.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<bool> save() async {
if (isSaving.value) return false;
final payload = <String, String>{
'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;
}
}
}
@@ -2,11 +2,158 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_service/views/widgets/my_scafold.dart';
import '../complaints/complaint_list_controller.dart';
import '../../../views/home/complaints/complaint_resolve_page.dart';
/// شاشة الشكاوى لموظف خدمة العملاء.
///
/// كانت هيكلاً فارغاً (`body: []`). الآن: قائمة مرتّبة بغير المحلول أولاً،
/// وتنبيه بالمتأخر، وفتح أداة التسوية بضغطة.
class Complaint extends StatelessWidget {
const Complaint({super.key});
@override
Widget build(BuildContext context) {
return MyScaffold(title: "View complaint".tr, isleading: true, body: []);
final c = Get.put(ComplaintListController());
final cs = Theme.of(context).colorScheme;
return MyScaffold(
title: "View complaint".tr,
isleading: true,
body: [
Obx(() {
if (c.isLoading.value && c.complaints.isEmpty) {
return const Padding(
padding: EdgeInsets.only(top: 60),
child: Center(child: CircularProgressIndicator()),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ‏المتأخر أولاً وبصرياً: رقم يجب ألا يكبر، فإخفاؤه في قائمة
// ‏طويلة يعني أن أحداً لن يلاحظه.
if (c.staleCount > 0)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: cs.error.withOpacity(0.08),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.error.withOpacity(0.3)),
),
child: Row(children: [
Icon(Icons.warning_amber_rounded, color: cs.error, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'${c.staleCount} شكوى مفتوحة منذ أكثر من أسبوع',
style: TextStyle(
color: cs.error,
fontSize: 13,
fontWeight: FontWeight.bold),
),
),
]),
),
_Filters(c: c),
const SizedBox(height: 8),
if (c.visible.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 60),
child: Center(child: Text('لا شكاوى')),
)
else
...c.visible.map((item) => _ComplaintTile(item: item, c: c)),
],
);
}),
],
);
}
}
class _Filters extends StatelessWidget {
const _Filters({required this.c});
final ComplaintListController c;
@override
Widget build(BuildContext context) {
const options = <String, String>{
'': 'الكل',
'Open': 'مفتوحة',
'In Progress': 'قيد المعالجة',
'Resolved': 'محلولة',
};
return Obx(() => Wrap(
spacing: 8,
children: options.entries
.map((e) => ChoiceChip(
label: Text(e.value, style: const TextStyle(fontSize: 12)),
selected: c.statusFilter.value == e.key,
onSelected: (_) => c.statusFilter.value = e.key,
))
.toList(),
));
}
}
class _ComplaintTile extends StatelessWidget {
const _ComplaintTile({required this.item, required this.c});
final Map<String, dynamic> item;
final ComplaintListController c;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final status = item['statusComplaint']?.toString() ?? 'Open';
final color = status == 'Resolved'
? Colors.green
: status == 'In Progress'
? Colors.orange
: cs.error;
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
leading: Container(
width: 8,
height: 44,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
),
),
title: Text(
item['description']?.toString().trim().isNotEmpty == true
? item['description'].toString()
: 'بدون وصف',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'رحلة ${item['ride_id']} · ${item['complaint_type'] ?? 'عام'}'
' · ${item['date_filed'] ?? ''}',
style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant),
),
),
trailing: Icon(Icons.chevron_left, color: cs.onSurfaceVariant),
onTap: () async {
final changed = await Get.to(
() => ComplaintResolvePage(complaint: item));
if (changed == true) c.fetch();
},
),
);
}
}
@@ -0,0 +1,371 @@
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)),
);
}