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:
co-authored by
Claude Fable 5
parent
3ab74a13a8
commit
de4bbd00ac
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user