first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
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),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,313 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:siro_driver/views/home/my_wallet/pay_out_screen.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.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';
// تذكير: ستحتاج إلى إضافة حزمة flutter_svg إلى ملف pubspec.yaml
// dependencies:
// flutter_svg: ^2.0.7
/// بطاقة المحفظة بتصميم سوري فاخر مستوحى من فن الأرابيسك والفسيفساء
class CardSeferWalletDriver extends StatelessWidget {
const CardSeferWalletDriver({super.key});
// SVG لنقشة أرابيسك هندسية لاستخدامها كخلفية
final String arabesquePattern = '''
<svg width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="arabesque" patternUnits="userSpaceOnUse" width="50" height="50" patternTransform="scale(1.2)">
<g fill="#E7C582" fill-opacity="0.1">
<path d="M25 0 L35.35 9.65 L50 25 L35.35 40.35 L25 50 L14.65 40.35 L0 25 L14.65 9.65 Z"/>
<path d="M50 0 L60.35 9.65 L75 25 L60.35 40.35 L50 50 L39.65 40.35 L25 25 L39.65 9.65 Z"/>
<path d="M0 50 L9.65 39.65 L25 25 L9.65 10.35 L0 0 L-9.65 10.35 L-25 25 L-9.65 39.65 Z"/>
</g>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#arabesque)"/>
</svg>
''';
@override
Widget build(BuildContext context) {
return Center(
child: GetBuilder<CaptainWalletController>(
builder: (captainWalletController) {
return GestureDetector(
onTap: () => _showCashOutDialog(context, captainWalletController),
child: Container(
width: Get.width * 0.9,
height: Get.height * 0.25,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: const Color(0xFF003C43).withOpacity(0.5),
blurRadius: 25,
spreadRadius: -5,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: Stack(
children: [
// الخلفية الرئيسية
Container(color: const Color(0xFF003C43)),
// طبقة النقشة
SvgPicture.string(arabesquePattern, fit: BoxFit.cover),
// طبقة التأثير الزجاجي (Glassmorphism)
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
child: Container(color: Colors.black.withOpacity(0.1)),
),
// محتوى البطاقة
_buildCardContent(captainWalletController),
],
),
),
),
);
},
),
);
}
Widget _buildCardContent(CaptainWalletController captainWalletController) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'رصيد انطلق',
style: AppStyle.headTitle.copyWith(
fontFamily: 'Amiri', // خط يوحي بالفخامة
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.bold,
),
),
// أيقونة شريحة البطاقة
const Icon(Icons.sim_card_outlined,
color: Color(0xFFE7C582), size: 30),
],
),
Column(
children: [
Text(
'الرصيد الحالي'.tr,
style: AppStyle.title.copyWith(
color: Colors.white.withOpacity(0.7),
fontSize: 16,
),
),
const SizedBox(height: 4),
// استخدام AnimatedSwitcher لإضافة حركة عند تحديث الرصيد
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
child: Text(
'${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
key:
ValueKey<String>(captainWalletController.totalAmountVisa),
style: AppStyle.headTitle2.copyWith(
color: const Color(0xFFE7C582), // Antique Gold
fontSize: 40,
fontWeight: FontWeight.w900,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
box.read(BoxName.nameDriver).toString().split(' ')[0],
style: AppStyle.title.copyWith(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
letterSpacing: 0.5,
),
),
Text(
"سحب الرصيد".tr,
style: AppStyle.title.copyWith(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
],
),
],
),
);
}
void _showCashOutDialog(
BuildContext context, CaptainWalletController captainWalletController) {
double minAmount = 20000.0; // الحد الأدنى للسحب
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
Get.defaultDialog(
barrierDismissible: false,
title: 'هل تريد سحب أرباحك؟'.tr,
titleStyle: AppStyle.title
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.account_balance_wallet,
color: AppColor.primaryColor, size: 30),
const SizedBox(height: 15),
Text(
'${'رصيدك الإجمالي:'.tr} ${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
style: AppStyle.title.copyWith(fontSize: 16),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'طريقة الدفع:'.tr,
style: AppStyle.title.copyWith(fontSize: 16),
),
const SizedBox(width: 10),
const MyDropDownSyria(),
],
),
const SizedBox(height: 20),
Form(
key: captainWalletController.formKey,
child: MyTextForm(
controller: captainWalletController.phoneWallet,
label: "أدخل رقم محفظتك".tr,
hint: "مثال: 0912345678".tr,
type: TextInputType.phone,
),
),
],
),
confirm: MyElevatedButton(
title: 'تأكيد'.tr,
onPressed: () async {
box.write(
BoxName.phoneWallet, captainWalletController.phoneWallet);
box.write(BoxName.walletType,
Get.find<SyrianPayoutController>().dropdownValue.toString());
if (captainWalletController.formKey.currentState!.validate()) {
Get.back();
Get.to(() => PayoutScreen(
amountToWithdraw:
double.parse(captainWalletController.totalAmountVisa),
payoutPhoneNumber:
captainWalletController.phoneWallet.text.toString(),
walletType: Get.find<SyrianPayoutController>()
.dropdownValue
.toString(),
));
// String amountAfterFee =
// (double.parse(captainWalletController.totalAmountVisa) - 5)
// .toStringAsFixed(0);
// await Get.put(PaymobPayout()).payToWalletDriverAll(
// amountAfterFee,
// Get.find<SyrianPayoutController>().dropdownValue.toString(),
// captainWalletController.phoneWallet.text.toString(),
// );
}
},
kolor: AppColor.greenColor,
),
cancel: MyElevatedButton(
title: 'إلغاء'.tr,
onPressed: () {
Get.back();
},
kolor: AppColor.redColor,
));
} else {
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text("تنبيه".tr),
content: Text(
'${'المبلغ في محفظتك أقل من الحد الأدنى للسحب وهو'.tr} $minAmount ${'ل.س'.tr}',
),
actions: <Widget>[
CupertinoDialogAction(
isDefaultAction: true,
child: Text("موافق".tr),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
}
// هذا الكود من الملف الأصلي وهو ضروري لعمل الحوار
class MyDropDownSyria extends StatelessWidget {
const MyDropDownSyria({super.key});
@override
Widget build(BuildContext context) {
Get.put(SyrianPayoutController());
return GetBuilder<SyrianPayoutController>(builder: (controller) {
return DropdownButton<String>(
value: controller.dropdownValue,
icon: const Icon(Icons.arrow_drop_down),
elevation: 16,
style: const TextStyle(color: Colors.deepPurple, fontSize: 16),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String? newValue) {
controller.changeValue(newValue);
},
items: <String>['Syriatel', 'Cash Mobile', 'Sham Cash']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value.tr),
);
}).toList(),
);
});
}
}
// هذا المتحكم ضروري لعمل القائمة المنسدلة
class SyrianPayoutController extends GetxController {
String dropdownValue = 'Syriatel';
void changeValue(String? newValue) {
if (newValue != null) {
dropdownValue = newValue;
update();
}
}
}

