Files
tripz-llc/apps/driver/lib/views/home/my_wallet/walet_captain.dart
T
HamzaandClaude Opus 4.8 6234e66471 feat: Gemini AI (doc OCR + face match) + role-assignment endpoints
- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY
- documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill
- roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret)
- config: gemini + platform.secret; .env.example entries

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:01:26 +03:00

729 lines
25 KiB
Dart
Executable File

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(),
),
),
);
});
}
}