feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق

الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
  يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
  `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
  قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.

N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.

فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
  التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-18 13:06:46 +03:00
co-authored by Claude Fable 5
parent 3ab74a13a8
commit de4bbd00ac
271 changed files with 1333 additions and 81068 deletions
@@ -1,140 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class BankController extends GetxController {
String selectedBank = '';
Map<String, String> bankNames = {
'Ahli United Bank'.tr: 'AUB',
'Citi Bank N.A. Egypt'.tr: 'CITI',
'MIDBANK'.tr: 'MIDB',
'Banque Du Caire'.tr: 'BDC',
'HSBC Bank Egypt S.A.E'.tr: 'HSBC',
'Credit Agricole Egypt S.A.E'.tr: 'ECAE',
'Egyptian Gulf Bank'.tr: 'EGB',
'The United Bank'.tr: 'UB',
'Qatar National Bank Alahli'.tr: 'QNB',
'Arab Bank PLC'.tr: 'ARAB',
'Emirates National Bank of Dubai'.tr: 'ENBD',
'Al Ahli Bank of Kuwait – Egypt'.tr: 'ABK',
'National Bank of Kuwait – Egypt'.tr: 'NBK',
'Arab Banking Corporation - Egypt S.A.E'.tr: 'EABC',
'First Abu Dhabi Bank'.tr: 'FAB',
'Abu Dhabi Islamic Bank – Egypt'.tr: 'ADIB',
'Commercial International Bank - Egypt S.A.E'.tr: 'CIB',
'Housing And Development Bank'.tr: 'HDB',
'Banque Misr'.tr: 'MISR',
'Arab African International Bank'.tr: 'AAIB',
'Egyptian Arab Land Bank'.tr: 'EALB',
'Export Development Bank of Egypt'.tr: 'EDBE',
'Faisal Islamic Bank of Egypt'.tr: 'FAIB',
'Blom Bank'.tr: 'BLOM',
'Abu Dhabi Commercial Bank – Egypt'.tr: 'ADCB',
'Alex Bank Egypt'.tr: 'BOA',
'Societe Arabe Internationale De Banque'.tr: 'SAIB',
'National Bank of Egypt'.tr: 'NBE',
'Al Baraka Bank Egypt B.S.C.'.tr: 'ABRK',
'Egypt Post'.tr: 'POST',
'Nasser Social Bank'.tr: 'NSB',
'Industrial Development Bank'.tr: 'IDB',
'Suez Canal Bank'.tr: 'SCB',
'Mashreq Bank'.tr: 'MASHA',
'Arab Investment Bank'.tr: 'AIB',
'General Authority For Supply Commodities'.tr: 'GASCA',
'Arab International Bank'.tr: 'AIB',
'Agricultural Bank of Egypt'.tr: 'PDAC',
'National Bank of Greece'.tr: 'NBG',
'Central Bank Of Egypt'.tr: 'CBE',
'ATTIJARIWAFA BANK Egypt'.tr: 'BBE',
};
@override
void onInit() {
super.onInit();
selectedBank = bankNames.values.first;
}
void updateSelectedBank(String? bankShortName) {
selectedBank = bankShortName ?? '';
update();
}
List<DropdownMenuItem<String>> getDropdownItems() {
return bankNames.keys.map<DropdownMenuItem<String>>((bankFullName) {
return DropdownMenuItem<String>(
value: bankNames[bankFullName],
child: Text(bankFullName),
);
}).toList();
}
void showBankPicker(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => CupertinoActionSheet(
title: Text('Select a Bank'.tr),
actions: bankNames.keys.map((String bankFullName) {
return CupertinoActionSheetAction(
child: Text(bankFullName),
onPressed: () {
updateSelectedBank(bankNames[bankFullName]);
Navigator.pop(context);
},
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
class BankDropdown extends StatelessWidget {
final BankController bankController = Get.put(BankController());
@override
Widget build(BuildContext context) {
return GetBuilder<BankController>(
init: bankController,
builder: (controller) {
return CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => controller.showBankPicker(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: CupertinoColors.systemGrey4),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
controller.selectedBank != null
? controller.bankNames.keys.firstWhere(
(key) =>
controller.bankNames[key] ==
controller.selectedBank,
orElse: () => 'Select a Bank'.tr,
)
: 'Select a Bank'.tr,
style: TextStyle(
color: controller.selectedBank != null
? CupertinoColors.black
: CupertinoColors.systemGrey,
),
),
const Icon(CupertinoIcons.chevron_down, size: 20),
],
),
),
);
},
);
}
}
@@ -1,410 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../controller/home/payment/captain_wallet_controller.dart';
import '../../../controller/home/payment/paymob_payout.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
class CardTripzWalletDriver extends StatelessWidget {
const CardTripzWalletDriver({super.key});
@override
Widget build(BuildContext context) {
// التأكد من تهيئة الكنترولر
Get.put(PaymobPayout());
return Center(
child: GetBuilder<CaptainWalletController>(
builder: (controller) {
// جلب الرصيد
double balance = double.tryParse(controller.totalAmountVisa) ?? 0.0;
return Container(
width: double.infinity,
height: 220,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [
Color(0xFF1A2980),
Color(0xFF26D0CE)
], // تدرج أزرق احترافي
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF1A2980).withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 10),
),
],
),
child: Stack(
children: [
// خلفية زخرفية
Positioned(
right: -30,
top: -30,
child: Icon(Icons.account_balance_wallet,
size: 150, color: Colors.white.withOpacity(0.1)),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// الهيدر
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"محفظة تريبز", // Tripz Wallet
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
const SizedBox(height: 4),
const Text(
"رصيد الكابتن",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Row(
children: [
Icon(Icons.verified_user,
color: Colors.white, size: 16),
SizedBox(width: 5),
Text("مؤمنة",
style: TextStyle(
color: Colors.white, fontSize: 12)),
],
),
)
],
),
// الرصيد
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
controller.totalAmountVisa,
style: const TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
const SizedBox(width: 5),
Text(
"ج.م",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
],
),
// زر السحب واسم السائق
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
box.read(BoxName.nameDriver) ?? "الكابتن",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
),
),
// زر طلب السحب
InkWell(
onTap: () =>
_handleCashOutRequest(context, balance),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [
Text(
"سحب الأرباح", // Cash Out
style: TextStyle(
color: const Color(0xFF1A2980),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded,
size: 18, color: const Color(0xFF1A2980)),
],
),
),
),
],
),
],
),
),
],
),
);
},
),
);
}
// --- Logic for Cash Out ---
void _handleCashOutRequest(BuildContext context, double balance) {
// 1. التحقق من الحد الأدنى (مصر = 100، غير مصر = 20)
double minAmount = box.read(BoxName.countryCode) != 'Egypt' ? 100.0 : 20.0;
if (balance < minAmount) {
_showErrorDialog(context, "رصيدك غير كافي",
"عشان تقدر تسحب، لازم رصيدك يكون معدي $minAmount جنيه.\nرصيدك الحالي: $balance جنيه");
return;
}
// 2. فتح نافذة السحب الآمنة
_showSecurePayoutSheet(context, balance);
}
void _showSecurePayoutSheet(BuildContext context, double balance) {
final controller = Get.find<CaptainWalletController>();
final payoutController = Get.find<PaymobPayout>();
// تنظيف الحقل
controller.phoneWallet.clear();
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// رأس النافذة
Center(
child: Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10)),
),
),
const SizedBox(height: 20),
const Text("طلب سحب رصيد",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text("سيتم تحويل المبلغ لمحفظتك الإلكترونية",
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
const SizedBox(height: 20),
// اختيار المحفظة
const Text("نوع المحفظة",
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildWalletDropdown(payoutController),
const SizedBox(height: 15),
// إدخال الرقم
Form(
key: controller.formKey,
child: MyTextForm(
controller: controller.phoneWallet,
label: "رقم المحفظة (فودافون/اتصالات..)",
hint: "01xxxxxxxxx",
type: TextInputType.phone,
),
),
const SizedBox(height: 20),
// تفاصيل العملية (الفاتورة)
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
children: [
_buildSummaryRow("مبلغ السحب", "$balance ج.م"),
_buildSummaryRow("مصاريف التحويل", "5.0 ج.م",
isDeduction: true),
const Divider(),
_buildSummaryRow("الصافي ليك", "${balance - 5} ج.م",
isTotal: true),
],
),
),
const SizedBox(height: 20),
// زر التأكيد
SizedBox(
width: double.infinity,
child: MyElevatedButton(
title: "تأكيد السحب",
kolor: const Color(0xFF1A2980),
onPressed: () async {
if (controller.formKey.currentState!.validate()) {
// إغلاق الشيت
Get.back();
// تنفيذ العملية
String amountAfterFees = (balance - 5).toStringAsFixed(0);
await payoutController.payToWalletDriverAll(
amountAfterFees,
payoutController.dropdownValue.toString(),
controller.phoneWallet.text.toString(),
);
}
},
),
),
const SizedBox(height: 10),
],
),
),
),
isScrollControlled: true,
);
}
// --- Helper Widgets ---
Widget _buildWalletDropdown(PaymobPayout controller) {
return GetBuilder<PaymobPayout>(
builder: (_) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: controller.dropdownValue,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down_rounded,
color: Color(0xFF1A2980)),
items: <String>['etisalat', 'aman', 'orange', 'vodafone']
.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Row(
children: [
const Icon(Icons.account_balance_wallet_outlined,
size: 18, color: Colors.grey),
const SizedBox(width: 10),
Text(value,
style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
);
}).toList(),
onChanged: (newValue) {
controller.dropdownValue = newValue!;
controller.update();
},
),
),
);
},
);
}
Widget _buildSummaryRow(String title, String value,
{bool isDeduction = false, bool isTotal = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
style: TextStyle(
color: isTotal ? Colors.black : Colors.grey[600],
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 16 : 14,
)),
Text(value,
style: TextStyle(
color: isDeduction
? Colors.red
: (isTotal ? const Color(0xFF1A2980) : Colors.black),
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 18 : 14,
)),
],
),
);
}
void _showErrorDialog(BuildContext context, String title, String message) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title, style: const TextStyle(color: Colors.red)),
content: Text(message),
actions: [
CupertinoDialogAction(
child: const Text("تمام"),
onPressed: () => Navigator.pop(context),
)
],
),
);
}
}
@@ -1,170 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
// --- ملاحظات هامة ---
// 1. تأكد من إضافة الرابط الجديد إلى ملف AppLink الخاص بك:
// static const String payWithEcashDriver = "$server/payment/payWithEcashDriver.php";
//
// 2. تأكد من أنك تخزن 'driverId' في الـ box الخاص بك، مثلاً:
// box.read(BoxName.driverID)
/// دالة جديدة لبدء عملية الدفع للسائق عبر ecash
Future<void> payWithEcashDriver(BuildContext context, String amount) async {
try {
// يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
);
if (didAuthenticate) {
// استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
var res = await CRUD().postWallet(
link: AppLink.payWithEcashDriver,
payload: {
// أرسل البيانات التي يحتاجها السيرفر
"amount": amount,
// "driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
"driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
},
);
// التأكد من أن السيرفر أعاد رابط الدفع بنجاح
if (res != null &&
res['status'] == 'success' &&
res['message'] != null) {
final String paymentUrl = res['message'];
// الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash للسائق
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EcashDriverPaymentScreen(paymentUrl: paymentUrl),
),
);
} else {
// عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'Failed to initiate payment. Please try again.'.tr,
style: AppStyle.title,
),
);
}
}
}
} catch (e) {
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'An error occurred during the payment process.'.tr,
style: AppStyle.title,
),
);
}
}
/// شاشة جديدة ومبسطة خاصة بدفع السائقين عبر ecash
class EcashDriverPaymentScreen extends StatefulWidget {
final String paymentUrl;
const EcashDriverPaymentScreen({required this.paymentUrl, Key? key})
: super(key: key);
@override
State<EcashDriverPaymentScreen> createState() =>
_EcashDriverPaymentScreenState();
}
class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
print('Ecash Driver WebView URL Finished: $url');
// هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
// لا حاجة لاستدعاء أي API هنا، فالـ Webhook يقوم بكل العمل
if (url.contains("success.php")) {
showProcessingDialog();
}
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
// دالة لعرض رسالة "العملية قيد المعالجة"
void showProcessingDialog() {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(
"Payment Successful".tr,
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
"Your payment is being processed and your wallet will be updated shortly."
.tr,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
Navigator.pop(context); // Close the dialog
Navigator.pop(context); // Close the payment screen
},
style: TextButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK".tr,
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
body: WebViewWidget(controller: _controller),
);
}
}
@@ -1,196 +0,0 @@
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import '../../../controller/payment/smsPaymnet/pay_out_syria_controller.dart';
class PayoutScreen extends StatefulWidget {
// استقبال كل البيانات المطلوبة جاهزة
final double amountToWithdraw;
final String payoutPhoneNumber;
final String walletType;
const PayoutScreen({
super.key,
required this.amountToWithdraw,
required this.payoutPhoneNumber,
required this.walletType,
});
@override
_PayoutScreenState createState() => _PayoutScreenState();
}
class _PayoutScreenState extends State<PayoutScreen> {
final _payoutService = PayoutService();
final _localAuth = LocalAuthentication();
bool _isLoading = false;
Future<void> _handlePayoutRequest() async {
try {
// 1. طلب المصادقة البيومترية
bool didAuthenticate = await _localAuth.authenticate(
localizedReason: 'استخدم بصمة الإصبع لتأكيد عملية السحب',
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
),
);
if (didAuthenticate && mounted) {
setState(() => _isLoading = true);
// 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة
final result = await _payoutService.requestPayout(
driverId:
box.read(BoxName.driverID).toString(), // استبدله بـ box.read
amount: widget.amountToWithdraw,
payoutPhoneNumber: widget.payoutPhoneNumber,
walletType: widget.walletType,
);
setState(() => _isLoading = false);
if (result != null && result.contains("successfully")) {
// 3. عرض رسالة النجاح النهائية
_showSuccessDialog();
} else {
_showErrorDialog(result ?? "حدث خطأ غير معروف.");
}
}
} catch (e) {
setState(() => _isLoading = false);
_showErrorDialog("جهازك لا يدعم المصادقة البيومترية أو لم يتم إعدادها.");
debugPrint("Biometric error: $e");
}
}
@override
Widget build(BuildContext context) {
// حساب المبلغ الإجمالي المخصوم
final totalDeducted = widget.amountToWithdraw + PayoutService.payoutFee;
return Scaffold(
appBar: AppBar(title: const Text("تأكيد سحب الأموال")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.wallet, size: 64, color: Colors.blue),
const SizedBox(height: 16),
Text(
"تأكيد تفاصيل عملية السحب",
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
_buildSummaryCard(totalDeducted),
const SizedBox(height: 32),
_isLoading
? const Center(child: CircularProgressIndicator())
: ElevatedButton.icon(
onPressed: _handlePayoutRequest,
icon: const Icon(Icons.fingerprint),
label: const Text("تأكيد السحب بالبصمة"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
Widget _buildSummaryCard(double totalDeducted) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_summaryRow("المبلغ المسحوب:",
"${widget.amountToWithdraw.toStringAsFixed(2)} ل.س"),
const Divider(),
_summaryRow("عمولة السحب:",
"${PayoutService.payoutFee.toStringAsFixed(2)} ل.س"),
const Divider(thickness: 1.5),
_summaryRow(
"الإجمالي المخصوم من رصيدك:",
"${totalDeducted.toStringAsFixed(2)} ل.س",
isTotal: true,
),
const SizedBox(height: 16),
_summaryRow("سيتم التحويل إلى هاتف:", widget.payoutPhoneNumber),
_summaryRow("عبر محفظة:", widget.walletType),
],
),
),
);
}
Widget _summaryRow(String title, String value, {bool isTotal = false}) {
final titleStyle = TextStyle(
fontSize: 16,
color: isTotal ? Theme.of(context).primaryColor : Colors.black87,
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
);
final valueStyle = titleStyle.copyWith(
fontWeight: FontWeight.bold,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: titleStyle),
Text(value, style: valueStyle),
],
),
);
}
void _showErrorDialog(String message) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('حدث خطأ'),
content: Text(message),
actions: [
TextButton(
child: const Text('موافق'),
onPressed: () => Navigator.of(ctx).pop())
],
),
);
}
void _showSuccessDialog() {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('تم إرسال طلبك بنجاح'),
content: Text(
"سيتم تحويل المال إلى المحفظة التي أوردتها (${widget.walletType})، إلى الرقم ${widget.payoutPhoneNumber}، خلال مدة قصيرة. يرجى الانتظار، ستصلك رسالة تأكيد من محفظتك حال وصولها. شكراً لك."),
actions: [
TextButton(
child: const Text('موافق'),
onPressed: () {
Navigator.of(ctx).pop();
Navigator.of(context).pop();
},
),
],
),
);
}
}
@@ -1,407 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart' as intl; // For date formatting
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
class PaymentHistoryDriverPage extends StatelessWidget {
const PaymentHistoryDriverPage({super.key});
@override
Widget build(BuildContext context) {
// التأكد من حقن الكونترولر
Get.put(DriverWalletHistoryController());
return Scaffold(
backgroundColor: const Color(0xFFF8F9FD), // خلفية هادئة عصرية
appBar: AppBar(
title: Text(
'Financial Wallet'.tr,
style: const TextStyle(
color: Colors.black87, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: const Icon(Icons.filter_list_rounded,
color: AppColor.primaryColor),
onPressed: () {
// يمكن إضافة فلترة هنا مستقبلاً
},
)
],
),
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
// تجميع البيانات للمخطط
final List<Map<String, dynamic>> chartData =
_processChartData(controller.archive);
return Column(
children: [
// 1. المخطط البياني الأسبوعي (Weekly Chart)
_buildWeeklyChartSection(chartData),
const SizedBox(height: 10),
// 2. قائمة المعاملات (Transactions List)
Expanded(
child: controller.archive.isEmpty
? _buildEmptyState(context)
: _buildTransactionList(controller.archive),
),
],
);
},
),
);
}
// --- قسم المخطط البياني ---
Widget _buildWeeklyChartSection(List<Map<String, dynamic>> data) {
// حساب أعلى قيمة لضبط ارتفاع الأعمدة نسبياً
double maxAmount = 0;
for (var item in data) {
if ((item['amount'] as double) > maxAmount) maxAmount = item['amount'];
}
// تجنب القسمة على صفر
if (maxAmount == 0) maxAmount = 1;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Weekly Earnings".tr,
style: AppStyle.headTitle2.copyWith(fontSize: 16),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.greenColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"Last 7 Days".tr,
style: const TextStyle(
fontSize: 10,
color: AppColor.greenColor,
fontWeight: FontWeight.bold),
),
)
],
),
const SizedBox(height: 20),
// رسم الأعمدة
SizedBox(
height: 120,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: data.map((dayData) {
double heightFactor = (dayData['amount'] as double) / maxAmount;
// التأكد من أن العمود له ارتفاع بسيط حتى لو القيمة صفر للجمالية
if (heightFactor < 0.05) heightFactor = 0.05;
bool isToday = dayData['isToday'] ?? false;
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Tooltip للمبلغ عند الضغط (اختياري)
// Bar
Flexible(
child: Container(
width: 12,
height: 100 * heightFactor,
decoration: BoxDecoration(
color: isToday
? AppColor.primaryColor
: AppColor.primaryColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
gradient: isToday
? LinearGradient(
colors: [
AppColor.primaryColor,
AppColor.primaryColor.withOpacity(0.7)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter)
: null,
),
),
),
const SizedBox(height: 8),
// Label (Sat, Sun...)
Text(
dayData['dayName'],
style: TextStyle(
fontSize: 11,
fontWeight:
isToday ? FontWeight.bold : FontWeight.normal,
color: isToday ? AppColor.primaryColor : Colors.grey,
),
),
],
);
}).toList(),
),
),
],
),
);
}
// --- معالجة البيانات للمخطط ---
List<Map<String, dynamic>> _processChartData(List transactionList) {
// 1. تحضير خريطة لأيام الأسبوع (من السبت إلى الجمعة مثلاً)
// نستخدم آخر 7 أيام ديناميكياً
List<Map<String, dynamic>> weekData = [];
DateTime now = DateTime.now();
// إنشاء هيكل لآخر 7 أيام
for (int i = 6; i >= 0; i--) {
DateTime date = now.subtract(Duration(days: i));
String dayKey =
intl.DateFormat('yyyy-MM-dd').format(date); // مفتاح للمقارنة
String dayName =
intl.DateFormat('E', 'ar').format(date); // اسم اليوم (سبت، أحد..)
// اختصار أسماء الأيام بالعربية إذا لزم الأمر
dayName = dayName
.replaceAll('السبت', 'س')
.replaceAll('الأحد', 'ح')
.replaceAll('الاثنين', 'ن')
.replaceAll('الثلاثاء', 'ث')
.replaceAll('الأربعاء', 'ر')
.replaceAll('الخميس', 'خ')
.replaceAll('الجمعة', 'ج');
weekData.add({
'date': dayKey,
'dayName': dayName,
'amount': 0.0,
'isToday': i == 0,
});
}
// 2. تجميع المبالغ من الترانزكشن
for (var trans in transactionList) {
if (trans['created_at'] != null) {
try {
DateTime transDate = DateTime.parse(trans['created_at']);
String transKey = intl.DateFormat('yyyy-MM-dd').format(transDate);
double amount = double.tryParse(trans['amount'].toString()) ?? 0.0;
// نجمع فقط المبالغ الإيجابية (الدخل) للمخطط، أو يمكن جمع الكل حسب رغبتك
// هنا سأجمع كل الحركات الموجبة (الربح)
if (amount > 0) {
for (var day in weekData) {
if (day['date'] == transKey) {
day['amount'] += amount;
break;
}
}
}
} catch (e) {
// ignore date parse error
}
}
}
return weekData;
}
// --- قائمة المعاملات ---
Widget _buildTransactionList(List transactions) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
"Recent Transactions".tr,
style: AppStyle.headTitle2.copyWith(fontSize: 18),
),
),
Expanded(
child: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: transactions.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
var transaction = transactions[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionCard(transaction: transaction),
),
),
);
},
),
),
),
],
);
}
// --- حالة القائمة الفارغة ---
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(Icons.receipt_long_rounded,
size: 60, color: Colors.grey[400]),
),
const SizedBox(height: 16),
Text(
'No transactions yet'.tr,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w500),
),
],
),
);
}
}
// --- بطاقة المعاملة (Modern Card) ---
class _TransactionCard extends StatelessWidget {
final Map<String, dynamic> transaction;
const _TransactionCard({required this.transaction});
@override
Widget build(BuildContext context) {
// 1. معالجة البيانات
final double amount =
double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0;
final bool isCredit = amount >= 0; // موجب = دخل، سالب = خصم
final String dateStr = transaction['created_at'] ?? '';
// تنسيق التاريخ ليكون مقروءاً (مثلاً: Yesterday, 10:30 PM)
String displayDate = dateStr;
try {
DateTime dt = DateTime.parse(dateStr);
displayDate = intl.DateFormat('dd MMM, hh:mm a', 'en').format(dt);
// استخدم 'ar' بدل 'en' إذا أردت التاريخ بالعربي
} catch (_) {}
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.03),
spreadRadius: 1,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isCredit
? AppColor.greenColor.withOpacity(0.1)
: AppColor.redColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
// أيقونات توضيحية
isCredit ? Icons.add_card_rounded : Icons.money_off_rounded,
color: isCredit ? AppColor.greenColor : AppColor.redColor,
size: 24,
),
),
title: Text(
// نوع العملية (يمكن تحسينه بناءً على payment_method)
isCredit ? 'Trip Earnings'.tr : 'Commission / Deduct'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: Colors.black87,
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
displayDate,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
// العملة المصرية
'${isCredit ? '+' : ''}${amount.toStringAsFixed(0)} ${'EGP'.tr}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isCredit ? AppColor.greenColor : AppColor.redColor,
),
),
if (!isCredit) // تلميح إضافي للمصروفات
Text(
"Paid".tr,
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
)
],
),
),
);
}
}
@@ -1,606 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/controller/payment/payment_controller.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
import '../../widgets/my_textField.dart';
class PointsCaptain extends StatefulWidget {
final Color kolor;
final String countPoint;
final double pricePoint;
const PointsCaptain({
super.key,
required this.kolor,
required this.countPoint,
required this.pricePoint,
});
@override
State<PointsCaptain> createState() => _PointsCaptainState();
}
class _PointsCaptainState extends State<PointsCaptain> {
final PaymentController paymentController = Get.put(PaymentController());
final CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
// متغيرات لحالة التحميل العامة
bool isCardLoading = false;
bool isWalletLoading = false;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () async {
// تم التعديل: إزالة شرط الدولة وتشغيل قائمة Paymob للجميع
// لأنها تدعم البطاقات الدولية الآن
_showPaymentBottomSheet(context);
},
child: Container(
width: Get.width * .28,
height: Get.width * .28,
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
widget.kolor.withOpacity(0.4),
widget.kolor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: widget.kolor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(2, 4),
)
],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.stars_rounded, color: Colors.white, size: 28),
const SizedBox(height: 5),
Text(
'${widget.countPoint} ${"points".tr}',
style: AppStyle.subtitle.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Container(
margin: const EdgeInsets.only(top: 5),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${widget.pricePoint} ${box.read(BoxName.countryCode) == 'Jordan' ? 'JOD'.tr : 'L.E'.tr}',
style: AppStyle.title.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
void _showPaymentBottomSheet(BuildContext context) {
// تصفير الحالات عند فتح القائمة
isCardLoading = false;
isWalletLoading = false;
// التحقق مما إذا كان المستخدم في مصر (لعرض خيار المحفظة)
bool isEgypt = box.read(BoxName.countryCode) == 'Egypt';
Get.bottomSheet(
StatefulBuilder(builder: (context, setStateSheet) {
return Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 20),
Text(
'Choose Payment Method'.tr,
style: AppStyle.headTitle2,
),
const SizedBox(height: 10),
Text(
'${'You will pay'.tr}: ${widget.pricePoint} ${isEgypt ? 'LE'.tr : 'currency_unit'.tr}', // تحسين بسيط للعملة
style: AppStyle.title.copyWith(color: Colors.grey),
),
const SizedBox(height: 25),
// --- إضافة تنبيه بخصوص البطاقات الدولية ---
Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: AppColor.primaryColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.public, color: AppColor.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Global Payment".tr,
style: TextStyle(
color: AppColor.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
"We accept all international Credit/Debit cards."
.tr,
style: TextStyle(
color: AppColor.primaryColor.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
],
),
),
// --- زر الدفع بالبطاقة ---
_buildPaymentButton(
title: 'Pay with Credit Card'.tr,
icon: Icons.credit_card,
color: AppColor.primaryColor,
isLoading: isCardLoading,
onTap: () async {
setStateSheet(() => isCardLoading = true);
try {
var res = await CRUD().postWallet(
link: AppLink.payWithPayMobCardDriver,
payload: _getPayload(),
);
if (res != null && res['message'] != null) {
Get.back(); // إغلاق القائمة
Get.to(() => GenericPaymentWebView(
iframeUrl: res['message'],
countPrice: widget.countPoint,
isWallet: false,
));
} else {
setStateSheet(() => isCardLoading = false);
mySnackeBarError("Failed to get payment link".tr);
}
} catch (e) {
setStateSheet(() => isCardLoading = false);
mySnackeBarError("Failed to initialize card payment".tr);
}
},
),
// عرض خيار المحفظة فقط إذا كان المستخدم في مصر
if (isEgypt) ...[
const SizedBox(height: 15),
_buildPaymentButton(
title: 'Pay with Wallet'.tr,
icon: Icons.account_balance_wallet,
color: const Color(0xffE60000),
isLoading: isWalletLoading,
onTap: () {
Get.back();
_showWalletNumberDialog();
},
),
],
const SizedBox(height: 20),
],
),
);
}),
isScrollControlled: true,
);
}
Map<String, dynamic> _getPayload() {
return {
"amount": widget.pricePoint.toStringAsFixed(2),
"email": box.read(BoxName.emailDriver).toString(),
"first_name": box.read(BoxName.nameDriver).toString().split(' ')[0],
"last_name": box.read(BoxName.nameDriver).toString().split(' ').length > 1
? box.read(BoxName.nameDriver).toString().split(' ')[1]
: "Driver",
"phone_number": box.read(BoxName.phoneDriver),
};
}
Widget _buildPaymentButton({
required String title,
required IconData icon,
required Color color,
required bool isLoading,
required VoidCallback onTap,
}) {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
onPressed: isLoading ? null : onTap,
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Colors.white),
const SizedBox(width: 10),
Text(title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
],
),
),
);
}
void _showWalletNumberDialog() {
bool isDialogLoading = false;
Get.defaultDialog(
barrierDismissible: false,
title: 'E-Wallet Number'.tr,
contentPadding: const EdgeInsets.all(20),
content: StatefulBuilder(builder: (context, setStateDialog) {
return Column(
children: [
Form(
key: paymentController.formKey,
child: MyTextForm(
controller: paymentController.walletphoneController,
label: 'Phone Number'.tr,
hint: '01xxxxxxxxx',
type: TextInputType.phone,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: isDialogLoading
? null
: () async {
if (paymentController.formKey.currentState!
.validate()) {
setStateDialog(() => isDialogLoading = true);
box.write(BoxName.phoneWallet,
paymentController.walletphoneController.text);
try {
var payload = _getPayload();
payload['phone_number'] =
paymentController.walletphoneController.text;
var res = await CRUD().postWallet(
link: AppLink.payWithWallet,
payload: payload,
);
if (res != null && res['message'] != null) {
Get.back();
Get.to(() => GenericPaymentWebView(
iframeUrl: res['message'],
countPrice: widget.countPoint,
isWallet: true,
));
} else {
setStateDialog(() => isDialogLoading = false);
mySnackeBarError("Failed to get payment link".tr);
}
} catch (e) {
if (mounted) {
setStateDialog(() => isDialogLoading = false);
}
mySnackeBarError(
"Failed to initialize wallet payment".tr);
}
}
},
child: isDialogLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: Text('Confirm & Pay'.tr,
style: const TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 10),
if (!isDialogLoading)
TextButton(
onPressed: () => Get.back(),
child: Text("Cancel".tr,
style: const TextStyle(color: Colors.grey)),
)
],
);
}),
);
}
}
// -----------------------------------------------------------------------------
// صفحة الويب (WebView) مع اللودينج الخاص بها
// -----------------------------------------------------------------------------
class GenericPaymentWebView extends StatefulWidget {
final String iframeUrl;
final String countPrice;
final bool isWallet;
const GenericPaymentWebView({
Key? key,
required this.iframeUrl,
required this.countPrice,
required this.isWallet,
}) : super(key: key);
@override
State<GenericPaymentWebView> createState() => _GenericPaymentWebViewState();
}
class _GenericPaymentWebViewState extends State<GenericPaymentWebView> {
late final WebViewController _controller;
final controller = Get.find<CaptainWalletController>();
bool isLoadingPage = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageStarted: (_) {
if (mounted) setState(() => isLoadingPage = true);
},
onPageFinished: (url) {
if (mounted) setState(() => isLoadingPage = false);
if (url.contains("success")) {
_fetchPaymentStatus();
} else if (url.contains("failed") || url.contains("error")) {
showCustomDialog(
title: "Error".tr,
message: 'Payment Failed'.tr,
isSuccess: false,
);
}
},
onWebResourceError: (error) {
if (mounted) setState(() => isLoadingPage = false);
},
))
..loadRequest(Uri.parse(widget.iframeUrl));
}
Future<void> _fetchPaymentStatus() async {
final String userId = widget.isWallet
? '+2${box.read(BoxName.phoneWallet)}'
: box.read(BoxName.phoneDriver);
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
final response = await CRUD().postWallet(
link: AppLink.paymetVerifyDriver,
payload: {
'user_id': userId,
'driverID': box.read(BoxName.driverID),
'paymentMethod': 'visa-in',
},
);
Get.back(); // إغلاق لودينج التحقق
if (response != 'failure' && response != 'token_expired') {
if (response['status'] == 'success') {
final payment = response['message'];
final amount = payment['amount'].toString();
final bonus = payment['bonus'].toString();
final paymentID = payment['paymentID'].toString();
await controller.refreshCaptainWallet();
showCustomDialog(
title: "payment_success".tr,
message:
"${"transaction_id".tr}: $paymentID\n${"amount_paid".tr}: $amount EGP\n${"bonus_added".tr}: $bonus ${"points".tr}",
isSuccess: true,
shouldCloseScreen: true,
);
} else {
showCustomDialog(
title: "transaction_failed".tr,
message: response['message'].toString(),
isSuccess: false,
);
}
} else {
showCustomDialog(
title: "connection_failed".tr,
message: "Could not verify payment status".tr,
isSuccess: false,
);
}
} catch (e) {
Get.back();
showCustomDialog(
title: "server_error".tr,
message: "server_error_message".tr,
isSuccess: false,
);
}
}
void showCustomDialog({
required String title,
required String message,
required bool isSuccess,
bool shouldCloseScreen = false,
}) {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSuccess
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
),
child: Icon(
isSuccess ? Icons.check_circle : Icons.error_outline,
color: isSuccess ? Colors.green : Colors.red,
size: 40,
),
),
const SizedBox(height: 15),
Text(
title,
style: AppStyle.headTitle2.copyWith(
color: isSuccess ? Colors.green : Colors.red,
),
),
const SizedBox(height: 10),
Text(
message,
textAlign: TextAlign.center,
style: AppStyle.title,
),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isSuccess ? Colors.green : Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
Get.back();
if (shouldCloseScreen) {
Get.back();
}
},
child: Text("OK".tr,
style: const TextStyle(color: Colors.white)),
),
)
],
),
),
),
barrierDismissible: false,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Secure Payment'.tr),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (isLoadingPage)
Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text("Loading Payment Page...".tr,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 16)),
],
),
),
),
],
),
);
}
}
@@ -1,145 +0,0 @@
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/home/payment/captain_wallet_controller.dart';
class TransferBudgetPage extends StatelessWidget {
const TransferBudgetPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(CaptainWalletController());
return MyScafolld(
title: "Transfer budget".tr,
body: [
GetBuilder<CaptainWalletController>(
builder: (captainWalletController) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .7,
width: double.infinity,
child: Form(
key: captainWalletController.formKeyTransfer,
child: Column(
children: [
const SizedBox(
height: 20,
),
MyTextForm(
controller: captainWalletController
.newDriverPhoneController,
label: 'phone number of driver'.tr,
hint: 'phone number of driver',
type: TextInputType.phone),
MyTextForm(
controller: captainWalletController
.amountFromBudgetController,
label: 'insert amount'.tr,
hint:
'${'You have in account'.tr} ${captainWalletController.totalAmountVisa}',
type: TextInputType.number),
captainWalletController.isNewTransfer
? const MyCircularProgressIndicator()
: captainWalletController
.amountToNewDriverMap.isEmpty
? MyElevatedButton(
title: 'Next'.tr,
onPressed: () async {
await captainWalletController
.detectNewDriverFromMyBudget();
})
: const SizedBox(),
captainWalletController.amountToNewDriverMap.isNotEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
'Name :'.tr +
captainWalletController
.amountToNewDriverMap[0]['name']
.toString(),
textAlign: TextAlign.center,
style: AppStyle.title,
),
),
const SizedBox(
height: 5,
),
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
"${"NationalID".tr} ${captainWalletController.amountToNewDriverMap[0]['national_number']}",
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 5,
),
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
"${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${'LE'.tr}",
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 15,
),
captainWalletController
.amountToNewDriverMap.isNotEmpty
? MyElevatedButton(
title: 'Transfer'.tr,
onPressed: () async {
if (double.parse(
captainWalletController
.amountFromBudgetController
.text) <
double.parse(
captainWalletController
.totalAmountVisa) -
5) {
await captainWalletController
.addTransferDriversWallet(
'TransferFrom',
'TransferTo',
);
} else {
MyDialog().getDialog(
"You dont have money in your Wallet"
.tr,
"You dont have money in your Wallet or you should less transfer 5 LE to activate"
.tr, () {
Get.back();
});
}
})
: const SizedBox()
],
),
)
: const SizedBox()
],
)),
),
);
}),
],
isleading: true);
}
}
@@ -1,728 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'dart:ui';
// --- IMPORTS ---
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/controller/home/payment/paymob_payout.dart';
import 'package:sefer_driver/views/home/my_wallet/bank_account_egypt.dart';
import 'package:sefer_driver/views/home/my_wallet/payment_history_driver_page.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart';
import '../../../controller/payment/driver_payment_controller.dart';
import 'card_wallet_widget.dart';
import 'points_captain.dart';
import 'transfer_budget_page.dart';
import 'weekly_payment_page.dart';
// --- COLOR PALETTE (Modern & Professional) ---
const kBgColor = Color(0xFFF8F9FA); // خلفية فاتحة مريحة للعين
const kPrimaryColor = Color(0xFF0056D2); // أزرق احترافي (يشبه PayPal/Visa)
const kCardColor = Colors.white;
const kTextPrimary = Color(0xFF2D3436);
const kTextSecondary = Color(0xFF636E72);
const kGreenSuccess = Color(0xFF00B894);
const kRedDanger = Color(0xFFD63031);
const kGold = Color(0xFFFDCB6E);
class WalletCaptainRefactored extends StatelessWidget {
WalletCaptainRefactored({super.key});
final CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
// تحديد لون حالة النقاط (أخضر = أمان، أحمر = خطر وقف حال)
Color _getPointsStatusColor(String pointsStr) {
final points = double.tryParse(pointsStr) ?? 0.0;
if (points < -300) return kRedDanger;
if (points < 0) return Colors.orange;
return kGreenSuccess;
}
@override
Widget build(BuildContext context) {
captainWalletController.refreshCaptainWallet();
return Scaffold(
backgroundColor: kBgColor,
appBar: AppBar(
backgroundColor: kCardColor,
elevation: 0,
centerTitle: true,
title: const Text(
'محفظة الكابتن',
style: TextStyle(color: kTextPrimary, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: kTextPrimary),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded, color: kPrimaryColor),
onPressed: () => captainWalletController.refreshCaptainWallet(),
)
],
),
body: GetBuilder<CaptainWalletController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(
child: CircularProgressIndicator(color: kPrimaryColor));
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. كارت النقاط (حالة الحساب)
_buildPointsStatusCard(context, controller),
const SizedBox(height: 20),
const CardTripzWalletDriver(), // Kept as is.
const SizedBox(height: 20),
// 2. ملخص الرصيد (كاش وفيزا)
const Text("ملخص الرصيد",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildBalanceCard(
title: "المديونية (كاش معك)",
amount: controller.totalAmount,
icon: Icons.money_off_csred_rounded,
color: kRedDanger,
subtitle: "مبالغ تم تحصيلها كاش",
)),
const SizedBox(width: 12),
Expanded(
child: _buildBalanceCard(
title: "رصيدك بالمحفظة",
amount: controller.totalAmountVisa,
icon: Icons.account_balance_wallet,
color: kGreenSuccess,
subtitle: "رصيد قابل للسحب/التحويل",
)),
],
),
const SizedBox(height: 20),
// 3. الإجراءات السريعة (Quick Actions)
const Text("إجراءات سريعة",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildQuickActionsGrid(context, controller),
const SizedBox(height: 20),
// 4. إعدادات السحب (Payout Settings) - مهم جداً لمصر
_buildPayoutSection(context, controller),
const SizedBox(height: 20),
// 5. البونص والعروض
if (controller.walletDate['message'] != null)
_buildPromoSection(controller),
const SizedBox(height: 20),
// 6. شحن النقاط
const Text("باقات شحن النقاط",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildPointsPackages(),
const SizedBox(height: 30),
],
),
);
},
),
);
}
// --- WIDGETS ---
Widget _buildPointsStatusCard(
BuildContext context, CaptainWalletController controller) {
Color statusColor =
_getPointsStatusColor(controller.totalPoints.toString());
bool isBlocked = double.parse(controller.totalPoints.toString()) < -300;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isBlocked
? [kRedDanger, Colors.red.shade300]
: [kPrimaryColor, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: (isBlocked ? kRedDanger : kPrimaryColor).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5))
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("رصيد التشغيل (النقاط)",
style: TextStyle(
color: Colors.white.withOpacity(0.9), fontSize: 16)),
InkWell(
onTap: () => _showInfoDialog(context),
child: const Icon(Icons.info_outline, color: Colors.white),
)
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
controller.totalPoints.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold),
),
const SizedBox(width: 5),
const Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Icon(Icons.diamond, color: kGold, size: 24),
),
],
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
isBlocked
? "حسابك موقوف مؤقتاً - اشحن فوراً"
: "حالة الحساب: نشط ✅",
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
);
}
Widget _buildBalanceCard(
{required String title,
required String amount,
required IconData icon,
required Color color,
required String subtitle}) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 10),
Text(title,
style: TextStyle(
color: kTextSecondary,
fontSize: 12,
fontWeight: FontWeight.w600)),
const SizedBox(height: 5),
Text("$amount ج.م",
style: TextStyle(
color: kTextPrimary,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text(subtitle,
style: TextStyle(color: Colors.grey.shade400, fontSize: 10)),
],
),
);
}
Widget _buildQuickActionsGrid(
BuildContext context, CaptainWalletController controller) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.1,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: [
_buildActionButton(
icon: Icons.swap_horiz_rounded,
label: "تحويل نقاط",
color: Colors.orange,
onTap: () => _showBuyFromBudgetDialog(controller),
),
_buildActionButton(
icon: Icons.send_to_mobile_rounded,
label: "تحويل لكابتن",
color: Colors.purple,
onTap: () => _handleTransferBudget(controller),
),
_buildActionButton(
icon: Icons.history_rounded,
label: "سجل العمليات",
color: Colors.blueGrey,
onTap: () async {
// await Get.put(DriverWalletHistoryController()).getArchivePayment();
Get.to(() => const PaymentHistoryDriverPage());
},
),
],
);
}
Widget _buildActionButton(
{required IconData icon,
required String label,
required Color color,
required VoidCallback onTap}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(15),
child: Container(
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: color.withOpacity(0.1),
radius: 22,
child: Icon(icon, color: color),
),
const SizedBox(height: 8),
Text(label,
style:
const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
],
),
),
);
}
Widget _buildPayoutSection(
BuildContext context, CaptainWalletController controller) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: kPrimaryColor.withOpacity(0.2)),
boxShadow: [
BoxShadow(color: kPrimaryColor.withOpacity(0.05), blurRadius: 10)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(Icons.credit_card_rounded, color: kPrimaryColor),
SizedBox(width: 10),
Text("إعدادات سحب الرصيد (Payout)",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 10),
const Text(
"عشان تقدر تسحب فلوسك، لازم تضيف حساب بنكي أو محفظة إلكترونية.",
style: TextStyle(fontSize: 12, color: kTextSecondary)),
const SizedBox(height: 15),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
// استدعاء دالة إضافة الحساب البنكي الموجودة أصلاً
addBankCodeEgypt(controller);
},
icon: const Icon(Icons.add_card, size: 18),
label: const Text("إضافة حساب بنكي / محفظة",
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 10),
InkWell(
onTap: () async {
await Get.put(DriverWalletHistoryController())
.getWeekllyArchivePayment();
Get.to(() => const WeeklyPaymentPage());
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text("عرض الأرباح الأسبوعية",
style: TextStyle(
color: kPrimaryColor, fontWeight: FontWeight.bold)),
Icon(Icons.arrow_forward_ios, size: 12, color: kPrimaryColor),
],
),
)
],
),
);
}
Widget _buildPromoSection(CaptainWalletController controller) {
// التأكد من وجود البيانات لتجنب الأخطاء
int morningCount = 0;
int afternoonCount = 0;
try {
// morningCount = controller.walletDate['message'][0]['morning_count'] ?? 0;
morningCount = controller.morningCount;
afternoonCount = controller.afternoonCount;
} catch (e) {
// ignore
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("بونص اليوم 🎁",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildPromoCard(
title: "بونص الصبحية",
desc: "اعمل 5 رحلات من 7ص لـ 10ص",
current: morningCount,
max: 5,
color: Colors.orange,
onTap: () => controller.addDriverWalletFromPromo('Morning Promo', 50),
),
const SizedBox(height: 10),
_buildPromoCard(
title: "بونص آخر النهار",
desc: "اعمل 5 رحلات من 3م لـ 6م",
current: afternoonCount,
max: 5,
color: Colors.indigo,
onTap: () =>
controller.addDriverWalletFromPromo('Afternoon Promo', 50),
),
],
);
}
Widget _buildPromoCard(
{required String title,
required String desc,
required int current,
required int max,
required Color color,
required VoidCallback onTap}) {
double progress = (current / max).clamp(0.0, 1.0);
return InkWell(
onTap: () {
if (current >= max) {
onTap();
} else {
Get.snackbar("شد حيلك", "لسه فاضلك رحلات عشان تاخد البونص");
}
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
children: [
CircularProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade200,
color: color,
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(fontWeight: FontWeight.bold)),
Text(desc,
style:
const TextStyle(fontSize: 11, color: kTextSecondary)),
],
),
),
if (current >= max)
const Icon(Icons.check_circle, color: kGreenSuccess)
else
Text("$current/$max",
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
],
),
),
);
}
Widget _buildPointsPackages() {
return SizedBox(
height: 140,
child: ListView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
children: [
_buildPackageCard(price: 100, points: "100", color: Colors.blueGrey),
_buildPackageCard(
price: 200,
points: "210",
color: const Color(0xFFCD7F32)), // Bronze
_buildPackageCard(
price: 400, points: "435", color: Colors.grey), // Silver
_buildPackageCard(
price: 1000,
points: "1100",
color: const Color(0xFFFFD700)), // Gold
],
),
);
}
Widget _buildPackageCard(
{required double price, required String points, required Color color}) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: PointsCaptain(
kolor: color,
pricePoint: price,
countPoint: points,
),
);
}
// --- LOGIC DIALOGS ---
void _showInfoDialog(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('نظام النقاط'),
content: const Text(
'عشان تستقبل رحلات لازم رصيدك يكون أكبر من -300 جنيه.\nلو الرصيد قل عن كده، التطبيق هيوقف استقبال الطلبات لحد ما تشحن.'),
actions: [
CupertinoDialogAction(
child: const Text("تمام"), onPressed: () => Navigator.pop(ctx))
],
),
);
}
void _showBuyFromBudgetDialog(CaptainWalletController controller) {
Get.defaultDialog(
title: 'تحويل من المحفظة للنقاط',
content: Form(
key: controller.formKey,
child: Column(
children: [
Text("رصيدك الحالي بالمحفظة: ${controller.totalAmountVisa} ج.م",
style: const TextStyle(
color: kGreenSuccess, fontWeight: FontWeight.bold)),
const SizedBox(height: 15),
MyTextForm(
controller: controller.amountFromBudgetController,
label: 'المبلغ المراد تحويله',
hint: 'أدخل المبلغ',
type: TextInputType.number,
),
],
),
),
confirm: MyElevatedButton(
title: 'تأكيد التحويل',
onPressed: () async {
await _authenticateAndExecute(() async {
if (double.parse(controller.amountFromBudgetController.text) <=
double.parse(controller.totalAmountVisa.toString())) {
await controller.payFromBudget();
} else {
Get.back();
mySnackeBarError('رصيد المحفظة غير كافي للعملية');
}
});
},
),
cancel:
TextButton(onPressed: () => Get.back(), child: const Text("إلغاء")),
);
}
void _handleTransferBudget(CaptainWalletController controller) async {
await _authenticateAndExecute(() async {
if (double.parse(controller.totalAmountVisa) > 15) {
Get.to(() => const TransferBudgetPage());
} else {
mySnackeBarError("رصيد محفظتك لا يكفي للتحويل (الحد الأدنى 15 ج.م)");
}
});
}
Future<void> _authenticateAndExecute(Function onSuccess) async {
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'يرجى استخدام البصمة لتأكيد العملية المالية',
options: const AuthenticationOptions(
biometricOnly: true, sensitiveTransaction: true),
);
if (didAuthenticate) {
onSuccess();
} else {
MyDialog().getDialog(
'فشل التحقق', 'لم يتم التعرف على البصمة', () => Get.back());
}
} else {
// في حالة عدم وجود بصمة، يمكن تنفيذ العملية مباشرة أو طلب كلمة مرور (حسب رغبتك)
// هنا سننفذها للتسهيل
// onSuccess();
}
}
}
// --- الدوال القديمة (كما هي للإبقاء على اللوجيك) ---
Future<dynamic> addBankCodeEgypt(
CaptainWalletController captainWalletController) {
return Get.defaultDialog(
title: "بيانات السحب (Payout Info)",
content: Form(
key: captainWalletController.formKeyAccount,
child: Column(
children: [
const Text(
"أدخل بيانات حسابك البنكي أو المحفظة لاستقبال أرباحك الأسبوعية",
textAlign: TextAlign.center),
const SizedBox(height: 15),
MyTextForm(
controller: captainWalletController.cardBank,
label: "رقم الحساب / المحفظة / الآيبان",
hint: 'مثال: 010xxxxxxx أو رقم الحساب',
type: TextInputType.number,
),
const SizedBox(height: 10),
BankDropdown() // تأكد من أن هذا الودجت يعرض البنوك والمحافظ المصرية
],
),
),
confirm: MyElevatedButton(
title: 'حفظ البيانات',
onPressed: () async {
if (captainWalletController.formKeyAccount.currentState!.validate()) {
Get.back();
// هنا يتم إرسال البيانات للسيرفر
var res =
await CRUD().post(link: AppLink.updateAccountBank, payload: {
"bankCode": Get.find<BankController>().selectedBank.toString(),
"accountBank": captainWalletController.cardBank.text.toString(),
"id": box.read(BoxName.driverID).toString()
});
if (res != 'failure') {
mySnackbarSuccess('تم حفظ بيانات الدفع بنجاح');
}
}
},
),
);
}
// الودجت الخاص باختيار المحفظة (فودافون كاش، اتصالات، إلخ)
class MyDropDown1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
Get.put(PaymobPayout());
return GetBuilder<PaymobPayout>(builder: (controller) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: controller.dropdownValue,
icon: const Icon(Icons.keyboard_arrow_down_rounded),
style: const TextStyle(
color: kTextPrimary, fontWeight: FontWeight.bold),
onChanged: (String? newValue) {
controller.dropdownValue = newValue!;
controller.update();
},
items: <String>[
'etisalat',
'aman',
'orange',
'vodafone',
'Bank Account'
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value), // يمكن تعريب الأسماء هنا إذا أردت
);
}).toList(),
),
),
);
});
}
}
@@ -1,223 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
class WeeklyPaymentPage extends StatelessWidget {
const WeeklyPaymentPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverWalletHistoryController());
return Scaffold(
appBar: AppBar(
title: Text('Weekly Summary'.tr),
backgroundColor: Colors.white,
elevation: 1,
),
backgroundColor: Colors.grey[100],
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
return Column(
children: [
// 1. Prominent Summary Card at the top
_buildSummaryCard(controller),
// 2. A title for the transactions list
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Row(
children: [
Icon(Icons.list_alt, color: Colors.grey[600]),
const SizedBox(width: 8),
Text(
'Transactions this week'.tr,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
],
),
),
// 3. The animated list of transactions
Expanded(
child: controller.weeklyList.isEmpty
? _buildEmptyState(context)
: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemCount: controller.weeklyList.length,
itemBuilder: (BuildContext context, int index) {
var transaction = controller.weeklyList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionListItem(
transaction: transaction),
),
),
);
},
),
),
),
],
);
},
),
);
}
// A widget for the top summary card.
Widget _buildSummaryCard(DriverWalletHistoryController controller) {
final String totalAmount = controller.weeklyList.isEmpty
? '0.00'
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
return Card(
margin: const EdgeInsets.all(16.0),
elevation: 4,
shadowColor: AppColor.primaryColor.withOpacity(0.2),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.primaryColor, AppColor.greenColor],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
const Icon(Icons.account_balance_wallet,
color: Colors.white, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Weekly Earnings'.tr,
style: AppStyle.title
.copyWith(color: Colors.white70, fontSize: 16),
),
const SizedBox(height: 4),
Text(
'$totalAmount ${'SYP'.tr}',
style: AppStyle.number
.copyWith(color: Colors.white, fontSize: 32),
),
],
),
),
],
),
),
);
}
// A dedicated widget for the list item.
Widget _TransactionListItem({required Map<String, dynamic> transaction}) {
final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown';
final String amount = transaction['amount']?.toString() ?? '0';
final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? '');
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Icon(_getPaymentIcon(paymentMethod),
color: AppColor.primaryColor, size: 22),
),
title: Text(
'$amount ${'SYP'.tr}',
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Text(
date != null
? DateFormat('EEEE, hh:mm a', Get.locale?.toString())
.format(date) // e.g., Tuesday, 10:11 AM
: 'Invalid Date',
style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]),
),
trailing: Chip(
label: Text(
_getTranslatedPaymentMethod(paymentMethod),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
backgroundColor: Colors.grey.shade200,
padding: const EdgeInsets.symmetric(horizontal: 6),
side: BorderSide.none,
),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No transactions this week'.tr,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey[600]),
),
],
),
);
}
// Helper to get a specific icon for each payment method.
IconData _getPaymentIcon(String paymentMethod) {
switch (paymentMethod.toLowerCase()) {
case 'visa':
return Icons.credit_card;
case 'frombudget':
return Icons.account_balance_wallet_outlined;
case 'remainder':
return Icons.receipt_long;
case 'cash':
return Icons.money;
default:
return Icons.payment;
}
}
// Helper to get translated or formatted payment method names.
String _getTranslatedPaymentMethod(String paymentMethod) {
switch (paymentMethod) {
case 'Remainder':
return 'Remainder'.tr;
case 'fromBudget':
return 'From Budget'.tr;
default:
return paymentMethod;
}
}
}