View File

@@ -0,0 +1,170 @@
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),
);
}
}

View File

@@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_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();
},
),
],
),
);
}
}

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:siro_driver/constant/finance_design_system.dart';
import 'package:siro_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
import 'widgets/transaction_preview_item.dart';
class PaymentHistoryDriverPage extends StatelessWidget {
const PaymentHistoryDriverPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverWalletHistoryController());
return Scaffold(
backgroundColor: FinanceDesignSystem.backgroundColor,
appBar: AppBar(
title: Text('Payment History'.tr,
style: TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
onPressed: () => Get.back(),
),
),
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
if (controller.archive.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history_rounded, size: 80, color: Colors.grey.shade300),
const SizedBox(height: 16),
Text('No transactions yet'.tr,
style: TextStyle(color: Colors.grey.shade400, fontWeight: FontWeight.bold)),
],
),
);
}
return AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemCount: controller.archive.length,
itemBuilder: (BuildContext context, int index) {
final tx = controller.archive[index];
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: TransactionPreviewItem(
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
subtitle: tx['created_at'] ?? '',
amount: amount.abs().toStringAsFixed(0),
date: tx['created_at']?.split(' ')[0] ?? '',
type: amount >= 0 ? 'credit' : 'debit',
onTap: () {},
),
),
),
);
},
),
);
},
),
);
}
}

View File

