372 lines
15 KiB
Dart
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)),
|
|
);
|
|
}
|