Update: 2026-08-07 05:03:39
This commit is contained in:
@@ -464,6 +464,16 @@ class AppLink {
|
||||
static String serviceApp = "$server/serviceapp";
|
||||
static String editCarPlate = "$serviceApp/editCarPlate.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 addCriminalDocuments = "$authCaptin/addCriminalDocuments.php";
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/snackbar.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../controller/admin/complaint_controller.dart';
|
||||
import 'complaint_resolve_page.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
@@ -125,6 +126,20 @@ class ComplaintListPage extends StatelessWidget {
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: _buildStatusIndicator(c['statusComplaint'], statusColor, cs),
|
||||
// زر التسوية على البطاقة نفسها: البطاقة تعرض وتُحدّث الحالة فقط،
|
||||
// وأداة التعويض وتصنيف السبب في شاشة مستقلة.
|
||||
trailing: IconButton(
|
||||
tooltip: 'تسوية القضية',
|
||||
icon: Icon(Icons.gavel_outlined, color: cs.primary, size: 20),
|
||||
onPressed: () async {
|
||||
final changed = await Get.to(() => ComplaintResolvePage(
|
||||
complaint: Map<String, dynamic>.from(c as Map),
|
||||
));
|
||||
if (changed == true) {
|
||||
Get.find<ComplaintController>().getComplaints();
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
c['description']?.toString() ?? 'بدون وصف',
|
||||
maxLines: 1,
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../controller/admin/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)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user