@@ -0,0 +1,900 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:siro_driver/controller/payment/payment_controller.dart';
import 'package:siro_driver/controller/payment/smsPaymnet/payment_services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../constant/box_name.dart';
import '../../../constant/finance_design_system.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
import 'ecash.dart';
class PointsCaptain extends StatelessWidget {
final PaymentController paymentController = Get.put(PaymentController());
final CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
PointsCaptain({
super.key,
required this.kolor,
required this.countPoint,
required this.pricePoint,
});
final Color kolor;
final String countPoint;
final double pricePoint;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(right: 12, bottom: 4),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
elevation: 4,
shadowColor: kolor.withValues(alpha: 0.3),
child: InkWell(
onTap: () => _showPaymentOptions(context),
borderRadius: BorderRadius.circular(20),
child: Container(
width: 130,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
kolor.withValues(alpha: 0.05),
Colors.white,
],
),
border:
Border.all(color: kolor.withValues(alpha: 0.2), width: 1.5),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: kolor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.account_balance_wallet_rounded,
color: kolor, size: 24),
),
const SizedBox(height: 10),
Text(
'$countPoint ${'SYP'.tr}',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: FinanceDesignSystem.primaryDark,
),
),
const SizedBox(height: 4),
Text(
'${'Price:'.tr} ${pricePoint.toStringAsFixed(0)} ${'SYP'.tr}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
}
void _showPaymentOptions(BuildContext context) {
Get.bottomSheet(
isScrollControlled: true,
Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Select Payment Method".tr,
style: FinanceDesignSystem.headingStyle),
IconButton(
icon: const Icon(Icons.close_rounded, color: Colors.grey),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 8),
Text("${'Amount to charge:'.tr} $countPoint ${'SYP'.tr}",
style: FinanceDesignSystem.subHeadingStyle),
const SizedBox(height: 24),
_buildPaymentMethodTile(
icon: Icons.credit_card_rounded,
title: 'Debit Card'.tr,
subtitle: 'E-Cash payment gateway'.tr,
color: Colors.blue,
onTap: () {
Get.back();
payWithEcashDriver(context, pricePoint.toString());
},
),
const SizedBox(height: 16),
_buildPaymentMethodTile(
image: 'assets/images/syriatel.jpeg',
title: 'Syriatel Cash'.tr,
subtitle: 'Pay using Syriatel mobile wallet'.tr,
color: Colors.red,
onTap: () => _showPhoneInputDialog(context, 'Syriatel'),
),
const SizedBox(height: 16),
_buildPaymentMethodTile(
image: 'assets/images/shamCash.png',
title: 'Sham Cash'.tr,
subtitle: 'Pay using Sham Cash wallet'.tr,
color: Colors.orange,
onTap: () async {
Get.back();
bool isAuthSupported =
await LocalAuthentication().isDeviceSupported();
if (isAuthSupported) {
bool didAuthenticate =
await LocalAuthentication().authenticate(
localizedReason: 'Confirm payment with biometrics'.tr,
);
if (!didAuthenticate) return;
}
Get.to(() => PaymentScreenSmsProvider(amount: pricePoint));
},
),
],
),
),
),
);
}
Widget _buildPaymentMethodTile({
IconData? icon,
String? image,
required String title,
required String subtitle,
required Color color,
required VoidCallback onTap,
}) {
return Material(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: Row(
children: [
Container(
width: 56,
height: 56,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: image != null
? Image.asset(image, fit: BoxFit.contain)
: Icon(icon, color: color, size: 30),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: FinanceDesignSystem.primaryDark)),
const SizedBox(height: 2),
Text(subtitle,
style: TextStyle(
color: Colors.grey.shade600, fontSize: 12)),
],
),
),
Icon(Icons.arrow_forward_ios_rounded,
size: 14, color: Colors.grey.shade400),
],
),
),
),
);
}
void _showPhoneInputDialog(BuildContext context, String provider) {
Get.back();
Get.defaultDialog(
title: 'Wallet Phone Number'.tr,
content: Form(
key: paymentController.formKey,
child: MyTextForm(
controller: paymentController.walletphoneController,
label: 'Phone Number'.tr,
hint: provider == 'Syriatel' ? '963991234567' : '963941234567',
type: TextInputType.phone,
),
),
confirm: MyElevatedButton(
title: 'Confirm'.tr,
onPressed: () async {
if (paymentController.formKey.currentState!.validate()) {
Get.back();
box.write(BoxName.phoneWallet,
paymentController.walletphoneController.text);
if (provider == 'Syriatel') {
await payWithSyriaTelWallet(
context, pricePoint.toString(), 'SYP');
} else {
await payWithMTNWallet(context, pricePoint.toString(), 'SYP');
}
}
},
),
);
}
}
class PaymentScreen extends StatefulWidget {
final String iframeUrl;
final String countPrice;
const PaymentScreen(
{required this.iframeUrl, super.key, required this.countPrice});
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late final WebViewController _controller;
final controller = Get.find<CaptainWalletController>();
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
if (url.contains("success")) {
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
} else if (url.contains("failed")) {
showCustomDialog(
title: "Error".tr,
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
},
))
..loadRequest(Uri.parse(widget.iframeUrl));
}
Future<void> _fetchPaymentStatus() async {
final String userId = box.read(BoxName.phoneDriver);
await Future.delayed(const Duration(seconds: 2));
try {
final response = await CRUD().postWallet(
link: AppLink.paymetVerifyDriver,
payload: {
'user_id': userId,
'driverID': box.read(BoxName.driverID),
'paymentMethod': 'visa-in',
},
);
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.getCaptainWalletFromBuyPoints();
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,
);
} else {
showCustomDialog(
title: "transaction_failed".tr,
message: response['message'].toString(),
isSuccess: false,
);
}
} else {
showCustomDialog(
title: "connection_failed".tr,
message: response.toString(),
isSuccess: false,
);
}
} catch (e) {
showCustomDialog(
title: "server_error".tr,
message: "server_error_message".tr,
isSuccess: false,
);
}
}
void showCustomDialog({
required String title,
required String message,
required bool isSuccess,
}) {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
message,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
style: TextButton.styleFrom(
backgroundColor: isSuccess ? Colors.green : Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK",
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('إتمام الدفع')),
body: WebViewWidget(controller: _controller),
);
}
}
class PaymentScreenWallet extends StatefulWidget {
final String iframeUrl;
final String countPrice;
const PaymentScreenWallet(
{required this.iframeUrl, super.key, required this.countPrice});
@override
State<PaymentScreenWallet> createState() => _PaymentScreenWalletState();
}
class _PaymentScreenWalletState extends State<PaymentScreenWallet> {
late final WebViewController _controller;
final controller = Get.find<CaptainWalletController>();
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
if (url.contains("success")) {
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
} else if (url.contains("failed")) {
showCustomDialog(
title: "Error".tr,
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
isSuccess: false,
);
}
},
))
..loadRequest(Uri.parse(widget.iframeUrl));
}
Future<void> _fetchPaymentStatus() async {
final String userId = '+963${box.read(BoxName.phoneWallet)}';
await Future.delayed(const Duration(seconds: 2));
try {
final response = await CRUD().postWallet(
link: AppLink.paymetVerifyDriver,
payload: {
'user_id': userId,
'driverID': box.read(BoxName.driverID),
'paymentMethod': 'visa-in',
},
);
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.getCaptainWalletFromBuyPoints();
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,
);
} else {
showCustomDialog(
title: "transaction_failed".tr,
message: response['message'].toString(),
isSuccess: false,
);
}
} else {
showCustomDialog(
title: "connection_failed".tr,
message: response.toString(),
isSuccess: false,
);
}
} catch (e) {
showCustomDialog(
title: "server_error".tr,
message: "server_error_message".tr,
isSuccess: false,
);
}
}
void showCustomDialog({
required String title,
required String message,
required bool isSuccess,
}) {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: isSuccess ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
message,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
style: TextButton.styleFrom(
backgroundColor: isSuccess ? Colors.green : Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK",
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('إتمام الدفع')),
body: WebViewWidget(controller: _controller),
);
}
}
Future<void> payWithMTNWallet(
BuildContext context, String amount, String currency) async {
// استخدام مؤشر تحميل لتجربة مستخدم أفضل
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
String phone = box.read(BoxName.phoneWallet);
String driverID = box.read(BoxName.driverID).toString();
String formattedAmount = double.parse(amount).toStringAsFixed(0);
print("🚀 بدء عملية دفع MTN");
print(
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
// التحقق من البصمة (اختياري)
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
if (isAuthSupported) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
);
if (!didAuthenticate) {
if (Get.isDialogOpen ?? false) Get.back();
print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
return;
}
}
// 1⃣ استدعاء mtn_start_payment.php (الملف الجديد)
var responseData = await CRUD().postWalletMtn(
link: AppLink.payWithMTNStart,
payload: {
"amount": formattedAmount,
"passengerId": driverID,
"phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
},
);
print("✅ استجابة الخادم (mtn_start_payment.php):");
print(responseData);
// --- بداية التعديل المهم ---
// التحقق القوي من الاستجابة لتجنب الأخطاء
Map<String, dynamic> startRes;
if (responseData is Map<String, dynamic>) {
// إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة
startRes = responseData;
} else if (responseData is String) {
// إذا كانت نص، حاول تحليلها كـ JSON
try {
startRes = json.decode(responseData);
} catch (e) {
throw Exception(
"فشل في تحليل استجابة الخادم. الاستجابة: $responseData");
}
} else {
// نوع غير متوقع
throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
}
if (startRes['status'] != 'success') {
final errorMsg = startRes['message']['Error']?.toString().tr ??
"فشل بدء عملية الدفع. حاول مرة أخرى.";
throw Exception(errorMsg);
}
// --- نهاية التعديل المهم ---
// استخراج البيانات بأمان
final messageData = startRes["message"];
final invoiceNumber = messageData["invoiceNumber"].toString();
final operationNumber = messageData["operationNumber"].toString();
final guid = messageData["guid"].toString();
print(
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
if (Get.isDialogOpen == true)
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
// 2⃣ عرض واجهة إدخال OTP
String? otp = await showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) {
String input = "";
return AlertDialog(
title: const Text("أدخل كود التحقق"),
content: TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "كود OTP"),
onChanged: (val) => input = val,
),
actions: [
TextButton(
child: const Text("تأكيد"),
onPressed: () => Navigator.of(context).pop(input),
),
TextButton(
child: const Text("إلغاء"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
if (otp == null || otp.isEmpty) {
print("❌ لم يتم إدخال OTP");
return;
}
print("🔐 تم إدخال OTP: $otp");
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
// 3⃣ استدعاء mtn_confirm.php
var confirmRes = await CRUD().postWalletMtn(
link: AppLink.payWithMTNConfirm,
payload: {
"invoiceNumber": invoiceNumber,
"operationNumber": operationNumber,
"guid": guid,
"otp": otp,
"phone": phone,
},
);
if (Get.isDialogOpen ?? false) Get.back();
print("✅ استجابة mtn_confirm.php:");
// print(confirmRes);
Log.print('confirmRes: ${confirmRes}');
if (confirmRes != null && confirmRes['status'] == 'success') {
Get.defaultDialog(
title: "✅ نجاح",
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
);
} else {
String errorMsg =
confirmRes?['message']['message']?.toString() ?? "فشل في تأكيد الدفع";
Get.defaultDialog(
title: "❌ فشل",
content: Text(errorMsg.tr),
);
}
} catch (e, s) {
print("🔥 خطأ أثناء الدفع عبر MTN:");
print(e);
print(s);
if (Get.isDialogOpen ?? false) Get.back();
Get.defaultDialog(
title: 'حدث خطأ',
content: Text(e.toString().replaceFirst("Exception: ", "")),
);
}
}
Future<void> payWithSyriaTelWallet(
BuildContext context, String amount, String currency) async {
// Show a loading indicator for better user experience
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
String phone = box.read(BoxName.phoneWallet);
String driverID = box.read(BoxName.driverID).toString();
String formattedAmount = double.parse(amount).toStringAsFixed(0);
// --- CHANGE 1: Updated log messages for clarity ---
print("🚀 Starting Syriatel payment process");
print(
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
// Optional: Biometric authentication
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
if (isAuthSupported) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
);
if (!didAuthenticate) {
if (Get.isDialogOpen ?? false) Get.back();
print("❌ User did not authenticate with biometrics");
return;
}
}
// --- CHANGE 2: Updated API link and payload for starting payment ---
// Make sure you have defined `payWithSyriatelStart` in your AppLink class
var responseData = await CRUD().postWalletMtn(
link: AppLink.payWithSyriatelStart, // Use the new Syriatel start link
payload: {
"amount": formattedAmount,
"driverId": driverID, // Key changed from 'passengerId' to 'driverId'
"phone": phone,
"lang": box.read(BoxName.lang) ?? 'ar',
},
);
print("✅ Server response (start_payment.php):");
Log.print('responseData: ${responseData}');
// Robustly parse the server's JSON response
Map<String, dynamic> startRes;
if (responseData is Map<String, dynamic>) {
startRes = responseData;
} else if (responseData is String) {
try {
startRes = json.decode(responseData);
} catch (e) {
throw Exception(
"Failed to parse server response. Response: $responseData");
}
} else {
throw Exception("Received an unexpected data type from the server.");
}
if (startRes['status'] != 'success') {
String errorMsg = startRes['message']?.toString() ??
"Failed to start the payment process. Please try again.";
throw Exception(errorMsg);
}
// --- CHANGE 3: Extract `transactionID` from the response ---
// The response structure is now simpler. We only need the transaction ID.
final messageData = startRes["message"];
final transactionID = messageData["transactionID"].toString();
print("📄 TransactionID: $transactionID");
if (Get.isDialogOpen == true) Get.back(); // Close loading indicator
// Show the OTP input dialog
String? otp = await showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) {
String input = "";
return AlertDialog(
title: const Text("أدخل كود التحقق"),
content: TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: "كود OTP"),
onChanged: (val) => input = val,
),
actions: [
TextButton(
child: const Text("تأكيد"),
onPressed: () => Navigator.of(context).pop(input),
),
TextButton(
child: const Text("إلغاء"),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
if (otp == null || otp.isEmpty) {
print("❌ OTP was not entered.");
return;
}
print("🔐 OTP entered: $otp");
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
// --- CHANGE 4: Updated API link and payload for confirming payment ---
// Make sure you have defined `payWithSyriatelConfirm` in your AppLink class
var confirmRes = await CRUD().postWalletMtn(
// Changed from postWalletMtn if they are different
link: AppLink.payWithSyriatelConfirm, // Use the new Syriatel confirm link
payload: {
"transactionID": transactionID, // Use the transaction ID
"otp": otp,
// The other parameters (phone, guid, etc.) are no longer needed
},
);
if (Get.isDialogOpen ?? false) Get.back();
print("✅ Response from confirm_payment.php:");
Log.print('confirmRes: ${confirmRes}');
if (confirmRes != null && confirmRes['status'] == 'success') {
Get.defaultDialog(
title: "✅ نجاح",
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
);
} else {
// --- CHANGE 5: Simplified error message extraction ---
// The new PHP script sends the error directly in the 'message' field.
String errorMsg =
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
Get.defaultDialog(
title: "❌ فشل",
content: Text(errorMsg.tr),
);
}
} catch (e, s) {
// --- CHANGE 6: Updated general error log message ---
print("🔥 Error during Syriatel Wallet payment:");
print(e);
print(s);
if (Get.isDialogOpen ?? false) Get.back();
Get.defaultDialog(
title: 'حدث خطأ',
content: Text(e.toString().replaceFirst("Exception: ", "")),
);
}
}

