Update: 2026-08-07 05:56:50

This commit is contained in:
Hamza-Ayed
2026-08-07 05:56:50 +03:00
parent 7a932580a3
commit 67465df000
14 changed files with 1257 additions and 9 deletions
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/admin/courier_settlement_controller.dart';
/// شاشة تسوية أرباح سائق التوصيل.
///
/// المسار: إدخال → معاينة → تنفيذ. زر التنفيذ لا يظهر قبل المعاينة.
class CourierSettlementPage extends StatelessWidget {
const CourierSettlementPage({super.key});
@override
Widget build(BuildContext context) {
final c = Get.put(CourierSettlementController());
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(title: const Text('تسوية أرباح التوصيل')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_Card(
title: 'الفترة والسائق',
icon: Icons.tune,
child: Column(
children: [
TextField(
controller: c.courierCtrl,
decoration: const InputDecoration(
labelText: 'معرّف السائق',
hintText: 'driver id',
),
),
const SizedBox(height: 12),
TextField(
controller: c.startCtrl,
decoration: const InputDecoration(
labelText: 'من',
hintText: 'YYYY-MM-DD HH:MM:SS',
),
),
const SizedBox(height: 12),
TextField(
controller: c.endCtrl,
decoration: const InputDecoration(
labelText: 'إلى',
hintText: 'YYYY-MM-DD HH:MM:SS',
),
),
const SizedBox(height: 16),
Obx(() => SizedBox(
width: double.infinity,
height: 44,
child: OutlinedButton.icon(
onPressed: c.isLoading.value ? null : c.runPreview,
icon: c.isLoading.value
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.calculate_outlined),
label: Text(c.isLoading.value ? 'جارٍ الحساب…' : 'احسب الصافي'),
),
)),
],
),
),
const SizedBox(height: 16),
Obx(() => c.preview.value == null
? const SizedBox.shrink()
: _Result(c: c)),
],
),
),
);
}
}
class _Result extends StatelessWidget {
const _Result({required this.c});
final CourierSettlementController c;
@override
Widget build(BuildContext context) {
final p = c.preview.value!;
final done = c.executed.value;
final num net = p['net_decimal'] ?? 0;
final String cur = p['currency']?.toString() ?? '';
final bool willPay = net > 0;
return _Card(
title: 'الصافي',
icon: Icons.receipt_long_outlined,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_line('الطلبات المسلَّمة', '${p['orders_count'] ?? 0}'),
const Divider(height: 20),
_line('أجور التوصيل', '+ ${_dec(p['payout_total'])} $cur',
color: Colors.green.shade700),
_line('دَين النقد', '− ${_dec(p['cash_debt_total'])} $cur',
color: Colors.orange.shade800),
if ((p['carried_over'] ?? 0) != 0)
_line('مُرحَّل من دورة سابقة', '− ${_dec(p['carried_over'])} $cur',
color: Colors.orange.shade800),
const Divider(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('الصافي',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text('$net $cur',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: willPay ? Colors.green.shade700 : Colors.red.shade700,
)),
],
),
const SizedBox(height: 8),
Text(
willPay
? 'سيُصرف هذا المبلغ لمحفظة السائق.'
// ‏السالب لا يُخصم من محفظته: قد تكون فارغة، والخصم القسري
// ‏يفاجئه ويوقفه عن العمل. الدَين لا يضيع — يُطرح لاحقاً.
: 'الصافي سالب — يُرحَّل للدورة القادمة ولا يُخصم من محفظته.',
style: const TextStyle(fontSize: 12, height: 1.5),
),
const SizedBox(height: 16),
if (done == null)
Obx(() => SizedBox(
width: double.infinity,
height: 46,
child: ElevatedButton.icon(
onPressed: c.isExecuting.value
? null
: () => _confirm(context, c, net, cur, willPay),
icon: c.isExecuting.value
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: Icon(willPay ? Icons.payments_outlined : Icons.archive_outlined),
label: Text(willPay ? 'ثبّت واصرف' : 'ثبّت ورحّل'),
),
))
else
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: done['status'] == 'failed'
? Colors.red.withOpacity(0.08)
: Colors.green.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'تسوية #${done['settlement_id']} — ${_statusLabel(done['status'])}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
/// ‏تأكيد قبل الصرف. التثبيت لا يُلغى، والدورة لا تُنفَّذ مرتين — فالضغطة
/// الواحدة يجب أن تكون مقصودة.
void _confirm(BuildContext context, CourierSettlementController c, num net,
String cur, bool willPay) {
Get.defaultDialog(
title: 'تأكيد التسوية',
middleText: willPay
? 'سيُصرف $net $cur لمحفظة السائق. لا يمكن التراجع.'
: 'سيُثبَّت دَين $net $cur ويُرحَّل للدورة القادمة.',
textConfirm: 'تأكيد',
textCancel: 'إلغاء',
onConfirm: () {
Get.back();
c.runExecute();
},
);
}
String _statusLabel(dynamic s) => switch (s) {
'paid' => 'صُرفت بنجاح',
'carried' => 'رُحّلت للدورة القادمة',
'failed' => 'ثُبّتت والتحويل فشل — تحتاج تسوية يدوية',
_ => '$s',
};
String _dec(dynamic smallestUnit) {
final v = int.tryParse('$smallestUnit') ?? 0;
// ‏الفلس الأردني: ثلاث خانات عشرية. يطابق FOOD_CURRENCY_DIVISOR=1000.
return (v / 1000).toStringAsFixed(3);
}
Widget _line(String label, String value, {Color? color}) => Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 13)),
Text(value,
style: TextStyle(
fontSize: 13, fontWeight: FontWeight.w600, color: color)),
],
),
);
}
class _Card extends StatelessWidget {
const _Card({required this.title, required this.icon, required this.child});
final String title;
final IconData icon;
final Widget child;
@override
Widget build(BuildContext context) => 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: 14),
child,
],
),
),
);
}