Files
Siro/siro_admin/lib/views/admin/food/courier_settlement_page.dart
T

241 lines
9.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
],
),
),
);
}