View File

@@ -0,0 +1,145 @@
import 'package:siro_driver/constant/style.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/my_scafold.dart';
import 'package:siro_driver/views/widgets/my_textField.dart';
import 'package:siro_driver/views/widgets/mycircular.dart';
import 'package:siro_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);
}
}

View File

@@ -0,0 +1,443 @@
import 'package:local_auth/local_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_driver/constant/finance_design_system.dart';
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:siro_driver/views/widgets/mycircular.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/my_textField.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/main.dart';
import 'package:siro_driver/views/home/my_wallet/payment_history_driver_page.dart';
import 'package:siro_driver/controller/payment/driver_payment_controller.dart';
// Import new widgets
import 'points_captain.dart';
import 'transfer_budget_page.dart';
import 'widgets/balance_card.dart';
import 'widgets/quick_actions.dart';
import 'widgets/financial_summary_card.dart';
import 'widgets/promo_gamification_card.dart';
import 'widgets/transaction_preview_item.dart';
class WalletCaptainRefactored extends StatelessWidget {
WalletCaptainRefactored({super.key});
final CaptainWalletController controller = Get.put(CaptainWalletController());
@override
Widget build(BuildContext context) {
controller.refreshCaptainWallet();
return Scaffold(
backgroundColor: FinanceDesignSystem.backgroundColor,
appBar: AppBar(
title: Text('Driver Balance'.tr,
style: TextStyle(
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios_new_rounded,
color: FinanceDesignSystem.primaryDark, size: 20),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: Icon(Icons.refresh_rounded,
color: FinanceDesignSystem.primaryDark),
onPressed: () => controller.refreshCaptainWallet(),
tooltip: 'Refresh'.tr,
),
],
),
body: GetBuilder<CaptainWalletController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: FinanceDesignSystem.horizontalPadding,
vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. Header / Balance
BalanceCard(
balance: controller.totalPoints.toString(),
isNegative:
double.tryParse(controller.totalPoints.toString()) !=
null &&
double.parse(controller.totalPoints.toString()) <
-30000,
lastUpdated: "Just now".tr,
),
const SizedBox(
height: FinanceDesignSystem.verticalSectionPadding),
// 2. Quick Actions
QuickActionsGrid(
onAddBalance: () =>
_showAddBalanceOptions(context, controller),
onWithdraw: () => addSyrianPaymentMethod(controller),
onTransfer: () => Get.to(() => TransferBudgetPage()),
onHistory: () async {
await Get.put(DriverWalletHistoryController())
.getArchivePayment();
Get.to(() => const PaymentHistoryDriverPage());
},
),
const SizedBox(
height: FinanceDesignSystem.verticalSectionPadding),
// 3. Earnings Summary
FinancialSummaryCard(
title: 'Earnings Summary'.tr,
subtitle: 'ملخص الأرباح'.tr,
items: [
SummaryItem(
icon: Icons.money_rounded,
label: 'Cash Earnings'.tr,
amount: controller.totalAmount,
color: FinanceDesignSystem.successGreen,
),
SummaryItem(
icon: Icons.credit_card_rounded,
label: 'Card Earnings'.tr,
amount: controller.totalAmountVisa,
color: FinanceDesignSystem.accentBlue,
),
],
),
const SizedBox(
height: FinanceDesignSystem.verticalSectionPadding),
// 3. Recharge Balance Packages
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Recharge Balance'.tr,
style: FinanceDesignSystem.headingStyle),
Icon(Icons.info_outline_rounded,
size: 18, color: Colors.grey.shade400),
],
),
const SizedBox(height: 12),
SizedBox(
height: 140, // Increased height for modern cards
child: ListView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
children: [
PointsCaptain(
kolor: Colors.blueGrey,
pricePoint: 100,
countPoint: '100'),
PointsCaptain(
kolor: Colors.brown,
pricePoint: 200,
countPoint: '210'),
PointsCaptain(
kolor: Colors.amber,
pricePoint: 400,
countPoint: '450'),
PointsCaptain(
kolor: Colors.orange,
pricePoint: 1000,
countPoint: '1100'),
],
),
),
const SizedBox(
height: FinanceDesignSystem.verticalSectionPadding),
// 5. Promotions
Text('Promotions'.tr, style: FinanceDesignSystem.headingStyle),
const SizedBox(height: 12),
PromoGamificationCard(
title: 'Morning Promo'.tr,
subtitle: "from 7:00am to 10:00am".tr,
currentProgress: controller.walletDate['message']?[0]
?['morning_count'] ??
0,
targetProgress: 5,
reward: "+50 SYP",
onTap: () =>
controller.addDriverWalletFromPromo('Morning Promo', 50),
),
const SizedBox(height: 16),
PromoGamificationCard(
title: 'Afternoon Promo'.tr,
subtitle: "from 3:00pm to 6:00 pm".tr,
currentProgress: controller.walletDate['message']?[0]
?['afternoon_count'] ??
0,
targetProgress: 5,
reward: "+50 SYP",
onTap: () => controller.addDriverWalletFromPromo(
'Afternoon Promo', 50),
),
const SizedBox(
height: FinanceDesignSystem.verticalSectionPadding),
// 6. Transactions Preview
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Recent Transactions'.tr,
style: FinanceDesignSystem.headingStyle),
TextButton(
onPressed: () async {
await Get.put(DriverWalletHistoryController())
.getArchivePayment();
Get.to(() => const PaymentHistoryDriverPage());
},
child: Text('View All'.tr,
style: TextStyle(
color: FinanceDesignSystem.accentBlue,
fontWeight: FontWeight.bold)),
),
],
),
GetBuilder<DriverWalletHistoryController>(
init: DriverWalletHistoryController(),
builder: (historyController) {
if (historyController.archive.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Text("No transactions yet".tr,
style: TextStyle(color: Colors.grey.shade400))),
);
}
// Show only last 3
final lastThree =
historyController.archive.take(3).toList();
return Column(
children: lastThree.map((tx) {
final double amount =
double.tryParse(tx['amount']?.toString() ?? '0') ??
0;
return TransactionPreviewItem(
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
subtitle: tx['created_at'] ?? '',
amount: amount.abs().toStringAsFixed(0),
date: tx['created_at']?.split(' ')[0] ?? '',
type: amount >= 0 ? 'credit' : 'debit',
onTap: () {},
);
}).toList(),
);
},
),
const SizedBox(height: 40),
],
),
);
},
),
);
}
void _showAddBalanceOptions(
BuildContext context, CaptainWalletController controller) {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Add Balance".tr, style: FinanceDesignSystem.headingStyle),
const SizedBox(height: 8),
Text("Select how you want to charge your account".tr,
style: FinanceDesignSystem.subHeadingStyle),
const SizedBox(height: 24),
ListTile(
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color:
FinanceDesignSystem.accentBlue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12)),
child: Icon(Icons.account_balance_wallet_rounded,
color: FinanceDesignSystem.accentBlue),
),
title: Text("Pay from my budget".tr),
subtitle: Text(
"${'You have in account'.tr} ${controller.totalAmountVisa}"),
onTap: () {
Get.back();
_showPayFromBudgetDialog(controller);
},
),
const Divider(),
const SizedBox(height: 16),
Text("Recharge Balance Packages".tr,
style: FinanceDesignSystem.subHeadingStyle
.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
SizedBox(
height: 140,
child: ListView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
children: [
PointsCaptain(
kolor: Colors.blueGrey,
pricePoint: 100,
countPoint: '100'),
PointsCaptain(
kolor: Colors.brown, pricePoint: 200, countPoint: '210'),
PointsCaptain(
kolor: Colors.amber, pricePoint: 400, countPoint: '450'),
PointsCaptain(
kolor: Colors.orange,
pricePoint: 1000,
countPoint: '1100'),
],
),
),
],
),
),
);
}
void _showPayFromBudgetDialog(CaptainWalletController controller) {
Get.defaultDialog(
title: 'Pay from my budget'.tr,
content: Form(
key: controller.formKey,
child: MyTextForm(
controller: controller.amountFromBudgetController,
label: '${'You have in account'.tr} ${controller.totalAmountVisa}',
hint: '${'You have in account'.tr} ${controller.totalAmountVisa}',
type: TextInputType.number,
),
),
confirm: MyElevatedButton(
title: 'Pay'.tr,
onPressed: () async {
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
biometricOnly: true,
sensitiveTransaction: true,
);
if (didAuthenticate) {
if (double.parse(controller.amountFromBudgetController.text) <
double.parse(controller.totalAmountVisa)) {
await controller.payFromBudget();
} else {
Get.back();
mySnackeBarError('Your Budget less than needed'.tr);
}
} else {
MyDialog().getDialog(
'Authentication failed'.tr, ''.tr, () => Get.back());
}
} else {
MyDialog().getDialog(
'Biometric Authentication'.tr,
'You should use Touch ID or Face ID to confirm payment'.tr,
() => Get.back());
}
},
),
cancel: MyElevatedButton(title: 'Cancel'.tr, onPressed: () => Get.back()),
);
}
}
Future<dynamic> addSyrianPaymentMethod(
CaptainWalletController captainWalletController) {
return Get.defaultDialog(
title: "Insert Payment Details".tr,
content: Form(
key: captainWalletController.formKeyAccount,
child: Column(
children: [
Text(
"Insert your mobile wallet details to receive your money weekly"
.tr),
MyTextForm(
controller: captainWalletController.cardBank,
label: "Insert mobile wallet number".tr,
hint: '0912 345 678',
type: TextInputType.phone),
const SizedBox(height: 10),
MyDropDownSyria()
],
)),
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: () async {
if (captainWalletController.formKeyAccount.currentState!
.validate()) {
Get.back();
var res =
await CRUD().post(link: AppLink.updateAccountBank, payload: {
"paymentProvider":
Get.find<SyrianPayoutController>().dropdownValue.toString(),
"accountNumber":
captainWalletController.cardBank.text.toString(),
"id": box.read(BoxName.driverID).toString()
});
if (res != 'failure') {
mySnackbarSuccess('Payment details added successfully'.tr);
}
}
}));
}
class SyrianPayoutController extends GetxController {
String dropdownValue = 'syriatel';
void changeValue(String? newValue) {
if (newValue != null) {
dropdownValue = newValue;
update();
}
}
}
class MyDropDownSyria extends StatelessWidget {
const MyDropDownSyria({super.key});
@override
Widget build(BuildContext context) {
Get.put(SyrianPayoutController());
final theme = Theme.of(context);
return GetBuilder<SyrianPayoutController>(builder: (controller) {
return DropdownButton<String>(
value: controller.dropdownValue,
icon: const Icon(Icons.arrow_drop_down),
elevation: 16,
isExpanded: true,
dropdownColor: theme.cardColor,
style: TextStyle(color: theme.textTheme.bodyLarge?.color),
underline: Container(height: 2, color: theme.primaryColor),
onChanged: (String? newValue) => controller.changeValue(newValue),
items: <String>['syriatel', 'mtn']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value.tr));
}).toList(),
);
});
}
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:get/get.dart';
import 'package:siro_driver/constant/finance_design_system.dart';
import 'package:siro_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
import 'widgets/transaction_preview_item.dart';
class WeeklyPaymentPage extends StatelessWidget {
const WeeklyPaymentPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverWalletHistoryController());
return Scaffold(
backgroundColor: FinanceDesignSystem.backgroundColor,
appBar: AppBar(
title: Text('Weekly Summary'.tr,
style: TextStyle(
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios_new_rounded,
color: FinanceDesignSystem.primaryDark, size: 20),
onPressed: () => Get.back(),
),
),
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
return Column(
children: [
_buildWeeklyStatsHeader(controller),
const SizedBox(height: 16),
_buildWeekSelector(),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text('Transactions this week'.tr,
style: FinanceDesignSystem.headingStyle),
const Spacer(),
Icon(Icons.list_rounded,
color: Colors.grey.shade400, size: 20),
],
),
),
const SizedBox(height: 8),
Expanded(
child: controller.weeklyList.isEmpty
? _buildEmptyState(context)
: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
itemCount: controller.weeklyList.length,
itemBuilder: (BuildContext context, int index) {
final tx = controller.weeklyList[index];
final double amount = double.tryParse(
tx['amount']?.toString() ?? '0') ??
0;
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 250),
child: SlideAnimation(
verticalOffset: 25,
child: FadeInAnimation(
child: TransactionPreviewItem(
title:
amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
subtitle: tx['dateUpdated'] ?? '',
amount: amount.abs().toStringAsFixed(0),
date:
tx['dateUpdated']?.split(' ')[0] ?? '',
type: amount >= 0 ? 'credit' : 'debit',
method: tx['paymentMethod'],
onTap: () {},
),
),
),
);
},
),
),
),
],
);
},
),
);
}
Widget _buildWeeklyStatsHeader(DriverWalletHistoryController controller) {
final totalAmount = controller.weeklyList.isEmpty
? '0.00'
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
final double earnings = double.tryParse(totalAmount) ?? 0;
final int trips = controller.weeklyList.length;
final double commission = earnings * 0.15; // Example 15%
final double netProfit = earnings - commission;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: FinanceDesignSystem.balanceGradient,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: FinanceDesignSystem.primaryDark.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Text('Total Weekly Earnings'.tr,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8), fontSize: 14)),
const SizedBox(height: 8),
Text('${earnings.toStringAsFixed(0)} SYP',
style: const TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold)),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem('Total Trips'.tr, trips.toString(),
Icons.directions_car_rounded),
_buildStatItem('Commission'.tr, commission.toStringAsFixed(0),
Icons.percent_rounded),
_buildStatItem('Net Profit'.tr, netProfit.toStringAsFixed(0),
Icons.account_balance_rounded),
],
),
],
),
);
}
Widget _buildStatItem(String label, String value, IconData icon) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10)),
child: Icon(icon, color: Colors.white, size: 18),
),
const SizedBox(height: 8),
Text(value,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14)),
Text(label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6), fontSize: 10)),
],
);
}
Widget _buildWeekSelector() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02), blurRadius: 10)
],
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left_rounded), onPressed: () {}),
Expanded(
child: Center(
child: Text('Dec 15 - Dec 21, 2024'.tr,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: FinanceDesignSystem.primaryDark)),
),
),
IconButton(
icon: const Icon(Icons.chevron_right_rounded),
onPressed: () {}),
],
),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bar_chart_rounded, size: 64, color: Colors.grey.shade300),
const SizedBox(height: 16),
Text('No transactions this week'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.grey)),
],
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/finance_design_system.dart';
class BalanceCard extends StatelessWidget {
final String balance;
final bool isNegative;
final String lastUpdated;
const BalanceCard({
super.key,
required this.balance,
required this.isNegative,
this.lastUpdated = "Just now",
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
gradient: isNegative
? FinanceDesignSystem.dangerGradient
: FinanceDesignSystem.balanceGradient,
boxShadow: [
BoxShadow(
color: (isNegative
? FinanceDesignSystem.dangerRed
: FinanceDesignSystem.primaryDark)
.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Available Balance".tr,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
"الرصيد المتاح".tr,
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12,
),
),
],
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
isNegative
? Icons.warning_rounded
: Icons.account_balance_wallet_rounded,
color: Colors.white,
size: 20,
),
),
],
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
balance,
style: FinanceDesignSystem.balanceStyle,
),
const SizedBox(width: 8),
Text(
"SYP".tr,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 16),
Divider(color: Colors.white.withOpacity(0.15)),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.history_rounded,
color: Colors.white.withOpacity(0.5),
size: 14,
),
const SizedBox(width: 6),
Text(
"${'Last updated:'.tr} $lastUpdated",
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12,
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/finance_design_system.dart';
class FinancialSummaryCard extends StatelessWidget {
final String title;
final String? subtitle;
final List<SummaryItem> items;
const FinancialSummaryCard({
super.key,
required this.title,
this.subtitle,
required this.items,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: FinanceDesignSystem.headingStyle),
if (subtitle != null)
Text(subtitle!, style: FinanceDesignSystem.subHeadingStyle),
],
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
separatorBuilder: (context, index) =>
Divider(color: Colors.grey.withValues(alpha: 0.1), height: 24),
itemBuilder: (context, index) {
final item = items[index];
return Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(item.icon, color: item.color, size: 20),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: FinanceDesignSystem.primaryDark,
),
),
if (item.subLabel != null)
Text(
item.subLabel!,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${item.amount} ${'SYP'.tr}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark,
),
),
if (item.trend != null)
Text(
item.trend!,
style: TextStyle(
fontSize: 11,
color: item.trendColor ??
FinanceDesignSystem.successGreen,
fontWeight: FontWeight.w500,
),
),
],
),
],
);
},
),
),
],
);
}
}
class SummaryItem {
final IconData icon;
final String label;
final String? subLabel;
final String amount;
final Color color;
final String? trend;
final Color? trendColor;
SummaryItem({
required this.icon,
required this.label,
this.subLabel,
required this.amount,
required this.color,
this.trend,
this.trendColor,
});
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/finance_design_system.dart';
class PromoGamificationCard extends StatelessWidget {
final String title;
final String subtitle;
final int currentProgress;
final int targetProgress;
final String reward;
final VoidCallback? onTap;
const PromoGamificationCard({
super.key,
required this.title,
required this.subtitle,
required this.currentProgress,
required this.targetProgress,
required this.reward,
this.onTap,
});
@override
Widget build(BuildContext context) {
final double progress = (currentProgress / targetProgress).clamp(0.0, 1.0);
final bool isCompleted = progress >= 1.0;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: (isCompleted
? FinanceDesignSystem.successGreen
: FinanceDesignSystem.accentBlue)
.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
reward,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isCompleted
? FinanceDesignSystem.successGreen
: FinanceDesignSystem.accentBlue,
),
),
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${'Progress:'.tr} $currentProgress / $targetProgress",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
if (isCompleted)
Icon(Icons.check_circle_rounded,
color: FinanceDesignSystem.successGreen, size: 16),
],
),
const SizedBox(height: 8),
Stack(
children: [
Container(
height: 10,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(5),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: 10,
width: MediaQuery.of(context).size.width *
progress, // Simplified for now
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isCompleted
? [
FinanceDesignSystem.successGreen,
Colors.lightGreenAccent
]
: [FinanceDesignSystem.accentBlue, Colors.blueAccent],
),
borderRadius: BorderRadius.circular(5),
),
),
],
),
if (isCompleted && onTap != null) ...[
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: FinanceDesignSystem.successGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
),
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text("Claim Reward".tr),
),
),
],
],
),
);
}
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/finance_design_system.dart';
class QuickActionsGrid extends StatelessWidget {
final VoidCallback onAddBalance;
final VoidCallback onWithdraw;
final VoidCallback onTransfer;
final VoidCallback onHistory;
const QuickActionsGrid({
super.key,
required this.onAddBalance,
required this.onWithdraw,
required this.onTransfer,
required this.onHistory,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
children: [
_ActionItem(
icon: Icons.add_circle_outline_rounded,
label: "Add Balance".tr,
subLabel: "شحن",
color: FinanceDesignSystem.accentBlue,
onTap: onAddBalance,
),
_ActionItem(
icon: Icons.file_download_outlined,
label: "Withdraw".tr,
subLabel: "سحب",
color: FinanceDesignSystem.successGreen,
onTap: onWithdraw,
),
_ActionItem(
icon: Icons.swap_horiz_rounded,
label: "Transfer".tr,
subLabel: "تحويل",
color: Colors.orange,
onTap: onTransfer,
),
_ActionItem(
icon: Icons.history_rounded,
label: "History".tr,
subLabel: "السجل",
color: FinanceDesignSystem.primaryDark,
onTap: onHistory,
),
],
);
}
}
class _ActionItem extends StatelessWidget {
final IconData icon;
final String label;
final String subLabel;
final Color color;
final VoidCallback onTap;
const _ActionItem({
required this.icon,
required this.label,
required this.subLabel,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.1)),
borderRadius:
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark,
),
textAlign: TextAlign.center,
),
Text(
subLabel,
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/finance_design_system.dart';
class TransactionPreviewItem extends StatelessWidget {
final String title;
final String subtitle;
final String amount;
final String date;
final String type; // 'credit' or 'debit'
final String? method;
final VoidCallback? onTap;
const TransactionPreviewItem({
super.key,
required this.title,
required this.subtitle,
required this.amount,
required this.date,
required this.type,
this.method,
this.onTap,
});
@override
Widget build(BuildContext context) {
final bool isCredit = type.toLowerCase() == 'credit';
final Color statusColor = isCredit
? FinanceDesignSystem.successGreen
: FinanceDesignSystem.dangerRed;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
isCredit
? Icons.arrow_downward_rounded
: Icons.arrow_upward_rounded,
color: statusColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
date,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
if (method != null) ...[
const SizedBox(width: 8),
Container(
width: 3,
height: 3,
decoration: BoxDecoration(
color: Colors.grey.shade400,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
method!,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${isCredit ? '+' : '-'}$amount ${'SYP'.tr}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade400,
),
),
],
),
],
),
),
),
);
}
}