diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 4111900..243e3a1 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/lib/binding/initial_binding.dart b/lib/binding/initial_binding.dart new file mode 100644 index 0000000..551c451 --- /dev/null +++ b/lib/binding/initial_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controller/admin/dashboard_controller.dart'; + +class InitialBinding extends Bindings { + @override + void dependencies() { + Get.put(DashboardController(), permanent: true); + } +} diff --git a/lib/constant/colors.dart b/lib/constant/colors.dart index efde492..156772a 100644 --- a/lib/constant/colors.dart +++ b/lib/constant/colors.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter/material.dart'; - class AppColor { // --- Core Design Tokens --- @@ -42,5 +40,5 @@ class AppColor { static const Color greenColor = success; static const Color blueColor = info; static const Color yellowColor = warning; + static const Color deepPurpleAccent = accent; // Map to accent } - diff --git a/lib/constant/links.dart b/lib/constant/links.dart index ba7469f..93a4c2c 100644 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -119,6 +119,7 @@ class AppLink { /////////---getKazanPercent===//////////// static String getKazanPercent = "$ride/kazan/get.php"; static String addKazanPercent = "$ride/kazan/add.php"; + static String updateKazanPercent = "$ride/kazan/update.php"; ////-----------------DriverPayment------------------ static String addDriverpayment = "$tripzPaymentServer/payment/add.php"; diff --git a/lib/controller/admin/complaint_controller.dart b/lib/controller/admin/complaint_controller.dart new file mode 100644 index 0000000..52634fe --- /dev/null +++ b/lib/controller/admin/complaint_controller.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import '../../constant/links.dart'; +import '../functions/crud.dart'; + +class ComplaintController extends GetxController { + var complaintList = [].obs; + var isLoading = false.obs; + final CRUD _crud = CRUD(); + + @override + void onInit() { + super.onInit(); + getComplaints(); + } + + Future getComplaints() async { + isLoading.value = true; + try { + var response = await _crud.get(link: AppLink.getComplaintAllData); + if (response != null && response != 'failure' && response != 'token_expired') { + var decoded = response is String ? jsonDecode(response) : response; + if (decoded['status'] == "success") { + complaintList.assignAll(decoded['message']); + } + } else { + complaintList.clear(); + } + } catch (e) { + Get.snackbar("خطأ", "فشل جلب الشكاوى: $e"); + } finally { + isLoading.value = false; + } + } + + Future updateComplaintStatus(String id, String status, String resolution) async { + isLoading.value = true; + try { + var response = await _crud.post(link: "${AppLink.server}/serviceapp/update_complaint.php", payload: { + "id": id, + "statusComplaint": status, + "resolution": resolution, + }); + if (response != null && response is Map && response['status'] == "success") { + await getComplaints(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل تحديث الشكوى: $e"); + return false; + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controller/admin/dashboard_controller.dart b/lib/controller/admin/dashboard_controller.dart index c18375d..3f9e642 100644 --- a/lib/controller/admin/dashboard_controller.dart +++ b/lib/controller/admin/dashboard_controller.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sefer_admin1/constant/links.dart'; import 'package:sefer_admin1/controller/functions/crud.dart'; +import 'package:sefer_admin1/controller/auth/otp_helper.dart'; import '../../constant/api_key.dart'; import '../../constant/box_name.dart'; @@ -25,6 +26,19 @@ class DashboardController extends GetxController { var res = await CRUD().get(link: AppLink.getdashbord, payload: {}); print('📡 Main dashboard response: $res'); + if (res == 'token_expired') { + print('❌ Admin token expired. Attempting seamless auto-login.'); + box.remove(BoxName.jwt); + try { + final otpHelper = Get.put(OtpHelper()); + await otpHelper.checkAdminLogin(); + } catch (e) { + Get.offAllNamed('/login'); + } + return; + } + + if (res != 'failure' && res != null) { try { var d = res is String ? jsonDecode(res) : res; diff --git a/lib/controller/admin/driver_docs_controller.dart b/lib/controller/admin/driver_docs_controller.dart new file mode 100644 index 0000000..ef88f70 --- /dev/null +++ b/lib/controller/admin/driver_docs_controller.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import '../../constant/links.dart'; +import '../functions/crud.dart'; + +class DriverDocsController extends GetxController { + var pendingDrivers = [].obs; + var isLoading = false.obs; + var isMoreLoading = false.obs; + var hasMore = true.obs; + int _offset = 0; + final int _limit = 10; + final CRUD _crud = CRUD(); + + @override + void onInit() { + super.onInit(); + getPendingDrivers(); + } + + Future getPendingDrivers({bool refresh = true}) async { + if (refresh) { + isLoading.value = true; + _offset = 0; + hasMore.value = true; + } else { + if (isMoreLoading.value || !hasMore.value) return; + isMoreLoading.value = true; + } + + try { + var response = await _crud.post( + link: AppLink.getDriversPending, + payload: {"limit": _limit.toString(), "offset": _offset.toString()}, + ); + if (response != null && response != 'failure' && response != 'token_expired') { + var decoded = response is String ? jsonDecode(response) : response; + if (decoded['status'] == "success") { + List newItems = decoded['message'] ?? []; + if (refresh) { + pendingDrivers.assignAll(newItems); + } else { + pendingDrivers.addAll(newItems); + } + _offset += newItems.length; + if (newItems.length < _limit) { + hasMore.value = false; + } + } + } + } catch (e) { + Get.snackbar("خطأ", "فشل جلب السائقين: $e"); + } finally { + isLoading.value = false; + isMoreLoading.value = false; + } + } + + Future loadMore() async { + await getPendingDrivers(refresh: false); + } + + Future?> getDriverFullDetails(String id) async { + try { + var response = await _crud.get(link: "${AppLink.getDriverDetails}?id=$id"); + if (response != null && response != 'failure' && response != 'token_expired') { + var decoded = response is String ? jsonDecode(response) : response; + if (decoded['status'] == "success") { + return decoded['data']; + } + } + } catch (e) { + Get.snackbar("خطأ", "فشل جلب تفاصيل السائق: $e"); + } + return null; + } + + Future approveDriver(String id) async { + isLoading.value = true; + try { + var response = await _crud.post(link: AppLink.updateDriverFromAdmin, payload: { + "id": id, + "status": "active", + }); + if (response != null && response is Map && response['status'] == "success") { + await getPendingDrivers(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل اعتماد السائق: $e"); + return false; + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controller/admin/kazan_controller.dart b/lib/controller/admin/kazan_controller.dart new file mode 100644 index 0000000..9ab1ac8 --- /dev/null +++ b/lib/controller/admin/kazan_controller.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import '../../constant/links.dart'; +import '../functions/crud.dart'; + +class KazanController extends GetxController { + var kazanData = {}.obs; + var isLoading = false.obs; + final CRUD _crud = CRUD(); + + @override + void onInit() { + super.onInit(); + getKazan(); + } + + Future getKazan() async { + isLoading.value = true; + try { + var response = await _crud.get(link: "${AppLink.getKazanPercent}?country=syria"); + if (response != null && response != 'failure' && response != 'token_expired') { + var decoded = response is String ? jsonDecode(response) : response; + if (decoded['status'] == "success") { + var message = decoded['message']; + if (message is List && message.isNotEmpty) { + kazanData.value = message[0]; + } + } + } + } catch (e) { + Get.snackbar("خطأ", "فشل جلب بيانات التسعير: $e"); + } finally { + isLoading.value = false; + } + } + + Future updateKazan(Map data) async { + isLoading.value = true; + try { + final String link = data.containsKey('id') ? AppLink.updateKazanPercent : AppLink.addKazanPercent; + + Map payload = {}; + data.forEach((key, value) { + payload[key] = value.toString(); + }); + + var response = await _crud.post(link: link, payload: payload); + if (response != null && response is Map && response['status'] == "success") { + await getKazan(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل تحديث التسعير: $e"); + return false; + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controller/admin/promo_controller.dart b/lib/controller/admin/promo_controller.dart new file mode 100644 index 0000000..c3f4d80 --- /dev/null +++ b/lib/controller/admin/promo_controller.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; +import 'package:get/get.dart'; +import '../../constant/links.dart'; +import '../functions/crud.dart'; + +class PromoController extends GetxController { + var promoList = [].obs; + var isLoading = false.obs; + final CRUD _crud = CRUD(); + + @override + void onInit() { + super.onInit(); + getPromos(); + } + + Future getPromos() async { + isLoading.value = true; + try { + var response = await _crud.get(link: AppLink.getPassengersPromo); + if (response != null && response != 'failure' && response != 'token_expired') { + var decoded = response is String ? jsonDecode(response) : response; + if (decoded['status'] == "success") { + promoList.assignAll(decoded['message']); + } + } else { + promoList.clear(); + } + } catch (e) { + Get.snackbar("خطأ", "فشل جلب أكواد الخصم: $e"); + } finally { + isLoading.value = false; + } + } + + Future addPromo(Map data) async { + isLoading.value = true; + try { + var response = await _crud.post(link: AppLink.addPassengersPromo, payload: data); + if (response != null && response is Map && response['status'] == "success") { + await getPromos(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل إضافة كود الخصم: $e"); + return false; + } finally { + isLoading.value = false; + } + } + + Future deletePromo(String id) async { + try { + var response = await _crud.post(link: AppLink.deletePassengersPromo, payload: {"id": id}); + if (response != null && response is Map && response['status'] == "success") { + await getPromos(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل حذف كود الخصم: $e"); + return false; + } + } + + Future updatePromo(Map data) async { + isLoading.value = true; + try { + var response = await _crud.post(link: AppLink.updatePassengersPromo, payload: data); + if (response != null && response is Map && response['status'] == "success") { + await getPromos(); + return true; + } + return false; + } catch (e) { + Get.snackbar("خطأ", "فشل تحديث كود الخصم: $e"); + return false; + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/controller/auth/otp_helper.dart b/lib/controller/auth/otp_helper.dart index f81478d..7e7ae85 100644 --- a/lib/controller/auth/otp_helper.dart +++ b/lib/controller/auth/otp_helper.dart @@ -11,6 +11,7 @@ import '../../main.dart'; import '../../print.dart'; import '../../views/admin/admin_home_page.dart'; import '../../views/widgets/snackbar.dart'; +import '../admin/dashboard_controller.dart'; import '../functions/crud.dart'; import '../functions/encrypt_decrypt.dart'; @@ -92,7 +93,8 @@ class OtpHelper extends GetxController { if (response != 'failure') { // إذا كان الرد يتطلب OTP (السيرفر يرجعها بداخل message) final msg = response['message']; - if (response['status'] == 'otp_required' || (msg is Map && msg['status'] == 'otp_required')) { + if (response['status'] == 'otp_required' || + (msg is Map && msg['status'] == 'otp_required')) { String phone = (msg is Map ? msg['phone'] : response['phone']) ?? ''; _showOtpDialog(phone, password, fingerprint); return false; // ننتظر إكمال الـ OTP @@ -127,6 +129,11 @@ class OtpHelper extends GetxController { if (response != 'failure') { bool success = await _handleLoginSuccess(response, password); if (success) { + try { + if (Get.isRegistered()) { + Get.find().getDashBoard(); + } + } catch (e) {} Get.offAll(() => const AdminHomePage()); } } @@ -152,7 +159,8 @@ class OtpHelper extends GetxController { await box.write('admin_role', role); Log.print('Admin role saved: $role'); } - if (data['phone'] != null) await box.write(BoxName.adminPhone, data['phone']); + if (data['phone'] != null) + await box.write(BoxName.adminPhone, data['phone']); } await box.write(BoxName.phoneVerified, true); @@ -234,7 +242,8 @@ class OtpHelper extends GetxController { if (response != 'failure') { final msg = response['message']; - if (response['status'] == 'otp_required' || (msg is Map && msg['status'] == 'otp_required')) { + if (response['status'] == 'otp_required' || + (msg is Map && msg['status'] == 'otp_required')) { String phone = (msg is Map ? msg['phone'] : response['phone']) ?? ''; _showOtpDialog(phone, password, fingerprint); return; // ننتظر إدخال رمز التحقق @@ -248,6 +257,11 @@ class OtpHelper extends GetxController { } else if (response['jwt'] != null) { box.write(BoxName.jwt, c(response['jwt'])); } + try { + if (Get.isRegistered()) { + Get.find().getDashBoard(); + } + } catch (e) {} Get.offAll(() => const AdminHomePage()); } else { Log.print('Auto-login failed, redirecting to login page'); diff --git a/lib/main.dart b/lib/main.dart index 503206c..d52d865 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,13 +7,16 @@ import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:sefer_admin1/views/auth/login_page.dart'; -import 'package:sefer_admin1/views/auth/register_page.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'constant/box_name.dart'; import 'controller/firebase/firbase_messge.dart'; import 'controller/functions/encrypt_decrypt.dart'; import 'firebase_options.dart'; import 'models/db_sql.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'constant/colors.dart'; +import 'routes.dart'; +import 'binding/initial_binding.dart'; final box = GetStorage(); const storage = FlutterSecureStorage(); @@ -63,12 +66,41 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { return GetMaterialApp( debugShowCheckedModeBanner: false, - initialRoute: '/login', - getPages: [ - GetPage(name: '/login', page: () => const AdminLoginPage()), - GetPage(name: '/register', page: () => const RegisterPage()), - ], - home: const AdminLoginPage(), + title: 'Intaleq Admin', + locale: const Locale('ar'), + fallbackLocale: const Locale('en'), + themeMode: ThemeMode.dark, + darkTheme: ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: AppColor.bg, + primaryColor: AppColor.accent, + colorScheme: const ColorScheme.dark( + primary: AppColor.accent, + secondary: AppColor.accent, + surface: AppColor.surface, + background: AppColor.bg, + error: AppColor.danger, + ), + dividerColor: AppColor.divider, + textTheme: + GoogleFonts.cairoTextTheme(ThemeData.dark().textTheme).copyWith( + bodyLarge: GoogleFonts.inter(color: AppColor.textPrimary), + bodyMedium: GoogleFonts.inter(color: AppColor.textPrimary), + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColor.bg, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: AppColor.textPrimary, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + initialBinding: InitialBinding(), + initialRoute: box.read(BoxName.phoneVerified) == true ? "/" : "/login", + getPages: routes, ); } } diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 0000000..f4bf922 --- /dev/null +++ b/lib/routes.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; +import 'views/admin/admin_home_page.dart'; +import 'views/auth/login_page.dart'; +import 'views/auth/register_page.dart'; +import 'views/admin/promo/promo_management_page.dart'; +import 'views/admin/pricing/kazan_editor_page.dart'; +import 'views/admin/complaints/complaint_list_page.dart'; + +import 'package:get/get.dart'; +import 'views/admin/admin_home_page.dart'; +import 'views/auth/login_page.dart'; +import 'views/auth/register_page.dart'; +import 'views/admin/promo/promo_management_page.dart'; +import 'views/admin/pricing/kazan_editor_page.dart'; +import 'views/admin/complaints/complaint_list_page.dart'; +import 'views/admin/drivers/driver_documents_review_page.dart'; + +List> routes = [ + GetPage(name: "/", page: () => const AdminHomePage()), + GetPage(name: "/login", page: () => const AdminLoginPage()), + GetPage(name: "/register", page: () => const RegisterPage()), + GetPage(name: "/promo", page: () => PromoManagementPage()), + GetPage(name: "/kazan", page: () => KazanEditorPage()), + GetPage(name: "/complaints", page: () => ComplaintListPage()), + GetPage(name: "/driver-docs", page: () => DriverDocsReviewPage()), +]; + diff --git a/lib/views/admin/admin_home_page.dart b/lib/views/admin/admin_home_page.dart index 6a98fad..ab2db00 100644 --- a/lib/views/admin/admin_home_page.dart +++ b/lib/views/admin/admin_home_page.dart @@ -8,6 +8,8 @@ import 'package:sefer_admin1/views/admin/drivers/driver_gift_check_page.dart'; import 'package:sefer_admin1/views/admin/drivers/driver_tracker_screen.dart'; import '../../constant/box_name.dart'; +import '../../constant/colors.dart'; +import '../../constant/style.dart'; import '../../controller/admin/dashboard_controller.dart'; import '../../controller/admin/static_controller.dart'; import '../../controller/functions/crud.dart'; @@ -50,19 +52,20 @@ class _AdminHomePageState extends State String _searchQuery = ''; // ══════════════════ DESIGN TOKENS ══════════════════ - static const Color _bg = Color(0xFF0D1117); - static const Color _surface = Color(0xFF161B22); - static const Color _surfaceElevated = Color(0xFF1C2333); - static const Color _accent = Color(0xFF00D4AA); // Emerald-teal - static const Color _accentSoft = Color(0xFF00D4AA20); - static const Color _accentBorder = Color(0xFF00D4AA40); - static const Color _danger = Color(0xFFFF5370); - static const Color _warning = Color(0xFFFFCB6B); - static const Color _info = Color(0xFF82AAFF); - static const Color _success = Color(0xFFC3E88D); - static const Color _textPrimary = Color(0xFFE6EDF3); - static const Color _textSecondary = Color(0xFF7D8590); - static const Color _divider = Color(0xFF21262D); + // --- Unified with AppColor --- + static const Color _bg = AppColor.bg; + static const Color _surface = AppColor.surface; + static const Color _surfaceElevated = AppColor.surfaceElevated; + static const Color _accent = AppColor.accent; + static const Color _accentSoft = AppColor.accentSoft; + static const Color _accentBorder = AppColor.accentBorder; + static const Color _danger = AppColor.danger; + static const Color _warning = AppColor.warning; + static const Color _info = AppColor.info; + static const Color _success = AppColor.success; + static const Color _textPrimary = AppColor.textPrimary; + static const Color _textSecondary = AppColor.textSecondary; + static const Color _divider = AppColor.divider; @override void initState() { @@ -740,6 +743,19 @@ class _AdminHomePageState extends State () => Get.to(() => IntaleqTrackerScreen())), ], ), + ActionCategory( + title: 'إدارة النظام الجديد', + items: [ + ActionItem('أكواد الخصم', Icons.confirmation_number_rounded, _accent, + () => Get.toNamed('/promo')), + ActionItem('تعديل الأسعار', Icons.settings_suggest_rounded, _warning, + () => Get.toNamed('/kazan')), + ActionItem('الشكاوى', Icons.report_problem_rounded, _danger, + () => Get.toNamed('/complaints')), + ActionItem('مراجعة الوثائق', Icons.assignment_ind_rounded, _info, + () => Get.toNamed('/driver-docs')), + ], + ), ActionCategory( title: 'العمليات', items: [ @@ -912,10 +928,10 @@ class _AdminHomePageState extends State Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF4CAF50).withOpacity(0.12), + color: const Color(0xFF4CAF50).withAlpha(30), // ~0.12 opacity shape: BoxShape.circle, border: Border.all( - color: const Color(0xFF4CAF50).withOpacity(0.25)), + color: const Color(0xFF4CAF50).withAlpha(64)), // ~0.25 opacity ), child: const Icon(Icons.message_rounded, color: Color(0xFF4CAF50), size: 28), diff --git a/lib/views/admin/complaints/complaint_list_page.dart b/lib/views/admin/complaints/complaint_list_page.dart new file mode 100644 index 0000000..4e5e5dd --- /dev/null +++ b/lib/views/admin/complaints/complaint_list_page.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../controller/admin/complaint_controller.dart'; +import '../../widgets/my_scafold.dart'; +import '../../widgets/elevated_btn.dart'; +import '../../widgets/my_textField.dart'; + +class ComplaintListPage extends StatelessWidget { + ComplaintListPage({super.key}); + + final ComplaintController controller = Get.put(ComplaintController()); + + @override + Widget build(BuildContext context) { + return MyScafolld( + title: 'إدارة الشكاوى'.tr, + isleading: true, + body: [ + Obx(() => controller.isLoading.value && controller.complaintList.isEmpty + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: () => controller.getComplaints(), + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), + itemCount: controller.complaintList.length, + itemBuilder: (context, index) { + final complaint = controller.complaintList[index]; + return _buildComplaintCard(context, complaint); + }, + ), + )), + ], + ); + } + + Widget _buildComplaintCard(BuildContext context, dynamic c) { + Color statusColor = _getStatusColor(c['statusComplaint']); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: AppStyle.cardDecoration, + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: _buildStatusIndicator(c['statusComplaint'], statusColor), + title: Text( + c['complaint_type']?.toString() ?? 'شكوى عامة', + style: AppStyle.title, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + 'الرحلة: ${c['ride_id']}', + style: AppStyle.caption, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.person_rounded, size: 12, color: AppColor.textSecondary), + const SizedBox(width: 4), + Text(c['passengerName'] ?? 'غير معروف', style: AppStyle.caption), + const SizedBox(width: 12), + Icon(Icons.drive_eta_rounded, size: 12, color: AppColor.textSecondary), + const SizedBox(width: 4), + Text(c['driverName'] ?? 'غير معروف', style: AppStyle.caption), + ], + ), + ], + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(color: AppColor.divider), + _buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف'), + const SizedBox(height: 12), + _buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد'), + const SizedBox(height: 12), + _buildRideDetails(c), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: MyElevatedButton( + title: 'تحديث الحالة / حل الشكوى', + onPressed: () => _showResolveDialog(context, c), + kolor: AppColor.accent, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStatusIndicator(String? status, Color color) { + return Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withOpacity(0.12), + shape: BoxShape.circle, + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Icon( + status == 'Resolved' ? Icons.check_circle_rounded : Icons.pending_rounded, + color: color, + size: 20, + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppStyle.caption.copyWith(color: AppColor.accent)), + const SizedBox(height: 4), + Text(value, style: AppStyle.body), + ], + ); + } + + Widget _buildRideDetails(dynamic c) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColor.divider), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSmallStat('السعر', '${c['priceOfRide']} ل.س'), + _buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}★'), + _buildSmallStat('النوع', c['ascarType'] ?? 'N/A'), + ], + ), + ], + ), + ); + } + + Widget _buildSmallStat(String label, String value) { + return Column( + children: [ + Text(label, style: AppStyle.caption.copyWith(fontSize: 10)), + const SizedBox(height: 2), + Text(value, style: AppStyle.number.copyWith(fontSize: 12)), + ], + ); + } + + Color _getStatusColor(String? status) { + switch (status) { + case 'Open': return AppColor.danger; + case 'In Progress': return AppColor.warning; + case 'Resolved': return AppColor.success; + default: return AppColor.textSecondary; + } + } + + void _showResolveDialog(BuildContext context, dynamic c) { + final TextEditingController resController = TextEditingController(text: c['resolution']); + String selectedStatus = c['statusComplaint'] ?? 'Open'; + + Get.bottomSheet( + StatefulBuilder( + builder: (context, setModalState) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('تحديث حالة الشكوى', style: AppStyle.headTitle), + const SizedBox(height: 20), + _buildStatusDropdown(selectedStatus, (val) { + setModalState(() => selectedStatus = val!); + }), + const SizedBox(height: 20), + MyTextForm( + controller: resController, + label: 'قرار الحل / الملاحظات', + hint: 'اكتب تفاصيل الحل هنا...', + type: TextInputType.multiline, + prefixIcon: Icons.gavel_rounded, + ), + const SizedBox(height: 24), + MyElevatedButton( + title: 'حفظ التحديث', + onPressed: () async { + bool success = await controller.updateComplaintStatus( + c['id'].toString(), + selectedStatus, + resController.text + ); + if (success) Get.back(); + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + isScrollControlled: true, + ); + } + + Widget _buildStatusDropdown(String current, Function(String?) onChanged) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: AppColor.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColor.divider), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: current, + isExpanded: true, + dropdownColor: AppColor.surfaceElevated, + items: ['Open', 'In Progress', 'Resolved'] + .map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: AppStyle.body))) + .toList(), + onChanged: onChanged, + ), + ), + ); + } +} diff --git a/lib/views/admin/drivers/driver_documents_review_page.dart b/lib/views/admin/drivers/driver_documents_review_page.dart new file mode 100644 index 0000000..d81c0a3 --- /dev/null +++ b/lib/views/admin/drivers/driver_documents_review_page.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../controller/admin/driver_docs_controller.dart'; +import '../../widgets/my_scafold.dart'; +import '../../widgets/elevated_btn.dart'; +import '../../../constant/links.dart'; + +class DriverDocsReviewPage extends StatelessWidget { + DriverDocsReviewPage({super.key}); + + final DriverDocsController controller = Get.put(DriverDocsController()); + + @override + Widget build(BuildContext context) { + return MyScafolld( + title: 'مراجعة طلبات التسجيل'.tr, + isleading: true, + body: [ + Obx(() => controller.isLoading.value && controller.pendingDrivers.isEmpty + ? const Center(child: CircularProgressIndicator()) + : controller.pendingDrivers.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.how_to_reg_rounded, size: 64, color: AppColor.textMuted), + const SizedBox(height: 16), + Text('لا يوجد طلبات تسجيل حالياً', style: AppStyle.subtitle), + ], + ), + ) + : RefreshIndicator( + onRefresh: () => controller.getPendingDrivers(), + child: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (!controller.isLoading.value && + !controller.isMoreLoading.value && + scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 200) { + controller.loadMore(); + } + return false; + }, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: controller.pendingDrivers.length + (controller.hasMore.value ? 1 : 0), + itemBuilder: (context, index) { + if (index == controller.pendingDrivers.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + final driver = controller.pendingDrivers[index]; + return _buildDriverCard(context, driver); + }, + ), + ), + )), + ], + ); + } + + Widget _buildDriverCard(BuildContext context, dynamic driver) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: AppStyle.cardDecoration, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: CircleAvatar( + backgroundColor: AppColor.accentSoft, + child: Text(driver['first_name']?[0] ?? 'D', style: const TextStyle(color: AppColor.accent)), + ), + title: Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.title), + subtitle: Text(driver['phone'] ?? '', style: AppStyle.caption), + trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: AppColor.textSecondary), + onTap: () => _showDriverDetails(context, driver['id'].toString()), + ), + ); + } + + void _showDriverDetails(BuildContext context, String id) async { + final details = await controller.getDriverFullDetails(id); + if (details == null) return; + + final driver = details['driver']; + final List docs = details['documents']; + + Get.to(() => MyScafolld( + title: 'تفاصيل السائق', + isleading: true, + body: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDriverHeader(driver), + const SizedBox(height: 24), + Text('الوثائق المرفوعة', style: AppStyle.title), + const SizedBox(height: 12), + ...docs.map((doc) => _buildDocCard(doc)).toList(), + const SizedBox(height: 32), + MyElevatedButton( + title: 'اعتماد وتفعيل الحساب', + icon: Icons.check_circle_rounded, + kolor: AppColor.success, + onPressed: () async { + bool success = await controller.approveDriver(id); + if (success) { + Get.back(); + Get.snackbar('نجاح', 'تم تفعيل حساب السائق بنجاح'); + } + }, + ), + const SizedBox(height: 100), + ], + ), + ), + ], + )); + } + + Widget _buildDriverHeader(dynamic driver) { + return Container( + padding: const EdgeInsets.all(20), + decoration: AppStyle.elevatedCard, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.headTitle), + Text(driver['phone'] ?? '', style: AppStyle.subtitle), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.warning.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text('Pending', style: AppStyle.caption.copyWith(color: AppColor.warning)), + ), + ], + ), + const Divider(height: 32, color: AppColor.divider), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A'), + _buildSmallInfo('الجنس', driver['gender'] ?? 'N/A'), + _buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A'), + ], + ), + ], + ), + ); + } + + Widget _buildSmallInfo(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppStyle.caption.copyWith(fontSize: 10)), + const SizedBox(height: 2), + Text(value, style: AppStyle.body.copyWith(fontSize: 12, fontWeight: FontWeight.bold)), + ], + ); + } + + Widget _buildDocCard(dynamic doc) { + String imageUrl = doc['link'] ?? ''; + // Ensure URL is absolute + if (!imageUrl.startsWith('http')) { + imageUrl = '${AppLink.server}/upload/drivers/$imageUrl'; + } + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: AppStyle.cardDecoration, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + const Icon(Icons.file_present_rounded, color: AppColor.accent, size: 20), + const SizedBox(width: 8), + Text(doc['doc_type'] ?? 'وثيقة', style: AppStyle.title.copyWith(fontSize: 14)), + ], + ), + ), + ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + child: Image.network( + imageUrl, + width: double.infinity, + height: 200, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + height: 200, + color: AppColor.surfaceElevated, + child: const Center(child: Icon(Icons.broken_image_rounded, size: 48, color: AppColor.textMuted)), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/admin/pricing/kazan_editor_page.dart b/lib/views/admin/pricing/kazan_editor_page.dart new file mode 100644 index 0000000..c48399b --- /dev/null +++ b/lib/views/admin/pricing/kazan_editor_page.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../controller/admin/kazan_controller.dart'; +import '../../widgets/my_scafold.dart'; +import '../../widgets/elevated_btn.dart'; + +class KazanEditorPage extends StatelessWidget { + KazanEditorPage({super.key}); + + final KazanController controller = Get.put(KazanController()); + + @override + Widget build(BuildContext context) { + return MyScafolld( + title: 'تعديل أسعار كازان'.tr, + isleading: true, + body: [ + Obx(() => controller.isLoading.value && controller.kazanData.isEmpty + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('النسب العامة'), + _buildKazanCard(), + const SizedBox(height: 24), + _buildSectionHeader('أسعار الفئات الإضافية'), + _buildPricesGrid(), + const SizedBox(height: 32), + MyElevatedButton( + title: 'حفظ جميع التعديلات', + icon: Icons.save_rounded, + onPressed: () => _handleSave(), + ), + const SizedBox(height: 100), + ], + ), + )), + ], + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 12, left: 4), + child: Text( + title, + style: AppStyle.title.copyWith(color: AppColor.accent), + ), + ); + } + + Widget _buildKazanCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: AppStyle.cardDecoration, + child: Column( + children: [ + _buildSliderItem( + 'نسبة كازان العامة', + 'kazan', + 'النسبة المئوية التي تقتطعها المنصة من كل رحلة', + Icons.percent_rounded, + ), + const Divider(height: 32, color: AppColor.divider), + _buildPriceInputRow( + 'سعر الوقود المرجعي', + 'fuelPrice', + 'السعر المستخدم في حسابات تعويض الوقود', + Icons.local_gas_station_rounded, + ), + ], + ), + ); + } + + Widget _buildPriceInputRow(String title, String key, String desc, IconData icon) { + final TextEditingController textController = TextEditingController( + text: controller.kazanData[key]?.toString() ?? '0' + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Icon(icon, size: 20, color: AppColor.accent), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)), + Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)), + ], + ), + ), + Container( + width: 100, + height: 40, + decoration: BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.divider), + ), + child: TextField( + controller: textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent), + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 10), + ), + onChanged: (val) => controller.kazanData[key] = val, + ), + ), + const SizedBox(width: 8), + Text('ل.س', style: AppStyle.caption), + ], + ), + ); + } + + Widget _buildPricesGrid() { + final Map priceFields = { + 'comfortPrice': {'label': 'Comfort', 'icon': Icons.chair_rounded}, + 'speedPrice': {'label': 'Speed', 'icon': Icons.flash_on_rounded}, + 'familyPrice': {'label': 'Family', 'icon': Icons.groups_rounded}, + 'deliveryPrice': {'label': 'Delivery', 'icon': Icons.delivery_dining_rounded}, + 'freePrice': {'label': 'Free', 'icon': Icons.money_off_rounded}, + 'latePrice': {'label': 'Late Night', 'icon': Icons.nightlight_round}, + 'heavyPrice': {'label': 'Heavy Load', 'icon': Icons.inventory_2_rounded}, + 'naturePrice': {'label': 'Nature', 'icon': Icons.forest_rounded}, + }; + + return Container( + decoration: AppStyle.cardDecoration, + padding: const EdgeInsets.all(12), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 2.5, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: priceFields.length, + itemBuilder: (context, index) { + String key = priceFields.keys.elementAt(index); + var field = priceFields[key]; + return _buildCompactPriceInputCard(key, field['label'], field['icon']); + }, + ), + ); + } + + Widget _buildCompactPriceInputCard(String key, String label, IconData icon) { + final TextEditingController textController = TextEditingController( + text: controller.kazanData[key]?.toString() ?? '0' + ); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.divider.withAlpha(100)), + ), + child: Row( + children: [ + Icon(icon, size: 16, color: AppColor.textSecondary), + const SizedBox(width: 6), + Expanded( + child: Text(label, style: AppStyle.caption.copyWith(fontSize: 11), overflow: TextOverflow.ellipsis), + ), + SizedBox( + width: 50, + child: TextField( + controller: textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: AppStyle.number.copyWith(fontSize: 14), + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (val) => controller.kazanData[key] = val, + ), + ), + ], + ), + ); + } + + Widget _buildSliderItem(String title, String key, String desc, IconData icon) { + double value = double.tryParse(controller.kazanData[key]?.toString() ?? '0') ?? 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: AppColor.accent), + const SizedBox(width: 8), + Text(title, style: AppStyle.title), + ], + ), + Text( + '${value.toInt()}%', + style: AppStyle.number.copyWith(fontSize: 18), + ), + ], + ), + const SizedBox(height: 4), + Text(desc, style: AppStyle.caption), + Slider( + value: value.clamp(0, 100), + min: 0, + max: 100, + activeColor: AppColor.accent, + inactiveColor: AppColor.divider, + onChanged: (val) { + controller.kazanData[key] = val.toInt().toString(); + }, + ), + ], + ); + } + + void _handleSave() async { + final data = Map.from(controller.kazanData); + data['adminId'] = 'admin'; // Should be dynamic from auth service + data['country'] = 'syria'; + + bool success = await controller.updateKazan(data); + if (success) { + Get.snackbar("نجاح", "تم تحديث الأسعار بنجاح", + backgroundColor: AppColor.successSoft, + colorText: AppColor.textPrimary, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(16) + ); + } + } +} diff --git a/lib/views/admin/promo/promo_management_page.dart b/lib/views/admin/promo/promo_management_page.dart new file mode 100644 index 0000000..67dd578 --- /dev/null +++ b/lib/views/admin/promo/promo_management_page.dart @@ -0,0 +1,269 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../controller/admin/promo_controller.dart'; +import '../../widgets/my_scafold.dart'; +import '../../widgets/elevated_btn.dart'; +import '../../widgets/my_textField.dart'; +import '../../widgets/mydialoug.dart'; + +class PromoManagementPage extends StatelessWidget { + PromoManagementPage({super.key}); + + final PromoController controller = Get.put(PromoController()); + + @override + Widget build(BuildContext context) { + return MyScafolld( + title: 'إدارة أكواد الخصم'.tr, + isleading: true, + action: IconButton( + icon: const Icon(Icons.add_circle_outline_rounded, color: AppColor.accent), + onPressed: () => _showPromoSheet(context), + ), + body: [ + Obx(() => controller.isLoading.value && controller.promoList.isEmpty + ? const Center(child: CircularProgressIndicator()) + : controller.promoList.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.confirmation_number_outlined, size: 64, color: AppColor.textMuted), + const SizedBox(height: 16), + Text('لا يوجد أكواد خصم حالياً', style: AppStyle.subtitle), + ], + ), + ) + : RefreshIndicator( + onRefresh: () => controller.getPromos(), + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), + itemCount: controller.promoList.length, + itemBuilder: (context, index) { + final promo = controller.promoList[index]; + return _buildPromoCard(context, promo); + }, + ), + )), + ], + ); + } + + Widget _buildPromoCard(BuildContext context, dynamic promo) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: AppStyle.cardDecoration, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColor.accentSoft, + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.local_offer_rounded, color: AppColor.accent), + ), + title: Text( + promo['promo_code']?.toString() ?? 'N/A', + style: AppStyle.title, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text(promo['description']?.toString() ?? '', style: AppStyle.caption), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.money_rounded, size: 14, color: AppColor.success), + const SizedBox(width: 4), + Text('${promo['amount']} ${'دينار'.tr}', style: AppStyle.number.copyWith(color: AppColor.success)), + const SizedBox(width: 12), + Icon(Icons.person_rounded, size: 14, color: AppColor.info), + const SizedBox(width: 4), + Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: AppStyle.caption), + ], + ), + ], + ), + trailing: PopupMenuButton( + icon: const Icon(Icons.more_vert_rounded, color: AppColor.textSecondary), + color: AppColor.surfaceElevated, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + const Icon(Icons.edit_rounded, size: 18, color: AppColor.info), + const SizedBox(width: 8), + Text('تعديل'.tr), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete_outline_rounded, size: 18, color: AppColor.danger), + const SizedBox(width: 8), + Text('حذف'.tr, style: const TextStyle(color: AppColor.danger)), + ], + ), + ), + ], + onSelected: (value) { + if (value == 'edit') { + _showPromoSheet(context, promo: promo); + } else if (value == 'delete') { + MyDialog().getDialog( + 'حذف كود الخصم', + 'هل أنت متأكد من حذف كود الخصم ${promo['promo_code']}؟', + () => controller.deletePromo(promo['id'].toString()).then((_) => Get.back()), + ); + } + }, + ), + ), + ); + } + + void _showPromoSheet(BuildContext context, {dynamic promo}) { + final TextEditingController codeController = TextEditingController(text: promo?['promo_code']); + final TextEditingController amountController = TextEditingController(text: promo?['amount']?.toString()); + final TextEditingController descController = TextEditingController(text: promo?['description']); + final TextEditingController passengerController = TextEditingController(text: promo?['passengerID'] ?? 'none'); + final TextEditingController startDateController = TextEditingController(text: promo?['validity_start_date'] ?? DateTime.now().toString().split(' ')[0]); + final TextEditingController endDateController = TextEditingController(text: promo?['validity_end_date'] ?? DateTime.now().add(const Duration(days: 30)).toString().split(' ')[0]); + + Get.bottomSheet( + Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration(color: AppColor.divider, borderRadius: BorderRadius.circular(2)), + ), + const SizedBox(height: 24), + Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: AppStyle.headTitle), + const SizedBox(height: 24), + MyTextForm( + controller: codeController, + label: 'كود الخصم', + hint: 'مثال: WELCOME20', + type: TextInputType.text, + prefixIcon: Icons.local_offer_rounded, + ), + MyTextForm( + controller: amountController, + label: 'المبلغ', + hint: 'أدخل قيمة الخصم', + type: TextInputType.number, + prefixIcon: Icons.attach_money_rounded, + ), + MyTextForm( + controller: descController, + label: 'الوصف', + hint: 'وصف كود الخصم', + type: TextInputType.text, + prefixIcon: Icons.description_rounded, + ), + MyTextForm( + controller: passengerController, + label: 'معرف الراكب (أو none للعام)', + hint: 'none', + type: TextInputType.text, + prefixIcon: Icons.person_rounded, + ), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () async { + DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.tryParse(startDateController.text) ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime(2030), + ); + if (picked != null) { + startDateController.text = picked.toString().split(' ')[0]; + } + }, + child: IgnorePointer( + child: MyTextForm( + controller: startDateController, + label: 'يبدأ في', + hint: 'YYYY-MM-DD', + type: TextInputType.none, + prefixIcon: Icons.calendar_today_rounded, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: InkWell( + onTap: () async { + DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.tryParse(endDateController.text) ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime(2030), + ); + if (picked != null) { + endDateController.text = picked.toString().split(' ')[0]; + } + }, + child: IgnorePointer( + child: MyTextForm( + controller: endDateController, + label: 'ينتهي في', + hint: 'YYYY-MM-DD', + type: TextInputType.none, + prefixIcon: Icons.event_busy_rounded, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + MyElevatedButton( + title: promo == null ? 'إضافة' : 'حفظ التعديلات', + onPressed: () async { + final data = { + if (promo != null) 'id': promo['id'].toString(), + 'promo_code': codeController.text, + 'amount': amountController.text, + 'description': descController.text, + 'passengerID': passengerController.text, + 'validity_start_date': startDateController.text, + 'validity_end_date': endDateController.text, + }; + bool success = promo == null + ? await controller.addPromo(data) + : await controller.updatePromo(data); + if (success) Get.back(); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ), + + isScrollControlled: true, + ); + } +} diff --git a/lib/views/widgets/elevated_btn.dart b/lib/views/widgets/elevated_btn.dart index 8ca37b5..518a70e 100644 --- a/lib/views/widgets/elevated_btn.dart +++ b/lib/views/widgets/elevated_btn.dart @@ -9,39 +9,60 @@ import '../../constant/style.dart'; class MyElevatedButton extends StatelessWidget { final String title; final VoidCallback onPressed; - final Color kolor; - final int vibrateDuration; + final Color? kolor; + final IconData? icon; + const MyElevatedButton({ Key? key, required this.title, required this.onPressed, - this.kolor = AppColor.primaryColor, - this.vibrateDuration = 100, + this.kolor, + this.icon, }) : super(key: key); @override Widget build(BuildContext context) { - return ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(kolor), + return Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: (kolor ?? AppColor.accent).withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), - onPressed: () async { - // Handle haptic feedback for both iOS and Android - if (Platform.isIOS) { - HapticFeedback.selectionClick(); - } else { - // Vibration.vibrate(duration: 100); - // Vibrate.vibrateWithPauses(pauses); - } - - // Ensure the onPressed callback is called after haptic feedback - onPressed(); - }, - child: Text( - title, - textAlign: TextAlign.center, - style: AppStyle.title.copyWith(color: AppColor.secondaryColor), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: kolor ?? AppColor.accent, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + onPressed: () { + HapticFeedback.lightImpact(); + onPressed(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, size: 20), + const SizedBox(width: 8), + ], + Text( + title, + style: AppStyle.title.copyWith(color: Colors.white), + ), + ], + ), ), ); } } + diff --git a/lib/views/widgets/my_scafold.dart b/lib/views/widgets/my_scafold.dart index fe50446..29996c0 100644 --- a/lib/views/widgets/my_scafold.dart +++ b/lib/views/widgets/my_scafold.dart @@ -9,40 +9,50 @@ class MyScafolld extends StatelessWidget { super.key, required this.title, required this.body, - this.action = const Icon( - Icons.clear, - color: AppColor.secondaryColor, - ), + this.action, required this.isleading, }); final String title; final List body; - final Widget action; + final Widget? action; final bool isleading; @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColor.secondaryColor, + backgroundColor: AppColor.bg, appBar: AppBar( - backgroundColor: AppColor.secondaryColor, + backgroundColor: AppColor.bg, elevation: 0, + leadingWidth: 70, leading: isleading - ? IconButton( - onPressed: () { - Get.back(); - }, - icon: const Icon( - Icons.arrow_back_ios_new, - color: AppColor.primaryColor, + ? Center( + child: Container( + margin: const EdgeInsets.only(right: 16), + decoration: BoxDecoration( + color: AppColor.surfaceElevated, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColor.divider), + ), + child: IconButton( + onPressed: () => Get.back(), + icon: const Icon( + Icons.arrow_back_ios_new_rounded, + color: AppColor.textPrimary, + size: 18, + ), + ), ), ) - : const SizedBox(), - actions: [action], + : null, + actions: [ + if (action != null) action!, + const SizedBox(width: 16), + ], title: Text( title, - style: AppStyle.title.copyWith(fontSize: 30), + style: AppStyle.headTitle, ), ), body: SafeArea(child: Stack(children: body))); diff --git a/lib/views/widgets/my_textField.dart b/lib/views/widgets/my_textField.dart index 54f2bf0..5b46599 100644 --- a/lib/views/widgets/my_textField.dart +++ b/lib/views/widgets/my_textField.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../constant/colors.dart'; +import '../../constant/style.dart'; class MyTextForm extends StatelessWidget { const MyTextForm({ @@ -11,55 +12,66 @@ class MyTextForm extends StatelessWidget { required this.label, required this.hint, required this.type, + this.prefixIcon, }); + final TextEditingController controller; final String label, hint; final TextInputType type; + final IconData? prefixIcon; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: SizedBox( - width: Get.width * .8, - child: TextFormField( - keyboardType: type, - cursorColor: AppColor.accentColor, - controller: controller, - decoration: InputDecoration( - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide( - color: AppColor.primaryColor, - width: 2.0, - ), - borderRadius: BorderRadius.circular(10), - ), - focusColor: AppColor.accentColor, - fillColor: AppColor.accentColor, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12))), - labelText: label.tr, - hintText: hint.tr, + padding: const EdgeInsets.only(bottom: 16), + child: TextFormField( + keyboardType: type, + style: AppStyle.body, + cursorColor: AppColor.accent, + controller: controller, + decoration: InputDecoration( + filled: true, + fillColor: AppColor.surface, + prefixIcon: prefixIcon != null + ? Icon(prefixIcon, color: AppColor.textSecondary, size: 20) + : null, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColor.divider), + borderRadius: BorderRadius.circular(16), ), - validator: (value) { - if (value!.isEmpty) { - return 'Please enter $label.'.tr; - } - - if (type == TextInputType.emailAddress) { - if (!value.contains('@')) { - return 'Please enter a valid email.'.tr; - } - } else if (type == TextInputType.phone) { - if (value.length != 10) { - return 'Please enter a valid phone number.'.tr; - } - } - - return null; - }, + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColor.accent, width: 2), + borderRadius: BorderRadius.circular(16), + ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColor.danger), + borderRadius: BorderRadius.circular(16), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColor.danger, width: 2), + borderRadius: BorderRadius.circular(16), + ), + labelText: label.tr, + labelStyle: AppStyle.subtitle, + hintText: hint.tr, + hintStyle: AppStyle.caption, ), + validator: (value) { + if (value == null || value.isEmpty) { + return '${'Please enter'.tr} ${label.tr}'; + } + + if (type == TextInputType.emailAddress) { + if (!GetUtils.isEmail(value)) { + return 'Please enter a valid email.'.tr; + } + } + + return null; + }, ), ); } } + diff --git a/lib/views/widgets/mydialoug.dart b/lib/views/widgets/mydialoug.dart index 50cd848..3089e04 100755 --- a/lib/views/widgets/mydialoug.dart +++ b/lib/views/widgets/mydialoug.dart @@ -42,104 +42,70 @@ class MyDialog extends GetxController { sigmaX: DialogConfig.blurStrength, sigmaY: DialogConfig.blurStrength, ), - child: Theme( - data: ThemeData.light().copyWith( - dialogBackgroundColor: CupertinoColors.systemBackground, + child: AlertDialog( + backgroundColor: AppColor.surfaceElevated, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Text( + title, + textAlign: TextAlign.center, + style: AppStyle.headTitle, ), - child: CupertinoAlertDialog( - title: Column( + content: midTitle != null + ? Text( + midTitle, + textAlign: TextAlign.center, + style: AppStyle.subtitle, + ) + : null, + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + actions: [ + Row( children: [ - Text( - title, - style: AppStyle.title.copyWith( - fontSize: 20, - fontWeight: FontWeight.w700, - letterSpacing: -0.5, - color: AppColor.primaryColor, + Expanded( + child: TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: AppColor.divider), + ), + ), + child: Text('Cancel'.tr, style: AppStyle.subtitle), ), ), - const SizedBox(height: 8), - ], - ), - content: Column( - children: [ - CupertinoButton( - padding: const EdgeInsets.all(8), - onPressed: () async { - HapticFeedback.selectionClick(); - // await textToSpeechController.speakText(title); - // await textToSpeechController.speakText(midTitle!); - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: - AppColor.primaryColor.withAlpha(26), // 0.1 opacity - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - CupertinoIcons.speaker_2_fill, - color: AppColor.primaryColor, - size: 24, + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + HapticFeedback.mediumImpact(); + onPressed(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.accent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, ), + child: Text('OK'.tr, style: AppStyle.title.copyWith(color: Colors.white)), ), ), - const SizedBox(height: 8), - Text( - midTitle!, - style: AppStyle.title.copyWith( - fontSize: 16, - height: 1.3, - color: Colors.black87, - ), - textAlign: TextAlign.center, - ), ], ), - actions: [ - CupertinoDialogAction( - onPressed: () { - HapticFeedback.lightImpact(); - Get.back(); - }, - child: Text( - 'Cancel'.tr, - style: TextStyle( - color: AppColor.redColor, - fontWeight: FontWeight.w600, - fontSize: 17, - ), - ), - ), - CupertinoDialogAction( - onPressed: () { - HapticFeedback.mediumImpact(); - onPressed(); - }, - child: Text( - 'OK'.tr, - style: TextStyle( - color: AppColor.greenColor, - fontWeight: FontWeight.w600, - fontSize: 17, - ), - ), - ), - ], - ), + ], ), ), ), - barrierDismissible: true, - barrierColor: Colors.black.withAlpha(102), // 0.4 opacity + barrierColor: Colors.black54, ); } } class MyDialogContent extends GetxController { void getDialog(String title, Widget? content, VoidCallback onPressed) { - // final textToSpeechController = Get.put(TextToSpeechController()); - HapticFeedback.mediumImpact(); Get.dialog( @@ -157,87 +123,61 @@ class MyDialogContent extends GetxController { sigmaX: DialogConfig.blurStrength, sigmaY: DialogConfig.blurStrength, ), - child: Theme( - data: ThemeData.light().copyWith( - dialogBackgroundColor: CupertinoColors.systemBackground, + child: AlertDialog( + backgroundColor: AppColor.surfaceElevated, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Text( + title, + textAlign: TextAlign.center, + style: AppStyle.headTitle, ), - child: CupertinoAlertDialog( - title: Column( + content: content != null + ? SingleChildScrollView(child: content) + : null, + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + actions: [ + Row( children: [ - Text( - title, - style: AppStyle.title.copyWith( - fontSize: 20, - fontWeight: FontWeight.w700, - letterSpacing: -0.5, - color: AppColor.primaryColor, + Expanded( + child: TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: AppColor.divider), + ), + ), + child: Text('Cancel'.tr, style: AppStyle.subtitle), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + HapticFeedback.mediumImpact(); + onPressed(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.accent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: Text('OK'.tr, style: AppStyle.title.copyWith(color: Colors.white)), ), ), - const SizedBox(height: 8), ], ), - content: Column( - children: [ - CupertinoButton( - padding: const EdgeInsets.all(8), - onPressed: () async { - HapticFeedback.selectionClick(); - // await textToSpeechController.speakText(title); - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: - AppColor.primaryColor.withAlpha(26), // 0.1 opacity - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - CupertinoIcons.headphones, - color: AppColor.primaryColor, - size: 24, - ), - ), - ), - const SizedBox(height: 12), - content!, - ], - ), - actions: [ - CupertinoDialogAction( - onPressed: () { - HapticFeedback.lightImpact(); - Get.back(); - }, - child: Text( - 'Cancel', - style: TextStyle( - color: AppColor.redColor, - fontWeight: FontWeight.w600, - fontSize: 17, - ), - ), - ), - CupertinoDialogAction( - onPressed: () { - HapticFeedback.mediumImpact(); - onPressed(); - }, - child: Text( - 'OK'.tr, - style: TextStyle( - color: AppColor.greenColor, - fontWeight: FontWeight.w600, - fontSize: 17, - ), - ), - ), - ], - ), + ], ), ), ), - barrierDismissible: true, - barrierColor: Colors.black.withAlpha(102), // 0.4 opacity + barrierColor: Colors.black54, ); } } +