diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8fa203e..7930ad4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -163,10 +163,10 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) - - SDWebImageWebPCoder (0.14.6): + - SDWebImage (5.21.6): + - SDWebImage/Core (= 5.21.6) + - SDWebImage/Core (5.21.6) + - SDWebImageWebPCoder (0.15.0): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - sqflite_darwin (0.0.4): @@ -289,8 +289,8 @@ SPEC CHECKSUMS: path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - SDWebImage: f29024626962457f3470184232766516dee8dfea - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 0046868..4dd1401 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -497,7 +497,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -628,7 +628,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -679,7 +679,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7c48750..fba2a60 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,49 +1,57 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Intaleq Admin - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - intaleq_admin - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - + + NSLocalNetworkUsageDescription + نحتاج إلى الوصول إلى الشبكة المحلية لاكتشاف الأجهزة القريبة. + + NSBonjourServices + + _http._tcp + _https._tcp + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Intaleq Admin + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + intaleq_admin + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + \ No newline at end of file diff --git a/lib/constant/links.dart b/lib/constant/links.dart index 3b0fd9d..28e5799 100644 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -11,14 +11,27 @@ class AppLink { // static final String endPoint = box.read(BoxName.serverChosen); // static final String server = Env.seferCairoServer; - static final String server = 'https://api.intaleq.xyz/intaleq'; + static final String server = 'https://api.intaleq.xyz/intaleq_v1'; static String loginJwtDriver = "https://api.intaleq.xyz/intaleq/loginAdmin.php"; +//============================= +//============================= + static final getAllFingerprints = + '$server/migration/get_all_fingerprints.php'; + static final updateFingerprintAdmin = + '$server/migration/update_fingerprint_admin.php'; + static final getAllDriverFingerprints = + '$server/migration/get_all_driver_fingerprints.php'; + static final updateDriverFingerprintAdmin = + '$server/migration/update_driver_fingerprint_admin.php'; +//============================= +//============================= static String googleMapsLink = 'https://maps.googleapis.com/maps/api/'; static String llama = 'https://api.llama-api.com/chat/completions'; static String gemini = 'https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText'; + static String serverMonitor = "https://tripz-egypt.com/server/monitor.php"; static String test = "$server/test.php"; static String loginWalletAdmin = "$seferPaymentServer/loginWalletAdmin.php"; diff --git a/lib/controller/admin/captain_admin_controller.dart b/lib/controller/admin/captain_admin_controller.dart index 6532813..a256772 100644 --- a/lib/controller/admin/captain_admin_controller.dart +++ b/lib/controller/admin/captain_admin_controller.dart @@ -53,7 +53,7 @@ class CaptainAdminController extends GetxController { update(); var res = await CRUD().post( link: AppLink.find_driver_by_phone, - payload: {'phone': phone}, + payload: {'phone': "963$phone"}, ); var d = (res); if (d != 'failure') { diff --git a/lib/controller/admin/dashboard_controller.dart b/lib/controller/admin/dashboard_controller.dart index d599582..d41d13f 100644 --- a/lib/controller/admin/dashboard_controller.dart +++ b/lib/controller/admin/dashboard_controller.dart @@ -54,14 +54,14 @@ class DashboardController extends GetxController { } // 🔹 Check SMS credit - var res2 = await CRUD().kazumiSMS( - link: 'https://sms.kazumi.me/api/sms/check-credit', - payload: {"username": "Sefer", "password": AK.smsPasswordEgypt}, - ); + // var res2 = await CRUD().kazumiSMS( + // link: 'https://sms.kazumi.me/api/sms/check-credit', + // payload: {"username": "Sefer", "password": AK.smsPasswordEgypt}, + // ); - creditSMS = res2['credit']; - print('📱 SMS Credit Response: ${jsonEncode(res2)}'); - print('💰 creditSMS: $creditSMS'); + // creditSMS = res2['credit']; + // print('📱 SMS Credit Response: ${jsonEncode(res2)}'); + // print('💰 creditSMS: $creditSMS'); isLoading = false; update(); diff --git a/lib/controller/admin/static_controller.dart b/lib/controller/admin/static_controller.dart index 36210cb..05db94a 100644 --- a/lib/controller/admin/static_controller.dart +++ b/lib/controller/admin/static_controller.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; @@ -7,8 +8,72 @@ import '../../constant/links.dart'; import '../../print.dart'; import '../functions/crud.dart'; +// ══════════════════════════════════════════════════════════════ +// MODEL: Represents one employee's full data for a period +// ══════════════════════════════════════════════════════════════ +class EmployeeChartData { + final String name; + final Color color; + final List notesSpots; + final List callsSpots; + + const EmployeeChartData({ + required this.name, + required this.color, + required this.notesSpots, + required this.callsSpots, + }); + + int get totalNotes => notesSpots.fold(0, (sum, s) => sum + s.y.toInt()); + int get totalCalls => callsSpots.fold(0, (sum, s) => sum + s.y.toInt()); + + EmployeeChartData copyWith({ + List? notesSpots, + List? callsSpots, + }) { + return EmployeeChartData( + name: name, + color: color, + notesSpots: notesSpots ?? this.notesSpots, + callsSpots: callsSpots ?? this.callsSpots, + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// MODEL: Employment activation stats per employee +// ══════════════════════════════════════════════════════════════ +class EmploymentStat { + final String name; + final int count; + final Color color; + + const EmploymentStat({ + required this.name, + required this.count, + required this.color, + }); +} + +// ══════════════════════════════════════════════════════════════ +// CONTROLLER +// ══════════════════════════════════════════════════════════════ class StaticController extends GetxController { - // --- Date & State Management --- + // ─── Color Palette for Dynamic Employees ─────────────────── + static const List _employeeColors = [ + Color(0xFF00D4AA), // teal + Color(0xFF82AAFF), // blue + Color(0xFFFFCB6B), // amber + Color(0xFFC792EA), // purple + Color(0xFFFF5370), // red + Color(0xFFC3E88D), // green + Color(0xFFF07178), // coral + Color(0xFF89DDFF), // cyan + ]; + + Color _colorForIndex(int i) => _employeeColors[i % _employeeColors.length]; + + // ─── Date & State ─────────────────────────────────────────── DateTime? startDate = DateTime(DateTime.now().year, DateTime.now().month, 1); DateTime? endDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0); @@ -19,56 +84,53 @@ class StaticController extends GetxController { bool isComparing = false; bool isLoading = false; - // --- Daily Notes State --- + // ─── Daily Notes State ───────────────────────────────────── bool isLoadingNotes = false; List dailyNotesList = []; - // --- Chart Data (Current Range) --- + // ─── Main Chart Data ─────────────────────────────────────── List chartDataPassengers = []; List chartDataDrivers = []; List chartDataRides = []; List chartDataDriversMatchingNotes = []; - // Employee Data (Notes/General Stats) - List chartDataEmployeerama1 = []; - List chartDataEmployeeshahd = []; - List chartDataEmployeeRama2 = []; - List chartDataEmployeeSefer4 = []; - - // Employee Data (Calls/Activations Stats) - List chartDataCallsrama1 = []; - List chartDataCallsShahd = []; - List chartDataCallsRama2 = []; - List chartDataCallsSefer4 = []; - - // --- Chart Data (Comparison Range) --- List chartDataPassengersCompare = []; List chartDataDriversCompare = []; List chartDataRidesCompare = []; List chartDataDriversMatchingNotesCompare = []; - // Employee Comparison (Notes) - List chartDataEmployeerama1Compare = []; - List chartDataEmployeeshahdCompare = []; - List chartDataEmployeeRama2Compare = []; - List chartDataEmployeeSefer4Compare = []; + // ─── 🔥 DYNAMIC Employee Data ───────────────────────────── + // Key = employee name (from server), Value = their chart data + Map employeeData = {}; + Map employeeDataCompare = {}; - // Employee Comparison (Calls/Activations) - List chartDataCallsrama1Compare = []; - List chartDataCallsShahdCompare = []; - List chartDataCallsRama2Compare = []; - List chartDataCallsSefer4Compare = []; + // Set of all known employee names (union of current + compare) + Set get allEmployeeNames => { + ...employeeData.keys, + ...employeeDataCompare.keys, + }; - // --- Totals --- + // ─── Employment Stats ────────────────────────────────────── + List employmentStatsList = []; + + // ─── Totals ──────────────────────────────────────────────── String totalMonthlyPassengers = '0'; String totalMonthlyRides = '0'; String totalMonthlyDrivers = '0'; - // --- Raw Lists --- + // ─── Raw Lists ───────────────────────────────────────────── List staticList = []; - // --- Employment Type Stats List (Simple Count) --- - List> employmentStatsList = []; + // ─── Color Registry (stable across rebuilds) ─────────────── + final Map _employeeColorRegistry = {}; + + Color _getOrAssignColor(String name) { + if (!_employeeColorRegistry.containsKey(name)) { + _employeeColorRegistry[name] = + _colorForIndex(_employeeColorRegistry.length); + } + return _employeeColorRegistry[name]!; + } @override void onInit() { @@ -76,7 +138,7 @@ class StaticController extends GetxController { getAll(); } - // --- Helpers for View --- + // ─── Helpers ─────────────────────────────────────────────── double get daysInPeriod { if (startDate == null || endDate == null) return 31; return endDate!.difference(startDate!).inDays + 1.0; @@ -84,10 +146,17 @@ class StaticController extends GetxController { String get currentDateString { if (startDate == null || endDate == null) return ""; - return "${DateFormat('yyyy-MM-dd').format(startDate!)} : ${DateFormat('yyyy-MM-dd').format(endDate!)}"; + return "${DateFormat('yyyy-MM-dd').format(startDate!)} : " + "${DateFormat('yyyy-MM-dd').format(endDate!)}"; } - // --- Date Actions --- + String get compareDateString { + if (compareStartDate == null || compareEndDate == null) return ""; + return "${DateFormat('yyyy-MM-dd').format(compareStartDate!)} : " + "${DateFormat('yyyy-MM-dd').format(compareEndDate!)}"; + } + + // ─── Date Actions ────────────────────────────────────────── void updateDateRange(DateTime start, DateTime end) { startDate = start; endDate = end; @@ -120,29 +189,18 @@ class StaticController extends GetxController { chartDataDriversCompare.clear(); chartDataRidesCompare.clear(); chartDataDriversMatchingNotesCompare.clear(); - - chartDataEmployeerama1Compare.clear(); - chartDataEmployeeshahdCompare.clear(); - chartDataEmployeeRama2Compare.clear(); - chartDataEmployeeSefer4Compare.clear(); - - chartDataCallsrama1Compare.clear(); - chartDataCallsShahdCompare.clear(); - chartDataCallsRama2Compare.clear(); - chartDataCallsSefer4Compare.clear(); + employeeDataCompare.clear(); } - Map _getPayload(DateTime start, DateTime end) { - return { - "start_date": DateFormat('yyyy-MM-dd').format(start), - "end_date": DateFormat('yyyy-MM-dd').format(end), - "month": start.month.toString(), - "year": start.year.toString(), - }; - } + Map _getPayload(DateTime start, DateTime end) => { + "start_date": DateFormat('yyyy-MM-dd').format(start), + "end_date": DateFormat('yyyy-MM-dd').format(end), + "month": start.month.toString(), + "year": start.year.toString(), + }; - // --- Main Fetch Logic --- - Future getAll() async { + // ─── Main Fetch ──────────────────────────────────────────── + Future getAll() async { if (startDate == null || endDate == null) return; isLoading = true; @@ -152,8 +210,8 @@ class StaticController extends GetxController { fetchPassengers(isCompare: false), fetchRides(isCompare: false), fetchDrivers(isCompare: false), - fetchEmployee(isCompare: false), - fetchEditorCalls(isCompare: false), + fetchEmployeeDynamic(isCompare: false), + fetchEditorCallsDynamic(isCompare: false), fetchEmploymentStats(), ]); @@ -162,8 +220,8 @@ class StaticController extends GetxController { fetchPassengers(isCompare: true), fetchRides(isCompare: true), fetchDrivers(isCompare: true), - fetchEmployee(isCompare: true), - fetchEditorCalls(isCompare: true), + fetchEmployeeDynamic(isCompare: true), + fetchEditorCallsDynamic(isCompare: true), ]); } @@ -171,90 +229,124 @@ class StaticController extends GetxController { update(); } - // ... (Existing Functions _generateSpots, fetchPassengers, etc.) ... - List _generateSpots(List data, String dateKey, - String valueKey, DateTime startOfRange) { - List spots = []; - Map dataMap = {}; - for (var item in data) { - String dateStr = item[dateKey].toString(); - double val = double.tryParse(item[valueKey].toString()) ?? 0.0; - dataMap[dateStr] = val; - } - DateTime rangeEnd = - (startOfRange == startDate) ? endDate! : compareEndDate!; - int totalDays = rangeEnd.difference(startOfRange).inDays + 1; - for (int i = 0; i < totalDays; i++) { - DateTime currentDate = startOfRange.add(Duration(days: i)); - String dateKeyStr = DateFormat('yyyy-MM-dd').format(currentDate); - double value = dataMap[dateKeyStr] ?? 0.0; - spots.add(FlSpot((i + 1).toDouble(), value)); - } - return spots; + // ─── Spot Generator ─────────────────────────────────────── + List _generateSpots( + List data, + String dateKey, + String valueKey, + DateTime startOfRange, + DateTime endOfRange, + ) { + Map dataMap = { + for (var item in data) + item[dateKey].toString(): + double.tryParse(item[valueKey].toString()) ?? 0.0 + }; + + int totalDays = endOfRange.difference(startOfRange).inDays + 1; + return List.generate(totalDays, (i) { + final date = startOfRange.add(Duration(days: i)); + final key = DateFormat('yyyy-MM-dd').format(date); + return FlSpot((i + 1).toDouble(), dataMap[key] ?? 0.0); + }); } - Future fetchPassengers({bool isCompare = false}) async { - DateTime start = isCompare ? compareStartDate! : startDate!; - DateTime end = isCompare ? compareEndDate! : endDate!; - var res = await CRUD().get( - link: AppLink.getPassengersStatic, payload: _getPayload(start, end)); - var jsonResponse = jsonDecode(res); - if (jsonResponse['status'] == 'failure') return; - final List jsonData = jsonResponse['message']; - if (!isCompare && - jsonData.isNotEmpty && - jsonData[0]['totalMonthly'] != null) { - totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString(); + /// Generates spots map keyed by employee name from a date→name→value structure + Map> _generateEmployeeSpots( + Map> dateNameMap, + DateTime start, + DateTime end, + ) { + // Discover all employee names dynamically + final Set names = {}; + for (var dayData in dateNameMap.values) { + names.addAll(dayData.keys); } - List spots = - _generateSpots(jsonData, 'day', 'totalPassengers', start); + + int totalDays = end.difference(start).inDays + 1; + final Map> result = {}; + + for (final name in names) { + result[name] = List.generate(totalDays, (i) { + final date = start.add(Duration(days: i)); + final dateStr = DateFormat('yyyy-MM-dd').format(date); + final value = dateNameMap[dateStr]?[name] ?? 0.0; + return FlSpot((i + 1).toDouble(), value); + }); + } + return result; + } + + /// Parses a list of {date/day, NAME, count} records into a dateNameMap + Map> _parseDateNameMap(List jsonData) { + final Map> result = {}; + for (var item in jsonData) { + final dateStr = (item['date'] ?? item['day']).toString(); + final name = item['NAME'].toString().toLowerCase().trim(); + final count = double.tryParse(item['count'].toString()) ?? 0.0; + result.putIfAbsent(dateStr, () => {})[name] = + (result[dateStr]?[name] ?? 0) + count; + } + return result; + } + + // ─── Passengers ─────────────────────────────────────────── + Future fetchPassengers({bool isCompare = false}) async { + final start = isCompare ? compareStartDate! : startDate!; + final end = isCompare ? compareEndDate! : endDate!; + final res = await CRUD().get( + link: AppLink.getPassengersStatic, payload: _getPayload(start, end)); + final json = jsonDecode(res); + if (json['status'] == 'failure') return; + final List data = json['message']; + if (!isCompare && data.isNotEmpty && data[0]['totalMonthly'] != null) { + totalMonthlyPassengers = data[0]['totalMonthly'].toString(); + } + final spots = _generateSpots(data, 'day', 'totalPassengers', start, end); if (isCompare) chartDataPassengersCompare = spots; else chartDataPassengers = spots; } + // ─── Rides ──────────────────────────────────────────────── Future fetchRides({bool isCompare = false}) async { - DateTime start = isCompare ? compareStartDate! : startDate!; - DateTime end = isCompare ? compareEndDate! : endDate!; - var res = await CRUD() + final start = isCompare ? compareStartDate! : startDate!; + final end = isCompare ? compareEndDate! : endDate!; + final res = await CRUD() .get(link: AppLink.getRidesStatic, payload: _getPayload(start, end)); - var jsonResponse = jsonDecode(res); - if (jsonResponse['status'] == 'failure') return; - final List jsonData = jsonResponse['message']; - if (!isCompare && - jsonData.isNotEmpty && - jsonData[0]['totalMonthly'] != null) { - totalMonthlyRides = jsonData[0]['totalMonthly'].toString(); + final json = jsonDecode(res); + if (json['status'] == 'failure') return; + final List data = json['message']; + if (!isCompare && data.isNotEmpty && data[0]['totalMonthly'] != null) { + totalMonthlyRides = data[0]['totalMonthly'].toString(); } - List spots = _generateSpots(jsonData, 'day', 'totalRides', start); + final spots = _generateSpots(data, 'day', 'totalRides', start, end); if (isCompare) chartDataRidesCompare = spots; else chartDataRides = spots; } + // ─── Drivers ────────────────────────────────────────────── Future fetchDrivers({bool isCompare = false}) async { - DateTime start = isCompare ? compareStartDate! : startDate!; - DateTime end = isCompare ? compareEndDate! : endDate!; - var res = await CRUD().get( + final start = isCompare ? compareStartDate! : startDate!; + final end = isCompare ? compareEndDate! : endDate!; + final res = await CRUD().get( link: AppLink.getdriverstotalMonthly, payload: _getPayload(start, end)); - var jsonResponse = jsonDecode(res); - if (jsonResponse['status'] == 'failure') return; - final List jsonData = jsonResponse['message']; + final json = jsonDecode(res); + if (json['status'] == 'failure') return; + final List data = json['message']; if (!isCompare && - jsonData.isNotEmpty && - jsonData[0]['totalMonthlyDrivers'] != null) { - totalMonthlyDrivers = jsonData[0]['totalMonthlyDrivers'].toString(); + data.isNotEmpty && + data[0]['totalMonthlyDrivers'] != null) { + totalMonthlyDrivers = data[0]['totalMonthlyDrivers'].toString(); + staticList = data; } - if (!isCompare) { - staticList = jsonData; - } - - List spotsDrivers = - _generateSpots(jsonData, 'day', 'dailyTotalDrivers', start); - List spotsNotes = - _generateSpots(jsonData, 'day', 'dailyMatchingNotes', start); + final spotsDrivers = + _generateSpots(data, 'day', 'dailyTotalDrivers', start, end); + final spotsNotes = + _generateSpots(data, 'day', 'dailyMatchingNotes', start, end); if (isCompare) { chartDataDriversCompare = spotsDrivers; chartDataDriversMatchingNotesCompare = spotsNotes; @@ -264,237 +356,126 @@ class StaticController extends GetxController { } } - Future fetchEmployee({bool isCompare = false}) async { + // ─── 🔥 DYNAMIC: Employee Notes ─────────────────────────── + Future fetchEmployeeDynamic({bool isCompare = false}) async { try { - DateTime start = isCompare ? compareStartDate! : startDate!; - DateTime end = isCompare ? compareEndDate! : endDate!; - var res = await CRUD().get( + final start = isCompare ? compareStartDate! : startDate!; + final end = isCompare ? compareEndDate! : endDate!; + final res = await CRUD().get( link: AppLink.getEmployeeStatic, payload: _getPayload(start, end)); - - if (isCompare) { - chartDataEmployeerama1Compare = []; - chartDataEmployeeshahdCompare = []; - chartDataEmployeeRama2Compare = []; - chartDataEmployeeSefer4Compare = []; - } else { - chartDataEmployeerama1 = []; - chartDataEmployeeshahd = []; - chartDataEmployeeRama2 = []; - chartDataEmployeeSefer4 = []; - } - if (res == 'failure') return; - var jsonResponse = jsonDecode(res) as Map; - if (jsonResponse['status'] == 'failure') return; - final List jsonData = jsonResponse['message']; - if (jsonData.isEmpty) return; + final json = jsonDecode(res) as Map; + if (json['status'] == 'failure') return; + final List data = json['message']; + if (data.isEmpty) return; - Map> dateNameMap = {}; - for (var item in jsonData) { - String dateKeyStr = item['date'] ?? item['day']; - String name = item['NAME'].toString().toLowerCase().trim(); - double count = double.tryParse(item['count'].toString()) ?? 0.0; - if (!dateNameMap.containsKey(dateKeyStr)) dateNameMap[dateKeyStr] = {}; - if (dateNameMap[dateKeyStr]!.containsKey(name)) { - dateNameMap[dateKeyStr]![name] = - dateNameMap[dateKeyStr]![name]! + count; + final dateNameMap = _parseDateNameMap(data); + final spotsMap = _generateEmployeeSpots(dateNameMap, start, end); + + // Merge into employee data map + final target = isCompare ? employeeDataCompare : employeeData; + + spotsMap.forEach((name, spots) { + final color = _getOrAssignColor(name); + if (target.containsKey(name)) { + target[name] = target[name]!.copyWith(notesSpots: spots); } else { - dateNameMap[dateKeyStr]![name] = count; - } - } - - final targetLists = isCompare - ? { - 'rama1': chartDataEmployeerama1Compare, - 'shahd': chartDataEmployeeshahdCompare, - 'rama2': chartDataEmployeeRama2Compare, - 'mayar': chartDataEmployeeSefer4Compare - } - : { - 'rama1': chartDataEmployeerama1, - 'shahd': chartDataEmployeeshahd, - 'rama2': chartDataEmployeeRama2, - 'mayar': chartDataEmployeeSefer4 - }; - - int totalDays = end.difference(start).inDays + 1; - targetLists.forEach((key, listToFill) { - for (int i = 0; i < totalDays; i++) { - DateTime currentDate = start.add(Duration(days: i)); - String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate); - double value = 0; - Map? dayData = dateNameMap[currentDateStr]; - if (dayData != null) { - // if (key == 'mayar') { - // value = (dayData['mayar'] ?? 0) + - // (dayData['rama1'] ?? 0) + - // (dayData['sefer4'] ?? 0); - // } else { - value = dayData[key] ?? 0; - // } - } - listToFill.add(FlSpot((i + 1).toDouble(), value)); + target[name] = EmployeeChartData( + name: name, + color: color, + notesSpots: spots, + callsSpots: [], + ); } }); } catch (e) { - Log.print('Error in fetchEmployee: $e'); + Log.print('Error in fetchEmployeeDynamic: $e'); } } - Future fetchEditorCalls({bool isCompare = false}) async { + // ─── 🔥 DYNAMIC: Employee Calls ─────────────────────────── + Future fetchEditorCallsDynamic({bool isCompare = false}) async { try { - DateTime start = isCompare ? compareStartDate! : startDate!; - DateTime end = isCompare ? compareEndDate! : endDate!; - - var res = await CRUD().get( + final start = isCompare ? compareStartDate! : startDate!; + final end = isCompare ? compareEndDate! : endDate!; + final res = await CRUD().get( link: AppLink.getEditorStatsCalls, payload: _getPayload(start, end)); - - if (isCompare) { - chartDataCallsrama1Compare = []; - chartDataCallsShahdCompare = []; - chartDataCallsRama2Compare = []; - chartDataCallsSefer4Compare = []; - } else { - chartDataCallsrama1 = []; - chartDataCallsShahd = []; - chartDataCallsRama2 = []; - chartDataCallsSefer4 = []; - } - if (res == 'failure') return; + final json = jsonDecode(res) as Map; + if (json['status'] == 'failure') return; + final List data = json['message']; + if (data.isEmpty) return; - var jsonResponse = jsonDecode(res) as Map; - if (jsonResponse['status'] == 'failure') return; + final dateNameMap = _parseDateNameMap(data); + final spotsMap = _generateEmployeeSpots(dateNameMap, start, end); - final List jsonData = jsonResponse['message']; - if (jsonData.isEmpty) return; + final target = isCompare ? employeeDataCompare : employeeData; - Map> dateNameMap = {}; - - for (var item in jsonData) { - String dateKeyStr = item['date'] ?? item['day']; - String name = item['NAME'].toString().toLowerCase().trim(); - double count = double.tryParse(item['count'].toString()) ?? 0.0; - - if (!dateNameMap.containsKey(dateKeyStr)) { - dateNameMap[dateKeyStr] = {}; - } - - if (dateNameMap[dateKeyStr]!.containsKey(name)) { - dateNameMap[dateKeyStr]![name] = - dateNameMap[dateKeyStr]![name]! + count; + spotsMap.forEach((name, spots) { + final color = _getOrAssignColor(name); + if (target.containsKey(name)) { + target[name] = target[name]!.copyWith(callsSpots: spots); } else { - dateNameMap[dateKeyStr]![name] = count; - } - } - - final targetLists = isCompare - ? { - 'rama1': chartDataCallsrama1Compare, - 'shahd': chartDataCallsShahdCompare, - 'rama2': chartDataCallsRama2Compare, - 'mayar': chartDataCallsSefer4Compare, - } - : { - 'rama1': chartDataCallsrama1, - 'shahd': chartDataCallsShahd, - 'rama2': chartDataCallsRama2, - 'mayar': chartDataCallsSefer4, - }; - - int totalDays = end.difference(start).inDays + 1; - - targetLists.forEach((key, listToFill) { - for (int i = 0; i < totalDays; i++) { - DateTime currentDate = start.add(Duration(days: i)); - String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate); - - double value = 0; - Map? dayData = dateNameMap[currentDateStr]; - - if (dayData != null) { - // if (key == 'mayar_group') { - // value = (dayData['mayar'] ?? 0) + - // (dayData['rama1'] ?? 0) + - // (dayData['sefer4'] ?? 0); - // } else { - value = dayData[key] ?? 0; - // } - } - listToFill.add(FlSpot((i + 1).toDouble(), value)); + target[name] = EmployeeChartData( + name: name, + color: color, + notesSpots: [], + callsSpots: spots, + ); } }); } catch (e) { - Log.print('Error in fetchEditorCalls: $e'); + Log.print('Error in fetchEditorCallsDynamic: $e'); } } - // --- 🔴 FIXED: Fetch Employment Stats with Unique Check --- + // ─── Employment Stats ───────────────────────────────────── Future fetchEmploymentStats() async { try { - // لا نستخدم .clear() هنا، سنقوم باستبدال القائمة بالكامل في النهاية - - var res = await CRUD().get( + final res = await CRUD().get( link: AppLink.getEmployeeDriverAfterCallingRegister, payload: _getPayload(startDate!, endDate!)); - if (res == 'failure') return; + final json = jsonDecode(res); + if (json['status'] != 'success') return; + final List data = json['message']?['data'] ?? []; - var jsonResponse = jsonDecode(res); - if (jsonResponse['status'] == 'success') { - if (jsonResponse['message'] != null && - jsonResponse['message']['data'] != null) { - List data = jsonResponse['message']['data']; - - List allowedNames = ['shahd', 'mayar', 'rama1', 'rama2']; - - // استخدام Map لضمان عدم تكرار الأسماء (تجميع القيم) - Map uniqueMap = {}; - - for (var item in data) { - String name = - item['employmentType'].toString().toLowerCase().trim(); - int count = int.tryParse(item['count'].toString()) ?? 0; - - if (allowedNames.contains(name)) { - if (uniqueMap.containsKey(name)) { - uniqueMap[name] = uniqueMap[name]! + count; - } else { - uniqueMap[name] = count; - } - } - } - - // تحويل الـ Map إلى القائمة النهائية - List> tempList = []; - uniqueMap.forEach((key, value) { - tempList.add({'name': key, 'count': value}); - }); - - // استبدال القائمة القديمة بالقائمة الجديدة النظيفة - employmentStatsList = tempList; - } + // Aggregate by name (dynamic — no hardcoded allowed list) + final Map aggregated = {}; + for (var item in data) { + final name = item['employmentType'].toString().toLowerCase().trim(); + final count = int.tryParse(item['count'].toString()) ?? 0; + aggregated[name] = (aggregated[name] ?? 0) + count; } + + employmentStatsList = aggregated.entries.map((e) { + return EmploymentStat( + name: e.key, + count: e.value, + color: _getOrAssignColor(e.key), + ); + }).toList() + ..sort((a, b) => b.count.compareTo(a.count)); // sort descending } catch (e) { Log.print("Error fetchEmploymentStats: $e"); } } - // --- Fetch Daily Notes Log --- + // ─── Daily Notes ────────────────────────────────────────── Future fetchDailyNotes(DateTime date) async { try { isLoadingNotes = true; dailyNotesList.clear(); update(); - var res = await CRUD().post( + final res = await CRUD().post( link: AppLink.getNotesForEmployee, payload: {"date": DateFormat('yyyy-MM-dd').format(date)}); if (res != 'failure') { - var jsonResponse = (res); - if (jsonResponse['status'] == 'success') { - dailyNotesList = jsonResponse['message']; + final json = res; + if (json['status'] == 'success') { + dailyNotesList = json['message']; } } } catch (e) { @@ -504,4 +485,28 @@ class StaticController extends GetxController { update(); } } + + // ─── Computed Summaries for UI ──────────────────────────── + + /// Returns sorted list of employees by total notes descending + List get employeesSortedByNotes { + final list = employeeData.values.toList(); + list.sort((a, b) => b.totalNotes.compareTo(a.totalNotes)); + return list; + } + + /// Returns sorted list of employees by total calls descending + List get employeesSortedByCalls { + final list = employeeData.values.toList(); + list.sort((a, b) => b.totalCalls.compareTo(a.totalCalls)); + return list; + } + + /// Grand total notes across all employees + int get grandTotalNotes => + employeeData.values.fold(0, (s, e) => s + e.totalNotes); + + /// Grand total calls across all employees + int get grandTotalCalls => + employeeData.values.fold(0, (s, e) => s + e.totalCalls); } diff --git a/lib/controller/functions/wallet.dart b/lib/controller/functions/wallet.dart index be1aa63..43667a4 100644 --- a/lib/controller/functions/wallet.dart +++ b/lib/controller/functions/wallet.dart @@ -56,19 +56,19 @@ class WalletController extends GetxController { }); } - Future addDrivergift3000(String paymentMethod, driverID, point, phone) async { + Future addDrivergift300(String paymentMethod, driverID, point, phone) async { // paymentToken = await generateToken(count); // var paymentID = await getPaymentId(paymentMethod, point.toString()); var res = await CRUD().postWallet(link: AppLink.add300ToDriver, payload: { 'driverID': driverID.toString(), 'paymentID': paymentMethod, 'amount': point, - 'token': 'gift_connect_30000', + 'token': 'gift_connect_300', 'paymentMethod': paymentMethod, 'phone': phone, }); if (res != 'failure') { - Get.snackbar('success', 'addDrivergift3000', + Get.snackbar('success', 'addDrivergift300', backgroundColor: AppColor.greenColor); } else { Get.snackbar('error', res, backgroundColor: AppColor.redColor); diff --git a/lib/controller/notification_controller.dart b/lib/controller/notification_controller.dart index fc0df9a..80f8000 100644 --- a/lib/controller/notification_controller.dart +++ b/lib/controller/notification_controller.dart @@ -1,234 +1,237 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sefer_admin1/constant/box_name.dart'; -import 'package:sefer_admin1/constant/links.dart'; -import 'package:sefer_admin1/controller/firebase/firbase_messge.dart'; -import 'package:sefer_admin1/controller/functions/crud.dart'; -import 'package:sefer_admin1/main.dart'; -import 'package:sefer_admin1/views/widgets/elevated_btn.dart'; -import 'package:sefer_admin1/views/widgets/my_textField.dart'; - -import '../constant/style.dart'; -import '../print.dart'; import 'firebase/notification_service.dart'; class NotificationController extends GetxController { - final formKey = GlobalKey(); - final title = TextEditingController(); - final body = TextEditingController(); - List tokensDriver = []; - List tokensPassengers = []; + final GlobalKey formKey = GlobalKey(); + final TextEditingController titleController = TextEditingController(); + final TextEditingController bodyController = TextEditingController(); - // getTokensDrivers() async { - // await FirebaseMessagesController().loadAllPagesAndSendNotifications(); - // } + // ألوان الثيم (متناسقة مع باقي الصفحات المحسنة) + final Color _dialogColor = const Color(0xFF1A1F3A); + final Color _inputColor = const Color(0xFF0A0E27); + final Color _primaryAccent = const Color(0xFF6366F1); - // getTokensPassengers() async { - // await FirebaseMessagesController() - // .loadAllPagesAndSendNotificationsPassengers(); - // } - - Future sendNotificationDrivers() { - return Get.defaultDialog( - title: 'send notification'.tr, - titleStyle: AppStyle.title, - content: Form( - key: formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: MyTextForm( - controller: title, - label: 'title notification'.tr, - hint: 'title notification'.tr, - type: TextInputType.name), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: MyTextForm( - controller: body, - label: 'body notification'.tr, - hint: 'body notification'.tr, - type: TextInputType.name), - ), - ], - ), - ), - confirm: MyElevatedButton( - title: 'send'.tr, - onPressed: () async { - // tokensDriver = box.read(BoxName.tokensDrivers)['message']; - // Log.print('tokensDriver: ${tokensDriver}'); - // if (formKey.currentState!.validate()) { - // box.read(BoxName.tokensDrivers)['message'].length; - // for (var i = 0; - // i < box.read(BoxName.tokensDrivers)['message'].length; - // i++) { - // // for (var i = 0; i < 2; i++) { - // // print(i); - // var res = await CRUD() - // .post(link: AppLink.addNotificationCaptain, payload: { - // "driverID": box - // .read(BoxName.tokensDrivers)['message'][i]['id'] - // .toString(), - // "title": title.text, - // "body": body.text, - // "isPin": 'unPin', - // }); - // Log.print( - // 'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}'); - // // Log.print('tokensDriver[i]: ${tokensDriver[i]}'); - // Future.delayed(const Duration(microseconds: 50)); - NotificationService.sendNotification( - target: 'drivers', // الإرسال لجميع المشتركين في "service" - title: title.text, - body: body.text, - isTopic: true, - category: 'fromAdmin', // فئة توضح نوع الإشعار - ); - // FirebaseMessagesController().sendNotificationToAnyWithoutData( - // title.text, - // body.text, - // box - // .read(BoxName.tokensDrivers)['message'][i]['token'] - // .toString(), - // 'tone2.wav'); - // } - Get.back(); - // } - }), - cancel: MyElevatedButton( - title: 'cancel', - onPressed: () { - Get.back(); - })); + @override + void onClose() { + titleController.dispose(); + bodyController.dispose(); + super.onClose(); } - Future sendNotificationPassengers() { - return Get.defaultDialog( - title: 'send notification'.tr, - titleStyle: AppStyle.title, - content: Form( - key: formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: MyTextForm( - controller: title, - label: 'title notification'.tr, - hint: 'title notification'.tr, - type: TextInputType.name), + /// تنظيف الحقول + void _clearFields() { + titleController.clear(); + bodyController.clear(); + } + + /// إرسال إشعار للسائقين + Future sendNotificationDrivers() async { + _clearFields(); + await _showCustomDialog( + dialogTitle: 'إشعار للسائقين', + targetAudience: 'drivers', + icon: Icons.drive_eta_rounded, + iconColor: Colors.orangeAccent, + ); + } + + /// إرسال إشعار للركاب + Future sendNotificationPassengers() async { + _clearFields(); + await _showCustomDialog( + dialogTitle: 'إشعار للركاب', + targetAudience: 'passengers', + icon: Icons.people_alt_rounded, + iconColor: Colors.blueAccent, + ); + } + + /// دالة عامة لإظهار نافذة الإرسال بتصميم عصري + Future _showCustomDialog({ + required String dialogTitle, + required String targetAudience, + required IconData icon, + required Color iconColor, + }) { + return Get.dialog( + Dialog( + backgroundColor: _dialogColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + elevation: 10, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 1. أيقونة العنوان + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.15), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: iconColor.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon(icon, color: iconColor, size: 32), + ), + const SizedBox(height: 16), + + // 2. العنوان + Text( + dialogTitle, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'Segoe UI', + ), + ), + const SizedBox(height: 24), + + // 3. حقول الإدخال + _buildModernTextField( + controller: titleController, + hint: 'عنوان الإشعار', + icon: Icons.title_rounded, + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: bodyController, + hint: 'نص الرسالة', + icon: Icons.message_rounded, + maxLines: 3, + ), + const SizedBox(height: 32), + + // 4. الأزرار + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + foregroundColor: Colors.white54, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('إلغاء'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + if (titleController.text.trim().isEmpty || + bodyController.text.trim().isEmpty) { + Get.snackbar( + "تنبيه", + "الرجاء تعبئة جميع الحقول", + backgroundColor: Colors.amber.withOpacity(0.8), + colorText: Colors.white, + snackPosition: SnackPosition.TOP, + ); + return; + } + + Get.back(); // إغلاق النافذة + await _processSending(targetAudience); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _primaryAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text('إرسال'), + ), + ), + ], + ), + ], ), - Padding( - padding: const EdgeInsets.all(8.0), - child: MyTextForm( - controller: body, - label: 'body notification'.tr, - hint: 'body notification'.tr, - type: TextInputType.name), - ), - ], + ), ), ), - confirm: MyElevatedButton( - title: 'send'.tr, - onPressed: () async { - // tokensPassengers = box.read(BoxName.tokensPassengers); -// var tokensPassengersData = -// box.read(BoxName.tokensPassengers)['data']; + ), + barrierColor: Colors.black.withOpacity(0.7), // تعتيم الخلفية + ); + } -// // Debug print to check structure of the 'data' field -// print('Tokens Passengers Data: $tokensPassengersData'); + /// تنفيذ عملية الإرسال الفعلية + Future _processSending(String target) async { + // إظهار تنبيه بدء العملية + Get.snackbar( + "جاري الإرسال", + "يتم إرسال الإشعار لـ $target...", + backgroundColor: _primaryAccent.withOpacity(0.2), + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); -// if (tokensPassengersData is List) { -// for (var i = 0; i < tokensPassengersData.length; i++) { -// if (formKey.currentState!.validate()) { -// var res = await CRUD() -// .post(link: AppLink.addNotificationPassenger, payload: { -// "passenger_id": -// tokensPassengersData[i]['passengerID'].toString(), -// "title": title.text, -// "body": body.text, -// }); -// Log.print('res: ${res}'); -// FirebaseMessagesController() -// .sendNotificationToAnyWithoutData( -// title.text, -// body.text, -// tokensPassengersData[i]['token'] -// .toString(), // Access token correctly -// 'order.wav', -// ); -// } -// } - NotificationService.sendNotification( - target: 'passengers', // الإرسال لجميع المشتركين في "service" - title: title.text, - body: body.text, - isTopic: true, - category: 'fromAdmin', // فئة توضح نوع الإشعار - ); - Get.back(); -// } else { -// // Handle the case where 'data' is not a list -// print('Data is not a list: $tokensPassengersData'); -// } - }), - cancel: MyElevatedButton( - title: 'cancel', - onPressed: () { - Get.back(); - })); + try { + // استدعاء خدمة الإرسال + await NotificationService.sendNotification( + target: target, + title: titleController.text, + body: bodyController.text, + isTopic: true, + category: 'fromAdmin', + ); + + Get.snackbar( + "نجاح", + "تم إرسال الإشعار بنجاح", + backgroundColor: Colors.green.withOpacity(0.5), + colorText: Colors.white, + ); + } catch (e) { + Get.snackbar( + "خطأ", + "فشل إرسال الإشعار: $e", + backgroundColor: Colors.red.withOpacity(0.5), + colorText: Colors.white, + ); + } + } + + /// تصميم حقل الإدخال المخصص (Dark Input Field) + Widget _buildModernTextField({ + required TextEditingController controller, + required String hint, + required IconData icon, + int maxLines = 1, + }) { + return Container( + decoration: BoxDecoration( + color: _inputColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + child: TextField( + controller: controller, + style: const TextStyle(color: Colors.white), + maxLines: maxLines, + decoration: InputDecoration( + hintText: hint, + hintStyle: + TextStyle(color: Colors.white.withOpacity(0.3), fontSize: 14), + prefixIcon: Icon(icon, color: Colors.white38, size: 20), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + ); } } -// يلا دلوقتي! تطبيق سفر جاهز عشان تبدأ تستقبل الطلبات -// • افتح التطبيق دلوقتي، واستعد إنك تستقبل طلبات أكتر. كل ما تكون فاتح، فرصتك في الطلبات بتزيد! -// 2. خليك فاتح واستقبل طلبات أكتر مع تطبيق سفر -// • وجودك متصل في التطبيق هيخليك تستقبل طلبات أكتر. افتح التطبيق دلوقتي وما تفوتش الفرصة! -// 3. فرصتك لزيادة دخلك مع تطبيق سفر تبدأ من دلوقتي! -// • مجرد إنك تفتح التطبيق مش هيأثر عليك، بالعكس، هيزود فرصتك في طلبات أكتر. افتح التطبيق واشترك دلوقتي! - -//sms -// link sefer driver is https://shorturl.at/IHJcm1. -// // ميزات الأمان بعد 500 رحلة: -// • “بعد 500 رحلة مع سفر، تحصل على مميزات أمان إضافية لضمان راحتك.” -// • “نوفر لك ميزات أمان متقدمة بعد 500 رحلة لتجربة قيادة أكثر أمانًا.” -// • “مع 500 رحلة، تحصل على دعم أمني متقدم لتوفير أفضل تجربة قيادة.” -// 2. ميزات الصيانة: -// • “احصل على خدمات صيانة مجانية بعد عدد معين من الرحلات مع سفر.” -// • “استمتع بخدمات صيانة حصرية عند الوصول إلى عدد محدد من الرحلات.” -// • “مع سفر، نقدم لك عروض صيانة مميزة لتحافظ على سيارتك في أفضل حال.” -// 3. ميزات فتح حسابات البنوك: -// • “مع سفر، يمكنك فتح حساب بنكي بسهولة واستفادة من عروض مميزة.” -// • “افتح حساب بنكي مع تطبيق سفر واستفد من خدمات مالية حصرية.” -// • “نساعدك على فتح حساب بنكي بأفضل العروض بالتعاون مع البنوك المحلية.” -// 4. ميزات ورود السيارات ومعارض السيارات الخاصة بنا: -// • “استمتع بعروض مميزة لشراء السيارات من معارض سفر الحصرية.” -// • “اختر سيارتك المثالية من معارض سفر بأسعار تنافسية وخدمات مميزة.” -// • “نقدم لك أفضل عروض السيارات من معارضنا لتسهيل امتلاك سيارتك الجديدة.” -// 5. ميزات أوفر كار: -// • “أوفر كار من سفر توفر لك سيارات اقتصادية لزيادة دخلك بكفاءة.” -// • “مع أوفر كار، يمكنك العمل بسيارات اقتصادية وتحقيق أرباح أكبر.” -// • “تطبيق سفر يقدم لك أوفر كار، الخيار الاقتصادي المثالي لزيادة دخلك.” -// 6. مستوى الدخل المحدود والطلبات الاقتصادية: -// • “لأصحاب الدخل المحدود، وفرنا طلبات اقتصادية تضمن لك زيادة دخلك.” -// • “الطلبات الاقتصادية من سفر تساعدك على زيادة دخلك بسهولة وفعالية.” -// • “استفد من طلبات اقتصادية تناسب أصحاب الدخل المحدود لزيادة أرباحك.” -// 7. طلبات الليل: -// • “مع طلبات الليل من سفر، زود دخلك واستفد من فرص إضافية في المساء.” -// • “لا تفوت فرصة طلبات الليل مع سفر، زود دخلك في أي وقت.” -// • “طلبات الليل من سفر توفر لك فرصًا إضافية لتحقيق دخل أعلى.” -// 8. طلبات الكمفورت الأكثر راحة والسيارات المكيفة: -// • “قدّم خدمة مريحة مع طلبات الكمفورت من سفر والسيارات المكيفة.” -// • “طلبات الكمفورت توفر تجربة راقية للركاب بسيارات مكيفة ومريحة.” -// • “مع سفر، سيارات الكمفورت المكيفة تضمن راحة الركاب وزيادة الطلبات.” -// 9. طلبات السبيد: -// • “استقبل طلبات السبيد مع سفر لتقديم رحلات أسرع وزيادة دخلك.” -// • “طلبات السبيد توفر لك فرصة إكمال المزيد من الرحلات في وقت أقل.” -// • “مع طلبات السبيد من سفر، تقدم خدمة سريعة وفعالة لزيادة الأرباح.” -// 10. الطلبات الثابتة والمعتدلة السعر والنسبة الثابتة 8%: -// • “مع نسبة ثابتة 8%، تحصل على أفضل عروض الأسعار مع سفر.” -// • “استمتع بنسبة ثابتة 8%، أقل نسبة بين المنافسين لزيادة دخلك.” -// • “طلبات سفر الثابتة تضمن لك دخلاً مستقراً بنسبة أقل من 8%.” diff --git a/lib/controller/server/server_monitor_controller.dart b/lib/controller/server/server_monitor_controller.dart new file mode 100644 index 0000000..e3b2519 --- /dev/null +++ b/lib/controller/server/server_monitor_controller.dart @@ -0,0 +1,150 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:sefer_admin1/constant/links.dart'; + +import '../../print.dart'; + +// --- Models --- + +class ServerData { + final CpuInfo cpu; + final MemoryInfo memory; + final DiskInfo disk; + final Map services; + final List topProcesses; + final NetworkInfo network; + final UptimeInfo uptime; + final String timestamp; + + ServerData({ + required this.cpu, + required this.memory, + required this.disk, + required this.services, + required this.topProcesses, + required this.network, + required this.uptime, + required this.timestamp, + }); + + factory ServerData.fromJson(Map json) { + return ServerData( + cpu: CpuInfo.fromJson(json['cpu']), + memory: MemoryInfo.fromJson(json['memory']), + disk: DiskInfo.fromJson(json['disk']), + services: Map.from(json['services']), + topProcesses: (json['top_processes'] as List) + .map((i) => ProcessInfo.fromJson(i)) + .toList(), + network: NetworkInfo.fromJson(json['network']), + uptime: UptimeInfo.fromJson(json['uptime']), + timestamp: json['timestamp'], + ); + } +} + +class CpuInfo { + final double percent; + final int cores; + final double load1m; + CpuInfo({required this.percent, required this.cores, required this.load1m}); + factory CpuInfo.fromJson(Map json) => CpuInfo( + percent: json['percent'].toDouble(), + cores: json['cores'], + load1m: json['load_1m'].toDouble()); +} + +class MemoryInfo { + final double percent; + final double usedGb; + final double totalGb; + MemoryInfo( + {required this.percent, required this.usedGb, required this.totalGb}); + factory MemoryInfo.fromJson(Map json) => MemoryInfo( + percent: json['percent'].toDouble(), + usedGb: json['used_gb'].toDouble(), + totalGb: json['total_gb'].toDouble()); +} + +class DiskInfo { + final double percent; + final double usedGb; + final double totalGb; + DiskInfo( + {required this.percent, required this.usedGb, required this.totalGb}); + factory DiskInfo.fromJson(Map json) => DiskInfo( + percent: json['percent'].toDouble(), + usedGb: json['used_gb'].toDouble(), + totalGb: json['total_gb'].toDouble()); +} + +class ProcessInfo { + final String name; + final String usage; + ProcessInfo({required this.name, required this.usage}); + factory ProcessInfo.fromJson(Map json) => + ProcessInfo(name: json['name'], usage: json['usage']); +} + +class NetworkInfo { + final double receivedMb; + final double sentMb; + NetworkInfo({required this.receivedMb, required this.sentMb}); + factory NetworkInfo.fromJson(Map json) => NetworkInfo( + receivedMb: json['received_mb'].toDouble(), + sentMb: json['sent_mb'].toDouble()); +} + +class UptimeInfo { + final String formatted; + UptimeInfo({required this.formatted}); + factory UptimeInfo.fromJson(Map json) => + UptimeInfo(formatted: json['formatted']); +} + +// --- Controller --- + +class ServerMonitorController extends GetxController { + var isLoading = false.obs; + var serverData = Rxn(); + var errorMessage = ''.obs; + + Timer? _timer; // تخزين التايمر + + @override + void onInit() { + super.onInit(); + fetchServerData(); + // تحديث تلقائي كل 60 ثانية + _timer = Timer.periodic(Duration(seconds: 60), (_) { + fetchServerData(); + }); + } + + @override + void onClose() { + // إلغاء التحديث عند إغلاق الصفحة + _timer?.cancel(); + super.onClose(); + } + + Future fetchServerData() async { + try { + isLoading(true); + errorMessage(''); + + final response = await GetConnect().get(AppLink.serverMonitor); + + if (response.status.hasError) { + errorMessage.value = 'خطأ في الاتصال: ${response.statusText}'; + } else { + serverData.value = ServerData.fromJson(response.body); + } + } catch (e) { + errorMessage.value = 'حدث خطأ: $e'; + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/admin/admin_home_page.dart b/lib/views/admin/admin_home_page.dart index 1b41176..5058f49 100644 --- a/lib/views/admin/admin_home_page.dart +++ b/lib/views/admin/admin_home_page.dart @@ -1,598 +1,693 @@ import 'dart:math'; +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:get/get.dart'; -import 'package:sefer_admin1/constant/colors.dart'; -import 'package:sefer_admin1/controller/admin/dashboard_controller.dart'; -import 'package:sefer_admin1/controller/admin/static_controller.dart'; -import 'package:sefer_admin1/controller/notification_controller.dart'; -import 'package:sefer_admin1/main.dart'; +import 'package:sefer_admin1/views/admin/drivers/driver_gift_check_page.dart'; import 'package:sefer_admin1/views/admin/drivers/driver_tracker_screen.dart'; -import 'package:sefer_admin1/views/admin/error/error/error_page.dart'; -import 'package:sefer_admin1/views/widgets/mycircular.dart'; -// تأكد من صحة المسارات import '../../constant/box_name.dart'; +import '../../controller/admin/dashboard_controller.dart'; +import '../../controller/admin/static_controller.dart'; import '../../controller/functions/crud.dart'; +import '../../controller/notification_controller.dart'; +import '../../main.dart'; import '../invoice/invoice_list_page.dart'; -import '../widgets/my_textField.dart'; import 'captain/captain.dart'; import 'captain/syrian_driver_not_active.dart'; -import 'drivers/driver_gift_check_page.dart'; -import 'drivers/driver_the_best.dart'; import 'drivers/monitor_ride.dart'; import 'employee/employee_page.dart'; +import 'enceypt/driver_fingerprint_migration.dart'; +import 'enceypt/encrypt.dart'; +import 'enceypt/fingerprint_migration.dart'; +import 'error/error/error_page.dart'; import 'packages.dart'; import 'passenger/passenger.dart'; import 'rides/ride_lookup_page.dart'; +import 'server/monitor_server_page.dart'; import 'static/static.dart'; import 'wallet/wallet.dart'; -class AdminHomePage extends StatelessWidget { - AdminHomePage({super.key}); +class AdminHomePage extends StatefulWidget { + const AdminHomePage({super.key}); + @override + State createState() => _AdminHomePageState(); +} + +class _AdminHomePageState extends State + with SingleTickerProviderStateMixin { final TextEditingController _messageController = TextEditingController(); + final TextEditingController _searchController = TextEditingController(); + late AnimationController _pulseController; - // حساب عدد الأعمدة - int _calculateCrossAxisCount(BuildContext context, {bool isSmall = false}) { - double screenWidth = MediaQuery.of(context).size.width; - if (screenWidth > 1200) return isSmall ? 6 : 5; - if (screenWidth > 900) return isSmall ? 5 : 4; - if (screenWidth > 600) return isSmall ? 4 : 3; - return isSmall ? 3 : 2; - } + late bool isSuperAdmin; + late DashboardController dashboardController; + String _searchQuery = ''; - String _formatCurrency(dynamic value) { - if (value == null) return '0.00'; - final number = double.tryParse(value.toString()); - if (number != null) return number.toStringAsFixed(2); - return '0.00'; + // ══════════════════ 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); + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + String myPhone = box.read(BoxName.adminPhone).toString(); + isSuperAdmin = myPhone == '201023248456' || + myPhone == '963992952235' || + myPhone == '963942542053'; + dashboardController = Get.put(DashboardController()); } + @override + void dispose() { + _pulseController.dispose(); + _messageController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + // ══════════════════════════════════════════════════════════════ + // BUILD + // ══════════════════════════════════════════════════════════════ @override Widget build(BuildContext context) { - final DashboardController dashboardController = - Get.put(DashboardController()); - - // 1. تحديد هوية المستخدم الحالي - String myPhone = box.read(BoxName.adminPhone).toString(); - - // 2. تحديد من هو "السوبر أدمن" الذي يرى كل شيء - // يمكنك إضافة المزيد من الأرقام هنا باستخدام || أو قائمة - bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235'; - - // 3. بناء القائمة باستخدام (Collection If) - final List> actionItems = [ - // --- عناصر يراها الجميع --- - { - 'title': 'الركاب', - 'icon': Icons.people_alt_rounded, - 'color': Colors.blueAccent, - 'onPressed': () => - Get.to(() => Passengrs(), transition: Transition.fadeIn) - }, - { - 'title': 'الكباتن', - 'icon': Icons.sports_motorsports_rounded, - 'color': Colors.orangeAccent, - 'onPressed': () => - Get.to(() => CaptainsPage(), transition: Transition.fadeIn) - }, - { - 'title': 'الرحلات', - 'icon': Icons.directions_car_filled_rounded, - 'color': Colors.indigoAccent, - 'onPressed': () => - Get.to(() => RidesDashboardScreen(), transition: Transition.fadeIn) - // Get.to(() => RideLookupPage(), transition: Transition.fadeIn) - }, - { - 'title': ' الكباتن النشطين', - 'icon': Icons.directions_car_filled_rounded, - 'color': Colors.indigoAccent, - 'onPressed': () => - Get.to(() => IntaleqTrackerScreen(), transition: Transition.fadeIn) - }, - - // --- عناصر خاصة بالسوبر أدمن فقط (Using Collection If) --- - - if (isSuperAdmin) - { - 'title': 'مراقبة الرحلات'.tr, - 'icon': Icons.route, // أيقونة مناسبة لمراقبة الرحلات - 'color': Colors.purpleAccent, - 'onPressed': () => - Get.to(() => RideMonitorScreen(), transition: Transition.fadeIn), - }, - - if (isSuperAdmin) - { - 'title': 'المحفظة', // الأمور المالية حساسة - 'icon': Icons.account_balance_wallet_rounded, - 'color': Colors.purpleAccent, - 'onPressed': () => - Get.to(() => Wallet(), transition: Transition.fadeIn) - }, - if (isSuperAdmin) - { - 'title': 'دفع هدية 300'.tr, - 'icon': Icons.card_giftcard, // أيقونة الهدية - 'color': Colors.purpleAccent, - 'onPressed': () => Get.to(() => DriverGiftCheckPage(), - transition: Transition.fadeIn), - }, - - // if (isSuperAdmin) - { - 'title': 'الإحصائيات', - 'icon': Icons.bar_chart_rounded, - 'color': Colors.teal, - 'onPressed': () async { - await Get.put(StaticController()).getAll(); - Get.to(() => const StaticDash()); - } - }, - - // هذا هو الجزء الذي طلبته تحديداً - if (isSuperAdmin) - { - 'title': 'واتساب سائقين', - 'icon': Icons.message_rounded, - 'color': Colors.green, - 'onPressed': () => _showWhatsAppDialog(context) - }, - - // --- عودة للعناصر العامة (أو يمكنك تقييدها أيضاً) --- - - if (isSuperAdmin) - { - 'title': 'إشعار للسائقين', - 'icon': Icons.notifications_active_rounded, - 'color': Colors.deepOrange, - 'onPressed': () async => - await Get.put(NotificationController()).sendNotificationDrivers() - }, - if (isSuperAdmin) - { - 'title': 'إشعار للركاب', - 'icon': Icons.notification_important_rounded, - 'color': Colors.pinkAccent, - 'onPressed': () async => await Get.put(NotificationController()) - .sendNotificationPassengers() - }, - if (isSuperAdmin) - { - 'title': 'تسجيل كابتن', - 'icon': Icons.person_add_alt_1_rounded, - 'color': Colors.cyan, - 'onPressed': () => Get.to(() => DriversPendingPage()) - }, - - if (isSuperAdmin) // تحديث الباقات للمدير فقط - { - 'title': 'تحديث الباقات', - 'icon': Icons.inventory_2_rounded, - 'color': Colors.brown, - 'onPressed': () => Get.to(() => PackageUpdateScreen()) - }, - - if (isSuperAdmin) // الموظفون للمدير فقط - { - 'title': 'الموظفون', - 'icon': Icons.badge_rounded, - 'color': Colors.blueGrey, - 'onPressed': () => Get.to(() => EmployeePage()) - }, - - if (isSuperAdmin) - { - 'title': 'أفضل السائقين', - 'icon': Icons.star_rounded, - 'color': Colors.amber, - 'onPressed': () => Get.to(() => DriverTheBestRedesigned()) - }, - - if (isSuperAdmin) // الفواتير للمدير فقط - { - 'title': 'الفواتير', - 'icon': Icons.receipt_long_rounded, - 'color': Colors.green.shade800, - 'onPressed': () => Get.to(() => InvoiceListPage()) - }, - - if (isSuperAdmin) - { - 'title': 'سجل الأخطاء', - 'icon': Icons.error_outline_rounded, - 'color': Colors.redAccent, - 'onPressed': () => Get.to(() => ErrorListPage()) - }, - ]; - return Scaffold( - backgroundColor: const Color(0xFFF5F7FA), + backgroundColor: _bg, body: RefreshIndicator( - onRefresh: () async { - await dashboardController.getDashBoard(); - }, - child: GetBuilder(builder: (controller) { - if (controller.dashbord.isEmpty) { - return const Center(child: MyCircularProgressIndicator()); - } + onRefresh: () async => await dashboardController.getDashBoard(), + color: _accent, + backgroundColor: _surface, + child: GetBuilder( + builder: (controller) { + if (controller.dashbord.isEmpty) { + return _buildLoadingState(); + } - final data = controller.dashbord[0]; + final data = controller.dashbord[0]; + final categories = _getFilteredCategories(); - // إحصائيات لوحة التحكم - final List> statCards = [ - // يمكنك تطبيق نفس المنطق هنا لإخفاء الأرباح عن الموظفين العاديين - if (isSuperAdmin) - { - 'title': 'رصيد الرسائل', - 'value': controller.creditSMS.toString(), - 'icon': Icons.sms_outlined, - 'color': Colors.lightBlue - }, - { - 'title': 'الركاب', - 'value': data['countPassengers'].toString(), - 'icon': Icons.people_alt_outlined, - 'color': Colors.teal - }, - { - 'title': 'السائقون', - 'value': data['countDriver'].toString(), - 'icon': Icons.sports_motorsports_outlined, - 'color': Colors.orange - }, - { - 'title': 'رحلات الشهر', - 'value': data['countRideThisMonth'].toString(), - 'icon': Icons.calendar_month_outlined, - 'color': Colors.purple - }, + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + _buildSliverAppBar(controller), + _buildSearchBar(), + if (_searchQuery.isEmpty) + _buildQuickStatsSection(data, controller), + SliverPadding( + padding: const EdgeInsets.only(bottom: 60), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final category = categories[index]; + if (category.items.isEmpty) + return const SizedBox.shrink(); - if (isSuperAdmin) // إخفاء الأمور المالية - { - 'title': 'متوسط التكلفة', - 'value': _formatCurrency(data['avg_passenger_price']), - 'icon': Icons.monetization_on_outlined, - 'color': Colors.green - }, - - { - 'title': 'الرحلات المكتملة', - 'value': data['completed_rides'].toString(), - 'icon': Icons.check_circle_outline, - 'color': AppColor.greenColor - }, - { - 'title': 'الرحلات الملغاة', - 'value': data['cancelled_rides'].toString(), - 'icon': Icons.cancel_outlined, - 'color': AppColor.redColor - }, - - // if (isSuperAdmin) // إخفاء المدفوعات - { - 'title': 'مدفوعات السائقين', - 'value': _formatCurrency(data['payments']), - 'icon': Icons.payments_outlined, - 'color': Colors.indigo - }, - // if (isSuperAdmin) - { - 'title': 'محفظة انطلق', - 'value': _formatCurrency(data['seferWallet']), - 'icon': Icons.account_balance_wallet_outlined, - 'color': Colors.deepOrange - }, - - { - 'title': 'عدد التحويلات', - 'value': data['transfer_from_count'].toString(), - 'icon': Icons.swap_horiz_outlined, - 'color': Colors.brown - }, - // ... بقية العناصر التي لا تحتاج إخفاء - { - 'title': 'رحلات الصباح', - 'value': data['morning_ride_count'].toString(), - 'icon': Icons.wb_sunny_outlined, - 'color': Colors.amber.shade700 - }, - { - 'title': 'رحلات المساء', - 'value': data['evening_ride_count'].toString(), - 'icon': Icons.brightness_4_outlined, - 'color': Colors.blueGrey - }, - { - 'title': 'رحلات الليل', - 'value': data['night_ride_count'].toString(), - 'icon': Icons.nightlight_round_outlined, - 'color': Colors.black87 - }, - { - 'title': 'كومفورت', - 'value': data['comfort'].toString(), - 'icon': Icons.event_seat_outlined, - 'color': Colors.cyan - }, - { - 'title': 'سبيد', - 'value': data['speed'].toString(), - 'icon': Icons.speed_outlined, - 'color': Colors.red.shade700 - }, - { - 'title': 'ليدي', - 'value': data['lady'].toString(), - 'icon': Icons.woman_2_outlined, - 'color': Colors.pink - }, - ]; - - return CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - SliverAppBar( - expandedHeight: 120.0, - floating: true, - pinned: true, - backgroundColor: AppColor.primaryColor, - elevation: 0, - flexibleSpace: FlexibleSpaceBar( - titlePadding: - const EdgeInsets.only(left: 16, right: 16, bottom: 16), - title: const Text( - 'لوحة التحكم', - style: TextStyle( - fontWeight: FontWeight.bold, color: Colors.white), - ), - background: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppColor.primaryColor, - AppColor.primaryColor.withOpacity(0.8), - ], - ), - ), - child: Stack( - children: [ - Positioned( - left: -20, - top: -20, - child: Icon(Icons.dashboard, - size: 150, color: Colors.white.withOpacity(0.1)), - ), - ], + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 450), + child: SlideAnimation( + verticalOffset: 40.0, + child: FadeInAnimation( + child: _buildCategorySection(category), + ), + ), + ); + }, + childCount: categories.length, ), ), ), - actions: [ - IconButton( - onPressed: () async => - await dashboardController.getDashBoard(), - icon: const Icon(Icons.refresh, color: Colors.white), - ), - ], - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 20, 16, 10), - child: Text( - "نظرة عامة", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[800]), - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: _calculateCrossAxisCount(context), - mainAxisSpacing: 12.0, - crossAxisSpacing: 12.0, - childAspectRatio: 1.6, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - final card = statCards[index]; - return AnimationConfiguration.staggeredGrid( - position: index, - duration: const Duration(milliseconds: 375), - columnCount: _calculateCrossAxisCount(context), - child: ScaleAnimation( - child: FadeInAnimation( - child: _buildModernStatCard( - title: card['title'], - value: card['value'], - icon: card['icon'], - color: card['color'], - ), - ), - ), - ); - }, - childCount: statCards.length, - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 24, 16, 10), - child: Text( - "إدارة النظام", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[800]), - ), - ), - ), - SliverPadding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: - _calculateCrossAxisCount(context, isSmall: true), - mainAxisSpacing: 12.0, - crossAxisSpacing: 12.0, - childAspectRatio: 1.0, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - final item = actionItems[index]; - return AnimationConfiguration.staggeredGrid( - position: index, - duration: const Duration(milliseconds: 375), - columnCount: - _calculateCrossAxisCount(context, isSmall: true), - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: _buildActionCard( - context, - title: item['title'], - icon: item['icon'], - color: item['color'], - onTap: item['onPressed'], - ), - ), - ), - ); - }, - childCount: actionItems.length, - ), - ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 40)), - ], - ); - }), + ], + ); + }, + ), ), ); } - // --- بقية الدوال (buildModernStatCard, buildActionCard, showWhatsAppDialog) تبقى كما هي --- - Widget _buildModernStatCard({ - required String title, - required String value, - required IconData icon, - required Color color, - }) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 10, - offset: const Offset(0, 4), + // ══════════════════════════════════════════════════════════════ + // LOADING STATE + // ══════════════════════════════════════════════════════════════ + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [_accent.withOpacity(0.3), Colors.transparent], + ), + ), + child: const Center( + child: SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator( + color: _accent, + strokeWidth: 2.5, + ), + ), + ), ), + const SizedBox(height: 16), + Text('جاري التحميل...', + style: TextStyle(color: _textSecondary, fontSize: 13)), ], ), + ); + } + + // ══════════════════════════════════════════════════════════════ + // SLIVER APP BAR + // ══════════════════════════════════════════════════════════════ + Widget _buildSliverAppBar(DashboardController controller) { + return SliverAppBar( + expandedHeight: 130.0, + floating: true, + pinned: true, + backgroundColor: _bg, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + background: Stack( + fit: StackFit.expand, + children: [ + // Aurora gradient background + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0D2137), + Color(0xFF0D1117), + Color(0xFF0F1F1A), + ], + ), + ), + ), + // Subtle glow orbs + Positioned( + top: -30, + left: -40, + child: _GlowOrb(color: _accent, size: 150, opacity: 0.08), + ), + Positioned( + top: -20, + right: -20, + child: _GlowOrb(color: _info, size: 120, opacity: 0.06), + ), + // Content + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLogo(), + const SizedBox(height: 6), + Text( + isSuperAdmin ? 'Super Admin Panel' : 'Admin Panel', + style: TextStyle( + color: _textSecondary, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + ], + ), + ), + ), + ], + ), + ), + actions: [ + _buildHeaderAction( + Icons.refresh_rounded, () => controller.getDashBoard()), + const SizedBox(width: 8), + ], + ); + } + + Widget _buildLogo() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _pulseController, + builder: (_, __) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + _accent.withOpacity(0.3 + 0.1 * _pulseController.value), + _accent.withOpacity(0.1), + ], + ), + border: Border.all( + color: + _accent.withOpacity(0.4 + 0.2 * _pulseController.value), + width: 1, + ), + ), + child: const Icon( + Icons.admin_panel_settings_rounded, + color: _accent, + size: 18, + ), + ); + }, + ), + const SizedBox(width: 10), + ShaderMask( + shaderCallback: (bounds) => const LinearGradient( + colors: [_accent, _info], + ).createShader(bounds), + child: const Text( + 'Intaleq Admin', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 20, + letterSpacing: 0.5, + ), + ), + ), + ], + ); + } + + Widget _buildHeaderAction(IconData icon, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _divider), + ), + child: Icon(icon, color: _textSecondary, size: 18), + ), + ); + } + + // ══════════════════════════════════════════════════════════════ + // SEARCH BAR + // ══════════════════════════════════════════════════════════════ + Widget _buildSearchBar() { + return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Container( + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: _searchQuery.isNotEmpty ? _accentBorder : _divider, + width: _searchQuery.isNotEmpty ? 1.5 : 1, + ), + ), + child: TextField( + controller: _searchController, + onChanged: (val) => setState(() => _searchQuery = val), + style: const TextStyle(color: _textPrimary, fontSize: 14), + decoration: InputDecoration( + hintText: 'ابحث عن خدمة أو ميزة...', + hintStyle: const TextStyle(color: _textSecondary, fontSize: 13), + prefixIcon: + const Icon(Icons.search_rounded, color: _accent, size: 20), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: Icon(Icons.close_rounded, + color: _textSecondary, size: 18), + onPressed: () { + setState(() { + _searchQuery = ''; + _searchController.clear(); + }); + }, + ) + : null, + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + ), + ), + ); + } + + // ══════════════════════════════════════════════════════════════ + // QUICK STATS SECTION + // ══════════════════════════════════════════════════════════════ + Widget _buildQuickStatsSection(dynamic data, DashboardController controller) { + final highlights = [ + _HighlightData( + 'إجمالي الركاب', data['countPassengers'], Icons.group_rounded, _info), + _HighlightData('إجمالي السائقين', data['countDriver'], + Icons.drive_eta_rounded, _warning), + _HighlightData('رحلات الشهر', data['countRideThisMonth'], + Icons.calendar_today_rounded, const Color(0xFFC792EA)), + if (isSuperAdmin) + _HighlightData('المحفظة', _formatCurrency(data['seferWallet']), + Icons.account_balance_wallet_rounded, _accent), + ]; + + final detailedStats = _getDetailedStats(data, controller); + + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), + // Section label + Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 10), + child: Row( + children: [ + Container( + width: 3, + height: 14, + decoration: BoxDecoration( + color: _accent, + borderRadius: BorderRadius.circular(2), + ), ), - child: Icon(icon, color: color, size: 20), - ), - ], + const SizedBox(width: 8), + const Text('نظرة عامة', + style: TextStyle( + color: _textSecondary, + fontSize: 11, + fontWeight: FontWeight.w600, + letterSpacing: 1.2, + )), + ], + ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - value, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[900], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + + // Highlight Cards + SizedBox( + height: 108, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + itemCount: highlights.length, + itemBuilder: (ctx, i) => Padding( + padding: EdgeInsets.only( + right: i < highlights.length - 1 ? 10 : 0), + child: _buildHighlightCard(highlights[i]), ), - const SizedBox(height: 4), - Text( - title, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + ), ), + + const SizedBox(height: 16), + + // Detailed stats strip + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _divider), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(detailedStats.length, (i) { + final stat = detailedStats[i]; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildDetailStatItem(stat), + if (i < detailedStats.length - 1) + Container( + width: 1, + height: 36, + color: _divider, + ), + ], + ); + }), + ), + ), + ), + ), + ), + + const SizedBox(height: 8), ], ), ), ); } - Widget _buildActionCard( - BuildContext context, { - required String title, - required IconData icon, - required Color color, - required VoidCallback onTap, - }) { - return Material( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - elevation: 0, - child: InkWell( - onTap: onTap, + Widget _buildHighlightCard(_HighlightData h) { + return Container( + width: 148, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _surface, borderRadius: BorderRadius.circular(16), + border: Border.all(color: h.color.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: h.color.withOpacity(0.08), + blurRadius: 16, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: h.color.withOpacity(0.12), + borderRadius: BorderRadius.circular(9), + ), + child: Icon(h.icon, color: h.color, size: 16), + ), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: h.color.withOpacity(0.6), + ), + ), + ], + ), + const Spacer(), + Text( + h.value.toString(), + style: const TextStyle( + color: _textPrimary, + fontSize: 20, + fontWeight: FontWeight.w700, + height: 1.1, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), + Text( + h.label, + style: const TextStyle( + color: _textSecondary, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } + + Widget _buildDetailStatItem(Map stat) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(stat['icon'] as IconData, + color: stat['color'] as Color, size: 20), + const SizedBox(height: 6), + Text( + stat['value'].toString(), + style: const TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1, + ), + ), + const SizedBox(height: 3), + Text( + stat['title'] as String, + style: const TextStyle( + color: _textSecondary, + fontSize: 9, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + // ══════════════════════════════════════════════════════════════ + // CATEGORY SECTION + // ══════════════════════════════════════════════════════════════ + Widget _buildCategorySection(ActionCategory category) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), + child: Row( + children: [ + Container( + width: 3, + height: 14, + decoration: BoxDecoration( + color: _accent, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + Text( + category.title, + style: const TextStyle( + color: _textPrimary, + fontSize: 15, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container(height: 1, color: _divider), + ), + const SizedBox(width: 8), + Text( + '${category.items.length}', + style: const TextStyle( + color: _textSecondary, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 120, + childAspectRatio: 0.88, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: category.items.length, + itemBuilder: (context, index) => + _buildActionItem(category.items[index]), + ), + const SizedBox(height: 8), + ], + ); + } + + Widget _buildActionItem(ActionItem item) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: item.onPressed, + borderRadius: BorderRadius.circular(16), + splashColor: item.color.withOpacity(0.1), + highlightColor: item.color.withOpacity(0.05), child: Container( decoration: BoxDecoration( + color: _surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), + border: Border.all(color: _divider), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(11), decoration: BoxDecoration( - shape: BoxShape.circle, gradient: LinearGradient( - colors: [color.withOpacity(0.2), color.withOpacity(0.05)], begin: Alignment.topLeft, end: Alignment.bottomRight, + colors: [ + item.color.withOpacity(0.20), + item.color.withOpacity(0.08), + ], ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: item.color.withOpacity(0.25)), + boxShadow: [ + BoxShadow( + color: item.color.withOpacity(0.15), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], ), - child: Icon(icon, size: 28, color: color), + child: Icon(item.icon, color: item.color, size: 22), ), - const SizedBox(height: 12), - Text( - title, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.black87, + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + item.title, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: _textPrimary, + fontSize: 11, + fontWeight: FontWeight.w500, + height: 1.3, + ), ), ), ], @@ -602,82 +697,363 @@ class AdminHomePage extends StatelessWidget { ); } - void _showWhatsAppDialog(BuildContext context) { - Get.dialog( - AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: const Text('إرسال رسالة جماعية', - style: TextStyle(fontWeight: FontWeight.bold)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.send, size: 40, color: Colors.green), - ), - const SizedBox(height: 15), - MyTextForm( - controller: _messageController, - label: 'الرسالة', - hint: 'اكتب الرسالة هنا...', - type: TextInputType.text, - ), + // ══════════════════════════════════════════════════════════════ + // DATA HELPERS + // ══════════════════════════════════════════════════════════════ + List _getFilteredCategories() { + final all = _getAllActionCategories(); + if (_searchQuery.isEmpty) return all; + + return all + .map((cat) { + final matched = cat.items + .where((item) => + item.title.toLowerCase().contains(_searchQuery.toLowerCase())) + .toList(); + return matched.isEmpty + ? null + : ActionCategory(title: cat.title, items: matched); + }) + .whereType() + .toList(); + } + + List _getAllActionCategories() { + return [ + ActionCategory( + title: 'المستخدمين', + items: [ + ActionItem('الركاب', Icons.people_outline_rounded, _info, + () => Get.to(() => Passengrs())), + ActionItem('السائقون', Icons.drive_eta_rounded, _warning, + () => Get.to(() => CaptainsPage())), + ActionItem('المراقب', Icons.track_changes_rounded, _danger, + () => Get.to(() => IntaleqTrackerScreen())), + ], + ), + ActionCategory( + title: 'العمليات', + items: [ + ActionItem('الرحلات', Icons.map_rounded, const Color(0xFF82AAFF), + () => Get.to(() => RidesDashboardScreen())), + if (isSuperAdmin) + ActionItem( + 'مراقبة الرحلات', + Icons.remove_red_eye_rounded, + const Color(0xFFC792EA), + () => Get.to(() => RideMonitorScreen())), + ActionItem('الإحصائيات', Icons.bar_chart_rounded, _accent, () async { + await Get.put(StaticController()).getAll(); + Get.to(() => const StaticDash()); + }), + ], + ), + if (isSuperAdmin) + ActionCategory( + title: 'المالية والإدارة', + items: [ + ActionItem('المحفظة', Icons.account_balance_wallet_rounded, _accent, + () => Get.to(() => Wallet())), + ActionItem('هدية 300', Icons.card_giftcard_rounded, _warning, + () => Get.to(() => DriverGiftCheckPage())), + ActionItem('الفواتير', Icons.receipt_long_rounded, + const Color(0xFF80CBC4), () => Get.to(() => InvoiceListPage())), + ActionItem('الموظفون', Icons.badge_rounded, const Color(0xFFB0BEC5), + () => Get.to(() => EmployeePage())), ], ), - actionsPadding: const EdgeInsets.all(15), - actions: [ - TextButton( - onPressed: () { - _messageController.clear(); - Get.back(); - }, - child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), - ), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - ), - icon: const Icon(Icons.send, size: 18, color: Colors.white), - label: const Text('إرسال', style: TextStyle(color: Colors.white)), - onPressed: () async { - if (_messageController.text.isNotEmpty) { - Get.back(); - var driverPhones = - box.read(BoxName.tokensDrivers)['message'] as List?; - if (driverPhones == null || driverPhones.isEmpty) { - Get.snackbar('تنبيه', 'لا توجد بيانات اتصال للسائقين', - backgroundColor: Colors.amber.withOpacity(0.5)); - return; - } + if (isSuperAdmin) + ActionCategory( + title: 'النظام والتواصل', + items: [ + ActionItem('واتساب جماعي', Icons.message_rounded, + const Color(0xFF4CAF50), () => _showWhatsAppDialog(context)), + ActionItem( + 'إشعار سائقين', + Icons.notifications_active_rounded, + const Color(0xFFFF7043), + () => Get.put(NotificationController()) + .sendNotificationDrivers()), + ActionItem( + 'إشعار ركاب', + Icons.notification_important_rounded, + const Color(0xFFF06292), + () => Get.put(NotificationController()) + .sendNotificationPassengers()), + ActionItem('تسجيل سائق', Icons.person_add_rounded, _info, + () => Get.to(() => DriversPendingPage())), + ActionItem( + 'تحديث التطبيق', + Icons.system_update_rounded, + const Color(0xFFA1887F), + () => Get.to(() => PackageUpdateScreen())), + ActionItem('مراقب السيرفر', Icons.dns_rounded, _accent, + () => Get.to(() => ServerMonitorPage())), + ActionItem('سجل الأخطاء', Icons.error_outline_rounded, _danger, + () => Get.to(() => ErrorListPage())), + ActionItem('encrypt fp', Icons.error_outline_rounded, _danger, + () => Get.to(() => FingerprintMigrationTool())), + ActionItem('encrypt fp drivers', Icons.error_outline_rounded, + _danger, () => Get.to(() => DriverFingerprintMigrationTool())), + ActionItem( + 'أداة التشفير', + Icons.lock_rounded, + const Color(0xFF9575CD), + () => Get.to(() => EncryptToolPage( + adminToken: box.read(BoxName.adminPhone), + ))), + ], + ), + ]; + } - Get.snackbar('جاري الإرسال', 'بدأت عملية الإرسال في الخلفية...', - backgroundColor: Colors.blue.withOpacity(0.3)); + List> _getDetailedStats( + dynamic data, DashboardController controller) { + return [ + // if (isSuperAdmin) + // { + // 'title': 'رصيد الرسائل', + // 'value': controller.creditSMS, + // 'icon': Icons.sms_rounded, + // 'color': _info, + // }, + { + 'title': 'مكتملة', + 'value': data['completed_rides'], + 'icon': Icons.check_circle_rounded, + 'color': _success, + }, + { + 'title': 'ملغاة', + 'value': data['cancelled_rides'], + 'icon': Icons.cancel_rounded, + 'color': _danger, + }, + { + 'title': 'مدفوعات', + 'value': _formatCurrency(data['payments']), + 'icon': Icons.attach_money_rounded, + 'color': _warning, + }, + { + 'title': 'Comfort', + 'value': data['comfort'], + 'icon': Icons.chair_rounded, + 'color': const Color(0xFF80CBC4), + }, + { + 'title': 'Speed', + 'value': data['speed'], + 'icon': Icons.flash_on_rounded, + 'color': const Color(0xFFFFD54F), + }, + { + 'title': 'Lady', + 'value': data['lady'], + 'icon': Icons.woman_rounded, + 'color': const Color(0xFFF48FB1), + }, + ]; + } - for (var driverData in driverPhones) { - if (driverData['phone'] != null) { - await CRUD().sendWhatsAppAuth( - driverData['phone'].toString(), - _messageController.text, - ); - await Future.delayed( - Duration(seconds: Random().nextInt(3) + 1)); - } - } - _messageController.clear(); - Get.snackbar('نجاح', 'تمت العملية بنجاح', - backgroundColor: Colors.green.withOpacity(0.5)); - } - }, + // ══════════════════════════════════════════════════════════════ + // WHATSAPP DIALOG + // ══════════════════════════════════════════════════════════════ + void _showWhatsAppDialog(BuildContext context) { + Get.dialog( + Dialog( + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: _surfaceElevated, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: _divider), + boxShadow: const [ + BoxShadow( + color: Colors.black45, + blurRadius: 30, + offset: Offset(0, 12), + ), + ], ), - ], + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF4CAF50).withOpacity(0.12), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFF4CAF50).withOpacity(0.25)), + ), + child: const Icon(Icons.message_rounded, + color: Color(0xFF4CAF50), size: 28), + ), + const SizedBox(height: 16), + const Text( + 'إرسال واتساب جماعي', + style: TextStyle( + color: _textPrimary, + fontSize: 17, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + const Text( + 'سيتم إرسال الرسالة لجميع السائقين', + style: TextStyle(color: _textSecondary, fontSize: 11), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + color: _bg, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: _divider), + ), + child: TextField( + controller: _messageController, + maxLines: 4, + style: const TextStyle(color: _textPrimary, fontSize: 13), + decoration: const InputDecoration( + hintText: 'اكتب رسالتك هنا...', + hintStyle: TextStyle(color: _textSecondary, fontSize: 12), + border: InputBorder.none, + contentPadding: EdgeInsets.all(14), + ), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: _divider), + ), + ), + child: const Text( + 'إلغاء', + style: TextStyle(color: _textSecondary, fontSize: 13), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.send_rounded, size: 16), + label: + const Text('إرسال', style: TextStyle(fontSize: 13)), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF4CAF50), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + onPressed: () async { + if (_messageController.text.isNotEmpty) { + Get.back(); + var driverPhones = box + .read(BoxName.tokensDrivers)['message'] as List?; + if (driverPhones == null || driverPhones.isEmpty) + return; + + Get.snackbar( + 'بدأ الإرسال', + 'سيتم الإرسال في الخلفية', + backgroundColor: + const Color(0xFF4CAF50).withOpacity(0.15), + colorText: _textPrimary, + borderRadius: 12, + margin: const EdgeInsets.all(16), + icon: const Icon(Icons.check_circle_rounded, + color: Color(0xFF4CAF50)), + ); + + for (var driverData in driverPhones) { + if (driverData['phone'] != null) { + await CRUD().sendWhatsAppAuth( + driverData['phone'].toString(), + _messageController.text); + await Future.delayed( + Duration(seconds: Random().nextInt(3) + 1)); + } + } + _messageController.clear(); + } + }, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + String _formatCurrency(dynamic value) { + if (value == null) return '0.0'; + return double.tryParse(value.toString())?.toStringAsFixed(1) ?? '0.0'; + } +} + +// ══════════════════════════════════════════════════════════════ +// HELPER WIDGETS +// ══════════════════════════════════════════════════════════════ +class _GlowOrb extends StatelessWidget { + final Color color; + final double size; + final double opacity; + + const _GlowOrb( + {required this.color, required this.size, required this.opacity}); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [color.withOpacity(opacity), Colors.transparent], + ), ), ); } } + +// ══════════════════════════════════════════════════════════════ +// DATA CLASSES +// ══════════════════════════════════════════════════════════════ +class _HighlightData { + final String label; + final dynamic value; + final IconData icon; + final Color color; + _HighlightData(this.label, this.value, this.icon, this.color); +} + +class ActionItem { + final String title; + final IconData icon; + final Color color; + final VoidCallback onPressed; + ActionItem(this.title, this.icon, this.color, this.onPressed); +} + +class ActionCategory { + final String title; + final List items; + ActionCategory({required this.title, required this.items}); +} diff --git a/lib/views/admin/captain/captain.dart b/lib/views/admin/captain/captain.dart index 118eb7e..6cf84cd 100644 --- a/lib/views/admin/captain/captain.dart +++ b/lib/views/admin/captain/captain.dart @@ -1,15 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// تأكد من استيراد مكتبة الاتصال إذا أردت تفعيل زر الاتصال فعلياً -// import 'package:url_launcher/url_launcher.dart'; import '../../../constant/box_name.dart'; -import '../../../controller/functions/launch.dart'; import '../../../main.dart'; import '../../widgets/my_scafold.dart'; -import '../../widgets/my_textField.dart'; import '../../widgets/mycircular.dart'; -import '../../../constant/style.dart'; import '../../../controller/admin/captain_admin_controller.dart'; import 'captain_details.dart'; @@ -20,14 +15,9 @@ class CaptainsPage extends StatelessWidget { Get.put(CaptainAdminController()); final TextEditingController searchController = TextEditingController(); - // 🔴 هام جداً: قم بتغيير هذا المتغير بناءً على حالة تسجيل الدخول الحقيقية في تطبيقك - // مثال: bool isAdmin = Get.find().isAdmin; - // final bool isAdmin = true; // اجعلها false لتجربة وضع المستخدم العادي String myPhone = box.read(BoxName.adminPhone).toString(); - - // 2. تحديد من هو "السوبر أدمن" الذي يرى كل شيء - // يمكنك إضافة المزيد من الأرقام هنا باستخدام || أو قائمة bool isSuperAdmin = false; + @override Widget build(BuildContext context) { isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235'; @@ -37,44 +27,45 @@ class CaptainsPage extends StatelessWidget { isleading: true, body: [ Container( - height: MediaQuery.of(context).size.height, // لضمان أخذ المساحة - padding: const EdgeInsets.all(16.0), + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).primaryColor.withOpacity(0.03), + Colors.white, + ], + ), + ), child: Column( children: [ - // --- شريط البحث المحسن --- - _buildSearchSection(context), - - const SizedBox(height: 20), - - // --- قائمة النتائج --- + _buildHeaderSection(context), Expanded( child: GetBuilder( builder: (controller) { if (controller.isLoading) { - return const Center(child: MyCircularProgressIndicator()); + return _buildLoadingState(); } - if (controller.captainData['message'] == null || - controller.captainData['message'].isEmpty) { + final message = controller.captainData['message']; + + if (message == null) { return _buildEmptyState(); } - return ListView.separated( - physics: const BouncingScrollPhysics(), - itemCount: controller.captainData['message'].length, - separatorBuilder: (context, index) => - const SizedBox(height: 12), - itemBuilder: (context, index) { - final captain = - controller.captainData['message'][index]; - return _buildCaptainCard(context, captain); - }, - ); + // 🔥 الحل هنا: توحيد الشكل إلى List + final List captains = + message is List ? message : [message]; + + if (captains.isEmpty) { + return _buildEmptyState(); + } + + return _buildResultsList(context, captains); }, ), ), - // مساحة إضافية في الأسفل لتجنب تداخل المحتوى مع الحواف - const SizedBox(height: 80), ], ), ), @@ -82,53 +73,107 @@ class CaptainsPage extends StatelessWidget { ); } - // --- ودجت البحث --- - Widget _buildSearchSection(BuildContext context) { + // ================= HEADER ================= + + Widget _buildHeaderSection(BuildContext context) { return Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 2, + color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 2), ), ], ), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.manage_search_rounded, + color: Theme.of(context).primaryColor, + size: 24, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Find Captain'.tr, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + 'Search by phone number'.tr, + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + _buildModernSearchBar(context), + ], + ), + ); + } + + Widget _buildModernSearchBar(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey[200]!), + ), child: Row( children: [ Expanded( - child: MyTextForm( + child: TextField( controller: searchController, - label: 'Captain Phone Number'.tr, - hint: 'Enter phone number...'.tr, - type: TextInputType.phone, - // يمكنك إزالة الحواف من الـ TextField الأصلي إذا أردت ليتناسب مع الكونتينر + keyboardType: TextInputType.phone, + style: const TextStyle(fontSize: 15), + decoration: InputDecoration( + hintText: '0990000000'.tr, + hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), + prefixIcon: Icon(Icons.phone_android_rounded, + color: Colors.grey[400], size: 22), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + onSubmitted: (_) => _performSearch(), ), ), - Container( - decoration: BoxDecoration( + Padding( + padding: const EdgeInsets.all(6.0), + child: Material( color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(10), - ), - child: IconButton( - icon: const Icon(Icons.search, size: 28, color: Colors.white), - onPressed: () { - final phone = searchController.text; - if (phone.isNotEmpty) { - captainController.find_driver_by_phone(phone); - } else { - Get.snackbar( - 'Error'.tr, - 'Please enter a phone number to search.'.tr, - backgroundColor: Colors.red.withOpacity(0.8), - colorText: Colors.white, - ); - } - }, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: _performSearch, + borderRadius: BorderRadius.circular(12), + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.search, color: Colors.white, size: 24), + ), + ), ), ), ], @@ -136,128 +181,138 @@ class CaptainsPage extends StatelessWidget { ); } - // --- بطاقة الكابتن المحسنة --- - Widget _buildCaptainCard(BuildContext context, dynamic captain) { + void _performSearch() { + final phone = searchController.text.trim(); + if (phone.isNotEmpty) { + captainController.find_driver_by_phone(phone); + } + } + + // ================= RESULTS ================= + + Widget _buildResultsList(BuildContext context, List captains) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), + child: Row( + children: [ + Text( + 'Search Results'.tr, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${captains.length}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + ), + ], + ), + ), + Expanded( + child: ListView.separated( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 0, 20, 80), + itemCount: captains.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final captain = captains[index] as Map; + return _buildModernCaptainCard(context, captain); + }, + ), + ), + ], + ); + } + + // ================= CARD ================= + + Widget _buildModernCaptainCard( + BuildContext context, Map captain) { + final String fullName = + '${captain['first_name'] ?? ''} ${captain['last_name'] ?? ''}'; + final String phone = captain['phone']?.toString() ?? ''; + final String? email = captain['email']?.toString(); + return Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.08), - spreadRadius: 1, - blurRadius: 8, + color: Colors.black.withOpacity(0.04), + blurRadius: 12, offset: const Offset(0, 4), ), ], - border: Border.all(color: Colors.grey.withOpacity(0.1)), ), child: Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), onTap: () { Get.to(() => const CaptainDetailsPage(), arguments: {'data': captain}); }, child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(16.0), child: Row( children: [ - // صورة الكابتن أو أيقونة - CircleAvatar( - radius: 28, - backgroundColor: - Theme.of(context).primaryColor.withOpacity(0.1), - child: Icon( - Icons.person, - color: Theme.of(context).primaryColor, - size: 30, + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryColor.withOpacity(0.8), + Theme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.person_rounded, + color: Colors.white, + size: 28, ), ), - const SizedBox(width: 15), - - // المعلومات النصية + const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // الاسم Text( - '${captain['first_name']} ${captain['last_name']}', + fullName, style: const TextStyle( fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontWeight: FontWeight.w600, ), ), - const SizedBox(height: 4), - - // رقم الهاتف (مع المنطق) - Row( - children: [ - Icon(Icons.phone_iphone, - size: 14, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - _formatPhoneNumber(captain['phone'].toString()), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.grey[700], - fontFamily: - 'monospace', // لجعل الأرقام والنجوم متناسقة - ), - ), - ], - ), - - // الإيميل (يظهر فقط للأدمن) - if (isSuperAdmin && captain['email'] != null) ...[ + const SizedBox(height: 6), + Text(phone), + if (isSuperAdmin && email != null) ...[ const SizedBox(height: 4), - Row( - children: [ - Icon(Icons.email_outlined, - size: 14, color: Colors.grey[600]), - const SizedBox(width: 4), - Expanded( - child: Text( - captain['email'], - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + Text(email), ], ], ), ), - - // أزرار الإجراءات - Column( - children: [ - // زر الاتصال (فقط للأدمن) - if (isSuperAdmin) - IconButton( - onPressed: () { - // منطق الاتصال - makePhoneCall('+' + captain['phone']); - // Get.snackbar( - // 'Call', 'Calling ${captain['phone']}...'); - }, - icon: const Icon(Icons.call, color: Colors.green), - tooltip: 'Call Captain', - ), - - if (!isSuperAdmin) - const Icon(Icons.arrow_forward_ios, - size: 16, color: Colors.grey), - ], - ), ], ), ), @@ -266,37 +321,15 @@ class CaptainsPage extends StatelessWidget { ); } - // --- دالة تنسيق الرقم (المنطق المطلوب) --- - String _formatPhoneNumber(String phone) { - if (isSuperAdmin) { - return phone; // للأدمن: إظهار الرقم كاملاً - } else { - // للمستخدم العادي: إظهار آخر 4 أرقام فقط - if (phone.length <= 4) return phone; - String lastFour = phone.substring(phone.length - 4); - String maskedPart = '*' * (phone.length - 4); - return '$maskedPart$lastFour'; // النتيجة: *******1234 - } + // ================= STATES ================= + + Widget _buildLoadingState() { + return const Center(child: MyCircularProgressIndicator()); } - // --- تصميم الحالة الفارغة --- Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off_rounded, size: 80, color: Colors.grey[300]), - const SizedBox(height: 10), - Text( - 'No captains found.'.tr, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.grey[500], - ), - ), - ], - ), + return const Center( + child: Text("No captains found"), ); } } diff --git a/lib/views/admin/drivers/driver_gift_check_page.dart b/lib/views/admin/drivers/driver_gift_check_page.dart index 1f01cda..dedd222 100644 --- a/lib/views/admin/drivers/driver_gift_check_page.dart +++ b/lib/views/admin/drivers/driver_gift_check_page.dart @@ -80,7 +80,7 @@ class DriverGiftCheckerController extends GetxController { // } else { // الخطوة 3: إضافة الهدية statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة..."; - await _addGiftToDriver(driverId, phoneInput, "30000"); + await _addGiftToDriver(driverId, phoneInput, "300"); // } } catch (e) { statusLog.value = "حدث خطأ غير متوقع: $e"; @@ -95,7 +95,7 @@ class DriverGiftCheckerController extends GetxController { final wallet = Get.put(WalletController()); // استخدام الدالة الموجودة في نظامك - await wallet.addDrivergift3000('new driver', driverId, amount, phone); + await wallet.addDrivergift300('new driver', driverId, amount, phone); // statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!"; diff --git a/lib/views/admin/drivers/driver_tracker_screen.dart b/lib/views/admin/drivers/driver_tracker_screen.dart index effd69c..1384266 100644 --- a/lib/views/admin/drivers/driver_tracker_screen.dart +++ b/lib/views/admin/drivers/driver_tracker_screen.dart @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:http/http.dart' as http; -import 'package:url_launcher/url_launcher.dart'; // ضروري من أجل الاتصال +import 'package:sefer_admin1/constant/links.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../constant/box_name.dart'; import '../../../main.dart'; @@ -17,7 +18,8 @@ class IntaleqTrackerScreen extends StatefulWidget { State createState() => _IntaleqTrackerScreenState(); } -class _IntaleqTrackerScreenState extends State { +class _IntaleqTrackerScreenState extends State + with TickerProviderStateMixin { // === Map Controller === final MapController _mapController = MapController(); List _markers = []; @@ -32,58 +34,83 @@ class _IntaleqTrackerScreenState extends State { int dayCount = 0; Timer? _timer; + // === Animation Controllers === + late AnimationController _fadeController; + late AnimationController _scaleController; + // === Admin Info === String myPhone = box.read(BoxName.adminPhone).toString(); bool get isSuperAdmin => myPhone == '963942542053' || myPhone == '963992952235'; // === URLs === - final String _baseDir = "https://api.intaleq.xyz/intaleq/ride/location/"; + final String _baseDir = "${AppLink.server}/ride/location/"; @override void initState() { super.initState(); + _initAnimations(); fetchData(); - // === تعديل 1: التحديث كل 5 دقائق بدلاً من 15 ثانية === _timer = Timer.periodic(const Duration(minutes: 5), (timer) { if (mounted) fetchData(); }); } + void _initAnimations() { + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _fadeController.forward(); + _scaleController.forward(); + } + @override void dispose() { _timer?.cancel(); _mapController.dispose(); + _fadeController.dispose(); + _scaleController.dispose(); super.dispose(); } - // === دالة إجراء الاتصال === Future _makePhoneCall(String phoneNumber) async { final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber); if (await canLaunchUrl(launchUri)) { await launchUrl(launchUri); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("لا يمكن إجراء الاتصال لهذا الرقم")), - ); + _showSnackBar("لا يمكن إجراء الاتصال لهذا الرقم"); } } - // === Fetch Data Function === + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: const Color(0xFF2C3E50), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ); + } + Future fetchData() async { if (!mounted) return; setState(() => isLoading = true); try { - // 1. طلب التحديث من PHP String updateUrl = "${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}"; await http.get(Uri.parse(updateUrl)); String v = DateTime.now().millisecondsSinceEpoch.toString(); - // === Live Data === final responseLive = await http.get(Uri.parse("${_baseDir}locations_live.json?v=$v")); if (responseLive.statusCode == 200) { @@ -98,7 +125,6 @@ class _IntaleqTrackerScreenState extends State { }); } - // === Day Data === final responseDay = await http.get(Uri.parse("${_baseDir}locations_day.json?v=$v")); if (responseDay.statusCode == 200) { @@ -124,7 +150,6 @@ class _IntaleqTrackerScreenState extends State { } } - // === Build Markers === void _buildMarkers(List drivers) { List newMarkers = []; @@ -144,8 +169,8 @@ class _IntaleqTrackerScreenState extends State { newMarkers.add( Marker( point: LatLng(lat, lon), - width: 50, - height: 50, + width: 60, + height: 60, child: GestureDetector( onTap: () { _showDriverInfoDialog( @@ -158,31 +183,7 @@ class _IntaleqTrackerScreenState extends State { cancelled: cancelled, ); }, - child: Stack( - alignment: Alignment.center, - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - shape: BoxShape.circle, - boxShadow: const [ - BoxShadow(blurRadius: 3, color: Colors.black26) - ]), - ), - Transform.rotate( - angle: heading * (math.pi / 180), - child: Icon( - Icons.navigation, - color: isLiveMode - ? const Color(0xFF27AE60) - : const Color(0xFF2980B9), - size: 28, - ), - ), - ], - ), + child: _buildMarkerWidget(heading), ), ), ); @@ -193,7 +194,46 @@ class _IntaleqTrackerScreenState extends State { }); } - // === Dialog Function === + Widget _buildMarkerWidget(double heading) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: isLiveMode + ? [const Color(0xFF27AE60), const Color(0xFF229954)] + : [const Color(0xFF3498DB), const Color(0xFF2980B9)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: (isLiveMode + ? const Color(0xFF27AE60) + : const Color(0xFF3498DB)) + .withOpacity(0.5), + blurRadius: 12, + spreadRadius: 2, + ) + ], + ), + ), + Transform.rotate( + angle: heading * (math.pi / 180), + child: Icon( + Icons.navigation, + color: Colors.white, + size: 26, + ), + ), + ], + ); + } + void _showDriverInfoDialog({ required String driverId, required String name, @@ -206,243 +246,583 @@ class _IntaleqTrackerScreenState extends State { showDialog( context: context, builder: (_) => Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - backgroundColor: Colors.white, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text("بيانات الكابتن", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF2C3E50))), - const Divider(thickness: 1, height: 25), - _infoRow(Icons.person, "الاسم", name), - _infoRow(Icons.badge, "المعرف (ID)", driverId), - _infoRow(Icons.speed, "السرعة", "$speed كم/س"), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade200)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _statItem("مكتملة", completed, Colors.green), - Container( - width: 1, height: 30, color: Colors.grey.shade300), - _statItem("ملغاة", cancelled, Colors.red), - ], - ), - ), - - // === تعديل 2: جعل رقم الهاتف قابلاً للنقر === - if (isSuperAdmin) ...[ - const SizedBox(height: 15), - InkWell( - onTap: () { - if (phone.isNotEmpty) _makePhoneCall(phone); - }, - borderRadius: BorderRadius.circular(8), - child: Container( - padding: - const EdgeInsets.symmetric(vertical: 8, horizontal: 10), - decoration: BoxDecoration( - color: const Color(0xFFFFF3CD), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFFFEEBA))), - child: Row( - children: [ - Expanded( - child: _infoRow(Icons.phone, "الهاتف", phone, - isPrivate: true)), - const SizedBox(width: 5), - const Icon(Icons.call, color: Colors.green, size: 20), - ], - ), - ), - ), - ], - - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2C3E50), - foregroundColor: Colors.white, - ), - child: const Text("إغلاق"), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.grey.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 30, + spreadRadius: 5, ) ], ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isLiveMode + ? [const Color(0xFF27AE60), const Color(0xFF229954)] + : [const Color(0xFF3498DB), const Color(0xFF2980B9)], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.person_outline, color: Colors.white, size: 20), + const SizedBox(width: 8), + const Text( + "معلومات الكابتن", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + _buildInfoCard(Icons.person, "الاسم", name), + const SizedBox(height: 12), + _buildInfoCard(Icons.badge, "المعرف", driverId), + const SizedBox(height: 12), + _buildInfoCard(Icons.speed, "السرعة", "$speed كم/س"), + const SizedBox(height: 20), + _buildStatsContainer(completed, cancelled), + if (isSuperAdmin) ...[ + const SizedBox(height: 16), + _buildPhoneButton(phone), + ], + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2C3E50), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + "إغلاق", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + ], + ), + ), ), ), ); } - // Helper Widgets - Widget _infoRow(IconData icon, String label, String value, - {bool isPrivate = false}) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + Widget _buildInfoCard(IconData icon, String label, String value) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + ) + ], + ), child: Row( children: [ - Icon(icon, - size: 20, - color: isPrivate ? Colors.orange[800] : Colors.grey[600]), - const SizedBox(width: 8), - Text("$label: ", - style: - const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), - Expanded( - child: Text(value, - style: TextStyle( - fontSize: 14, - fontWeight: - isPrivate ? FontWeight.bold : FontWeight.normal), - textAlign: TextAlign.end)), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: const Color(0xFF2C3E50), size: 20), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Color(0xFF2C3E50), + ), + ), + ], + ), ], ), ); } - Widget _statItem(String label, String val, Color color) { + Widget _buildStatsContainer(String completed, String cancelled) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.grey.shade50, Colors.grey.shade100], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStatItem("✓ مكتملة", completed, const Color(0xFF27AE60)), + Container( + width: 1, + height: 40, + color: Colors.grey.shade300, + ), + _buildStatItem("✕ ملغاة", cancelled, const Color(0xFFE74C3C)), + ], + ), + ); + } + + Widget _buildStatItem(String label, String value, Color color) { return Column( children: [ - Text(val, - style: TextStyle( - color: color, fontWeight: FontWeight.bold, fontSize: 16)), - Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)), + Text( + value, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), ], ); } + Widget _buildPhoneButton(String phone) { + return InkWell( + onTap: () { + if (phone.isNotEmpty) _makePhoneCall(phone); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [const Color(0xFFFFA500), const Color(0xFFFF8C00)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0xFFFFA500).withOpacity(0.4), + blurRadius: 12, + spreadRadius: 2, + ) + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.call, color: Colors.white, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + phone, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( + extendBodyBehindAppBar: true, appBar: AppBar( - title: const Text("نظام تتبع الكباتن"), - backgroundColor: const Color(0xFF2C3E50), - foregroundColor: Colors.white), + elevation: 0, + backgroundColor: Colors.transparent, + centerTitle: true, + title: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFF2C3E50).withOpacity(0.9), + borderRadius: BorderRadius.circular(12), + // backdropFilter: const BackdropFilter(blur: 10), + ), + child: const Text( + "نظام تتبع الكابتن", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + foregroundColor: Colors.white, + ), body: Stack( children: [ FlutterMap( mapController: _mapController, options: const MapOptions( - initialCenter: LatLng(33.513, 36.276), initialZoom: 10.0), + initialCenter: LatLng(33.513, 36.276), + initialZoom: 10.0, + ), children: [ TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.tripz.app'), + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.tripz.app', + ), MarkerLayer(markers: _markers), ], ), - - // === Dashboard === - Positioned( - top: 20, - right: 15, - child: Container( - width: 260, - padding: const EdgeInsets.all(15), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 10) - ]), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Text("لوحة التحكم", - style: TextStyle(fontWeight: FontWeight.bold)), - const Divider(), - - // أزرار التبديل - Row( - children: [ - Expanded( - child: _modeBtn("أرشيف اليوم", !isLiveMode, () { - setState(() => isLiveMode = false); - fetchData(); - })), - const SizedBox(width: 8), - Expanded( - child: _modeBtn("مباشر", isLiveMode, () { - setState(() => isLiveMode = true); - fetchData(); - })), - ], - ), - const SizedBox(height: 15), - - // === عرض العدادين معاً === - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("$liveCount", - style: const TextStyle( - color: Color(0xFF27AE60), - fontWeight: FontWeight.bold, - fontSize: 14)), - const Text("نشط الآن (مباشر):", - style: TextStyle(fontSize: 12)), - ], - ), - const SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("$dayCount", - style: const TextStyle( - color: Color(0xFF2980B9), - fontWeight: FontWeight.bold, - fontSize: 14)), - const Text("إجمالي اليوم:", - style: TextStyle(fontSize: 12)), - ], - ), - - const SizedBox(height: 10), - Text(isLoading ? "جاري التحديث..." : "تحديث: $lastUpdated", - style: const TextStyle(fontSize: 10, color: Colors.grey)), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isLoading ? null : fetchData, - child: const Text("تحديث البيانات"))) - ], - ), - ), - ), + _buildDashboard(), ], ), ); } - Widget _modeBtn(String title, bool active, VoidCallback onTap) { - return InkWell( - onTap: onTap, - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: active ? const Color(0xFF3498DB) : Colors.white, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFF3498DB))), - child: Text(title, - style: TextStyle( - color: active ? Colors.white : const Color(0xFF3498DB), - fontWeight: active ? FontWeight.bold : FontWeight.normal)), + Widget _buildDashboard() { + return Positioned( + top: 100, + right: 16, + child: FadeTransition( + opacity: _fadeController, + child: ScaleTransition( + scale: _scaleController, + child: Container( + width: 300, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 30, + spreadRadius: 5, + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF2C3E50), + const Color(0xFF34495E) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Icon(Icons.dashboard, + color: Colors.white, size: 22), + const Text( + "لوحة التحكم", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Mode Buttons + Row( + children: [ + Expanded( + child: _buildModeButton( + "أرشيف اليوم", + !isLiveMode, + () { + setState(() => isLiveMode = false); + fetchData(); + }, + const Color(0xFF3498DB), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildModeButton( + "مباشر", + isLiveMode, + () { + setState(() => isLiveMode = true); + fetchData(); + }, + const Color(0xFF27AE60), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Stats + _buildStatRow( + icon: Icons.live_tv, + label: "نشط الآن (مباشر)", + value: liveCount.toString(), + color: const Color(0xFF27AE60), + ), + const SizedBox(height: 12), + _buildStatRow( + icon: Icons.history, + label: "إجمالي اليوم", + value: dayCount.toString(), + color: const Color(0xFF3498DB), + ), + const SizedBox(height: 14), + + // Last Update + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isLoading + ? "جاري التحديث..." + : "تحديث: $lastUpdated", + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + Icon( + isLoading + ? Icons.hourglass_bottom + : Icons.check_circle, + size: 14, + color: isLoading ? Colors.orange : Colors.green, + ), + ], + ), + ), + const SizedBox(height: 12), + + // Refresh Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading ? null : fetchData, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2C3E50), + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey.shade300, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isLoading) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + else + const Icon(Icons.refresh, size: 18), + const SizedBox(width: 8), + const Text( + "تحديث البيانات", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), ), ); } + + Widget _buildModeButton( + String title, + bool active, + VoidCallback onTap, + Color color, + ) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(vertical: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: active + ? LinearGradient( + colors: [color, color.withOpacity(0.8)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: active ? null : Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: active ? color : Colors.grey.shade300, + width: 1.5, + ), + boxShadow: active + ? [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 8, + spreadRadius: 1, + ) + ] + : null, + ), + child: Text( + title, + style: TextStyle( + color: active ? Colors.white : Colors.grey.shade700, + fontWeight: active ? FontWeight.bold : FontWeight.w600, + fontSize: 13, + ), + ), + ), + ); + } + + Widget _buildStatRow({ + required IconData icon, + required String label, + required String value, + required Color color, + }) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ), + ], + ); + } } diff --git a/lib/views/admin/drivers/monitor_ride.dart b/lib/views/admin/drivers/monitor_ride.dart index 72d764c..4b86240 100644 --- a/lib/views/admin/drivers/monitor_ride.dart +++ b/lib/views/admin/drivers/monitor_ride.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:sefer_admin1/constant/links.dart'; // Keep your specific imports import 'package:sefer_admin1/controller/functions/crud.dart'; -import 'package:sefer_admin1/print.dart'; /// -------------------------------------------------------------------------- /// 1. DATA MODELS @@ -43,8 +43,7 @@ class DriverLocation { class RideMonitorController extends GetxController { // CONFIGURATION - final String apiUrl = - "https://api.intaleq.xyz/intaleq/Admin/rides/monitorRide.php"; + final String apiUrl = "${AppLink.server}/Admin/rides/monitorRide.php"; // INPUT CONTROLLERS final TextEditingController phoneInputController = TextEditingController(); @@ -81,7 +80,15 @@ class RideMonitorController extends GetxController { void startSearch() { if (phoneInputController.text.trim().isEmpty) { - Get.snackbar("Error", "Please enter a phone number"); + Get.snackbar( + "تنبيه", + "يرجى إدخال رقم الهاتف أولاً", + backgroundColor: Colors.redAccent.withOpacity(0.9), + colorText: Colors.white, + snackPosition: SnackPosition.TOP, + margin: const EdgeInsets.all(15), + borderRadius: 15, + ); return; } @@ -92,8 +99,8 @@ class RideMonitorController extends GetxController { startPoint.value = null; endPoint.value = null; routePolyline.clear(); - driverName.value = "Loading..."; - rideStatus.value = "Loading..."; + driverName.value = "جاري التحميل..."; + rideStatus.value = "جاري التحميل..."; _isFirstLoad = true; // Switch UI @@ -114,7 +121,7 @@ class RideMonitorController extends GetxController { _timer?.cancel(); isTracking.value = false; isLoading.value = false; - phoneInputController.clear(); + // phoneInputController.clear(); // اختياري: يمكنك إبقائه لتسهيل البحث مرة أخرى } Future fetchRideData() async { @@ -124,11 +131,9 @@ class RideMonitorController extends GetxController { try { final response = await CRUD().post( link: apiUrl, - payload: {"phone": phone}, + payload: {"phone": "963$phone"}, ); - // Log.print('response: ${response}'); - if (response != 'failure') { final jsonResponse = response; @@ -140,12 +145,13 @@ class RideMonitorController extends GetxController { // 1. Parse Driver Info if (data['driver_details'] != null) { - driverName.value = data['driver_details']['fullname'] ?? "Unknown"; + driverName.value = + data['driver_details']['fullname'] ?? "سائق غير معروف"; } // 2. Parse Ride Info & Route if (data['ride_details'] != null) { - rideStatus.value = data['ride_details']['status'] ?? "Unknown"; + rideStatus.value = data['ride_details']['status'] ?? "غير معروف"; // Parse Start/End Locations (Format: "lat,lng") String? startStr = data['ride_details']['start_location']; @@ -174,21 +180,19 @@ class RideMonitorController extends GetxController { if (startPoint.value != null && endPoint.value != null) { _updateMapBounds(); } - print("No live location coordinates."); } hasError.value = false; } else { hasError.value = true; errorMessage.value = jsonResponse['message'] ?? - "Phone number not found or no active ride."; + "لم يتم العثور على رقم الهاتف أو لا توجد رحلة نشطة."; } } else { hasError.value = true; - errorMessage.value = "Connection Failed"; + errorMessage.value = "فشل الاتصال بالخادم"; } } catch (e) { - print("Polling Error: $e"); if (isLoading.value) { hasError.value = true; errorMessage.value = e.toString(); @@ -207,14 +211,12 @@ class RideMonitorController extends GetxController { final lng = double.parse(parts[1].trim()); return LatLng(lat, lng); } catch (e) { - print("Error parsing location string '$str': $e"); return null; } } // Logic to fit start, end, and driver on screen void _updateMapBounds() { - // Only auto-fit on the first successful load to avoid fighting user pan/zoom if (!_isFirstLoad) return; List pointsToFit = []; @@ -232,106 +234,184 @@ class RideMonitorController extends GetxController { mapController.fitCamera( CameraFit.bounds( bounds: bounds, - padding: - const EdgeInsets.all(80.0), // Padding so markers aren't on edge + padding: const EdgeInsets.all(80.0), ), ); - _isFirstLoad = false; // Disable auto-fit after initial success + _isFirstLoad = false; } catch (e) { - print("Map Controller not ready yet: $e"); + // Map Controller not ready yet } } } } /// -------------------------------------------------------------------------- -/// 3. UI SCREEN +/// 3. UI SCREEN (Modern Light Theme) /// -------------------------------------------------------------------------- class RideMonitorScreen extends StatelessWidget { const RideMonitorScreen({super.key}); + // 🎨 الألوان العصرية (Modern Palette) + final Color backgroundColor = const Color(0xFFF4F7FE); + final Color primaryColor = const Color(0xFF4318FF); + final Color textPrimary = const Color(0xFF2B3674); + final Color textSecondary = const Color(0xFFA3AED0); + @override Widget build(BuildContext context) { final RideMonitorController controller = Get.put(RideMonitorController()); return Scaffold( - appBar: AppBar( - title: const Text("Admin Ride Monitor"), - backgroundColor: Colors.blueAccent, - foregroundColor: Colors.white, - actions: [ - Obx(() => controller.isTracking.value - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.stopTracking, - tooltip: "Stop Tracking", - ) - : const SizedBox.shrink()), - ], + backgroundColor: backgroundColor, + // الإبقاء على AppBar فقط في شاشة البحث + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Obx(() { + if (controller.isTracking.value) + return const SizedBox + .shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة + return AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + iconTheme: IconThemeData(color: textPrimary), + title: Text( + "مراقبة الرحلات", + style: TextStyle( + color: textPrimary, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ); + }), ), body: Obx(() { if (!controller.isTracking.value) { return _buildSearchForm(context, controller); } - return _buildMapTrackingView(controller); + return _buildMapTrackingView(context, controller); }), ); } + // --------------------------------------------------------------------------- + // واجهة البحث (Search View) + // --------------------------------------------------------------------------- Widget _buildSearchForm( BuildContext context, RideMonitorController controller) { return Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.map_outlined, size: 80, color: Colors.blueAccent), - const SizedBox(height: 20), - const Text( - "Track Active Ride", - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Container( + padding: const EdgeInsets.all(32.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 10), + ) + ], ), - const SizedBox(height: 10), - const Text( - "Enter Driver or Passenger Phone Number", - style: TextStyle(color: Colors.grey), - ), - const SizedBox(height: 30), - TextField( - controller: controller.phoneInputController, - keyboardType: TextInputType.phone, - decoration: InputDecoration( - labelText: "Phone Number", - hintText: "e.g. 9639...", - border: - OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - prefixIcon: const Icon(Icons.phone), - ), - ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: controller.startSearch, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: + Icon(Icons.radar_rounded, size: 60, color: primaryColor), ), - child: const Text("Start Monitoring", - style: TextStyle(color: Colors.white, fontSize: 16)), - ), + const SizedBox(height: 24), + Text( + "تتبع رحلة نشطة", + style: TextStyle( + color: textPrimary, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + "أدخل رقم هاتف السائق أو الراكب للبدء", + style: TextStyle( + color: textSecondary, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white, width: 2), + ), + child: TextField( + controller: controller.phoneInputController, + keyboardType: TextInputType.phone, + textDirection: TextDirection.ltr, + style: TextStyle( + color: textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + decoration: InputDecoration( + hintText: "مثال: 0992952235...", + hintStyle: TextStyle(color: textSecondary), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 18, horizontal: 20), + prefixIcon: + Icon(Icons.phone_rounded, color: primaryColor), + ), + ), + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: controller.startSearch, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text( + "بدء المراقبة", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ), - ], + ), ), ), ); } - Widget _buildMapTrackingView(RideMonitorController controller) { + // --------------------------------------------------------------------------- + // واجهة الخريطة (Map View) + // --------------------------------------------------------------------------- + Widget _buildMapTrackingView( + BuildContext context, RideMonitorController controller) { return Stack( children: [ FlutterMap( @@ -352,10 +432,12 @@ class RideMonitorScreen extends StatelessWidget { polylines: [ Polyline( points: controller.routePolyline.value, - strokeWidth: 5.0, - color: Colors.blueAccent.withOpacity(0.8), + strokeWidth: 6.0, + color: primaryColor.withOpacity(0.9), borderStrokeWidth: 2.0, - borderColor: Colors.blue[900]!, + borderColor: primaryColor.withOpacity(0.3), + strokeCap: StrokeCap.round, + strokeJoin: StrokeJoin.round, ), ], ), @@ -363,25 +445,22 @@ class RideMonitorScreen extends StatelessWidget { // 2. START & END MARKERS MarkerLayer( markers: [ - // Start Point (Green Flag) + // Start Point (Green Dot) if (controller.startPoint.value != null) Marker( point: controller.startPoint.value!, - width: 40, - height: 40, - child: - const Icon(Icons.flag, color: Colors.green, size: 40), - alignment: Alignment.topCenter, + width: 30, + height: 30, + child: _buildPointMarker(const Color(0xFF10B981)), ), - // End Point (Red Flag) + // End Point (Red Dot) if (controller.endPoint.value != null) Marker( point: controller.endPoint.value!, - width: 40, - height: 40, - child: const Icon(Icons.flag, color: Colors.red, size: 40), - alignment: Alignment.topCenter, + width: 30, + height: 30, + child: _buildPointMarker(const Color(0xFFEF4444)), ), // Driver Car Marker @@ -391,30 +470,49 @@ class RideMonitorScreen extends StatelessWidget { controller.driverLocation.value!.latitude, controller.driverLocation.value!.longitude, ), - width: 60, - height: 60, + width: 80, + height: 80, child: Transform.rotate( angle: (controller.driverLocation.value!.heading * (3.14159 / 180)), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.directions_car_filled, - color: Colors - .black, // Dark car for visibility on blue line - size: 35, + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + spreadRadius: 2, + ) + ], + ), + child: Icon( + Icons.directions_car_rounded, + color: primaryColor, + size: 28, + ), ), + const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 1), + horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.8), - borderRadius: BorderRadius.circular(4), + color: textPrimary, + borderRadius: BorderRadius.circular(6), ), child: Text( - "${controller.driverLocation.value!.speed.toInt()} km", + "${controller.driverLocation.value!.speed.toInt()} كم", style: const TextStyle( - fontSize: 10, fontWeight: FontWeight.bold), + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textDirection: TextDirection.rtl, ), ) ], @@ -426,88 +524,196 @@ class RideMonitorScreen extends StatelessWidget { ], ), - // LOADING OVERLAY + // زر التراجع (إيقاف التتبع) أعلى الشاشة + Positioned( + top: MediaQuery.of(context).padding.top + 10, + right: 20, // أو left حسب لغة التطبيق + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: IconButton( + icon: Icon(Icons.close_rounded, color: textPrimary, size: 24), + onPressed: controller.stopTracking, + tooltip: "إيقاف المراقبة", + ), + ), + ), + + // LOADING OVERLAY (Smooth Frosted Glass like) if (controller.isLoading.value && controller.driverLocation.value == null && controller.startPoint.value == null) Container( - color: Colors.black45, - child: const Center( - child: CircularProgressIndicator(color: Colors.white)), + color: Colors.white.withOpacity(0.8), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + color: primaryColor, strokeWidth: 3), + const SizedBox(height: 16), + Text( + "جاري تحديد الموقع...", + style: TextStyle( + color: textPrimary, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ) + ], + ), + ), ), // ERROR OVERLAY if (controller.hasError.value) Center( child: Container( - margin: const EdgeInsets.all(20), - padding: const EdgeInsets.all(20), + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - boxShadow: const [ - BoxShadow(blurRadius: 10, color: Colors.black26) - ]), + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ) + ], + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.error_outline, color: Colors.red, size: 40), - const SizedBox(height: 10), - Text(controller.errorMessage.value, - textAlign: TextAlign.center), - const SizedBox(height: 10), - ElevatedButton( + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.error_outline_rounded, + color: Colors.red, size: 40), + ), + const SizedBox(height: 16), + Text( + "حدث خطأ", + style: TextStyle( + color: textPrimary, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + controller.errorMessage.value, + textAlign: TextAlign.center, + style: TextStyle(color: textSecondary, height: 1.5), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( onPressed: controller.stopTracking, - child: const Text("Back")) + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor, + foregroundColor: textPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: const Text("رجوع للبحث", + style: TextStyle(fontWeight: FontWeight.bold)), + ), + ) ], ), ), ), - // INFO CARD + // INFO CARD (Bottom Floating Card) if (!controller.hasError.value && !controller.isLoading.value) Positioned( - bottom: 20, + bottom: 30, left: 20, right: 20, - child: Card( - elevation: 8, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15)), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 10), + ) + ], + ), child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ - const CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Icon(Icons.person, color: Colors.white), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + ), + child: Icon(Icons.person_rounded, + color: primaryColor, size: 28), ), - const SizedBox(width: 12), + const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( controller.driverName.value, - style: const TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), + style: TextStyle( + color: textPrimary, + fontSize: 18, + fontWeight: FontWeight.bold, + ), overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4), Row( children: [ - Icon(Icons.circle, - size: 10, - color: - controller.rideStatus.value == 'Begin' - ? Colors.green - : Colors.grey), - const SizedBox(width: 5), - Text(controller.rideStatus.value, - style: const TextStyle( - fontWeight: FontWeight.w600)), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: controller.rideStatus.value + .toLowerCase() == + 'begin' + ? const Color(0xFF10B981) + : const Color(0xFFF59E0B), + ), + ), + const SizedBox(width: 6), + Text( + controller.rideStatus.value, + style: TextStyle( + color: textSecondary, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), ], ), ], @@ -515,25 +721,53 @@ class RideMonitorScreen extends StatelessWidget { ), ], ), - const Divider(), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Divider(height: 1, thickness: 1), + ), if (controller.driverLocation.value != null) Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildInfoBadge(Icons.speed, - "${controller.driverLocation.value!.speed.toStringAsFixed(1)} km/h"), - _buildInfoBadge( - Icons.access_time, - controller.driverLocation.value!.updatedAt - .split(' ') - .last), + _buildModernInfoBadge( + Icons.speed_rounded, + "${controller.driverLocation.value!.speed.toStringAsFixed(1)} كم/س", + const Color(0xFF3B82F6), + ), + Container( + width: 1, + height: 30, + color: Colors.grey.withOpacity(0.2)), + _buildModernInfoBadge( + Icons.access_time_rounded, + controller.driverLocation.value!.updatedAt + .split(' ') + .last, + const Color(0xFF8B5CF6), + ), ], ) else - const Text("Connecting to driver...", - style: TextStyle( - color: Colors.orange, - fontStyle: FontStyle.italic)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + color: primaryColor, strokeWidth: 2), + ), + const SizedBox(width: 10), + Text( + "جاري الاتصال بالسائق...", + style: TextStyle( + color: primaryColor, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ], + ), ], ), ), @@ -543,14 +777,56 @@ class RideMonitorScreen extends StatelessWidget { ); } - Widget _buildInfoBadge(IconData icon, String text) { + // --- Helper Widgets --- + + Widget _buildPointMarker(Color color) { + return Container( + decoration: BoxDecoration( + color: color.withOpacity(0.3), + shape: BoxShape.circle, + ), + child: Center( + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.5), + blurRadius: 6, + spreadRadius: 1, + ) + ], + ), + ), + ), + ); + } + + Widget _buildModernInfoBadge(IconData icon, String text, Color iconColor) { return Row( children: [ - Icon(icon, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text(text, - style: TextStyle( - color: Colors.grey[800], fontWeight: FontWeight.bold)), + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: iconColor), + ), + const SizedBox(width: 8), + Text( + text, + style: TextStyle( + color: textPrimary, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + textDirection: TextDirection.ltr, // للحفاظ على اتجاه الأرقام + ), ], ); } diff --git a/lib/views/admin/employee/employee_page.dart b/lib/views/admin/employee/employee_page.dart index d9732f4..08865d1 100644 --- a/lib/views/admin/employee/employee_page.dart +++ b/lib/views/admin/employee/employee_page.dart @@ -1,186 +1,564 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:sefer_admin1/constant/colors.dart'; -import 'package:sefer_admin1/constant/style.dart'; +import 'package:sefer_admin1/constant/links.dart'; import 'package:sefer_admin1/controller/employee_controller/employee_controller.dart'; -import 'package:sefer_admin1/controller/functions/launch.dart'; -import 'package:sefer_admin1/views/widgets/elevated_btn.dart'; -import 'package:sefer_admin1/views/widgets/my_scafold.dart'; -import 'package:sefer_admin1/views/widgets/my_textField.dart'; - -import '../../../constant/links.dart'; -import '../../../controller/functions/upload_image copy.dart'; +import 'package:sefer_admin1/controller/functions/upload_image copy.dart'; // تأكد من مسار الملف الصحيح +import 'package:url_launcher/url_launcher.dart'; class EmployeePage extends StatelessWidget { const EmployeePage({super.key}); @override Widget build(BuildContext context) { + // حقن الكنترولر Get.put(EmployeeController()); - return GetBuilder(builder: (employeeController) { - return Scaffold( - appBar: AppBar( - title: Text('Employee Page'.tr), - ), - body: ListView.builder( - itemCount: employeeController - .employee.length, // Set the item count based on the employee list - itemBuilder: (context, index) { - // Get the employee data for the current index - var employee = employeeController.employee[index]; - // Return a widget to display the employee information - return Padding( - padding: const EdgeInsets.all(3.0), - child: Container( - decoration: AppStyle.boxDecoration1, - child: ListTile( - trailing: IconButton( - onPressed: () { - Get.to(() => EmployeeDetails( - index: index, - )); - }, - icon: Icon( - Icons.shop_two, - color: employee['status'].toString().contains('ممتاز') - ? AppColor.greenColor - : AppColor.accentColor, - ), - ), - title: Column( + // ألوان الثيم + const Color bgColor = Color(0xFF0A0E27); + const Color cardColor = Color(0xFF1A1F3A); + const Color primaryAccent = Color(0xFF6366F1); + + return Scaffold( + backgroundColor: bgColor, + body: GetBuilder( + builder: (controller) { + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + // 1. App Bar + SliverAppBar( + expandedHeight: 100, + floating: true, + pinned: true, + backgroundColor: bgColor, + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + titlePadding: const EdgeInsets.only(bottom: 16), + title: Row( + mainAxisSize: MainAxisSize.min, children: [ - Text(employee['name']), - Text( - 'Phone: ${employee['phone']}\nEducation: ${employee['education']}'), - Text('Status: ${employee['status']}'), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryAccent.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.badge_rounded, + color: Colors.white, size: 18), + ), + const SizedBox(width: 10), + const Text( + 'الموظفون', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + fontFamily: 'Segoe UI', + ), + ), ], - ), // Display employee name - onTap: () { - // Add any action you want when the employee is tapped - }, - leading: IconButton( - onPressed: () { - makePhoneCall(employee['phone'].toString()); - // launchCommunication( - // 'phone', employee['phone'].toString(), ''); - }, - icon: const Icon(Icons.phone), + ), + centerTitle: true, + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + primaryAccent.withOpacity(0.15), + bgColor, + ], + ), + ), ), ), ), - ); - }, + + // 2. قائمة الموظفين + if (controller.employee.isEmpty) + const SliverFillRemaining( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.people_outline, + size: 60, color: Colors.white24), + SizedBox(height: 16), + Text("لا يوجد موظفين حالياً", + style: TextStyle(color: Colors.white54)), + ], + ), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final employee = controller.employee[index]; + return _EmployeeCard( + employee: employee, + index: index, + cardColor: cardColor, + primaryAccent: primaryAccent, + ); + }, + childCount: controller.employee.length, + ), + ), + ), + ], + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + final controller = Get.find(); + controller.id = controller.generateRandomId(8); + Get.to(() => _EmployeeFormScreen(controller: controller)); + }, + backgroundColor: primaryAccent, + child: const Icon(Icons.person_add_rounded, color: Colors.white), + ), + ); + } +} + +// === بطاقة الموظف (تصميم جديد ومحسن) === +class _EmployeeCard extends StatelessWidget { + final Map employee; + final int index; + final Color cardColor; + final Color primaryAccent; + + const _EmployeeCard({ + required this.employee, + required this.index, + required this.cardColor, + required this.primaryAccent, + }); + + @override + Widget build(BuildContext context) { + bool isExcellent = employee['status'].toString().contains('ممتاز'); + Color statusColor = isExcellent ? const Color(0xFF10B981) : Colors.amber; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.05)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => Get.to(() => EmployeeDetails(index: index)), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // الصف العلوي: الحالة + أيقونة + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + shape: BoxShape.circle, + ), + child: Icon(Icons.person, + color: Colors.white.withOpacity(0.7), size: 20), + ), + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Text( + employee['status'] ?? 'Unknown', + style: TextStyle( + color: statusColor, + fontSize: 12, + fontWeight: FontWeight.bold, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // الاسم في سطر كامل ومميز + Text( + employee['name'] ?? 'Unknown', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + height: 1.3, + ), + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Divider(height: 1, color: Colors.white10), + ), + + // تفاصيل التعليم والهاتف والموقع (مع دعم تعدد الأسطر) + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow(Icons.phone_iphone_rounded, + employee['phone'] ?? '', Colors.white54), + const SizedBox(height: 12), // مسافة أكبر بين العناصر + _buildInfoRow( + Icons.school_outlined, + employee['education'] ?? 'غير محدد', + primaryAccent), + const SizedBox(height: 12), // مسافة أكبر + _buildInfoRow(Icons.location_on_outlined, + employee['site'] ?? 'غير محدد', Colors.blueGrey), + ], + ), + ), + + // زر الاتصال الجانبي + const SizedBox(width: 16), + Material( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: () => + _makePhoneCall(employee['phone'].toString()), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(14), + child: const Icon(Icons.call, + color: Colors.green, size: 24), + ), + ), + ), + ], + ), + ], + ), + ), ), - floatingActionButton: FloatingActionButton( - onPressed: () { - employeeController.id = employeeController.generateRandomId(8); - Get.to( - employeeFields(employeeController), - ); - }, // Icon to display - backgroundColor: Colors.blue, // Button color (optional) - tooltip: 'Add Employee', - child: const Icon(Icons.add), // Tooltip text when long-pressed - ), - ); - }); + ), + ); } - Scaffold employeeFields(EmployeeController employeeController) { + Widget _buildInfoRow(IconData icon, String text, Color iconColor) { + return Row( + crossAxisAlignment: + CrossAxisAlignment.start, // محاذاة الأيقونة مع بداية النص + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), // ضبط بسيط لموقع الأيقونة + child: Icon(icon, size: 16, color: iconColor.withOpacity(0.8)), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + text, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 13, + height: 1.5, // تباعد الأسطر لسهولة القراءة + ), + // تم إزالة maxLines و overflow للسماح بالنص بالنزول لأسطر متعددة + ), + ), + ], + ); + } + + Future _makePhoneCall(String phoneNumber) async { + final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber); + if (await canLaunchUrl(launchUri)) { + await launchUrl(launchUri); + } + } +} + +// === شاشة إضافة موظف === +class _EmployeeFormScreen extends StatelessWidget { + final EmployeeController controller; + const _EmployeeFormScreen({required this.controller}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xFF0A0E27); + const Color inputColor = Color(0xFF1A1F3A); + return Scaffold( - appBar: AppBar(), - body: Form( - key: employeeController.formKey, - child: SizedBox( - height: 500, - child: ListView( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text("إضافة موظف جديد", + style: TextStyle(color: Colors.white)), + backgroundColor: bgColor, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Get.back(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Form( + key: controller.formKey, + child: Column( children: [ - MyElevatedButton( - title: 'front id', - onPressed: () async { - await ImageController().choosImage(AppLink.uploadEgypt, - 'idFrontEmployee', employeeController.id); - }), - MyElevatedButton( - title: 'back id', - onPressed: () async { - await ImageController().choosImage(AppLink.uploadEgypt, - 'idbackEmployee', employeeController.id); - }), - MyTextForm( - controller: employeeController.name, - label: 'name', - hint: 'name', - type: TextInputType.name), - MyTextForm( - controller: employeeController.education, - label: 'education', - hint: 'education', - type: TextInputType.name), - MyTextForm( - controller: employeeController.site, - label: 'site', - hint: 'site', - type: TextInputType.name), - MyTextForm( - controller: employeeController.phone, - label: 'phone', - hint: 'phone', + Row( + children: [ + Expanded( + child: _UploadButton( + title: "الهوية (أمام)", + icon: Icons.credit_card, + onPressed: () async { + await ImageController().choosImage(AppLink.uploadEgypt, + 'idFrontEmployee', controller.id); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _UploadButton( + title: "الهوية (خلف)", + icon: Icons.credit_card_outlined, + onPressed: () async { + await ImageController().choosImage(AppLink.uploadEgypt, + 'idbackEmployee', controller.id); + }, + ), + ), + ], + ), + const SizedBox(height: 24), + _buildModernTextField( + controller.name, "الاسم الكامل", Icons.person, inputColor), + const SizedBox(height: 16), + _buildModernTextField( + controller.phone, "رقم الهاتف", Icons.phone, inputColor, type: TextInputType.phone), - MyTextForm( - controller: employeeController.status, - label: 'status', - hint: 'status', - type: TextInputType.name), + const SizedBox(height: 16), + _buildModernTextField(controller.education, "التعليم / الملاحظات", + Icons.school, inputColor), + const SizedBox(height: 16), + _buildModernTextField(controller.site, "الموقع / العنوان", + Icons.location_on, inputColor), + const SizedBox(height: 16), + _buildModernTextField(controller.status, "الحالة (مثال: ممتاز)", + Icons.star, inputColor), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + if (controller.formKey.currentState!.validate()) { + await controller.addEmployee(); + Get.back(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6366F1), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: const Text("حفظ البيانات", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + ), + ), ], ), ), ), - bottomNavigationBar: MyElevatedButton( - title: 'upload', - onPressed: () async { - if (employeeController.formKey.currentState!.validate()) { - await employeeController.addEmployee(); - } - }, + ); + } + + Widget _buildModernTextField(TextEditingController controller, String hint, + IconData icon, Color fillColor, + {TextInputType type = TextInputType.text}) { + return Container( + decoration: BoxDecoration( + color: fillColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + child: TextFormField( + controller: controller, + keyboardType: type, + style: const TextStyle(color: Colors.white), + maxLines: null, // السماح بتعدد الأسطر عند الإدخال أيضاً + decoration: InputDecoration( + labelText: hint, + labelStyle: TextStyle(color: Colors.white.withOpacity(0.5)), + prefixIcon: Icon(icon, color: Colors.white38, size: 20), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + validator: (value) => + value == null || value.isEmpty ? 'حقل مطلوب' : null, ), ); } } -class EmployeeDetails extends StatelessWidget { - const EmployeeDetails({super.key, required this.index}); - final int index; +class _UploadButton extends StatelessWidget { + final String title; + final IconData icon; + final VoidCallback onPressed; + + const _UploadButton( + {required this.title, required this.icon, required this.onPressed}); + @override Widget build(BuildContext context) { - return MyScafolld( - title: 'Details', - isleading: true, - body: [ - GetBuilder(builder: (employeeController) { - return Column( - children: [ - SizedBox( - height: 200, - width: 400, - child: Image.network( - // https: //server.sefer.click/sefer.click/sefer/card_image/idFrontEmployee-GC15188P.jpg - 'https://server.sefer.click/sefer.click/sefer/card_image/idFrontEmployee-${employeeController.employee[index]['id']}.jpg'), - ), - const SizedBox( - height: 10, - ), - SizedBox( - height: 200, - width: 400, - child: Image.network( - 'https://server.sefer.click/sefer.click/sefer/card_image/idFrontEmployee-${employeeController.employee[index]['id']}.jpg'), - ) - ], - ); - }) - ], + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + color: const Color(0xFF1A1F3A), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFF6366F1).withOpacity(0.3), + style: BorderStyle.solid), + ), + child: Column( + children: [ + Icon(icon, color: const Color(0xFF6366F1), size: 30), + const SizedBox(height: 8), + Text(title, + style: const TextStyle(color: Colors.white70, fontSize: 12)), + const SizedBox(height: 4), + const Text("اضغط للرفع", + style: TextStyle(color: Colors.white38, fontSize: 10)), + ], + ), + ), + ); + } +} + +// === شاشة التفاصيل === +class EmployeeDetails extends StatelessWidget { + final int index; + const EmployeeDetails({super.key, required this.index}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xFF0A0E27); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: + const Text('تفاصيل الموظف', style: TextStyle(color: Colors.white)), + backgroundColor: bgColor, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + ), + body: GetBuilder( + builder: (controller) { + final employeeId = controller.employee[index]['id']; + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("الهوية الأمامية", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + _buildImageViewer( + '${AppLink.server}/card_image/idFrontEmployee-$employeeId.jpg', + ), + const SizedBox(height: 30), + const Text("الهوية الخلفية", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + _buildImageViewer( + '${AppLink.server}/card_image/idbackEmployee-$employeeId.jpg', + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildImageViewer(String url) { + return Container( + width: double.infinity, + height: 220, + decoration: BoxDecoration( + color: const Color(0xFF1A1F3A), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + url, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.broken_image_rounded, + color: Colors.white24, size: 50), + SizedBox(height: 8), + Text("فشل تحميل الصورة", + style: TextStyle(color: Colors.white24)), + ], + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const Center( + child: CircularProgressIndicator(color: Color(0xFF6366F1))); + }, + ), + ), ); } } diff --git a/lib/views/admin/enceypt/driver_fingerprint_migration.dart b/lib/views/admin/enceypt/driver_fingerprint_migration.dart new file mode 100644 index 0000000..d7fa487 --- /dev/null +++ b/lib/views/admin/enceypt/driver_fingerprint_migration.dart @@ -0,0 +1,266 @@ +// ═══════════════════════════════════════════════════════════════ +// driver_fingerprint_migration.dart +// ─────────────────────────────────────────────────────────────── +// المنطق ببساطة: +// 1. خذ البصمة كما هي من DB +// 2. split('_') → احذف آخر جزء (OS version) +// 3. join('_') → encrypt → رفع +// +// مثال: +// "abc123_SamsungA51_13" → "abc123_SamsungA51" → encrypt +// "TECNO_LH7n-GL_14" → "TECNO_LH7n-GL" → encrypt +// "unknown_2412DPC0AG_15" → "unknown_2412DPC0AG" → encrypt +// ═══════════════════════════════════════════════════════════════ + +import 'package:flutter/material.dart'; + +import '../../../constant/links.dart'; +import '../../../controller/functions/crud.dart'; +import '../../../controller/functions/encrypt_decrypt.dart'; +import '../../../print.dart'; + +class DriverFingerprintMigrationTool extends StatefulWidget { + const DriverFingerprintMigrationTool({super.key}); + + @override + State createState() => + _DriverFingerprintMigrationToolState(); +} + +class _DriverFingerprintMigrationToolState + extends State { + bool _isRunning = false; + bool _isDone = false; + int _total = 0; + int _processed = 0; + int _updated = 0; + int _failed = 0; + String _currentLog = ''; + + static const int _batchSize = 50; + + // ───────────────────────────────────────────────────────────── + // المنطق الأساسي — حذف آخر جزء بعد "_" + // ───────────────────────────────────────────────────────────── + String _removeLastSegment(String raw) { + final parts = raw.split('_'); + if (parts.length <= 1) return raw; // جزء واحد — ما في شيء نحذفه + parts.removeLast(); + return parts.join('_'); + } + + Future _startMigration() async { + setState(() { + _isRunning = true; + _isDone = false; + _processed = 0; + _updated = 0; + _failed = 0; + _currentLog = 'جارٍ جلب بصمات السائقين...'; + }); + + try { + final records = await _fetchAll(); + if (records == null) { + _log('❌ فشل في جلب البيانات'); + setState(() => _isRunning = false); + return; + } + + _total = records.length; + _log('✅ تم جلب $_total بصمة — بدء المعالجة...'); + + for (int i = 0; i < records.length; i += _batchSize) { + final batch = records.skip(i).take(_batchSize).toList(); + _log('⚙️ معالجة ${i + 1} → ${i + batch.length} من $_total'); + await Future.wait(batch.map(_processSingle)); + if (i + _batchSize < records.length) { + await Future.delayed(const Duration(milliseconds: 300)); + } + } + + _log('🎉 اكتمل!\nمحدَّث: $_updated | فاشل: $_failed'); + setState(() { + _isDone = true; + _isRunning = false; + }); + } catch (e) { + _log('❌ خطأ: $e'); + setState(() => _isRunning = false); + } + } + + Future>?> _fetchAll() async { + try { + final response = await CRUD().post( + link: AppLink.getAllDriverFingerprints, + payload: {'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd'}, + ); + if (response == 'failure' || response == null) return null; + + final data = response['data']; + if (data is! List) return null; + + return List>.from(data); + } catch (e) { + Log.print('fetchAll error: $e'); + return null; + } + } + + Future _processSingle(Map record) async { + final captainId = record['captain_id']?.toString() ?? ''; + final rawFp = record['fingerPrint']?.toString() ?? ''; + + if (captainId.isEmpty || rawFp.isEmpty) { + setState(() { + _failed++; + _processed++; + }); + return; + } + + try { + // ── حذف آخر جزء (OS version) ───────────────────────────── + final String newRaw = _removeLastSegment(rawFp); + final String encrypted = EncryptionHelper.instance.encryptData(newRaw); + + Log.print('🔄 [$captainId] "$rawFp" → "$newRaw" → encrypted'); + + // ── رفع للسيرفر ────────────────────────────────────────── + final res = await CRUD().post( + link: AppLink.updateDriverFingerprintAdmin, + payload: { + 'captain_id': captainId, + 'fingerprint': encrypted, + 'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd', + }, + ); + + if (res != 'failure' && res?['status'] == 'success') { + setState(() { + _updated++; + _processed++; + }); + } else { + setState(() { + _failed++; + _processed++; + }); + } + } catch (e) { + Log.print('❌ [$captainId]: $e'); + setState(() { + _failed++; + _processed++; + }); + } + } + + void _log(String msg) { + Log.print(msg); + setState(() => _currentLog = msg); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Driver FP Migration')), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade200), + ), + child: const Text( + '⚠️ تُستخدم مرة واحدة فقط\n\n' + '"abc123_Samsung_13" → "abc123_Samsung" → encrypt\n' + '"TECNO_LH7n_14" → "TECNO_LH7n" → encrypt', + style: + TextStyle(fontSize: 13, height: 1.7, fontFamily: 'monospace'), + ), + ), + const SizedBox(height: 24), + if (_total > 0) ...[ + Text('التقدم: $_processed / $_total', + style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _total > 0 ? _processed / _total : 0, + backgroundColor: Colors.grey.shade200, + color: _isDone ? Colors.green : Colors.blue, + minHeight: 8, + ), + const SizedBox(height: 16), + ], + if (_processed > 0) + Row(children: [ + _chip('محدَّث', _updated, Colors.green), + const SizedBox(width: 8), + _chip('فاشل', _failed, Colors.red), + ]), + const SizedBox(height: 16), + if (_currentLog.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text(_currentLog, + style: + const TextStyle(fontFamily: 'monospace', fontSize: 12)), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: (_isRunning || _isDone) ? null : _startMigration, + style: ElevatedButton.styleFrom( + backgroundColor: _isDone ? Colors.green : Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: _isRunning + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2)), + SizedBox(width: 12), + Text('جارٍ الترحيل...', + style: + TextStyle(color: Colors.white, fontSize: 16)), + ], + ) + : Text( + _isDone ? '✅ اكتمل الترحيل' : 'بدء ترحيل بصمات السائقين', + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ]), + ), + ); + } + + Widget _chip(String label, int value, Color color) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text('$label: $value', + style: TextStyle(color: color, fontWeight: FontWeight.bold)), + ); +} diff --git a/lib/views/admin/enceypt/encrypt.dart b/lib/views/admin/enceypt/encrypt.dart new file mode 100644 index 0000000..637cf5b --- /dev/null +++ b/lib/views/admin/enceypt/encrypt.dart @@ -0,0 +1,855 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:sefer_admin1/constant/links.dart'; +import 'package:sefer_admin1/controller/functions/crud.dart'; +import 'package:sefer_admin1/main.dart'; + +import '../../../constant/box_name.dart'; + +// ─── Custom Colors ──────────────────────────────────────────────────────────── +class _AppColors { + static const bg = Color(0xFF0A0D14); + static const surface = Color(0xFF111622); + static const card = Color(0xFF161D2E); + static const border = Color(0xFF1F2D4A); + static const accent = Color(0xFF00E5FF); + static const accentDim = Color(0xFF0097A7); + static const accentGlow = Color(0x2200E5FF); + static const accentDecrypt = Color(0xFF7C4DFF); + static const accentDecryptDim = Color(0xFF512DA8); + static const accentDecryptGlow = Color(0x227C4DFF); + static const textPrimary = Color(0xFFE8F0FE); + static const textSec = Color(0xFF7A8BAA); + static const success = Color(0xFF00E676); + static const error = Color(0xFFFF5252); +} + +class EncryptToolPage extends StatefulWidget { + final String adminToken; + + const EncryptToolPage({Key? key, required this.adminToken}) : super(key: key); + + @override + State createState() => _EncryptToolPageState(); +} + +class _EncryptToolPageState extends State + with SingleTickerProviderStateMixin { + final TextEditingController _inputController = TextEditingController(); + final TextEditingController _outputController = TextEditingController(); + + String _output = ''; + bool _loading = false; + String? _error; + + bool _isInputCopied = false; + bool _isOutputCopied = false; + + late final AnimationController _glowController; + late final Animation _glowAnimation; + + @override + void initState() { + super.initState(); + _glowController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(reverse: true); + _glowAnimation = Tween(begin: 0.4, end: 1.0).animate( + CurvedAnimation(parent: _glowController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _inputController.dispose(); + _outputController.dispose(); + _glowController.dispose(); + super.dispose(); + } + + // ─── Logic (unchanged) ────────────────────────────────────────────────────── + + Future _callTool(String action) async { + FocusScope.of(context).unfocus(); + + setState(() { + _loading = true; + _error = null; + _output = ''; + _outputController.clear(); + _isInputCopied = false; + _isOutputCopied = false; + }); + + try { + final response = await CRUD().post( + link: '${AppLink.server}/ggg.php', + payload: { + 'action': action, + 'text': _inputController.text, + 'admin_phone': box.read(BoxName.adminPhone) ?? '', + }, + ); + + if (response == 'failure') { + setState(() => _error = 'حدث خطأ في الاتصال بالخادم. حاول مرة أخرى.'); + } else { + if (response['status'] == 'success') { + setState(() { + _output = (response['result'] ?? '').toString(); + _outputController.text = _output; + }); + } else { + setState(() => + _error = response['message']?.toString() ?? 'حدث خطأ غير معروف.'); + } + } + } catch (e) { + setState(() => _error = 'مشكلة في الشبكة: $e'); + } finally { + setState(() => _loading = false); + } + } + + void _showSnackBar(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(isError ? Icons.error_outline : Icons.check_circle, + color: isError ? _AppColors.error : _AppColors.success), + const SizedBox(width: 12), + Text( + message, + style: const TextStyle( + fontFamily: 'Cairo', + fontWeight: FontWeight.bold, + color: _AppColors.textPrimary, + ), + ), + ], + ), + backgroundColor: + isError ? const Color(0xFF1A0808) : const Color(0xFF081A0F), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide( + color: isError ? _AppColors.error : _AppColors.success, + width: 1, + ), + ), + margin: const EdgeInsets.all(16), + duration: const Duration(seconds: 2), + ), + ); + } + + Future _copyText(bool isInput) async { + final textToCopy = isInput ? _inputController.text : _outputController.text; + + if (textToCopy.isEmpty) { + _showSnackBar('لا يوجد نص لنسخه!', isError: true); + return; + } + + await Clipboard.setData(ClipboardData(text: textToCopy)); + + if (isInput) { + setState(() => _isInputCopied = true); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) setState(() => _isInputCopied = false); + }); + } else { + setState(() => _isOutputCopied = true); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) setState(() => _isOutputCopied = false); + }); + } + + _showSnackBar('تم النسخ بنجاح!'); + } + + Future _pasteText(bool isInput) async { + final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); + final textToPaste = clipboardData?.text ?? ''; + + if (textToPaste.isEmpty) { + _showSnackBar('الحافظة فارغة!', isError: true); + return; + } + + setState(() { + if (isInput) { + _inputController.text = textToPaste; + } else { + _output = textToPaste; + _outputController.text = textToPaste; + } + }); + + _showSnackBar('تم اللصق بنجاح!'); + } + + // ─── UI Helpers ───────────────────────────────────────────────────────────── + + Widget _buildActionButtons({ + required bool isInput, + required bool isCopied, + Color accentColor = _AppColors.accent, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _MiniIconButton( + label: 'لصق', + icon: Icons.content_paste_rounded, + color: accentColor, + onTap: () => _pasteText(isInput), + ), + const SizedBox(width: 6), + _MiniIconButton( + label: isCopied ? 'تم!' : 'نسخ', + icon: isCopied ? Icons.check_circle_rounded : Icons.copy_rounded, + color: isCopied ? _AppColors.success : accentColor, + onTap: () => _copyText(isInput), + ), + ], + ); + } + + InputDecoration _buildInputDecoration(String hint) => InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: _AppColors.textSec, fontSize: 14), + filled: true, + fillColor: const Color(0xFF0C1120), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: _AppColors.border, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: _AppColors.accent, width: 1.5), + ), + contentPadding: const EdgeInsets.all(16), + ); + + // ─── Build ────────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _AppColors.bg, + body: Stack( + children: [ + // ── Ambient background glow ────────────────────────────────────────── + Positioned( + top: -120, + left: -80, + child: AnimatedBuilder( + animation: _glowAnimation, + builder: (_, __) => Opacity( + opacity: _glowAnimation.value * 0.25, + child: Container( + width: 380, + height: 380, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [Color(0xFF00E5FF), Colors.transparent], + ), + ), + ), + ), + ), + ), + Positioned( + bottom: -100, + right: -60, + child: AnimatedBuilder( + animation: _glowAnimation, + builder: (_, __) => Opacity( + opacity: (1 - _glowAnimation.value) * 0.2, + child: Container( + width: 320, + height: 320, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [Color(0xFF7C4DFF), Colors.transparent], + ), + ), + ), + ), + ), + ), + + // ── Content ────────────────────────────────────────────────────────── + SafeArea( + child: Column( + children: [ + // ── AppBar ───────────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 16, 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, + color: _AppColors.textSec, size: 20), + onPressed: () => Navigator.pop(context), + ), + const Spacer(), + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: _AppColors.accent, + boxShadow: [ + BoxShadow( + color: _AppColors.accentGlow, + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + ), + const SizedBox(width: 10), + const Text( + 'أداة التشفير', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _AppColors.textPrimary, + letterSpacing: 0.5, + ), + ), + ], + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _AppColors.accentGlow, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _AppColors.accent.withOpacity(0.3)), + ), + child: const Text( + 'AES-256', + style: TextStyle( + color: _AppColors.accent, + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + ), + ], + ), + ), + + // ── Scrollable body ─────────────────────────────────────────── + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 32), + physics: const BouncingScrollPhysics(), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 650), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ─── Input Card ───────────────────────────────────── + _GlassCard( + borderColor: _AppColors.accent.withOpacity(0.2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _AppColors.accentGlow, + borderRadius: + BorderRadius.circular(10), + ), + child: const Icon( + Icons.text_fields_rounded, + color: _AppColors.accent, + size: 18), + ), + const SizedBox(width: 12), + const Text( + 'النص الأصلي', + style: TextStyle( + color: _AppColors.textPrimary, + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + _buildActionButtons( + isInput: true, + isCopied: _isInputCopied, + accentColor: _AppColors.accent, + ), + ], + ), + const SizedBox(height: 16), + + // Input field + TextField( + controller: _inputController, + maxLines: 5, + minLines: 3, + textDirection: TextDirection.ltr, + style: const TextStyle( + color: _AppColors.textPrimary, + fontSize: 15, + height: 1.6, + fontFamily: 'monospace', + ), + onTap: () { + if (_inputController.text.isNotEmpty) { + _inputController.selection = + TextSelection( + baseOffset: 0, + extentOffset: + _inputController.text.length, + ); + } + }, + decoration: _buildInputDecoration( + 'اكتب أو الصق النص هنا...'), + ), + const SizedBox(height: 24), + + // Action buttons row + Row( + children: [ + // Encrypt + Expanded( + child: _ActionButton( + label: 'تشفير', + icon: Icons.lock_rounded, + isLoading: _loading, + onPressed: _loading + ? null + : () => _callTool('encrypt'), + gradient: const LinearGradient( + colors: [ + Color(0xFF00B4D8), + Color(0xFF00E5FF) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + glowColor: _AppColors.accentGlow, + borderColor: _AppColors.accent, + ), + ), + const SizedBox(width: 14), + // Decrypt + Expanded( + child: _ActionButton( + label: 'فك التشفير', + icon: Icons.lock_open_rounded, + isLoading: _loading, + onPressed: _loading + ? null + : () => _callTool('decrypt'), + gradient: const LinearGradient( + colors: [ + Color(0xFF5B2EA6), + Color(0xFF7C4DFF) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + glowColor: + _AppColors.accentDecryptGlow, + borderColor: _AppColors.accentDecrypt, + ), + ), + ], + ), + ], + ), + ), + + // ─── Error message ─────────────────────────────────── + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: _error != null + ? Container( + margin: const EdgeInsets.only(top: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1A0808), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: _AppColors.error + .withOpacity(0.4)), + ), + child: Row( + children: [ + const Icon( + Icons.error_outline_rounded, + color: _AppColors.error, + size: 20), + const SizedBox(width: 12), + Expanded( + child: Text( + _error!, + style: const TextStyle( + color: _AppColors.error, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + + // ─── Output Card ───────────────────────────────────── + const SizedBox(height: 20), + _GlassCard( + borderColor: + _AppColors.accentDecrypt.withOpacity(0.25), + headerWidget: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 14), + decoration: const BoxDecoration( + color: Color(0xFF16102A), + borderRadius: BorderRadius.vertical( + top: Radius.circular(20)), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _AppColors.accentDecryptGlow, + borderRadius: + BorderRadius.circular(10), + ), + child: const Icon( + Icons.shield_rounded, + color: _AppColors.accentDecrypt, + size: 18), + ), + const SizedBox(width: 12), + const Text( + 'النتيجة', + style: TextStyle( + color: _AppColors.textPrimary, + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + _buildActionButtons( + isInput: false, + isCopied: _isOutputCopied, + accentColor: _AppColors.accentDecrypt, + ), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: TextField( + controller: _outputController, + readOnly: true, + maxLines: 5, + minLines: 3, + textDirection: TextDirection.ltr, + style: const TextStyle( + color: _AppColors.textPrimary, + fontSize: 15, + height: 1.6, + fontFamily: 'monospace', + letterSpacing: 0.5, + ), + decoration: const InputDecoration( + hintText: 'ستظهر النتيجة هنا...', + hintStyle: TextStyle( + color: _AppColors.textSec, + fontSize: 14), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onTap: () { + if (_outputController.text.isNotEmpty) { + _outputController.selection = + TextSelection( + baseOffset: 0, + extentOffset: + _outputController.text.length, + ); + } + }, + ), + ), + ), + + // ─── Footer hint ───────────────────────────────────── + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.info_outline_rounded, + color: _AppColors.textSec, size: 13), + const SizedBox(width: 6), + const Text( + 'البيانات مشفرة بالكامل ولا تُخزَّن على الخادم', + style: TextStyle( + color: _AppColors.textSec, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// ─── Reusable Widgets ────────────────────────────────────────────────────────── + +/// Glass card with optional custom header widget +class _GlassCard extends StatelessWidget { + final Widget child; + final Widget? headerWidget; + final Color borderColor; + + const _GlassCard({ + required this.child, + this.headerWidget, + this.borderColor = _AppColors.border, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: _AppColors.card, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: borderColor, width: 1.2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.35), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (headerWidget != null) headerWidget!, + if (headerWidget == null) + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: child, + ) + else + child, + if (headerWidget == null) const SizedBox(height: 20), + ], + ), + ), + ); + } +} + +/// Gradient action button with glow +class _ActionButton extends StatefulWidget { + final String label; + final IconData icon; + final bool isLoading; + final VoidCallback? onPressed; + final Gradient gradient; + final Color glowColor; + final Color borderColor; + + const _ActionButton({ + required this.label, + required this.icon, + required this.isLoading, + required this.onPressed, + required this.gradient, + required this.glowColor, + required this.borderColor, + }); + + @override + State<_ActionButton> createState() => _ActionButtonState(); +} + +class _ActionButtonState extends State<_ActionButton> + with SingleTickerProviderStateMixin { + late final AnimationController _ctrl; + late final Animation _scale; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 120)); + _scale = Tween(begin: 1.0, end: 0.95) + .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut)); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => _ctrl.forward(), + onTapUp: (_) => _ctrl.reverse(), + onTapCancel: () => _ctrl.reverse(), + onTap: widget.onPressed, + child: AnimatedBuilder( + animation: _scale, + builder: (_, child) => + Transform.scale(scale: _scale.value, child: child), + child: AnimatedOpacity( + opacity: widget.onPressed == null ? 0.4 : 1.0, + duration: const Duration(milliseconds: 200), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + gradient: widget.gradient, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: widget.glowColor, + blurRadius: 16, + spreadRadius: 1, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.isLoading + ? [ + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2), + ), + ] + : [ + Icon(widget.icon, color: Colors.white, size: 18), + const SizedBox(width: 8), + Text( + widget.label, + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Small inline icon button (copy/paste) +class _MiniIconButton extends StatelessWidget { + final String label; + final IconData icon; + final Color color; + final VoidCallback onTap; + + const _MiniIconButton({ + required this.label, + required this.icon, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 14), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/admin/enceypt/fingerprint_migration.dart b/lib/views/admin/enceypt/fingerprint_migration.dart new file mode 100644 index 0000000..532d0da --- /dev/null +++ b/lib/views/admin/enceypt/fingerprint_migration.dart @@ -0,0 +1,355 @@ +// ═══════════════════════════════════════════════════════════════ +// fingerprint_migration.dart +// ─────────────────────────────────────────────────────────────── +// أداة ترحيل البصمات القديمة للنظام الجديد +// ─────────────────────────────────────────────────────────────── +// المشكلة: +// البصمة القديمة = encrypt(androidId_model_osVersion) +// البصمة الجديدة = encrypt(androidId_model) +// +// الحل: +// 1. نجيب كل البصمات من السيرفر (batch 50 في المرة) +// 2. نفك تشفير كل بصمة بـ EncryptionHelper +// 3. نحذف آخر جزء (osVersion) مع الـ _ قبله +// 4. نعيد التشفير +// 5. نرفع البصمة المحدّثة للسيرفر +// +// يُستخدم مرة واحدة فقط ثم يُحذف من التطبيق +// ═══════════════════════════════════════════════════════════════ + +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +import '../../../constant/links.dart'; +import '../../../controller/functions/crud.dart'; +import '../../../controller/functions/encrypt_decrypt.dart'; +import '../../../print.dart'; + +class FingerprintMigrationTool extends StatefulWidget { + const FingerprintMigrationTool({super.key}); + + @override + State createState() => + _FingerprintMigrationToolState(); +} + +class _FingerprintMigrationToolState extends State { + // ── حالة الترحيل ────────────────────────────────────────── + bool _isRunning = false; + bool _isDone = false; + int _total = 0; + int _processed = 0; + int _updated = 0; // بصمات تم تحديثها + int _skipped = 0; // بصمات كانت بالفعل بالنظام الجديد + int _failed = 0; // فشل في المعالجة + String _currentLog = ''; + + static const int _batchSize = 50; + + // ───────────────────────────────────────────────────────────── + // الدالة الرئيسية للترحيل + // ───────────────────────────────────────────────────────────── + Future _startMigration() async { + setState(() { + _isRunning = true; + _isDone = false; + _processed = 0; + _updated = 0; + _skipped = 0; + _failed = 0; + _currentLog = 'جارٍ جلب البصمات من السيرفر...'; + }); + + try { + // ── 1. جلب كل البصمات من السيرفر ────────────────────── + final allFingerprints = await _fetchAllFingerprints(); + + if (allFingerprints == null) { + _log('❌ فشل في جلب البيانات من السيرفر'); + setState(() => _isRunning = false); + return; + } + + _total = allFingerprints.length; + _log('✅ تم جلب $_total بصمة — بدء المعالجة...'); + + // ── 2. معالجة على batches ────────────────────────────── + for (int i = 0; i < allFingerprints.length; i += _batchSize) { + final batch = allFingerprints.skip(i).take(_batchSize).toList(); + + _log('⚙️ معالجة ${i + 1} → ${i + batch.length} من $_total'); + + // معالجة الـ batch بالتوازي + await Future.wait( + batch.map((record) => _processSingleRecord(record)), + ); + + // استراحة قصيرة بين الـ batches لحماية السيرفر + if (i + _batchSize < allFingerprints.length) { + await Future.delayed(const Duration(milliseconds: 300)); + } + } + + _log('🎉 اكتمل الترحيل!\n' + 'محدَّث: $_updated | متجاوز: $_skipped | فاشل: $_failed'); + + setState(() { + _isDone = true; + _isRunning = false; + }); + } catch (e) { + _log('❌ خطأ عام: $e'); + setState(() => _isRunning = false); + } + } + + // ───────────────────────────────────────────────────────────── + // جلب كل البصمات من السيرفر + // ───────────────────────────────────────────────────────────── + Future>?> _fetchAllFingerprints() async { + try { + final response = await CRUD().post( + link: AppLink.getAllFingerprints, // أضفه في AppLink + payload: { + 'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd' + }, // مفتاح أمان للـ endpoint + ); + + if (response == 'failure' || response == null) return null; + + final data = response['data']; + if (data is! List) return null; + + return List>.from(data); + } catch (e) { + Log.print('fetchAllFingerprints error: $e'); + return null; + } + } + + // ───────────────────────────────────────────────────────────── + // معالجة بصمة واحدة + // ───────────────────────────────────────────────────────────── + Future _processSingleRecord(Map record) async { + final String passengerID = record['passengerID']?.toString() ?? ''; + final String encryptedFp = record['fingerPrint']?.toString() ?? ''; + final String userType = record['userType']?.toString() ?? 'passenger'; + + if (passengerID.isEmpty || encryptedFp.isEmpty) { + setState(() { + _failed++; + _processed++; + }); + return; + } + + try { + // ── فك التشفير ──────────────────────────────────────── + final String rawFp = EncryptionHelper.instance.decryptData(encryptedFp); + + // ── تحليل البصمة ────────────────────────────────────── + // الشكل القديم: "androidId_model_osVersion" (3 أجزاء أو أكثر) + // الشكل الجديد: "androidId_model" (جزءان فقط) + final List parts = rawFp.split('_'); + + if (parts.length <= 2) { + // البصمة بالفعل بالنظام الجديد — تجاوزها + setState(() { + _skipped++; + _processed++; + }); + return; + } + + // ── حذف آخر جزء (osVersion) ────────────────────────── + // مثال: "abc123_SamsungA51_13" → "abc123_SamsungA51" + // نأخذ أول جزأين فقط بغض النظر عن عدد الأجزاء + final String newRawFp = '${parts[0]}_${parts[1]}'; + + // ── إعادة التشفير ───────────────────────────────────── + final String newEncryptedFp = + EncryptionHelper.instance.encryptData(newRawFp); + + // ── رفع البصمة الجديدة للسيرفر ─────────────────────── + final response = await CRUD().post( + link: AppLink.updateFingerprintAdmin, // أضفه في AppLink + payload: { + 'passengerID': passengerID, + 'fingerprint': newEncryptedFp, + 'userType': userType, + 'admin_key': 'iuyweiruinakjbfkajkjlkmalkcxnlahd', + }, + ); + + if (response != 'failure' && response?['status'] == 'success') { + setState(() { + _updated++; + _processed++; + }); + Log.print('✅ Updated: $passengerID | $rawFp → $newRawFp'); + } else { + setState(() { + _failed++; + _processed++; + }); + Log.print('❌ Failed update: $passengerID'); + } + } catch (e) { + // فشل فك التشفير أو إعادة التشفير + setState(() { + _failed++; + _processed++; + }); + Log.print('❌ Process error for $passengerID: $e'); + } + } + + void _log(String message) { + Log.print(message); + setState(() => _currentLog = message); + } + + // ───────────────────────────────────────────────────────────── + // UI + // ───────────────────────────────────────────────────────────── + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Fingerprint Migration Tool')), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── شرح الأداة ────────────────────────────────── + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.shade200), + ), + child: const Text( + '⚠️ هذه الأداة تُستخدم مرة واحدة فقط\n' + 'تقوم بتحديث بصمات الأجهزة القديمة\n' + 'لتكون متوافقة مع النظام الجديد (بدون OS version)', + style: TextStyle(fontSize: 14, height: 1.6), + ), + ), + Container( + child: TextButton( + onPressed: () { + print(EncryptionHelper.instance.decryptData( + 'dab40749cdecbfddf4696566448b384f0d272705b08b4ff779e085fbf3257026')); + }, + child: Text( + "Decrypt Test", + ), + ), + ), + Container( + child: TextButton( + onPressed: () { + print(EncryptionHelper.instance.encryptData( + '1B501143-C579-461C-B556-4E8B390EEFE1_iPhone')); + }, + child: Text( + "Encrypt Test", + ), + ), + ), + + const SizedBox(height: 24), + + // ── شريط التقدم ───────────────────────────────── + if (_total > 0) ...[ + Text('التقدم: $_processed / $_total'), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _total > 0 ? _processed / _total : 0, + backgroundColor: Colors.grey.shade200, + color: _isDone ? Colors.green : Colors.blue, + ), + const SizedBox(height: 16), + ], + + // ── إحصائيات ──────────────────────────────────── + if (_processed > 0) + Row(children: [ + _statChip('محدَّث', _updated, Colors.green), + const SizedBox(width: 8), + _statChip('متجاوز', _skipped, Colors.blue), + const SizedBox(width: 8), + _statChip('فاشل', _failed, Colors.red), + ]), + + const SizedBox(height: 16), + + // ── السجل الحالي ───────────────────────────────── + if (_currentLog.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text(_currentLog, + style: const TextStyle(fontFamily: 'monospace')), + ), + + const Spacer(), + + // ── زر التشغيل ────────────────────────────────── + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: (_isRunning || _isDone) ? null : _startMigration, + style: ElevatedButton.styleFrom( + backgroundColor: _isDone ? Colors.green : Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: _isRunning + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2), + ), + SizedBox(width: 12), + Text('جارٍ الترحيل...', + style: TextStyle(color: Colors.white)), + ], + ) + : Text( + _isDone ? '✅ اكتمل الترحيل' : 'بدء الترحيل', + style: + const TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _statChip(String label, int value, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text('$label: $value', + style: TextStyle(color: color, fontWeight: FontWeight.bold)), + ); + } +} diff --git a/lib/views/admin/error/error/error_page.dart b/lib/views/admin/error/error/error_page.dart index 83f9359..fe67bd3 100644 --- a/lib/views/admin/error/error/error_page.dart +++ b/lib/views/admin/error/error/error_page.dart @@ -128,14 +128,8 @@ class _ErrorListPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.grey[100], - appBar: AppBar( - title: const Text('سجل الأخطاء'), - backgroundColor: Colors.white, - foregroundColor: Colors.black, - elevation: 1.0, - centerTitle: true, - ), + backgroundColor: const Color(0xFFF0F4F8), + appBar: _buildAppBar(), body: Column( children: [ _SearchBar( @@ -151,22 +145,84 @@ class _ErrorListPageState extends State { ); } + PreferredSizeWidget _buildAppBar() { + return AppBar( + elevation: 0, + backgroundColor: const Color(0xFF0F172A), + foregroundColor: Colors.white, + centerTitle: true, + title: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 22), + const SizedBox(width: 8), + const Text( + "سجل الأخطاء", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } + Widget _buildBody() { if (_loading) { - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF0F172A)), + ), + ); } if (_errorMsg != null) { return Center( - child: Text( - _errorMsg!, - style: TextStyle(color: Colors.red[700]), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.red.shade300), + const SizedBox(height: 16), + Text( + _errorMsg!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 14, + ), + ), + ], ), ); } if (_items.isEmpty) { - return const Center(child: Text('لا توجد سجلات')); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Colors.green.shade300, + ), + const SizedBox(height: 16), + const Text( + 'لا توجد سجلات أخطاء', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ], + ), + ); } return RefreshIndicator( @@ -177,19 +233,25 @@ class _ErrorListPageState extends State { await _fetchLast20(); } }, + color: const Color(0xFF0F172A), child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), itemCount: _items.length, itemBuilder: (context, index) { - final e = _items[index]; - return _ErrorTile(e); + return _ErrorTile(_items[index], index); }, ), ); } + + @override + void dispose() { + _phoneCtrl.dispose(); + super.dispose(); + } } -class _SearchBar extends StatelessWidget { +class _SearchBar extends StatefulWidget { final TextEditingController controller; final VoidCallback onSearch; final VoidCallback onClear; @@ -200,40 +262,132 @@ class _SearchBar extends StatelessWidget { required this.onClear, }); + @override + State<_SearchBar> createState() => _SearchBarState(); +} + +class _SearchBarState extends State<_SearchBar> { + bool _isFocused = false; + @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.all(12), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: controller, - keyboardType: TextInputType.phone, - onSubmitted: (_) => onSearch(), - decoration: const InputDecoration( - hintText: 'بحث برقم الهاتف...', - prefixIcon: Icon(Icons.search), - border: InputBorder.none, + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.grey.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 16, + spreadRadius: 2, + ) + ], + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Focus( + onFocusChange: (focused) { + setState(() => _isFocused = focused); + }, + child: TextField( + controller: widget.controller, + keyboardType: TextInputType.phone, + textDirection: TextDirection.rtl, + onSubmitted: (_) => widget.onSearch(), + style: const TextStyle(fontSize: 15), + decoration: InputDecoration( + hintText: 'بحث برقم الهاتف', + hintStyle: TextStyle(color: Colors.grey.shade400), + prefixIcon: Icon( + Icons.search, + color: _isFocused + ? const Color(0xFF0F172A) + : Colors.grey.shade400, + ), + suffixIcon: widget.controller.text.isNotEmpty + ? InkWell( + onTap: () { + widget.controller.clear(); + setState(() {}); + }, + child: Icon(Icons.close, + color: Colors.grey.shade400, size: 20), + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + BorderSide(color: Colors.grey.shade300, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + BorderSide(color: Colors.grey.shade300, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Color(0xFF0F172A), + width: 2, + ), + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + onChanged: (_) => setState(() {}), + ), ), ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: onSearch, - child: const Text('بحث'), - ), - const SizedBox(width: 8), - TextButton( - onPressed: onClear, - child: const Text('مسح'), - ), - ], - ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: widget.onSearch, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0F172A), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Row( + children: [ + Icon(Icons.search, size: 18), + SizedBox(width: 6), + Text("بحث"), + ], + ), + ), + const SizedBox(width: 8), + OutlinedButton( + onPressed: widget.onClear, + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF0F172A), + side: const BorderSide( + color: Color(0xFF0F172A), + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + ), + child: const Icon(Icons.refresh, size: 20), + ), + ], + ), + ], ), ); } @@ -241,112 +395,265 @@ class _SearchBar extends StatelessWidget { class _ErrorTile extends StatelessWidget { final ErrorLog item; - const _ErrorTile(this.item); + final int index; + + const _ErrorTile(this.item, this.index); @override Widget build(BuildContext context) { - final theme = Theme.of(context); + // تحديد الألوان والأيقونات بناءً على نوع المستخدم + final isDriver = item.userType.toLowerCase().contains('driver') || + item.userType.toLowerCase().contains('سائق'); - // تحديد الألوان بناءً على نوع المستخدم - Color? typeBgColor; - Color? typeTextColor; + final userTypeColor = + isDriver ? const Color(0xFF10B981) : const Color(0xFFF59E0B); + final userTypeIcon = isDriver ? Icons.directions_car : Icons.person; + final userTypeLabel = isDriver ? "سائق" : "راكب"; - final type = item.userType.toLowerCase(); - if (type.contains('driver') || type.contains('سائق')) { - typeBgColor = Colors.green.shade100; - typeTextColor = Colors.green.shade800; - } else if (type.contains('passenger') || type.contains('راكب')) { - typeBgColor = Colors.amber.shade100; // لون ذهبي/أصفر - typeTextColor = Colors.amber.shade900; - } + final userTypeBgColor = isDriver + ? const Color(0xFF10B981).withOpacity(0.1) + : const Color(0xFFF59E0B).withOpacity(0.1); - return Card( + return Container( margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.grey.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 12, + spreadRadius: 1, + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( children: [ - // السطر الأول: نص الخطأ (قابل للنسخ) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SelectableText( - item.error.isEmpty ? '(بدون عنوان)' : item.error, - style: theme.textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), + // خط علوي ملون + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 4, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFFEF4444), + Colors.red.shade400, + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, ), ), - // تم إزالة _StatusPill (ويدجت الحالة New) من هنا - ], - ), - const SizedBox(height: 8), - // تفاصيل مختصرة (قابل للنسخ) - if (item.details.isNotEmpty) - SelectableText( - item.details, - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.grey[700]), ), - const SizedBox(height: 12), - // معلومات تقنية - Wrap( - spacing: 8, - runSpacing: 6, - children: [ - _KV('الهاتف', item.phone), - _KV('المستخدم', item.userId), - // تمرير الألوان المخصصة لنوع المستخدم - _KV( - 'النوع', - item.userType, - bgColor: typeBgColor, - textColor: typeTextColor, - ), - _KV('الجهاز', item.device), - _KV('التاريخ', item.createdAt), - _KV('ID', item.id), - ], + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // الصف الأول: رقم الخطأ ونوع المستخدم + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: userTypeBgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: userTypeColor, width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(userTypeIcon, size: 14, color: userTypeColor), + const SizedBox(width: 4), + Text( + userTypeLabel, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: userTypeColor, + ), + ), + ], + ), + ), + Text( + '#${item.id}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 12), + + // عنوان الخطأ (قابل للنسخ) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.red.shade200, + width: 1, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.warning_amber_rounded, + size: 18, color: Colors.red.shade600), + const SizedBox(width: 8), + Expanded( + child: SelectableText( + item.error.isEmpty ? '(بدون عنوان)' : item.error, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.red.shade900, + height: 1.4, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + // التفاصيل (إن وجدت) + if (item.details.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade200, + width: 1, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, + size: 16, color: Colors.grey.shade600), + const SizedBox(width: 8), + Expanded( + child: SelectableText( + item.details, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + height: 1.4, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + ], + + // معلومات تقنية + Wrap( + spacing: 6, + runSpacing: 6, + alignment: WrapAlignment.end, + children: [ + _buildInfoBadge( + icon: Icons.phone, + label: 'الهاتف', + value: item.phone, + color: Colors.blue, + ), + _buildInfoBadge( + icon: Icons.person_outline, + label: 'المعرف', + value: item.userId, + color: Colors.purple, + ), + _buildInfoBadge( + icon: Icons.devices, + label: 'Path', + value: item.device, + color: Colors.orange, + ), + _buildInfoBadge( + icon: Icons.schedule, + label: 'التاريخ', + value: item.createdAt, + color: Colors.teal, + ), + ], + ), + ], + ), ), ], ), ), ); } -} -class _KV extends StatelessWidget { - final String k; - final String v; - final Color? bgColor; - final Color? textColor; - - const _KV(this.k, this.v, {this.bgColor, this.textColor}); - - @override - Widget build(BuildContext context) { + Widget _buildInfoBadge({ + required IconData icon, + required String label, + required String value, + required Color color, + }) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: bgColor ?? Colors.grey[200], + color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.3), width: 1), ), - child: Text.rich(TextSpan( - style: TextStyle(fontSize: 11, color: textColor ?? Colors.grey[600]), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - TextSpan(text: "$k: "), - TextSpan( - text: v.isEmpty ? '—' : v, - style: TextStyle( - fontWeight: FontWeight.bold, - color: textColor ?? Colors.grey[800], + Icon(icon, size: 13, color: color), + const SizedBox(width: 4), + Flexible( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "$label: ", + style: TextStyle( + fontSize: 10, + color: color.withOpacity(0.7), + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: value.isEmpty ? '—' : value, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), ), ), ], - )), + ), ); } } diff --git a/lib/views/admin/packages.dart b/lib/views/admin/packages.dart index c695b8e..201afdf 100644 --- a/lib/views/admin/packages.dart +++ b/lib/views/admin/packages.dart @@ -1,71 +1,529 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; import 'package:sefer_admin1/constant/links.dart'; import 'package:sefer_admin1/controller/functions/crud.dart'; import 'package:sefer_admin1/views/widgets/my_textField.dart'; import '../../print.dart'; +// ══════════════════════════════════════════════════════════════ +// DESIGN TOKENS (same as AdminHomePage) +// ══════════════════════════════════════════════════════════════ +const Color _bg = Color(0xFF0D1117); +const Color _surface = Color(0xFF161B22); +const Color _surfaceElevated = Color(0xFF1C2333); +const Color _accent = Color(0xFF00D4AA); +const Color _danger = Color(0xFFFF5370); +const Color _warning = Color(0xFFFFCB6B); +const Color _info = Color(0xFF82AAFF); +const Color _textPrimary = Color(0xFFE6EDF3); +const Color _textSecondary = Color(0xFF7D8590); +const Color _divider = Color(0xFF21262D); + class PackageUpdateScreen extends StatelessWidget { + PackageUpdateScreen({super.key}); + final PackageController packageController = Get.put(PackageController()); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Package Update'), - ), - body: GetBuilder(builder: (packageController) { - return Center( - child: ListView.builder( - itemCount: packageController.packages.length, + backgroundColor: _bg, + appBar: _buildAppBar(), + body: GetBuilder( + builder: (controller) { + if (controller.isLoading.value) { + return const Center( + child: CircularProgressIndicator(color: _accent, strokeWidth: 2), + ); + } + + if (controller.packages.isEmpty) { + return _buildEmptyState(); + } + + return ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 40), + itemCount: controller.packages.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), itemBuilder: (context, index) { - var package = packageController.packages[index]; - return ListTile( - title: Text(package['appName']), - subtitle: Text( - 'Platform: ${package['platform']} \nVersion: ${package['version']}'), - trailing: const Icon(Icons.update), - onTap: () { - Get.defaultDialog( - title: 'Update', - middleText: '', - content: Column( + final package = controller.packages[index]; + return _buildPackageCard(context, package, controller); + }, + ); + }, + ), + ); + } + + // ─────────────────────────── APP BAR ─────────────────────────── + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: _bg, + elevation: 0, + surfaceTintColor: Colors.transparent, + leading: GestureDetector( + onTap: () => Get.back(), + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _divider), + ), + child: const Icon(Icons.arrow_back_ios_new_rounded, + color: _textSecondary, size: 16), + ), + ), + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + color: _accent.withOpacity(0.12), + borderRadius: BorderRadius.circular(9), + border: Border.all(color: _accent.withOpacity(0.25)), + ), + child: const Icon(Icons.system_update_rounded, + color: _accent, size: 16), + ), + const SizedBox(width: 10), + const Text( + 'تحديث التطبيق', + style: TextStyle( + color: _textPrimary, + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + actions: [ + GestureDetector( + onTap: () => packageController.fetchPackages(), + child: Container( + margin: const EdgeInsets.only(right: 16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _divider), + ), + child: const Icon(Icons.refresh_rounded, + color: _textSecondary, size: 18), + ), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container(height: 1, color: _divider), + ), + ); + } + + // ─────────────────────────── PACKAGE CARD ─────────────────────────── + Widget _buildPackageCard( + BuildContext context, dynamic package, PackageController controller) { + final platform = package['platform']?.toString() ?? ''; + final isAndroid = platform.toLowerCase().contains('android'); + final isIOS = platform.toLowerCase().contains('ios'); + + final Color platformColor = isAndroid + ? const Color(0xFF4CAF50) + : isIOS + ? _info + : _warning; + final IconData platformIcon = isAndroid + ? Icons.android_rounded + : isIOS + ? Icons.apple_rounded + : Icons.devices_rounded; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showUpdateDialog(context, package, controller), + borderRadius: BorderRadius.circular(16), + splashColor: _accent.withOpacity(0.06), + highlightColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _divider), + ), + child: Row( + children: [ + // Platform Icon + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + platformColor.withOpacity(0.20), + platformColor.withOpacity(0.06), + ], + ), + borderRadius: BorderRadius.circular(13), + border: Border.all(color: platformColor.withOpacity(0.25)), + ), + child: Icon(platformIcon, color: platformColor, size: 22), + ), + + const SizedBox(width: 14), + + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + package['appName']?.toString() ?? '—', + style: const TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Row( children: [ - Text(package['appName']), - Text(package['platform']), - Text(package['version']), - MyTextForm( - controller: packageController.versionController, - label: package['version'].toString(), - hint: package['version'].toString(), - type: const TextInputType.numberWithOptions( - decimal: true), + _buildTag(platform, platformColor), + const SizedBox(width: 6), + _buildVersionBadge( + package['version']?.toString() ?? '?'), + ], + ), + ], + ), + ), + + // Update button + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + color: _accent.withOpacity(0.10), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _accent.withOpacity(0.25)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.edit_rounded, color: _accent, size: 13), + SizedBox(width: 5), + Text( + 'تعديل', + style: TextStyle( + color: _accent, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildTag(String label, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withOpacity(0.10), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + label, + style: TextStyle( + color: color, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildVersionBadge(String version) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _divider, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + 'v$version', + style: const TextStyle( + color: _textSecondary, + fontSize: 10, + fontWeight: FontWeight.w500, + fontFamily: 'monospace', + ), + ), + ); + } + + // ─────────────────────────── EMPTY STATE ─────────────────────────── + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _surface, + shape: BoxShape.circle, + border: Border.all(color: _divider), + ), + child: const Icon(Icons.inventory_2_outlined, + color: _textSecondary, size: 32), + ), + const SizedBox(height: 16), + const Text('لا توجد حزم متاحة', + style: TextStyle( + color: _textPrimary, + fontSize: 15, + fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + const Text('اسحب للأسفل لإعادة التحميل', + style: TextStyle(color: _textSecondary, fontSize: 12)), + ], + ), + ); + } + + // ─────────────────────────── UPDATE DIALOG ─────────────────────────── + void _showUpdateDialog( + BuildContext context, dynamic package, PackageController controller) { + controller.versionController.clear(); + + Get.dialog( + Dialog( + backgroundColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: _surfaceElevated, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: _divider), + boxShadow: const [ + BoxShadow( + color: Colors.black54, + blurRadius: 30, + offset: Offset(0, 12), + ), + ], + ), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: _accent.withOpacity(0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _accent.withOpacity(0.25)), + ), + child: const Icon(Icons.system_update_rounded, + color: _accent, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'تحديث الإصدار', + style: TextStyle( + color: _textPrimary, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + Text( + package['appName']?.toString() ?? '', + style: const TextStyle( + color: _textSecondary, fontSize: 11), ), ], ), - onConfirm: () async { - await packageController.updatePackages( - package['id'].toString(), - packageController.versionController.text.toString(), - ); - }, - onCancel: () {}, - ); - }, - ); - }, + ), + ], + ), + + const SizedBox(height: 20), + Container(height: 1, color: _divider), + const SizedBox(height: 20), + + // Current info + Row( + children: [ + _buildInfoChip(Icons.devices_rounded, + package['platform']?.toString() ?? '', _info), + const SizedBox(width: 8), + _buildInfoChip(Icons.tag_rounded, + 'الحالي: ${package['version']}', _warning), + ], + ), + + const SizedBox(height: 18), + + // Input label + const Text( + 'الإصدار الجديد', + style: TextStyle( + color: _textSecondary, + fontSize: 11, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 8), + + // Text input + Container( + decoration: BoxDecoration( + color: _bg, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _divider), + ), + child: TextField( + controller: controller.versionController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle( + color: _textPrimary, + fontSize: 15, + fontFamily: 'monospace', + fontWeight: FontWeight.w600, + ), + decoration: InputDecoration( + hintText: package['version'].toString(), + hintStyle: const TextStyle( + color: _textSecondary, + fontFamily: 'monospace', + ), + prefixIcon: + const Icon(Icons.tag_rounded, color: _accent, size: 18), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 14), + ), + ), + ), + + const SizedBox(height: 24), + + // Actions + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: _divider), + ), + ), + child: const Text( + 'إلغاء', + style: TextStyle(color: _textSecondary, fontSize: 13), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Obx(() => ElevatedButton.icon( + icon: controller.isLoading.value + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.check_rounded, size: 16), + label: Text( + controller.isLoading.value ? 'جاري...' : 'تحديث', + style: const TextStyle(fontSize: 13), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _accent, + foregroundColor: _bg, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + onPressed: controller.isLoading.value + ? null + : () async { + await controller.updatePackages( + package['id'].toString(), + controller.versionController.text, + ); + }, + )), + ), + ], + ), + ], ), - ); - }), + ), + ), + ); + } + + Widget _buildInfoChip(IconData icon, String label, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 13), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + color: color, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ); } } +// ══════════════════════════════════════════════════════════════ +// CONTROLLER +// ══════════════════════════════════════════════════════════════ class PackageController extends GetxController { - List packages = []; // Observable list to hold package info + List packages = []; var isLoading = false.obs; final versionController = TextEditingController(); final formKey = GlobalKey(); @@ -76,32 +534,62 @@ class PackageController extends GetxController { fetchPackages(); } - // Method to fetch package data from API fetchPackages() async { + isLoading.value = true; var response = await CRUD().get(link: AppLink.getPackages, payload: {}); - if (response != 'failure') { var jsonData = jsonDecode(response); packages = jsonData['message']; update(); - Log.print('jsonData: ${jsonData}'); + Log.print('jsonData: $jsonData'); } + isLoading.value = false; } - updatePackages(String id, version) async { + updatePackages(String id, String version) async { + if (version.trim().isEmpty) { + Get.snackbar( + 'تنبيه', + 'يرجى إدخال رقم الإصدار', + backgroundColor: _warning.withOpacity(0.15), + colorText: _textPrimary, + borderRadius: 12, + margin: const EdgeInsets.all(16), + icon: const Icon(Icons.warning_rounded, color: _warning), + ); + return; + } + + isLoading.value = true; var response = await CRUD().post( link: AppLink.updatePackages, - payload: { - "id": id, - "version": version, - }, + payload: {"id": id, "version": version}, ); - Log.print('response: ${response}'); + Log.print('response: $response'); + isLoading.value = false; + if (response != 'failure') { Get.back(); + Get.snackbar( + 'تم التحديث', + 'تم تحديث الإصدار بنجاح', + backgroundColor: _accent.withOpacity(0.15), + colorText: _textPrimary, + borderRadius: 12, + margin: const EdgeInsets.all(16), + icon: const Icon(Icons.check_circle_rounded, color: _accent), + ); fetchPackages(); } else { - Get.snackbar('error', 'message'); + Get.snackbar( + 'خطأ', + 'فشل التحديث، يرجى المحاولة مجدداً', + backgroundColor: _danger.withOpacity(0.15), + colorText: _textPrimary, + borderRadius: 12, + margin: const EdgeInsets.all(16), + icon: const Icon(Icons.error_rounded, color: _danger), + ); } } } diff --git a/lib/views/admin/rides/ride_lookup_page.dart b/lib/views/admin/rides/ride_lookup_page.dart index 7c0c7a7..63fb1c5 100644 --- a/lib/views/admin/rides/ride_lookup_page.dart +++ b/lib/views/admin/rides/ride_lookup_page.dart @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; -import 'package:url_launcher/url_launcher.dart'; // ضروري للاتصال +import 'package:sefer_admin1/constant/links.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../controller/functions/crud.dart'; -import '../../../constant/box_name.dart'; // لتحديد هوية المستخدم الحالي -import '../../../main.dart'; // للوصول لـ box +import '../../../constant/box_name.dart'; +import '../../../main.dart'; -// ========================================== +// ═══════════════════════════════════════════════════════════════════════════ // 1. MODEL -// ========================================== +// ═══════════════════════════════════════════════════════════════════════════ + class RideDashboardModel { final String rideId; final String status; @@ -20,17 +22,14 @@ class RideDashboardModel { final String time; final String price; final String distance; - final String driverId; final String driverName; final String driverPhone; final String driverCompletedCount; final String driverCanceledCount; - final String passengerName; final String passengerPhone; final String passengerCompletedCount; - final String cancelReason; RideDashboardModel({ @@ -94,9 +93,10 @@ class RideDashboardModel { } } -// ========================================== +// ═══════════════════════════════════════════════════════════════════════════ // 2. CONTROLLER -// ========================================== +// ═══════════════════════════════════════════════════════════════════════════ + class RidesListController extends GetxController { var isLoading = false.obs; var allRidesList = []; @@ -105,17 +105,21 @@ class RidesListController extends GetxController { TextEditingController searchController = TextEditingController(); String currentStatus = 'Begin'; - // === التحقق من صلاحية الأدمن === - // نقرأ رقم الهاتف الحالي المخزن في التطبيق String myPhone = box.read(BoxName.adminPhone)?.toString() ?? ''; bool get isSuperAdmin { - // ضع هنا أرقام هواتف الأدمن المسموح لهم برؤية الأرقام والاتصال return myPhone == '963942542053' || myPhone == '963992952235'; } - final String apiUrl = - "https://api.intaleq.xyz/intaleq/Admin/rides/get_rides_by_status.php"; + final String apiUrl = "${AppLink.server}/Admin/rides/get_rides_by_status.php"; + + // ═══ Statistics ═══ + var beginCount = 0.obs; + var newCount = 0.obs; + var completedCount = 0.obs; + var canceledCount = 0.obs; + var totalRevenue = 0.0.obs; + var totalDistance = 0.0.obs; @override void onInit() { @@ -143,6 +147,20 @@ class RidesListController extends GetxController { } } + void calculateStatistics() { + beginCount.value = allRidesList.where((r) => r.status == 'Begin').length; + newCount.value = allRidesList.where((r) => r.status == 'New').length; + completedCount.value = + allRidesList.where((r) => r.status == 'Finished').length; + canceledCount.value = + allRidesList.where((r) => r.status.contains('Cancel')).length; + + totalRevenue.value = allRidesList.fold( + 0.0, (sum, ride) => sum + (double.tryParse(ride.price) ?? 0.0)); + totalDistance.value = allRidesList.fold( + 0.0, (sum, ride) => sum + (double.tryParse(ride.distance) ?? 0.0)); + } + Future fetchRides() async { isLoading.value = true; allRidesList.clear(); @@ -152,84 +170,220 @@ class RidesListController extends GetxController { await CRUD().post(link: apiUrl, payload: {"status": currentStatus}); if (response != 'failure' && response['status'] == 'success') { List data = []; - if (response['message'] is List) + if (response['message'] is List) { data = response['message']; - else if (response['data'] is List) data = response['data']; + } else if (response['data'] is List) { + data = response['data']; + } allRidesList = data.map((e) => RideDashboardModel.fromJson(e)).toList(); displayedRides.value = allRidesList; + calculateStatistics(); } } catch (e) { - print("Error fetching rides: $e"); + debugPrint("Error fetching rides: $e"); } finally { isLoading.value = false; } } } -// ========================================== -// 3. MAIN DASHBOARD SCREEN -// ========================================== -class RidesDashboardScreen extends StatelessWidget { +// ═══════════════════════════════════════════════════════════════════════════ +// 3. MAIN DASHBOARD SCREEN (ADVANCED SLIVER IMPLEMENTATION) +// ═══════════════════════════════════════════════════════════════════════════ + +class RidesDashboardScreen extends StatefulWidget { const RidesDashboardScreen({super.key}); @override - Widget build(BuildContext context) { - final controller = Get.put(RidesListController()); + State createState() => _RidesDashboardScreenState(); +} - return DefaultTabController( - length: 4, - child: Scaffold( - appBar: AppBar( - title: const Text("مراقبة الرحلات"), - bottom: TabBar( - isScrollable: true, - onTap: (index) { - List statuses = ['Begin', 'New', 'Completed', 'Canceled']; - controller.changeTab(statuses[index]); - }, - tabs: const [ - Tab(text: "جارية", icon: Icon(Icons.directions_car)), - Tab(text: "جديدة", icon: Icon(Icons.new_releases)), - Tab(text: "مكتملة", icon: Icon(Icons.check_circle)), - Tab(text: "ملغاة", icon: Icon(Icons.cancel)), - ], - ), - ), - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: TextField( - controller: controller.searchController, - onChanged: (val) => controller.filterRides(val), - decoration: InputDecoration( - hintText: "بحث...", - prefixIcon: const Icon(Icons.search), - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none), +class _RidesDashboardScreenState extends State + with SingleTickerProviderStateMixin { + late final RidesListController controller; + late TabController _tabController; + + // 🎨 الألوان العصرية + final Color bgColor = const Color(0xFFF4F7FE); + final Color primaryColor = const Color(0xFF4318FF); + final Color textPrimary = const Color(0xFF2B3674); + + @override + void initState() { + super.initState(); + controller = Get.put(RidesListController()); + _tabController = TabController(length: 4, vsync: this); + _tabController.addListener(_handleTabChange); + } + + @override + void dispose() { + _tabController.removeListener(_handleTabChange); + _tabController.dispose(); + super.dispose(); + } + + void _handleTabChange() { + if (_tabController.indexIsChanging) return; + List statuses = ['Begin', 'New', 'Completed', 'Canceled']; + controller.changeTab(statuses[_tabController.index]); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bgColor, + body: SafeArea( + top: false, // نسمح للـ AppBar بالتمدد لأعلى الشاشة + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + // 1. Sliver AppBar (المتحرك الذكي الذي يصغر عند التمرير) + SliverAppBar( + pinned: true, + floating: false, + expandedHeight: 310.0, // ارتفاع الجزء العلوي بالكامل + backgroundColor: primaryColor, + elevation: 4, + shadowColor: primaryColor.withOpacity(0.4), + iconTheme: const IconThemeData(color: Colors.white), + centerTitle: true, + title: const Text( + 'إدارة الرحلات', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + actions: [ + Container( + margin: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: const Text( + 'اليوم', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [primaryColor, primaryColor.withOpacity(0.8)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // الإحصائيات تختفي عند التمرير لأعلى + _buildStatisticsSection(), + const SizedBox(height: 12), + // شريط البحث يختفي عند التمرير لأعلى + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildSearchBar(), + ), + const SizedBox(height: 60), // مساحة للـ TabBar بالأسفل + ], + ), + ), + ), + // TabBar يثبت دائماً أسفل الـ AppBar عند التمرير + bottom: PreferredSize( + preferredSize: const Size.fromHeight(54), + child: Container( + decoration: const BoxDecoration( + color: + Color(0xFFF4F7FE), // لون خلفية التطبيق ليظهر بشكل مدمج + borderRadius: + BorderRadius.vertical(top: Radius.circular(24)), + ), + child: TabBar( + controller: _tabController, + isScrollable: true, + labelColor: primaryColor, + unselectedLabelColor: Colors.grey, + indicatorColor: primaryColor, + indicatorWeight: 3, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 14), + tabAlignment: TabAlignment.center, + dividerColor: Colors.transparent, + tabs: const [ + Tab( + icon: Icon(Icons.directions_car_rounded), + text: 'جارية'), + Tab( + icon: Icon(Icons.new_releases_rounded), + text: 'جديدة'), + Tab( + icon: Icon(Icons.check_circle_rounded), + text: 'مكتملة'), + Tab(icon: Icon(Icons.cancel_rounded), text: 'ملغاة'), + ], + ), ), ), ), - Expanded( - child: Obx(() { - if (controller.isLoading.value) - return const Center(child: CircularProgressIndicator()); - if (controller.displayedRides.isEmpty) - return const Center(child: Text("لا توجد رحلات")); - return ListView.builder( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - itemCount: controller.displayedRides.length, - itemBuilder: (context, index) { - final ride = controller.displayedRides[index]; - return _buildRideCard( - ride, controller.isSuperAdmin); // نمرر صلاحية الأدمن - }, + // 2. قائمة الرحلات (Sliver List) + SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: Obx(() { + if (controller.isLoading.value) { + return const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), + ); + } + + if (controller.displayedRides.isEmpty) { + return SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox_rounded, + size: 80, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'لا توجد رحلات في هذا القسم', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final ride = controller.displayedRides[index]; + return _buildRideCardCompact( + ride, controller.isSuperAdmin); + }, + childCount: controller.displayedRides.length, + ), ); }), ), @@ -239,247 +393,507 @@ class RidesDashboardScreen extends StatelessWidget { ); } - Widget _buildRideCard(RideDashboardModel ride, bool isAdmin) { + // --- Components --- + + Widget _buildStatisticsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Text( + 'نظرة عامة', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white.withOpacity(0.8), + ), + ), + ), + SingleChildScrollView( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Obx(() => _buildStatCard( + 'جارية', + controller.beginCount.toString(), + Icons.directions_car_rounded, + const Color(0xFF10B981))), + Obx(() => _buildStatCard('جديدة', controller.newCount.toString(), + Icons.new_releases_rounded, const Color(0xFF3B82F6))), + Obx(() => _buildStatCard( + 'مكتملة', + controller.completedCount.toString(), + Icons.check_circle_rounded, + const Color(0xFF14B8A6))), + Obx(() => _buildStatCard( + 'ملغاة', + controller.canceledCount.toString(), + Icons.cancel_rounded, + const Color(0xFFEF4444))), + Obx(() => _buildStatCard( + 'الإيرادات', + '${controller.totalRevenue.value.toStringAsFixed(0)}', + Icons.payments_rounded, + const Color(0xFFF59E0B))), + ], + ), + ), + ], + ); + } + + Widget _buildStatCard( + String label, String value, IconData icon, Color iconColor) { + return Container( + width: 105, + margin: const EdgeInsets.only(left: 10), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: iconColor, size: 24), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold, color: textPrimary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontWeight: FontWeight.bold), + ), + ], + ), + ); + } + + Widget _buildSearchBar() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 4), + ) + ], + ), + child: TextField( + controller: controller.searchController, + onChanged: (val) => controller.filterRides(val), + style: TextStyle(color: textPrimary, fontWeight: FontWeight.w600), + decoration: InputDecoration( + hintText: 'ابحث عن رقم الرحلة، السائق، أو الراكب...', + hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13), + prefixIcon: Icon(Icons.search_rounded, color: primaryColor), + suffixIcon: controller.searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close_rounded, color: Colors.grey), + onPressed: () { + controller.searchController.clear(); + controller.filterRides(''); + }, + ) + : null, + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + ), + ), + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // تصميم بطاقة الرحلة (المدمج والمنظم - Slim Design) + // ═══════════════════════════════════════════════════════════════════════════ + + Widget _buildRideCardCompact(RideDashboardModel ride, bool isAdmin) { Color statusColor = _getStatusColor(ride.status); String statusText = _getStatusText(ride.status); - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => Get.to(() => RideMapMonitorScreen( - ride: ride, isAdmin: isAdmin)), // نمرر الصلاحية للخريطة أيضاً - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("رحلة #${ride.rideId}", - style: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 16)), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: statusColor)), - child: Text(statusText, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.bold, - fontSize: 12)), - ), - ], - ), - const SizedBox(height: 10), - // Stats - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade200)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.grey.withOpacity(0.1)), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Get.to( + () => RideMapMonitorScreen(ride: ride, isAdmin: isAdmin), + transition: Transition.cupertino, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Header (ID + Status) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _statItem( - Icons.attach_money, - "السعر", - "${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0}", - Colors.green), + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.confirmation_number_rounded, + color: primaryColor, size: 18), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'رحلة #${ride.rideId}', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: textPrimary), + ), + Text( + '${ride.date} • ${ride.time}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + fontWeight: FontWeight.w600), + ), + ], + ), + ], + ), Container( - width: 1, height: 25, color: Colors.grey.shade300), - _statItem( - Icons.social_distance, - "المسافة", - "${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم", - Colors.blue), - Container( - width: 1, height: 25, color: Colors.grey.shade300), - _statItem( - Icons.access_time, - "الوقت", + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + statusText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: statusColor), + ), + ), + ], + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Divider(height: 1), + ), + + // 2. Locations (Timeline style) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + const Icon(Icons.my_location_rounded, + size: 14, color: Color(0xFF10B981)), + Container( + width: 2, + height: 16, + color: Colors.grey.withOpacity(0.3), + margin: const EdgeInsets.symmetric(vertical: 2)), + const Icon(Icons.location_on_rounded, + size: 14, color: Color(0xFFEF4444)), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ride.startLocation, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.black87), + ), + const SizedBox(height: 12), + Text( + ride.endLocation, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.black87), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + + // 3. Driver & Passenger (Slim Rows) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + _buildSlimUserRow( + icon: Icons.local_taxi_rounded, + title: 'السائق', + name: ride.driverName, + phone: ride.driverPhone, + color: const Color(0xFF3B82F6), + isAdmin: isAdmin, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 6), + child: Divider(height: 1, thickness: 0.5), + ), + _buildSlimUserRow( + icon: Icons.person_rounded, + title: 'الراكب', + name: ride.passengerName, + phone: ride.passengerPhone, + color: const Color(0xFF8B5CF6), + isAdmin: isAdmin, + ), + ], + ), + ), + + const SizedBox(height: 14), + + // 4. Information Chips + Row( + children: [ + _buildInfoChip( + Icons.payments_rounded, + '${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0} ل.س', + const Color(0xFF10B981)), + const SizedBox(width: 8), + _buildInfoChip( + Icons.straighten_rounded, + '${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم', + const Color(0xFFF59E0B)), + const SizedBox(width: 8), + _buildInfoChip( + Icons.access_time_rounded, ride.time.length > 5 ? ride.time.substring(0, 5) : ride.time, - Colors.orange), + const Color(0xFF3B82F6)), ], ), - ), - const SizedBox(height: 12), - _locationRow(Icons.my_location, ride.startLocation, Colors.blue), - const SizedBox(height: 6), - _locationRow(Icons.location_on, ride.endLocation, Colors.red), - const Divider(height: 20), - // === معلومات السائق والراكب مع ميزة إخفاء الرقم === - Row( - children: [ - Expanded( - child: _userInfo( - title: "الكابتن", - name: ride.driverName, - phone: ride.driverPhone, - isAdmin: isAdmin, - completed: ride.driverCompletedCount, - canceled: ride.driverCanceledCount)), - Container(width: 1, height: 40, color: Colors.grey.shade300), - const SizedBox(width: 10), - Expanded( - child: _userInfo( - title: "الراكب", - name: ride.passengerName, - phone: ride.passengerPhone, - isAdmin: isAdmin, - completed: ride.passengerCompletedCount)), - ], - ), - - if ((ride.status.contains('Cancel') || - ride.status == 'TimeOut') && - ride.cancelReason.isNotEmpty && - ride.cancelReason != 'لا يوجد سبب') ...[ - const SizedBox(height: 10), - Container( - width: double.infinity, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.red.shade50, + // 5. Cancel Reason (If any) + if ((ride.status.contains('Cancel') || + ride.status == 'TimeOut') && + ride.cancelReason.isNotEmpty && + ride.cancelReason != 'لا يوجد سبب') ...[ + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFEF4444).withOpacity(0.08), borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.red.shade200)), - child: Text("السبب: ${ride.cancelReason}", - style: - TextStyle(color: Colors.red.shade900, fontSize: 13)), - ) - ] - ], + border: Border.all( + color: const Color(0xFFEF4444).withOpacity(0.2)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outline_rounded, + size: 16, color: Color(0xFFEF4444)), + const SizedBox(width: 6), + Expanded( + child: Text( + 'السبب: ${ride.cancelReason}', + style: const TextStyle( + color: Color(0xFFB91C1C), + fontSize: 11, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ], + ], + ), ), ), ), ); } - // === ويدجت عرض المعلومات مع منطق الإخفاء === - Widget _userInfo( - {required String title, - required String name, - required String phone, - required bool isAdmin, - String? completed, - String? canceled}) { - // 1. منطق الإخفاء (Masking) + // تصميم الصف النحيف للمستخدم (لتوفير المساحة) + Widget _buildSlimUserRow({ + required IconData icon, + required String title, + required String name, + required String phone, + required Color color, + required bool isAdmin, + }) { String displayPhone = phone; if (!isAdmin && phone.length > 4) { - // إظهار آخر 4 أرقام فقط displayPhone = phone.substring(phone.length - 4).padLeft(phone.length, '*'); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Row( children: [ - Text(title, style: const TextStyle(fontSize: 10, color: Colors.grey)), - Text(name, + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + Text( + '$title:', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontWeight: FontWeight.bold), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), - - // 2. رقم الهاتف وزر الاتصال - Row( - children: [ - Text(displayPhone, - style: const TextStyle( - fontSize: 11, color: Colors.grey, letterSpacing: 1)), - if (isAdmin && phone.isNotEmpty) ...[ - const SizedBox(width: 4), - InkWell( - onTap: () => _makePhoneCall(phone), - child: const Icon(Icons.call, size: 14, color: Colors.green), - ) - ] - ], + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.black87), + ), ), - - if (completed != null) - Text("تم: $completed ${canceled != null ? '| ألغى: $canceled' : ''}", - style: const TextStyle(fontSize: 9, color: Colors.black54)), + Text( + displayPhone, + style: TextStyle( + fontSize: 11, color: Colors.grey[500], letterSpacing: 0.5), + ), + if (isAdmin && phone.isNotEmpty) ...[ + const SizedBox(width: 8), + GestureDetector( + onTap: () async { + String formattedPhone = phone; + if (!formattedPhone.startsWith('+')) + formattedPhone = '+$formattedPhone'; + final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone); + if (await canLaunchUrl(launchUri)) await launchUrl(launchUri); + }, + child: Icon(Icons.call_rounded, size: 16, color: color), + ), + ] ], ); } - Future _makePhoneCall(String phoneNumber) async { - // التحقق مما إذا كانت العلامة موجودة مسبقاً لتجنب التكرار (++963) - String formattedPhone = phoneNumber; - if (!formattedPhone.startsWith('+')) { - formattedPhone = '+$formattedPhone'; - } - - final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone); - - if (await canLaunchUrl(launchUri)) { - await launchUrl(launchUri); - } else { - // يمكنك هنا إضافة تنبيه بسيط في حال فشل فتح التطبيق - debugPrint("لا يمكن الاتصال بالرقم: $formattedPhone"); - } + // تصميم الرقاقة (Chip) للمعلومات السفلية + Widget _buildInfoChip(IconData icon, String text, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Flexible( + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, fontWeight: FontWeight.bold, color: color), + ), + ), + ], + ), + ), + ); } - // Helpers - Color _getStatusColor(String s) { - if (s == 'Begin' || s == 'Arrived') return Colors.green; - if (s == 'Finished') return Colors.teal; - if (s.contains('Cancel') || s == 'TimeOut') return Colors.red; - if (s == 'New') return Colors.blue; + // Helper Methods for Status + Color _getStatusColor(String status) { + if (status == 'Begin' || status == 'Arrived') + return const Color(0xFF10B981); + if (status == 'Finished') return const Color(0xFF14B8A6); + if (status.contains('Cancel') || status == 'TimeOut') + return const Color(0xFFEF4444); + if (status == 'New') return const Color(0xFF3B82F6); return Colors.grey; } - String _getStatusText(String s) { - if (s == 'Begin' || s == 'Arrived') return "جارية 🟢"; - if (s == 'Finished') return "مكتملة ✅"; - if (s == 'CancelFromDriver' || s == 'CancelFromDriverAfterApply') - return "ألغاها السائق 👨‍✈️"; - if (s == 'CancelFromPassenger') return "ألغاها الراكب 👤"; - if (s == 'TimeOut') return "انتهى الوقت ⏱️"; - if (s == 'New') return "جديدة 🆕"; - return "ملغاة ❌"; - } - - Widget _statItem(IconData icon, String label, String value, Color color) { - return Column(children: [ - Icon(icon, size: 18, color: color), - Text(value, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), - Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)) - ]); - } - - Widget _locationRow(IconData icon, String text, Color color) { - return Row(children: [ - Icon(icon, size: 16, color: color), - const SizedBox(width: 8), - Expanded( - child: Text(text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 13))) - ]); + String _getStatusText(String status) { + if (status == 'Begin' || status == 'Arrived') return 'جارية'; + if (status == 'Finished') return 'مكتملة'; + if (status == 'CancelFromDriver' || status == 'CancelFromDriverAfterApply') + return 'ألغى السائق'; + if (status == 'CancelFromPassenger') return 'ألغى الراكب'; + if (status == 'TimeOut') return 'انتهى الوقت'; + if (status == 'New') return 'جديدة'; + return 'ملغاة'; } } -// ========================================== -// 4. MAP MONITOR SCREEN -// ========================================== +// ═══════════════════════════════════════════════════════════════════════════ +// 5. MAP MONITOR SCREEN (Minor UI Polish) +// ═══════════════════════════════════════════════════════════════════════════ + class RideMapMonitorScreen extends StatefulWidget { final RideDashboardModel ride; - final bool isAdmin; // نستقبل الصلاحية هنا أيضاً - const RideMapMonitorScreen( - {super.key, required this.ride, required this.isAdmin}); + final bool isAdmin; + + const RideMapMonitorScreen({ + super.key, + required this.ride, + required this.isAdmin, + }); @override State createState() => _RideMapMonitorScreenState(); @@ -496,11 +910,15 @@ class _RideMapMonitorScreenState extends State { super.initState(); startPos = widget.ride.getStartLatLng(); endPos = widget.ride.getEndLatLng(); + if (widget.ride.status == 'Begin' || widget.ride.status == 'Arrived') { fetchDriverLocation(); _timer = Timer.periodic( - const Duration(seconds: 10), (_) => fetchDriverLocation()); + const Duration(seconds: 10), + (_) => fetchDriverLocation(), + ); } + WidgetsBinding.instance.addPostFrameCallback((_) => _fitBounds()); } @@ -516,27 +934,36 @@ class _RideMapMonitorScreenState extends State { if (startPos != null) points.add(startPos!); if (endPos != null) points.add(endPos!); if (driverPos != null) points.add(driverPos!); + if (points.isNotEmpty) { try { - mapController.fitCamera(CameraFit.bounds( + mapController.fitCamera( + CameraFit.bounds( bounds: LatLngBounds.fromPoints(points), - padding: const EdgeInsets.all(50))); + padding: const EdgeInsets.all(100), + ), + ); } catch (e) {} } } Future fetchDriverLocation() async { - String trackUrl = - "https://api.intaleq.xyz/intaleq/Admin/rides/get_driver_live_pos.php"; + String trackUrl = "${AppLink.server}/Admin/rides/get_driver_live_pos.php"; try { - var response = await CRUD() - .post(link: trackUrl, payload: {"driver_id": widget.ride.driverId}); + var response = await CRUD().post( + link: trackUrl, + payload: {"driver_id": widget.ride.driverId}, + ); + if (response != 'failure') { var d = response['message']; setState(() { - driverPos = LatLng(double.parse(d['latitude'].toString()), - double.parse(d['longitude'].toString())); + driverPos = LatLng( + double.parse(d['latitude'].toString()), + double.parse(d['longitude'].toString()), + ); }); + if (isFirstLoad) { _fitBounds(); isFirstLoad = false; @@ -549,113 +976,177 @@ class _RideMapMonitorScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("تتبع الرحلة #${widget.ride.rideId}"), - backgroundColor: Colors.white, - foregroundColor: Colors.black, - elevation: 1), + title: Text( + 'تتبع الرحلة #${widget.ride.rideId}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF2B3674), + elevation: 0, + centerTitle: true, + ), body: Stack( children: [ + // Map FlutterMap( mapController: mapController, options: MapOptions( - initialCenter: startPos ?? const LatLng(33.513, 36.276), - initialZoom: 13), + initialCenter: startPos ?? const LatLng(33.513, 36.276), + initialZoom: 13, + ), children: [ TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'), + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.tripz.app', + ), + // Route Polyline if (startPos != null && endPos != null) - PolylineLayer(polylines: [ - Polyline( + PolylineLayer( + polylines: [ + Polyline( points: [startPos!, endPos!], - strokeWidth: 4, - color: Colors.blue.withOpacity(0.7)) - ]), - MarkerLayer(markers: [ - if (startPos != null) - Marker( + strokeWidth: 5, + color: const Color(0xFF4318FF).withOpacity(0.8), + ), + ], + ), + // Markers + MarkerLayer( + markers: [ + // Start Point + if (startPos != null) + Marker( point: startPos!, width: 40, height: 40, - child: - const Icon(Icons.flag, color: Colors.green, size: 40), - alignment: Alignment.topCenter), - if (endPos != null) - Marker( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 5), + ], + ), + child: const Icon(Icons.flag_rounded, + color: Color(0xFF10B981), size: 24), + ), + alignment: Alignment.topCenter, + ), + // End Point + if (endPos != null) + Marker( point: endPos!, width: 40, height: 40, - child: const Icon(Icons.location_on, - color: Colors.red, size: 40), - alignment: Alignment.topCenter), - if (driverPos != null) - Marker( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 5), + ], + ), + child: const Icon(Icons.location_on_rounded, + color: Color(0xFFEF4444), size: 24), + ), + alignment: Alignment.topCenter, + ), + // Driver Current Position + if (driverPos != null) + Marker( point: driverPos!, width: 50, height: 50, child: Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow(blurRadius: 5, color: Colors.black26) - ]), - child: const Icon(Icons.directions_car, - color: Colors.blue, size: 30))), - ]), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0xFF3B82F6).withOpacity(0.3), + blurRadius: 8), + ], + ), + child: const Icon(Icons.directions_car_rounded, + color: Color(0xFF3B82F6), size: 28), + ), + ), + ], + ), ], ), + + // Info Panel (Floating) Positioned( - bottom: 20, - left: 15, - right: 15, - child: Card( - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15)), - child: Padding( - padding: const EdgeInsets.all(15.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("السعر: ${widget.ride.price}", - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.green)), - Text("المسافة: ${widget.ride.distance} كم") - ]), - const Divider(), - _mapInfo(Icons.person, "الكابتن: ${widget.ride.driverName}", - widget.ride.driverPhone), - const SizedBox(height: 5), - _mapInfo( - Icons.person_outline, - "الراكب: ${widget.ride.passengerName}", - widget.ride.passengerPhone), - const SizedBox(height: 5), - _simpleInfo( - Icons.my_location, "من: ${widget.ride.startLocation}"), - _simpleInfo( - Icons.location_on, "إلى: ${widget.ride.endLocation}"), - ], - ), + bottom: 24, + left: 16, + right: 16, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Driver Info + _buildMapUserInfo( + icon: Icons.local_taxi_rounded, + title: 'السائق', + name: widget.ride.driverName, + phone: widget.ride.driverPhone, + color: const Color(0xFF3B82F6), + ), + const SizedBox(height: 12), + // Passenger Info + _buildMapUserInfo( + icon: Icons.person_rounded, + title: 'الراكب', + name: widget.ride.passengerName, + phone: widget.ride.passengerPhone, + color: const Color(0xFF8B5CF6), + ), + ], ), ), - ) + ), + + // Fit Button + Positioned( + top: 16, + right: 16, + child: FloatingActionButton.small( + backgroundColor: Colors.white, + foregroundColor: const Color(0xFF2B3674), + onPressed: _fitBounds, + child: const Icon(Icons.center_focus_strong_rounded), + ), + ), ], ), - floatingActionButton: FloatingActionButton( - mini: true, - child: const Icon(Icons.center_focus_strong), - onPressed: _fitBounds), ); } - // ويدجت خاصة بالخريطة تطبق نفس منطق الإخفاء - Widget _mapInfo(IconData icon, String text, String phone) { + Widget _buildMapUserInfo({ + required IconData icon, + required String title, + required String name, + required String phone, + required Color color, + }) { String displayPhone = phone; if (!widget.isAdmin && phone.length > 4) { displayPhone = @@ -664,34 +1155,59 @@ class _RideMapMonitorScreenState extends State { return Row( children: [ - Icon(icon, size: 18, color: Colors.grey[700]), - const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, size: 20, color: color), + ), + const SizedBox(width: 12), Expanded( - child: Text("$text ($displayPhone)", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 2), + Text( + name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14))), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF2B3674)), + ), + ], + ), + ), if (widget.isAdmin && phone.isNotEmpty) - InkWell( + GestureDetector( onTap: () async { - final Uri launchUri = Uri(scheme: 'tel', path: phone); + String formattedPhone = phone; + if (!formattedPhone.startsWith('+')) + formattedPhone = '+$formattedPhone'; + final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone); if (await canLaunchUrl(launchUri)) await launchUrl(launchUri); }, - child: const Icon(Icons.call, size: 18, color: Colors.green), - ) + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: + const Icon(Icons.call_rounded, size: 20, color: Colors.green), + ), + ), ], ); } - - Widget _simpleInfo(IconData icon, String text) { - return Row(children: [ - Icon(icon, size: 18, color: Colors.grey[700]), - const SizedBox(width: 8), - Expanded( - child: Text(text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14))) - ]); - } } diff --git a/lib/views/admin/server/monitor_server_page.dart b/lib/views/admin/server/monitor_server_page.dart new file mode 100644 index 0000000..6b435c6 --- /dev/null +++ b/lib/views/admin/server/monitor_server_page.dart @@ -0,0 +1,669 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../controller/server/server_monitor_controller.dart'; + +class ServerMonitorPage extends StatelessWidget { + const ServerMonitorPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(ServerMonitorController()); + final themeColor = const Color(0xFF6366F1); + + return Scaffold( + backgroundColor: const Color(0xFF0A0E27), + body: RefreshIndicator( + onRefresh: controller.fetchServerData, + color: themeColor, + backgroundColor: const Color(0xFF1A1F3A), + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + // === 1. App Bar المتجاوب === + SliverAppBar( + expandedHeight: 100, + floating: true, + pinned: true, + backgroundColor: const Color(0xFF0A0E27), + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + titlePadding: const EdgeInsets.only(bottom: 16), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.dns_rounded, + color: Colors.white, size: 20), + const SizedBox(width: 8), + const Text( + 'Server Monitor', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.white, + fontFamily: 'Segoe UI', // أو أي خط تفضله + ), + ), + ], + ), + centerTitle: true, + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + themeColor.withOpacity(0.3), + const Color(0xFF0A0E27), + ], + ), + ), + ), + ), + actions: [ + _buildRefreshButton(controller), + ], + ), + + // === 2. المحتوى الرئيسي === + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + sliver: Obx(() { + if (controller.isLoading.value && + controller.serverData.value == null) { + return const SliverFillRemaining(child: _LoadingState()); + } + + if (controller.errorMessage.isNotEmpty) { + return SliverFillRemaining( + child: _ErrorState(controller: controller)); + } + + final data = controller.serverData.value!; + + return SliverToBoxAdapter( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: + 1000), // لمنع التمدد الزائد في الشاشات الكبيرة + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // معلومات الوقت والتشغيل + _HeaderInfo(data: data), + const SizedBox(height: 20), + + // بطاقات الأداء (CPU & RAM) + LayoutBuilder(builder: (context, constraints) { + return _buildCpuMemSection( + data, constraints.maxWidth > 600); + }), + const SizedBox(height: 20), + + // القسم المتغير (خدمات + عمليات + تخزين) + LayoutBuilder(builder: (context, constraints) { + // إذا كانت الشاشة كبيرة (تابلت/ديسكتوب) + if (constraints.maxWidth > 800) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // العمود الأول: الخدمات والشبكة + Expanded( + flex: 4, + child: Column( + children: [ + _ServicesCard(data: data), + const SizedBox(height: 20), + _StorageNetworkCard(data: data), + ], + ), + ), + const SizedBox(width: 20), + // العمود الثاني: العمليات + Expanded( + flex: 6, + child: _TopProcessesCard( + data: data, + height: + 600), // ارتفاع ثابت في وضع الكمبيوتر + ), + ], + ); + } + // إذا كانت الشاشة موبايل + else { + return Column( + children: [ + _ServicesCard(data: data), + const SizedBox(height: 16), + _StorageNetworkCard(data: data), + const SizedBox(height: 16), + _TopProcessesCard( + data: data), // ارتفاع ديناميكي + ], + ); + } + }), + + const SizedBox(height: 40), + ], + ), + ), + ), + ); + }), + ), + ], + ), + ), + ); + } + + Widget _buildRefreshButton(ServerMonitorController controller) { + return Obx(() => Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: controller.isLoading.value + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.refresh_rounded, color: Colors.white), + onPressed: controller.fetchServerData, + ), + )); + } + + // دمج بطاقات المعالج والذاكرة + Widget _buildCpuMemSection(dynamic data, bool isWide) { + List cards = [ + _MetricCard( + title: "المعالج (CPU)", + value: "${data.cpu.percent}%", + subtitle: "${data.cpu.cores} Cores", + icon: Icons.memory, + percent: data.cpu.percent.toDouble(), + color: const Color(0xFFFF6B6B), + ), + SizedBox(width: isWide ? 20 : 0, height: isWide ? 0 : 16), + _MetricCard( + title: "الذاكرة (RAM)", + value: "${data.memory.percent}%", + subtitle: "${data.memory.usedGb}/${data.memory.totalGb} GB", + icon: Icons.sd_storage_rounded, + percent: data.memory.percent.toDouble(), + color: const Color(0xFF4E54C8), + ), + ]; + + return isWide + ? Row( + children: cards + .map((e) => e is SizedBox ? e : Expanded(child: e)) + .toList()) + : Column(children: cards); + } +} + +// === مكونات فرعية معاد استخدامها (Widgets) === + +class _HeaderInfo extends StatelessWidget { + final dynamic data; + const _HeaderInfo({required this.data}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(50), + border: Border.all(color: Colors.white.withOpacity(0.1)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.timer_outlined, + size: 16, color: Colors.greenAccent.withOpacity(0.8)), + const SizedBox(width: 8), + Text( + "Uptime: ${data.uptime.formatted}", + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500), + ), + Container( + width: 1, + height: 12, + color: Colors.white24, + margin: const EdgeInsets.symmetric(horizontal: 12)), + Icon(Icons.update, + size: 16, color: Colors.blueAccent.withOpacity(0.8)), + const SizedBox(width: 8), + Text( + "Last Update: ${data.timestamp.split(' ')[1]}", + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + ], + ), + ); + } +} + +class _MetricCard extends StatelessWidget { + final String title; + final String value; + final String subtitle; + final IconData icon; + final double percent; + final Color color; + + const _MetricCard({ + required this.title, + required this.value, + required this.subtitle, + required this.icon, + required this.percent, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [color.withOpacity(0.9), color.withOpacity(0.6)], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: Colors.white, size: 24), + ), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 16), + Text(title, + style: const TextStyle(color: Colors.white70, fontSize: 14)), + const SizedBox(height: 4), + Text(subtitle, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: percent / 100, + minHeight: 6, + backgroundColor: Colors.black12, + valueColor: const AlwaysStoppedAnimation(Colors.white), + ), + ), + ], + ), + ); + } +} + +class _ServicesCard extends StatelessWidget { + final dynamic data; + const _ServicesCard({required this.data}); + + @override + Widget build(BuildContext context) { + return _BaseCard( + title: "حالة الخدمات", + icon: Icons.security, + iconColor: Colors.tealAccent, + child: Column( + children: data.services.entries.map((e) { + final isActive = e.value == 'active'; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF0F1629), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isActive + ? Colors.green.withOpacity(0.3) + : Colors.red.withOpacity(0.3), + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 4, + backgroundColor: + isActive ? Colors.greenAccent : Colors.redAccent, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + e.key.toUpperCase(), + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.w600), + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isActive + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + isActive ? "Running" : "Stopped", + style: TextStyle( + color: isActive ? Colors.greenAccent : Colors.redAccent, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } +} + +class _StorageNetworkCard extends StatelessWidget { + final dynamic data; + const _StorageNetworkCard({required this.data}); + + @override + Widget build(BuildContext context) { + return _BaseCard( + title: "التخزين والشبكة", + icon: Icons.cloud_queue_rounded, + iconColor: Colors.purpleAccent, + child: Column( + children: [ + _buildRowItem(Icons.pie_chart_outline, "Storage", + "${data.disk.percent}%", "${data.disk.usedGb} GB Used"), + const Divider(color: Colors.white10, height: 24), + _buildRowItem(Icons.download_rounded, "Download", + "${data.network.receivedMb} MB", "In"), + const SizedBox(height: 16), + _buildRowItem(Icons.upload_rounded, "Upload", + "${data.network.sentMb} MB", "Out"), + ], + ), + ); + } + + Widget _buildRowItem(IconData icon, String label, String value, String sub) { + return Row( + children: [ + Icon(icon, color: Colors.white54, size: 20), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: const TextStyle(color: Colors.white60, fontSize: 12)), + Text(sub, + style: TextStyle( + color: Colors.white.withOpacity(0.4), fontSize: 10)), + ], + ), + const Spacer(), + Text(value, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16)), + ], + ); + } +} + +class _TopProcessesCard extends StatelessWidget { + final dynamic data; + final double? height; + const _TopProcessesCard({required this.data, this.height}); + + @override + Widget build(BuildContext context) { + return Container( + height: height, // إذا كان null سيأخذ الارتفاع بناءً على المحتوى + decoration: BoxDecoration( + color: const Color(0xFF1A1F3A), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withOpacity(0.05)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(8)), + child: const Icon(Icons.analytics_rounded, + color: Colors.orange, size: 18), + ), + const SizedBox(width: 12), + const Text("Top Processes", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + ], + ), + ), + const Divider(height: 1, color: Colors.white10), + // نستخدم ListView.builder داخل Expanded إذا كان هناك ارتفاع محدد، وإلا Column للموبايل + height != null + ? Expanded(child: _buildList()) + : _buildList(shrinkWrap: true), + ], + ), + ); + } + + Widget _buildList({bool shrinkWrap = false}) { + return ListView.separated( + padding: const EdgeInsets.all(16), + physics: shrinkWrap + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + shrinkWrap: shrinkWrap, + itemCount: data.topProcesses.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final process = data.topProcesses[index]; + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.03), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Text("#${index + 1}", + style: const TextStyle( + color: Colors.white38, fontWeight: FontWeight.bold)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(process.name, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + process.usage, + style: const TextStyle( + color: Colors.orangeAccent, + fontSize: 12, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _BaseCard extends StatelessWidget { + final String title; + final IconData icon; + final Color iconColor; + final Widget child; + + const _BaseCard({ + required this.title, + required this.icon, + required this.iconColor, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF1A1F3A), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withOpacity(0.05)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8)), + child: Icon(icon, color: iconColor, size: 18), + ), + const SizedBox(width: 12), + Text(title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 20), + child, + ], + ), + ); + } +} + +class _LoadingState extends StatelessWidget { + const _LoadingState(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(color: Color(0xFF6366F1)), + const SizedBox(height: 16), + Text("Connecting to server...", + style: TextStyle(color: Colors.white.withOpacity(0.5))), + ], + ), + ); + } +} + +class _ErrorState extends StatelessWidget { + final ServerMonitorController controller; + const _ErrorState({required this.controller}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.cloud_off_rounded, + size: 60, color: Colors.redAccent), + const SizedBox(height: 16), + Text(controller.errorMessage.value, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white)), + const SizedBox(height: 24), + ElevatedButton( + onPressed: controller.fetchServerData, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6366F1), + shape: const StadiumBorder(), + ), + child: const Text("Try Again"), + ) + ], + ), + ), + ); + } +} diff --git a/lib/views/admin/static/static.dart b/lib/views/admin/static/static.dart index cd9d8dd..45b6014 100644 --- a/lib/views/admin/static/static.dart +++ b/lib/views/admin/static/static.dart @@ -3,822 +3,1324 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart' hide TextDirection; -import 'package:sefer_admin1/constant/colors.dart'; import 'package:sefer_admin1/controller/admin/static_controller.dart'; -import 'package:sefer_admin1/views/widgets/mycircular.dart'; -// import صفحة الملاحظات الجديدة import 'notes_driver_page.dart'; +// ══════════════════════════════════════════════════════════════ +// DESIGN TOKENS +// ══════════════════════════════════════════════════════════════ +const Color _bg = Color(0xFF0D1117); +const Color _surface = Color(0xFF161B22); +const Color _surfaceElevated = Color(0xFF1C2333); +const Color _accent = Color(0xFF00D4AA); +const Color _danger = Color(0xFFFF5370); +const Color _warning = Color(0xFFFFCB6B); +const Color _info = Color(0xFF82AAFF); +const Color _purple = Color(0xFFC792EA); +const Color _textPrimary = Color(0xFFE6EDF3); +const Color _textSecondary = Color(0xFF7D8590); +const Color _divider = Color(0xFF21262D); + class StaticDash extends StatelessWidget { const StaticDash({super.key}); @override Widget build(BuildContext context) { final controller = Get.put(StaticController()); - bool isRtl = Directionality.of(context) == TextDirection.rtl; return Scaffold( - backgroundColor: const Color(0xFFF0F2F5), - appBar: AppBar( - title: Text( - 'لوحة الإحصائيات'.tr, - style: const TextStyle( - color: Color(0xFF1A1A1A), - fontWeight: FontWeight.w800, - fontSize: 22, - ), - ), - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - iconTheme: const IconThemeData(color: Colors.black87), - leading: IconButton( - onPressed: () => Get.back(), - icon: Icon( - isRtl ? Icons.arrow_forward_ios : Icons.arrow_back_ios, - size: 20, - )), - actions: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 4)) - ], - ), - child: IconButton( - onPressed: () async { - await controller.getAll(); - }, - icon: const Icon(Icons.refresh_rounded, - color: AppColor.primaryColor), - ), - ), - ], - ), + backgroundColor: _bg, body: GetBuilder( - builder: (staticController) { - if (staticController.isLoading) { - return const Center(child: MyCircularProgressIndicator()); - } + builder: (c) { + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + // ── App Bar ──────────────────────────────────── + _SliverHeader(controller: c), - return AnimationLimiter( - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - physics: const BouncingScrollPhysics(), - children: AnimationConfiguration.toStaggeredList( - duration: const Duration(milliseconds: 500), - childAnimationBuilder: (widget) => SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation(child: widget), + if (c.isLoading) + const SliverFillRemaining(child: _LoadingState()) + else ...[ + // ── Date Badge ───────────────────────────── + SliverToBoxAdapter( + child: _DateBadge(controller: c), ), - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Text( - _formatDateRange(staticController), - style: TextStyle( - color: Colors.grey[600], - fontWeight: FontWeight.bold, - fontSize: 14), + + // ── KPI Row ──────────────────────────────── + SliverToBoxAdapter( + child: _KpiRow(controller: c), + ), + + // ── Section: Growth ──────────────────────── + const SliverToBoxAdapter( + child: _SectionLabel( + 'نظرة عامة على النمو', + Icons.trending_up_rounded, + ), + ), + + SliverToBoxAdapter( + child: AnimationLimiter( + child: Column( + children: AnimationConfiguration.toStaggeredList( + duration: const Duration(milliseconds: 450), + childAnimationBuilder: (w) => SlideAnimation( + verticalOffset: 40, + child: FadeInAnimation(child: w), + ), + children: [ + _DarkChartCard( + title: 'الركاب', + total: c.totalMonthlyPassengers, + spots: c.chartDataPassengers, + compareSpots: c.isComparing + ? c.chartDataPassengersCompare + : null, + color: _info, + icon: Icons.groups_rounded, + controller: c, + ), + const SizedBox(height: 12), + _DarkChartCard( + title: 'السائقون', + total: c.totalMonthlyDrivers, + spots: c.chartDataDrivers, + compareSpots: c.isComparing + ? c.chartDataDriversCompare + : null, + color: _warning, + icon: Icons.drive_eta_rounded, + controller: c, + ), + const SizedBox(height: 12), + _DarkChartCard( + title: 'الرحلات', + total: c.totalMonthlyRides, + spots: c.chartDataRides, + compareSpots: + c.isComparing ? c.chartDataRidesCompare : null, + color: _purple, + icon: Icons.map_rounded, + controller: c, + ), + const SizedBox(height: 24), + ], ), ), ), + ), - _buildSectionHeader("نظرة عامة على النمو", Icons.trending_up), - - // --- Passengers Card --- - _buildModernChartCard( - context, - title: 'الركاب', - totalValue: - staticController.totalMonthlyPassengers.toString(), - spots: staticController.chartDataPassengers - .cast() - .toList(), - compareSpots: staticController.isComparing - ? staticController.chartDataPassengersCompare - .cast() - .toList() - : null, - baseColor: const Color(0xFF2979FF), - icon: Icons.groups_rounded, - controller: staticController, + // ── Section: Team Performance ────────────── + const SliverToBoxAdapter( + child: _SectionLabel( + 'أداء فريق العمل', + Icons.workspaces_filled, ), - const SizedBox(height: 16), + ), - // --- Drivers Card --- - _buildModernChartCard( - context, - title: 'السائقون', - totalValue: staticController.totalMonthlyDrivers.toString(), - spots: staticController.chartDataDrivers - .cast() - .toList(), - compareSpots: staticController.isComparing - ? staticController.chartDataDriversCompare - .cast() - .toList() - : null, - baseColor: const Color(0xFFFF9100), - icon: Icons.drive_eta_rounded, - controller: staticController, + SliverToBoxAdapter( + child: Column( + children: [ + // Activations + _DynamicMultiLineCard( + title: 'تفعيل السائقين', + subtitle: 'Activations', + icon: Icons.how_to_reg_rounded, + controller: c, + getSpots: (emp) => emp.notesSpots, + getCompareSpots: (emp) => + c.employeeDataCompare[emp.name]?.notesSpots ?? [], + getTotal: (emp) => emp.totalNotes, + ), + const SizedBox(height: 12), + // Calls + _DynamicMultiLineCard( + title: 'عدد المكالمات', + subtitle: 'Calls', + icon: Icons.phone_in_talk_rounded, + controller: c, + getSpots: (emp) => emp.callsSpots, + getCompareSpots: (emp) => + c.employeeDataCompare[emp.name]?.callsSpots ?? [], + getTotal: (emp) => emp.totalCalls, + onTap: () => Get.to(() => const DailyNotesView()), + ), + const SizedBox(height: 24), + ], ), - const SizedBox(height: 16), + ), - // --- Rides Card --- - _buildModernChartCard( - context, - title: 'الرحلات', - totalValue: staticController.totalMonthlyRides.toString(), - spots: - staticController.chartDataRides.cast().toList(), - compareSpots: staticController.isComparing - ? staticController.chartDataRidesCompare - .cast() - .toList() - : null, - baseColor: const Color(0xFF651FFF), - icon: Icons.map_rounded, - controller: staticController, + // ── Section: Employee Leaderboard ────────── + if (c.employmentStatsList.isNotEmpty) ...[ + const SliverToBoxAdapter( + child: _SectionLabel( + 'لوحة الصدارة', + Icons.emoji_events_rounded, + ), ), - const SizedBox(height: 24), - - _buildSectionHeader( - "أداء فريق العمل", Icons.workspaces_filled), - - // --- Activations Card (No Click Action) --- - _buildMultiLineChartCard( - context: context, - title: 'تفعيل السائقين (Activations)', - spotsrama1: staticController.chartDataEmployeerama1, - spotsShahd: staticController.chartDataEmployeeshahd, - spotsRama: staticController.chartDataEmployeeRama2, - spotsMayar: staticController.chartDataEmployeeSefer4, - spotsrama1Compare: - staticController.chartDataEmployeerama1Compare, - spotsShahdCompare: - staticController.chartDataEmployeeshahdCompare, - spotsRamaCompare: - staticController.chartDataEmployeeRama2Compare, - spotsMayarCompare: - staticController.chartDataEmployeeSefer4Compare, - staticController: staticController, + SliverToBoxAdapter( + child: _EmployeeLeaderboard(stats: c.employmentStatsList), ), - - const SizedBox(height: 16), - - // --- 🔴 Modified: Calls Card (With Navigation) --- - _buildMultiLineChartCard( - context: context, - title: 'عدد المكالمات (Calls)', - // إضافة التوجيه للصفحة الجديدة عند الضغط - onTap: () { - Get.to(() => const DailyNotesView()); - }, - spotsrama1: staticController.chartDataCallsrama1, - spotsShahd: staticController.chartDataCallsShahd, - spotsRama: staticController.chartDataCallsRama2, - spotsMayar: staticController.chartDataCallsSefer4, - spotsrama1Compare: - staticController.chartDataCallsrama1Compare, - spotsShahdCompare: - staticController.chartDataCallsShahdCompare, - spotsRamaCompare: - staticController.chartDataCallsRama2Compare, - spotsMayarCompare: - staticController.chartDataCallsSefer4Compare, - staticController: staticController, - ), - - const SizedBox(height: 24), - - // --- Employment Stats List --- - if (staticController.employmentStatsList.isNotEmpty) ...[ - _buildSectionHeader( - "إجمالي الإدخالات حسب الموظف", Icons.list_alt_rounded), - _buildEmploymentStatsList( - staticController.employmentStatsList), - const SizedBox(height: 24), - ], - - _buildSectionHeader( - "متابعة التسجيل", Icons.assignment_turned_in), - - // --- Drivers Matching Notes Card --- - _buildModernChartCard( - context, - title: 'سائقين بعد الاتصال', - totalValue: staticController.staticList.isNotEmpty && - staticController.staticList[0] - ['totalMonthlyMatchingNotes'] != - null - ? "${staticController.staticList[0]['totalMonthlyMatchingNotes']}" - : "0", - spots: staticController.chartDataDriversMatchingNotes - .cast() - .toList(), - compareSpots: staticController.isComparing - ? staticController.chartDataDriversMatchingNotesCompare - .cast() - .toList() - : null, - baseColor: const Color(0xFF00BFA5), - icon: Icons.phone_in_talk_rounded, - controller: staticController, - ), - const SizedBox(height: 40), + const SliverToBoxAdapter(child: SizedBox(height: 24)), ], - ), - ), + + // ── Section: Registration Follow-up ──────── + const SliverToBoxAdapter( + child: _SectionLabel( + 'متابعة التسجيل', + Icons.assignment_turned_in_rounded, + ), + ), + + SliverToBoxAdapter( + child: Column( + children: [ + _DarkChartCard( + title: 'سائقين بعد الاتصال', + total: c.staticList.isNotEmpty && + c.staticList[0]['totalMonthlyMatchingNotes'] != + null + ? c.staticList[0]['totalMonthlyMatchingNotes'] + .toString() + : '0', + spots: c.chartDataDriversMatchingNotes, + compareSpots: c.isComparing + ? c.chartDataDriversMatchingNotesCompare + : null, + color: _accent, + icon: Icons.phone_in_talk_rounded, + controller: c, + ), + const SizedBox(height: 60), + ], + ), + ), + ], + ], ); }, ), ); } +} - // ... (Employment Stats List - No Change) - Widget _buildEmploymentStatsList(List> stats) { - Color getEmployeeColor(String name) { - String n = name.toLowerCase().trim(); - if (n.contains('shahd')) return Colors.redAccent; - if (n.contains('mayar')) return Colors.amber.shade700; - if (n.contains('rama2')) return Colors.green; - if (n.contains('rama1')) return Colors.blue; - return Colors.blueGrey; - } +// ══════════════════════════════════════════════════════════════ +// SLIVER HEADER +// ══════════════════════════════════════════════════════════════ +class _SliverHeader extends StatelessWidget { + final StaticController controller; + const _SliverHeader({required this.controller}); - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 15, - offset: const Offset(0, 5)) - ]), - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Row( - children: [ - Icon(Icons.people_outline, color: Colors.blueGrey.shade700), - const SizedBox(width: 10), - Text("الأداء الإجمالي (العدد)", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Colors.grey.shade800)), - ], + @override + Widget build(BuildContext context) { + return SliverAppBar( + expandedHeight: 100, + pinned: true, + floating: true, + backgroundColor: _bg, + elevation: 0, + leading: GestureDetector( + onTap: () => Get.back(), + child: Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _divider), ), - const Divider(height: 25), - Wrap( - spacing: 12, - runSpacing: 12, - children: stats.map((item) { - Color itemColor = getEmployeeColor(item['name'].toString()); - return Container( - width: 100, - padding: - const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - decoration: BoxDecoration( - color: itemColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: itemColor.withOpacity(0.5), width: 1.5)), - child: Column( - children: [ - Text( - item['name'].toString().toUpperCase(), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: itemColor), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 6), - Text( - item['count'].toString(), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Colors.grey.shade800), - ) - ], - ), - ); - }).toList(), + child: const Icon(Icons.arrow_back_ios_new_rounded, + color: _textSecondary, size: 16), + ), + ), + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF0F1F1A), _bg], + ), ), - ], - ), - ); - } - - // ... (Header Widget - No Change) - Widget _buildSectionHeader(String title, IconData icon) { - return Padding( - padding: const EdgeInsets.only(bottom: 12.0, top: 8.0, right: 4), - child: Row( - children: [ - Icon(icon, size: 20, color: Colors.grey[600]), - const SizedBox(width: 8), - Text(title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[800], - fontFamily: 'Cairo')), - ], - ), - ); - } - - // ... (Modern Chart Card - No Change) - Widget _buildModernChartCard(BuildContext context, - {required String title, - required String totalValue, - required List spots, - List? compareSpots, - required Color baseColor, - required IconData icon, - required StaticController controller}) { - List allSpots = [...spots]; - if (compareSpots != null) allSpots.addAll(compareSpots); - double maxY = _calculateMaxY(allSpots); - double interval = _calculateInterval(maxY); - double daysInPeriod = _getDaysInPeriod(controller); - double xInterval = _calculateXInterval(daysInPeriod); - - return Container( - height: 400, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: baseColor.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8)) - ]), - child: Column( - children: [ - _buildCardHeader(context, title, totalValue, icon, baseColor, - controller.isComparing), - Expanded( + child: Align( + alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), - child: Directionality( - textDirection: TextDirection.ltr, - child: LineChart(LineChartData( - minX: 1, - maxX: daysInPeriod > 0 ? daysInPeriod : 1, - minY: 0, - maxY: maxY, - lineTouchData: _buildTooltipData(controller), - gridData: _buildGridData(interval), - titlesData: _buildTitlesData( - interval, xInterval, daysInPeriod, controller), - borderData: FlBorderData(show: false), - lineBarsData: [ - if (compareSpots != null && compareSpots.isNotEmpty) - _buildLine(compareSpots, Colors.grey.withOpacity(0.4), - isDashed: true, isStep: true), - _buildLine(spots, baseColor, isStep: true), - ])), + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _accent.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _accent.withOpacity(0.25)), + ), + child: const Icon(Icons.bar_chart_rounded, + color: _accent, size: 18), + ), + const SizedBox(width: 10), + ShaderMask( + shaderCallback: (b) => const LinearGradient( + colors: [_accent, _info], + ).createShader(b), + child: const Text( + 'لوحة الإحصائيات', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + ), + ], ), ), ), - _buildControlBar(context, controller, baseColor), - ], + ), + ), + actions: [ + GestureDetector( + onTap: () async => await controller.getAll(), + child: Container( + margin: const EdgeInsets.only(right: 16, top: 10, bottom: 10), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _divider), + ), + child: const Icon(Icons.refresh_rounded, + color: _textSecondary, size: 18), + ), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container(height: 1, color: _divider), ), ); } +} - // --- 🔴 Modified: Multi Line Chart (Adding onTap) --- - Widget _buildMultiLineChartCard({ - required BuildContext context, - required String title, - required StaticController staticController, - // Add onTap parameter - VoidCallback? onTap, - required List spotsrama1, - required List spotsShahd, - required List spotsRama, - required List spotsMayar, - required List spotsrama1Compare, - required List spotsShahdCompare, - required List spotsRamaCompare, - required List spotsMayarCompare, - }) { - final allSpots = [ - ...spotsrama1, - ...spotsShahd, - ...spotsRama, - ...spotsMayar - ]; - double maxY = _calculateMaxY(allSpots); - double interval = _calculateInterval(maxY); - double daysInPeriod = _getDaysInPeriod(staticController); - double xInterval = _calculateXInterval(daysInPeriod); +// ══════════════════════════════════════════════════════════════ +// DATE BADGE +// ══════════════════════════════════════════════════════════════ +class _DateBadge extends StatelessWidget { + final StaticController controller; + const _DateBadge({required this.controller}); - return Container( - height: 460, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 15, - offset: const Offset(0, 5)) - ]), - child: Column( + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - // 🔴 Wrap Header with InkWell if onTap is provided - InkWell( - onTap: onTap, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: _buildCardHeader( - context, - title, - null, - Icons.bar_chart_rounded, - Colors.black54, - staticController.isComparing, - showArrow: onTap != null), // Pass showArrow flag - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Wrap( - spacing: 16, - runSpacing: 8, - alignment: WrapAlignment.center, + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _divider), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - _buildLegendItem("راما 1", Colors.blue), - _buildLegendItem("شهد", Colors.redAccent), - _buildLegendItem("راما 2", Colors.green), - _buildLegendItem("ميار", Colors.amber.shade700), + const Icon(Icons.calendar_today_rounded, + color: _accent, size: 13), + const SizedBox(width: 7), + Text( + controller.currentDateString, + style: const TextStyle( + color: _textSecondary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), ], ), ), - const SizedBox(height: 15), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), - child: Directionality( - textDirection: TextDirection.ltr, - child: LineChart(LineChartData( + if (controller.isComparing) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.grey.withOpacity(0.2)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: _textSecondary, + ), + ), + const SizedBox(width: 7), + Text( + controller.compareDateString, + style: const TextStyle( + color: _textSecondary, + fontSize: 12, + fontWeight: FontWeight.w500), + ), + ], + ), + ), + ] + ], + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// KPI ROW +// ══════════════════════════════════════════════════════════════ +class _KpiRow extends StatelessWidget { + final StaticController controller; + const _KpiRow({required this.controller}); + + @override + Widget build(BuildContext context) { + final items = [ + _KpiItem('الركاب', controller.totalMonthlyPassengers, + Icons.groups_rounded, _info), + _KpiItem('السائقون', controller.totalMonthlyDrivers, + Icons.drive_eta_rounded, _warning), + _KpiItem( + 'الرحلات', controller.totalMonthlyRides, Icons.map_rounded, _purple), + ]; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Row( + children: items + .map((item) => Expanded( + child: Padding( + padding: EdgeInsets.only(right: item != items.last ? 8 : 0), + child: _buildKpiCard(item), + ), + )) + .toList(), + ), + ); + } + + Widget _buildKpiCard(_KpiItem item) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: item.color.withOpacity(0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(item.icon, color: item.color, size: 18), + const SizedBox(height: 8), + Text( + item.value, + style: const TextStyle( + color: _textPrimary, + fontSize: 20, + fontWeight: FontWeight.w800, + height: 1, + ), + ), + const SizedBox(height: 3), + Text(item.label, + style: const TextStyle(color: _textSecondary, fontSize: 10)), + ], + ), + ); + } +} + +class _KpiItem { + final String label, value; + final IconData icon; + final Color color; + const _KpiItem(this.label, this.value, this.icon, this.color); +} + +// ══════════════════════════════════════════════════════════════ +// SECTION LABEL +// ══════════════════════════════════════════════════════════════ +class _SectionLabel extends StatelessWidget { + final String text; + final IconData icon; + const _SectionLabel(this.text, this.icon); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 12), + child: Row( + children: [ + Container( + width: 3, + height: 16, + decoration: BoxDecoration( + color: _accent, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Icon(icon, size: 16, color: _textSecondary), + const SizedBox(width: 8), + Text( + text, + style: const TextStyle( + color: _textPrimary, + fontSize: 15, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ], + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// DARK SINGLE-LINE CHART CARD +// ══════════════════════════════════════════════════════════════ +class _DarkChartCard extends StatelessWidget { + final String title, total; + final List spots; + final List? compareSpots; + final Color color; + final IconData icon; + final StaticController controller; + + const _DarkChartCard({ + required this.title, + required this.total, + required this.spots, + this.compareSpots, + required this.color, + required this.icon, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final allSpots = [...spots, ...?compareSpots]; + final maxY = _maxY(allSpots); + final interval = _interval(maxY); + final days = _days(controller); + final xInterval = _xInterval(days); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 340, + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.15)), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(18), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Icon(icon, color: color, size: 18), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.w700)), + Text('الإجمالي: $total', + style: const TextStyle( + color: _textSecondary, fontSize: 11)), + ]), + ]), + if (controller.isComparing) + Row(children: [ + Container( + width: 16, + height: 2, + decoration: BoxDecoration( + color: _textSecondary, + borderRadius: BorderRadius.circular(1), + )), + const SizedBox(width: 5), + const Text('سابق', + style: + TextStyle(color: _textSecondary, fontSize: 10)), + ]), + ], + ), + ), + // Chart + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 12, 10), + child: Directionality( + textDirection: TextDirection.ltr, + child: LineChart(LineChartData( minX: 1, - maxX: daysInPeriod > 0 ? daysInPeriod : 1, + maxX: days > 0 ? days : 1, minY: 0, maxY: maxY, - lineTouchData: _buildTooltipData(staticController), - gridData: _buildGridData(interval), - titlesData: _buildTitlesData( - interval, xInterval, daysInPeriod, staticController), + lineTouchData: _tooltipData(controller, color), + gridData: _gridData(interval), + titlesData: + _titlesData(interval, xInterval, days, controller), borderData: FlBorderData(show: false), lineBarsData: [ - if (staticController.isComparing) ...[ - _buildLine( - spotsrama1Compare, Colors.blue.withOpacity(0.3), - isDashed: true, isStep: false), - _buildLine(spotsShahdCompare, - Colors.redAccent.withOpacity(0.3), - isDashed: true, isStep: false), - _buildLine( - spotsRamaCompare, Colors.green.withOpacity(0.3), - isDashed: true, isStep: false), - _buildLine(spotsMayarCompare, - Colors.amber.shade700.withOpacity(0.3), - isDashed: true, isStep: false), + if (compareSpots != null && compareSpots!.isNotEmpty) + _line(compareSpots!, _textSecondary, + isDashed: true, isStep: true, width: 1.5), + _line(spots, color, isStep: true, width: 2.5), + ], + )), + ), + ), + ), + // Control Bar + _ControlBar(controller: controller, accentColor: color), + ], + ), + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// DYNAMIC MULTI-LINE CHART CARD 🔥 Uses dynamic employee data +// ══════════════════════════════════════════════════════════════ +class _DynamicMultiLineCard extends StatelessWidget { + final String title, subtitle; + final IconData icon; + final StaticController controller; + final List Function(EmployeeChartData) getSpots; + final List Function(EmployeeChartData) getCompareSpots; + final int Function(EmployeeChartData) getTotal; + final VoidCallback? onTap; + + const _DynamicMultiLineCard({ + required this.title, + required this.subtitle, + required this.icon, + required this.controller, + required this.getSpots, + required this.getCompareSpots, + required this.getTotal, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final employees = controller.employeeData.values.toList(); + // Sort by total descending for legend ordering + employees.sort((a, b) => getTotal(b).compareTo(getTotal(a))); + + final allSpots = employees.expand((e) => getSpots(e)).toList(); + final maxY = _maxY(allSpots); + final interval = _interval(maxY); + final days = _days(controller); + final xInterval = _xInterval(days); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _divider), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 15, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + // ── Header ────────────────────────────────────── + InkWell( + onTap: onTap, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(18), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: _accent.withOpacity(0.10), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: _accent.withOpacity(0.20)), + ), + child: Icon(icon, color: _accent, size: 18), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.w700)), + Text(subtitle, + style: const TextStyle( + color: _textSecondary, fontSize: 11)), + if (onTap != null) + const Text( + 'اضغط لعرض التفاصيل اليومية', + style: TextStyle( + color: _accent, + fontSize: 9, + fontWeight: FontWeight.w600), + ), + ]), + ]), + if (onTap != null) + const Icon(Icons.arrow_forward_ios_rounded, + size: 14, color: _textSecondary), + ], + ), + ), + ), + + // ── Dynamic Legend ────────────────────────────── + if (employees.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text('لا توجد بيانات', + style: TextStyle(color: _textSecondary)), + ) + else ...[ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 18, vertical: 4), + child: Wrap( + spacing: 10, + runSpacing: 8, + children: employees.map((emp) { + return _LegendChip( + name: emp.name.toUpperCase(), + color: emp.color, + total: getTotal(emp), + ); + }).toList(), + ), + ), + + const SizedBox(height: 8), + + // ── Chart ─────────────────────────────────── + SizedBox( + height: 240, + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 12, 10), + child: Directionality( + textDirection: TextDirection.ltr, + child: LineChart(LineChartData( + minX: 1, + maxX: days > 0 ? days : 1, + minY: 0, + maxY: maxY, + lineTouchData: _tooltipData(controller, _accent), + gridData: _gridData(interval), + titlesData: + _titlesData(interval, xInterval, days, controller), + borderData: FlBorderData(show: false), + lineBarsData: [ + // Compare lines (dashed) + if (controller.isComparing) + ...employees.map((emp) => _line( + getCompareSpots(emp), + emp.color.withOpacity(0.25), + isDashed: true, + isStep: false, + width: 1.5, + )), + // Main lines + ...employees.map((emp) => _line( + getSpots(emp), + emp.color, + isStep: false, + width: 2.5, + )), ], - _buildLine(spotsrama1, Colors.blue, isStep: false), - _buildLine(spotsShahd, Colors.redAccent, isStep: false), - _buildLine(spotsRama, Colors.green, isStep: false), - _buildLine(spotsMayar, Colors.amber.shade700, - isStep: false), - ])), + )), + ), + ), + ), + ], + + _ControlBar(controller: controller, accentColor: _accent), + ], + ), + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// LEGEND CHIP +// ══════════════════════════════════════════════════════════════ +class _LegendChip extends StatelessWidget { + final String name; + final Color color; + final int total; + const _LegendChip( + {required this.name, required this.color, required this.total}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.25)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 7, + height: 7, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text(name, + style: TextStyle( + color: color, fontSize: 10, fontWeight: FontWeight.w700)), + const SizedBox(width: 5), + Text('$total', + style: const TextStyle( + color: _textSecondary, + fontSize: 10, + fontWeight: FontWeight.w500)), + ], + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// EMPLOYEE LEADERBOARD 🔥 Fully dynamic +// ══════════════════════════════════════════════════════════════ +class _EmployeeLeaderboard extends StatelessWidget { + final List stats; + const _EmployeeLeaderboard({required this.stats}); + + @override + Widget build(BuildContext context) { + final maxCount = stats.isEmpty ? 1 : stats.first.count; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _divider), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _warning.withOpacity(0.12), + borderRadius: BorderRadius.circular(9), + border: Border.all(color: _warning.withOpacity(0.25)), + ), + child: const Icon(Icons.emoji_events_rounded, + color: _warning, size: 16), + ), + const SizedBox(width: 10), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('لوحة الصدارة', + style: TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.w700)), + Text('إجمالي الإدخالات حسب الموظف', + style: + TextStyle(color: _textSecondary, fontSize: 10)), + ], + ), + ]), + Text( + '${stats.fold(0, (s, e) => s + e.count)} إجمالي', + style: const TextStyle( + color: _textSecondary, + fontSize: 11, + fontWeight: FontWeight.w600), + ), + ], + ), + + const SizedBox(height: 20), + + // Rank rows + ...List.generate(stats.length, (i) { + final stat = stats[i]; + final pct = maxCount > 0 ? stat.count / maxCount : 0.0; + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Column( + children: [ + Row( + children: [ + // Rank Badge + _RankBadge(rank: i + 1, color: stat.color), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + stat.name.toUpperCase(), + style: TextStyle( + color: stat.color, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + Text( + '${stat.count}', + style: const TextStyle( + color: _textPrimary, + fontSize: 14, + fontWeight: FontWeight.w800, + fontFamily: 'monospace', + ), + ), + ], + ), + const SizedBox(height: 6), + // Progress bar + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: pct.toDouble(), + backgroundColor: _divider, + valueColor: + AlwaysStoppedAnimation(stat.color), + minHeight: 5, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + }), + ], + ), + ), + ); + } +} + +class _RankBadge extends StatelessWidget { + final int rank; + final Color color; + const _RankBadge({required this.rank, required this.color}); + + @override + Widget build(BuildContext context) { + final isTop3 = rank <= 3; + final medalColors = [_warning, _textSecondary, const Color(0xFFCD7F32)]; + + return Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isTop3 ? medalColors[rank - 1].withOpacity(0.12) : _divider, + shape: BoxShape.circle, + border: Border.all( + color: isTop3 + ? medalColors[rank - 1].withOpacity(0.4) + : Colors.transparent), + ), + child: Center( + child: Text( + '$rank', + style: TextStyle( + color: isTop3 ? medalColors[rank - 1] : _textSecondary, + fontSize: 12, + fontWeight: FontWeight.w800, + ), + ), + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// SHARED CONTROL BAR +// ══════════════════════════════════════════════════════════════ +class _ControlBar extends StatelessWidget { + final StaticController controller; + final Color accentColor; + const _ControlBar({required this.controller, required this.accentColor}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: _bg, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)), + border: const Border(top: BorderSide(color: _divider)), + ), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () async { + final now = DateTime.now(); + final currentEnd = controller.endDate ?? now; + final lastDate = currentEnd.isAfter(now) + ? currentEnd.add(const Duration(days: 1)) + : now.add(const Duration(days: 1)); + final picked = await showDateRangePicker( + context: context, + firstDate: DateTime(2020), + lastDate: lastDate, + initialDateRange: DateTimeRange( + start: controller.startDate ?? + now.subtract(const Duration(days: 30)), + end: currentEnd, + ), + builder: (ctx, child) => Theme( + data: Theme.of(ctx).copyWith( + colorScheme: const ColorScheme.dark( + primary: _accent, + onPrimary: _bg, + surface: _surfaceElevated, + onSurface: _textPrimary, + ), + ), + child: child!, + ), + ); + if (picked != null) { + controller.updateDateRange(picked.start, picked.end); + } + }, + borderRadius: BorderRadius.circular(9), + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(9), + border: Border.all(color: _divider), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.calendar_today_rounded, + size: 13, color: accentColor), + const SizedBox(width: 7), + Text( + _formatRange(controller), + style: const TextStyle( + color: _textSecondary, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ), ), - _buildControlBar(context, staticController, Colors.blueGrey), - ], - ), - ); - } - - // ... (Shared Components) ... - // 🔴 Modified _buildCardHeader to accept showArrow - Widget _buildCardHeader(BuildContext context, String title, - String? totalValue, IconData icon, Color color, bool isComparing, - {bool showArrow = false}) { - return Padding( - padding: const EdgeInsets.all(20.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(16)), - child: Icon(icon, color: color, size: 26)), - const SizedBox(width: 14), - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, + const SizedBox(width: 10), + InkWell( + onTap: controller.toggleComparison, + borderRadius: BorderRadius.circular(9), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: controller.isComparing + ? _danger.withOpacity(0.10) + : _surface, + borderRadius: BorderRadius.circular(9), + border: Border.all( + color: controller.isComparing + ? _danger.withOpacity(0.3) + : _divider), + ), + child: Row( + children: [ + Icon( + controller.isComparing + ? Icons.visibility_off_outlined + : Icons.compare_arrows_rounded, + size: 14, + color: controller.isComparing ? _danger : accentColor, + ), + const SizedBox(width: 5), + Text( + controller.isComparing ? 'إلغاء' : 'مقارنة', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey[800])), - if (totalValue != null) ...[ - const SizedBox(height: 4), - Text('الإجمالي: $totalValue', - style: TextStyle( - fontSize: 13, - color: Colors.grey[500], - fontWeight: FontWeight.w600)) + color: controller.isComparing ? _danger : accentColor, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), ], - // إضافة نص صغير يدل على إمكانية الضغط - if (showArrow) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text("اضغط لعرض التفاصيل اليومية", - style: TextStyle( - fontSize: 10, color: AppColor.primaryColor)), - ) - ]), - ], + ), + ), ), - Row(children: [ - if (showArrow) - Icon(Icons.arrow_forward_ios_rounded, - size: 16, color: Colors.grey[400]), - if (isComparing) ...[ - const SizedBox(width: 8), - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Colors.grey, shape: BoxShape.circle)), - const SizedBox(width: 4), - Text("الفترة السابقة", - style: TextStyle(fontSize: 10, color: Colors.grey[600])) - ] - ]) ], ), ); } - LineTouchData _buildTooltipData(StaticController controller) { - return LineTouchData( - handleBuiltInTouches: true, - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (touchedSpot) => Colors.black87, - tooltipPadding: const EdgeInsets.all(10), - tooltipRoundedRadius: 8, - getTooltipItems: (List touchedBarSpots) { - return touchedBarSpots.map((barSpot) { - DateTime? start = controller.startDate; - if (start != null) { - DateTime date = start.add(Duration(days: barSpot.x.toInt() - 1)); - String formattedDate = DateFormat('d MMM', 'en').format(date); - return LineTooltipItem( - '$formattedDate \n', - const TextStyle( - color: Colors.white70, - fontWeight: FontWeight.bold, - fontSize: 12), - children: [ - TextSpan( - text: barSpot.y.toInt().toString(), - style: TextStyle( - color: barSpot.bar.color, - fontWeight: FontWeight.w900, - fontSize: 14)) - ]); - } - return LineTooltipItem( - barSpot.y.toString(), - const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)); - }).toList(); - }, - ), - ); - } - - FlGridData _buildGridData(double interval) { - return FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: interval, - getDrawingHorizontalLine: (value) => - FlLine(color: Colors.grey.withOpacity(0.08), strokeWidth: 1)); - } - - FlTitlesData _buildTitlesData(double interval, double xInterval, - double daysInPeriod, StaticController controller) { - return FlTitlesData( - show: true, - rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - interval: xInterval, - getTitlesWidget: (value, meta) { - if (value <= 0 || value > daysInPeriod) - return const SizedBox.shrink(); - if (controller.startDate != null) { - int dayOffset = value.toInt() - 1; - DateTime date = - controller.startDate!.add(Duration(days: dayOffset)); - String text = DateFormat('d/M', 'en').format(date); - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text(text, - style: TextStyle( - color: Colors.grey[400], - fontSize: 10, - fontWeight: FontWeight.bold))); - } - return const SizedBox.shrink(); - })), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: interval, - reservedSize: 40, - getTitlesWidget: (value, meta) => Text(_formatNumber(value), - style: TextStyle(color: Colors.grey[400], fontSize: 10)))), - ); - } - - Widget _buildControlBar( - BuildContext context, StaticController controller, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(24)), - border: Border(top: BorderSide(color: Colors.grey.shade200))), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: InkWell( - onTap: () async { - DateTimeRange? picked = await showDateRangePicker( - context: context, - firstDate: DateTime(2020), - lastDate: DateTime.now().add(const Duration(days: 1)), - initialDateRange: DateTimeRange( - start: controller.startDate ?? - DateTime.now() - .subtract(const Duration(days: 30)), - end: controller.endDate ?? DateTime.now()), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: ColorScheme.light( - primary: AppColor.primaryColor, - onPrimary: Colors.white, - onSurface: Colors.black)), - child: child!); - }); - if (picked != null) - controller.updateDateRange(picked.start, picked.end); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - color: Colors.white), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.calendar_today_rounded, - size: 16, color: color), - const SizedBox(width: 8), - Text(_formatDateRange(controller), - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.grey[800]), - overflow: TextOverflow.ellipsis) - ])))), - const SizedBox(width: 12), - TextButton.icon( - onPressed: controller.toggleComparison, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8))), - icon: Icon( - controller.isComparing - ? Icons.visibility_off_outlined - : Icons.compare_arrows_rounded, - size: 18, - color: controller.isComparing ? Colors.redAccent : color), - label: Text(controller.isComparing ? "إلغاء" : "مقارنة", - style: TextStyle( - fontSize: 12, - color: controller.isComparing ? Colors.redAccent : color, - fontWeight: FontWeight.w600))) - ], - ), - ); - } - - LineChartBarData _buildLine(List spots, Color color, - {bool isDashed = false, bool isStep = false}) { - return LineChartBarData( - spots: spots, - isCurved: !isStep, // Curved if not step - curveSmoothness: 0.25, - isStepLineChart: isStep, - color: color, - barWidth: isDashed ? 2 : 2.5, - isStrokeCapRound: !isStep, - dotData: const FlDotData(show: false), - dashArray: isDashed ? [5, 5] : null, - lineChartStepData: const LineChartStepData(stepDirection: 0.5), - ); - } - - Widget _buildLegendItem(String name, Color color) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration(color: color, shape: BoxShape.circle)), - const SizedBox(width: 6), - Text(name, - style: TextStyle( - fontSize: 12, - color: Colors.grey[700], - fontWeight: FontWeight.w600)) - ]); - } - - double _getDaysInPeriod(StaticController controller) { - if (controller.startDate == null || controller.endDate == null) return 30; - return controller.endDate!.difference(controller.startDate!).inDays + 1.0; - } - - double _calculateXInterval(double daysInPeriod) { - if (daysInPeriod > 60) return 30; - if (daysInPeriod > 30) return 10; - if (daysInPeriod > 10) return 5; - return 1; - } - - String _formatDateRange(StaticController controller) { - if (controller.startDate == null || controller.endDate == null) - return controller.currentDateString; - String start = - "${controller.startDate!.day}/${controller.startDate!.month}/${controller.startDate!.year}"; - String end = - "${controller.endDate!.day}/${controller.endDate!.month}/${controller.endDate!.year}"; - return "$start - $end"; - } - - double _calculateMaxY(List spots) { - if (spots.isEmpty) return 10; - double maxVal = 0; - for (var spot in spots) { - if (spot.y > maxVal) maxVal = spot.y; - } - if (maxVal == 0) return 5; - return (maxVal * 1.2).ceilToDouble(); - } - - double _calculateInterval(double maxY) { - if (maxY <= 10) return 2; - if (maxY <= 50) return 10; - if (maxY <= 100) return 20; - if (maxY <= 500) return 100; - if (maxY <= 1000) return 200; - return maxY / 5; - } - - String _formatNumber(double value) { - if (value >= 1000) return '${(value / 1000).toStringAsFixed(1)}k'; - return value.toInt().toString(); + String _formatRange(StaticController c) { + if (c.startDate == null || c.endDate == null) return c.currentDateString; + return "${c.startDate!.day}/${c.startDate!.month}/${c.startDate!.year}" + " - ${c.endDate!.day}/${c.endDate!.month}/${c.endDate!.year}"; } } + +// ══════════════════════════════════════════════════════════════ +// LOADING STATE +// ══════════════════════════════════════════════════════════════ +class _LoadingState extends StatelessWidget { + const _LoadingState(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + color: _accent, + strokeWidth: 2, + backgroundColor: _accent.withOpacity(0.1), + ), + ), + const SizedBox(height: 16), + const Text('جاري تحميل الإحصائيات...', + style: TextStyle(color: _textSecondary, fontSize: 13)), + ], + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════ +// CHART HELPERS (pure functions) +// ══════════════════════════════════════════════════════════════ +LineChartBarData _line( + List spots, + Color color, { + bool isDashed = false, + bool isStep = false, + double width = 2.5, +}) { + return LineChartBarData( + spots: spots, + isCurved: !isStep, + curveSmoothness: 0.25, + isStepLineChart: isStep, + color: color, + barWidth: width, + isStrokeCapRound: !isStep, + dotData: const FlDotData(show: false), + dashArray: isDashed ? [5, 5] : null, + lineChartStepData: const LineChartStepData(stepDirection: 0.5), + belowBarData: BarAreaData( + show: !isDashed, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [color.withOpacity(0.12), color.withOpacity(0.0)], + ), + ), + ); +} + +LineTouchData _tooltipData(StaticController c, Color accentColor) { + return LineTouchData( + handleBuiltInTouches: true, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (_) => _surfaceElevated, + tooltipBorder: const BorderSide(color: _divider), + tooltipPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + tooltipRoundedRadius: 10, + getTooltipItems: (spots) { + return spots.map((barSpot) { + final start = c.startDate; + String dateStr = ''; + if (start != null) { + final date = start.add(Duration(days: barSpot.x.toInt() - 1)); + dateStr = DateFormat('d MMM', 'en').format(date); + } + return LineTooltipItem( + '$dateStr\n', + const TextStyle( + color: _textSecondary, + fontWeight: FontWeight.w600, + fontSize: 11), + children: [ + TextSpan( + text: barSpot.y.toInt().toString(), + style: TextStyle( + color: barSpot.bar.color, + fontWeight: FontWeight.w900, + fontSize: 15), + ), + ], + ); + }).toList(); + }, + ), + ); +} + +FlGridData _gridData(double interval) { + return FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: interval, + getDrawingHorizontalLine: (_) => + const FlLine(color: Color(0xFF21262D), strokeWidth: 1), + ); +} + +FlTitlesData _titlesData( + double interval, + double xInterval, + double days, + StaticController c, +) { + return FlTitlesData( + show: true, + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: xInterval, + getTitlesWidget: (value, _) { + if (value <= 0 || value > days) return const SizedBox.shrink(); + if (c.startDate == null) return const SizedBox.shrink(); + final date = c.startDate!.add(Duration(days: value.toInt() - 1)); + return Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + DateFormat('d/M', 'en').format(date), + style: const TextStyle( + color: _textSecondary, + fontSize: 9, + fontWeight: FontWeight.w600), + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: interval, + reservedSize: 36, + getTitlesWidget: (value, _) => Text( + _formatNum(value), + style: const TextStyle(color: _textSecondary, fontSize: 9), + ), + ), + ), + ); +} + +double _maxY(List spots) { + if (spots.isEmpty) return 10; + final max = spots.map((s) => s.y).reduce((a, b) => a > b ? a : b); + return max <= 0 ? 5 : (max * 1.2).ceilToDouble(); +} + +double _interval(double maxY) { + if (maxY <= 10) return 2; + if (maxY <= 50) return 10; + if (maxY <= 100) return 20; + if (maxY <= 500) return 100; + if (maxY <= 1000) return 200; + return maxY / 5; +} + +double _days(StaticController c) { + if (c.startDate == null || c.endDate == null) return 30; + return c.endDate!.difference(c.startDate!).inDays + 1.0; +} + +double _xInterval(double days) { + if (days > 60) return 30; + if (days > 30) return 10; + if (days > 10) return 5; + return 1; +} + +String _formatNum(double v) { + if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}k'; + return v.toInt().toString(); +} diff --git a/lib/views/auth/login_page.dart b/lib/views/auth/login_page.dart index c8faf24..580b4c6 100644 --- a/lib/views/auth/login_page.dart +++ b/lib/views/auth/login_page.dart @@ -7,16 +7,37 @@ import '../../controller/auth/otp_helper.dart'; import '../../controller/functions/crud.dart'; import '../../print.dart'; +// ─── Colors (نفس نظام الألوان المستخدم في التطبيق) ────────────────────────── +class _C { + static const bg = Color(0xFF0A0D14); + static const card = Color(0xFF161D2E); + static const border = Color(0xFF1F2D4A); + static const accent = Color(0xFF00E5FF); + static const accentGlow = Color(0x2200E5FF); + static const accentDim = Color(0xFF0097A7); + static const textPrimary = Color(0xFFE8F0FE); + static const textSec = Color(0xFF7A8BAA); + static const error = Color(0xFFFF5252); + static const inputBg = Color(0xFF0C1120); +} + class AdminLoginPage extends StatefulWidget { const AdminLoginPage({super.key}); + @override State createState() => _AdminLoginPageState(); } -class _AdminLoginPageState extends State { +class _AdminLoginPageState extends State + with SingleTickerProviderStateMixin { final _phoneController = TextEditingController(); final _formKey = GlobalKey(); bool _isLoading = false; + + late final AnimationController _glowCtrl; + late final Animation _glowAnim; + + // ─── Logic (بدون تغيير) ──────────────────────────────────────────────────── Future _submit() async { final allowedPhones = Env.ALLOWED_ADMIN_PHONES; Log.print('allowedPhones: ${allowedPhones}'); @@ -42,56 +63,387 @@ class _AdminLoginPageState extends State { @override void initState() { super.initState(); - _initializeToken(); // استدعاء دالة async بدون await + _initializeToken(); + + _glowCtrl = AnimationController( + vsync: this, + duration: const Duration(seconds: 4), + )..repeat(reverse: true); + _glowAnim = Tween(begin: 0.3, end: 1.0).animate( + CurvedAnimation(parent: _glowCtrl, curve: Curves.easeInOut), + ); } void _initializeToken() async { await CRUD().getJWT(); } + @override + void dispose() { + _phoneController.dispose(); + _glowCtrl.dispose(); + super.dispose(); + } + + // ─── Build ───────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { Get.put(OtpHelper()); return Scaffold( - appBar: AppBar(title: const Text('دخول الإدارة')), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Form( - key: _formKey, - child: Column( - children: [ - // IntlPhoneField( - // initialCountryCode: 'SY', - // decoration: const InputDecoration(labelText: 'رقم الهاتف'), - // onChanged: (phone) { - // _phoneController.text = phone.completeNumber; - // }, - // validator: (phone) { - // if (phone == null || phone.completeNumber.isEmpty) { - // return 'الرجاء إدخال رقم الهاتف'; - // } - // return null; - // }, - // ), - TextFormField( - controller: _phoneController, - keyboardType: TextInputType.phone, - decoration: const InputDecoration(labelText: 'رقم الهاتف'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'الرجاء إدخال رقم الهاتف'; - } - return null; - }, + backgroundColor: _C.bg, + body: Stack( + children: [ + // ── Ambient glow top-right ────────────────────────────────────────── + Positioned( + top: -150, + right: -100, + child: AnimatedBuilder( + animation: _glowAnim, + builder: (_, __) => Opacity( + opacity: _glowAnim.value * 0.18, + child: Container( + width: 400, + height: 400, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [Color(0xFF00E5FF), Colors.transparent], + ), + ), + ), + ), + ), + ), + // ── Ambient glow bottom-left ──────────────────────────────────────── + Positioned( + bottom: -120, + left: -80, + child: AnimatedBuilder( + animation: _glowAnim, + builder: (_, __) => Opacity( + opacity: (1 - _glowAnim.value) * 0.15, + child: Container( + width: 340, + height: 340, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [Color(0xFF7C4DFF), Colors.transparent], + ), + ), + ), + ), + ), + ), + + // ── Main content ─────────────────────────────────────────────────── + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 28), + physics: const BouncingScrollPhysics(), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 24), + + // ── Logo / Icon ───────────────────────────────────── + _buildLogo(), + const SizedBox(height: 32), + + // ── Title ─────────────────────────────────────────── + const Text( + 'لوحة الإدارة', + style: TextStyle( + color: _C.textPrimary, + fontSize: 26, + fontWeight: FontWeight.w800, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + const Text( + 'أدخل رقم هاتفك للمتابعة', + style: TextStyle( + color: _C.textSec, + fontSize: 14, + ), + ), + const SizedBox(height: 40), + + // ── Card ──────────────────────────────────────────── + Container( + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: _C.card, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: _C.accent.withOpacity(0.18), width: 1.2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 32, + offset: const Offset(0, 12), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Field label ───────────────────────────── + const Row( + children: [ + Icon(Icons.phone_android_rounded, + color: _C.accent, size: 16), + SizedBox(width: 8), + Text( + 'رقم الهاتف', + style: TextStyle( + color: _C.textSec, + fontSize: 13, + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + ], + ), + const SizedBox(height: 10), + + // ── Phone field ───────────────────────────── + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + textDirection: TextDirection.ltr, + style: const TextStyle( + color: _C.textPrimary, + fontSize: 16, + fontFamily: 'monospace', + letterSpacing: 1.2, + ), + decoration: InputDecoration( + hintText: '+963 XXX XXX XXX', + hintStyle: const TextStyle( + color: _C.textSec, + fontSize: 14, + letterSpacing: 0.5), + filled: true, + fillColor: _C.inputBg, + prefixIcon: const Icon(Icons.dialpad_rounded, + color: _C.accentDim, size: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: _C.border, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: _C.accent, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: _C.error, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide( + color: _C.error, width: 1.5), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 16), + errorStyle: const TextStyle( + color: _C.error, fontSize: 12), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'الرجاء إدخال رقم الهاتف'; + } + return null; + }, + ), + const SizedBox(height: 28), + + // ── Submit button ──────────────────────────── + _isLoading + ? const Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator( + color: _C.accent, + strokeWidth: 2.5, + ), + ), + ) + : _SubmitButton(onPressed: () { + if (_formKey.currentState!.validate()) { + _submit(); + } + }), + ], + ), + ), + + const SizedBox(height: 32), + + // ── Footer ────────────────────────────────────────── + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _C.accent, + boxShadow: [ + BoxShadow( + color: _C.accentGlow, + blurRadius: 6, + spreadRadius: 1, + ), + ], + ), + ), + const SizedBox(width: 8), + const Text( + 'وصول مقيّد للمشرفين فقط', + style: TextStyle( + color: _C.textSec, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + // ─── Logo Widget ───────────────────────────────────────────────────────── + Widget _buildLogo() { + return AnimatedBuilder( + animation: _glowAnim, + builder: (_, child) => Container( + width: 90, + height: 90, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _C.card, + border: Border.all( + color: _C.accent.withOpacity(0.3 + _glowAnim.value * 0.3), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: _C.accentGlow.withOpacity(_glowAnim.value * 0.6), + blurRadius: 30, + spreadRadius: 4, + ), + ], + ), + child: child, + ), + child: const Icon( + Icons.admin_panel_settings_rounded, + color: _C.accent, + size: 42, + ), + ); + } +} + +// ─── Submit Button ───────────────────────────────────────────────────────────── +class _SubmitButton extends StatefulWidget { + final VoidCallback onPressed; + + const _SubmitButton({required this.onPressed}); + + @override + State<_SubmitButton> createState() => _SubmitButtonState(); +} + +class _SubmitButtonState extends State<_SubmitButton> + with SingleTickerProviderStateMixin { + late final AnimationController _ctrl; + late final Animation _scale; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 100)); + _scale = Tween(begin: 1.0, end: 0.96) + .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut)); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => _ctrl.forward(), + onTapUp: (_) => _ctrl.reverse(), + onTapCancel: () => _ctrl.reverse(), + onTap: widget.onPressed, + child: AnimatedBuilder( + animation: _scale, + builder: (_, child) => + Transform.scale(scale: _scale.value, child: child), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 17), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF00B4D8), Color(0xFF00E5FF)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: const Color(0x3300E5FF), + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 6), + ), + ], + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.send_rounded, color: Colors.white, size: 18), + SizedBox(width: 10), + Text( + 'إرسال رمز التحقق', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), ), - const SizedBox(height: 20), - _isLoading - ? const CircularProgressIndicator() - : ElevatedButton( - onPressed: _submit, - child: const Text('إرسال رمز التحقق'), - ) ], ), ), diff --git a/lib/views/invoice/add_invoice_page.dart b/lib/views/invoice/add_invoice_page.dart index e5a92ea..61d6cce 100644 --- a/lib/views/invoice/add_invoice_page.dart +++ b/lib/views/invoice/add_invoice_page.dart @@ -4,10 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; -import 'package:sefer_admin1/constant/colors.dart'; import 'package:sefer_admin1/constant/links.dart'; -import 'package:sefer_admin1/views/widgets/my_scafold.dart'; -import 'package:sefer_admin1/views/widgets/my_textField.dart'; import '../../constant/box_name.dart'; import '../../constant/info.dart'; @@ -28,6 +25,11 @@ class _AddInvoicePageState extends State { File? _imageFile; bool _isLoading = false; + // الألوان المستخدمة في الثيم + final Color primaryColor = const Color(0xFF4F46E5); // Indigo + final Color secondaryColor = const Color(0xFF818CF8); // Lighter Indigo + final Color bgColor = const Color(0xFFF3F4F6); // Light Gray Background + String generateInvoiceNumber() { final now = DateTime.now(); return "INV-${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}-${now.microsecond}"; @@ -36,7 +38,7 @@ class _AddInvoicePageState extends State { Future uploadInvoice() async { if (!_formKey.currentState!.validate()) return; - final driverID = '123'; // ← عدّله حسب نظامك + final driverID = '123'; // قيمة افتراضية أو يمكن جلبها من الكونترولر final invoiceNumber = generateInvoiceNumber(); final amount = _amountController.text.trim(); final itemName = _itemNameController.text.trim(); @@ -45,11 +47,13 @@ class _AddInvoicePageState extends State { setState(() => _isLoading = true); try { + // إعداد الترويسة (Headers) final headers = { 'Authorization': 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}', 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', }; + final uri = Uri.parse(AppLink.addInvoice); final request = http.MultipartRequest('POST', uri) ..fields['driverID'] = driverID @@ -59,41 +63,62 @@ class _AddInvoicePageState extends State { ..fields['date'] = date ..headers.addAll(headers); + // إضافة الصورة إذا وجدت if (_imageFile != null) { - final imageName = _imageFile!.path.split('/').last; - final imageStream = http.ByteStream(_imageFile!.openRead()); - final imageLength = await _imageFile!.length(); - - request.files.add(http.MultipartFile( + final multipartFile = await http.MultipartFile.fromPath( 'image', - imageStream, - imageLength, - filename: imageName, - )); - } else {} + _imageFile!.path, + ); + request.files.add(multipartFile); + } final response = await request.send(); final respStr = await response.stream.bytesToString(); - final data = jsonDecode(respStr); + // محاولة تحليل الاستجابة + Map data; + try { + data = jsonDecode(respStr); + } catch (e) { + data = {'status': 'error', 'message': 'Invalid server response'}; + } if (data['status'] == 'success') { - Get.snackbar('تم الحفظ', 'تم حفظ الفاتورة بنجاح', - backgroundColor: Colors.green.shade100); + Get.snackbar( + 'نجاح', + 'تم حفظ الفاتورة بنجاح', + backgroundColor: Colors.green.withOpacity(0.1), + colorText: Colors.green[800], + snackPosition: SnackPosition.TOP, + margin: const EdgeInsets.all(10), + borderRadius: 20, + ); _itemNameController.clear(); _amountController.clear(); setState(() => _imageFile = null); - Get.back(); // العودة للصفحة السابقة + + // تأخير بسيط قبل العودة لتحديث الصفحة السابقة + Future.delayed(const Duration(seconds: 1), () { + Get.back(result: true); + }); } else { - Get.snackbar('خطأ', data['message'], - backgroundColor: Colors.red.shade100); + Get.snackbar( + 'تنبيه', + data['message'] ?? 'حدث خطأ غير معروف', + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red[800], + ); } - } catch (e, stacktrace) { - Get.snackbar('فشل الإرسال', e.toString(), - backgroundColor: Colors.red.shade100); + } catch (e) { + Get.snackbar( + 'خطأ في الاتصال', + e.toString(), + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red[800], + ); } finally { - setState(() => _isLoading = false); + if (mounted) setState(() => _isLoading = false); } } @@ -114,76 +139,263 @@ class _AddInvoicePageState extends State { @override Widget build(BuildContext context) { - return MyScafolld( - title: 'إضافة فاتورة جديدة', - body: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: ListView( - children: [ - MyTextForm( - controller: _itemNameController, - label: 'اسم البضاعة', - hint: 'مثال: قطع غيار', - type: TextInputType.text, - // validator: (val) => - // val!.isEmpty ? 'الرجاء إدخال اسم البضاعة' : null, - ), - const SizedBox(height: 16), - MyTextForm( - controller: _amountController, - label: 'قيمة الفاتورة', - hint: 'مثال: 150.75', - type: TextInputType.numberWithOptions(decimal: true), - // validator: (val) => - // val!.isEmpty ? 'الرجاء إدخال المبلغ' : null, - ), - const SizedBox(height: 20), - Text('صورة الفاتورة (اختياري)', - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 10), - Container( - height: 180, - decoration: BoxDecoration( - color: Colors.grey.shade200, - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(10), - ), - child: _imageFile != null - ? ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.file(_imageFile!, fit: BoxFit.cover), - ) - : const Center(child: Text('لم يتم اختيار صورة')), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - onPressed: pickInvoiceImage, - icon: const Icon(Icons.image), - label: const Text('اختيار صورة'), - ), - const SizedBox(height: 30), - ElevatedButton( - onPressed: _isLoading ? null : uploadInvoice, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - backgroundColor: AppColor.primaryColor, - ), - child: _isLoading - ? const CircularProgressIndicator(color: Colors.white) - : const Text( - 'حفظ الفاتورة', - style: TextStyle(color: Colors.white), - ), - ), - ], + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + title: const Text( + 'إضافة فاتورة جديدة', + style: + TextStyle(color: Color(0xFF1F2937), fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new, color: Color(0xFF1F2937)), + onPressed: () => Get.back(), + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, bgColor], ), ), ), - ], - isleading: true, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // قسم البيانات الأساسية + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + _buildModernTextField( + controller: _itemNameController, + label: 'اسم البضاعة / الخدمة', + icon: Icons.inventory_2_outlined, + hint: 'مثال: صيانة سيارة', + ), + const SizedBox(height: 20), + _buildModernTextField( + controller: _amountController, + label: 'قيمة الفاتورة (د.أ)', + icon: Icons.attach_money, + hint: '0.00', + isNumber: true, + ), + ], + ), + ), + + const SizedBox(height: 25), + + // قسم الصورة + Text( + 'صورة الفاتورة', + style: TextStyle( + color: Colors.grey[700], + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 10), + + InkWell( + onTap: pickInvoiceImage, + borderRadius: BorderRadius.circular(20), + child: Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _imageFile != null + ? primaryColor + : Colors.grey.shade300, + width: 2, + style: _imageFile != null + ? BorderStyle.solid + : BorderStyle.solid, + ), + image: _imageFile != null + ? DecorationImage( + image: FileImage(_imageFile!), + fit: BoxFit.cover, + ) + : null, + ), + child: _imageFile == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.05), + shape: BoxShape.circle, + ), + child: Icon(Icons.add_a_photo_rounded, + size: 40, color: primaryColor), + ), + const SizedBox(height: 10), + Text( + 'اضغط لرفع صورة الفاتورة', + style: TextStyle( + color: Colors.grey[500], + fontWeight: FontWeight.w500, + ), + ), + ], + ) + : Stack( + children: [ + Positioned( + top: 10, + right: 10, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + ), + child: Icon(Icons.edit, + color: primaryColor, size: 20), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 40), + + // زر الحفظ + SizedBox( + height: 55, + child: ElevatedButton( + onPressed: _isLoading ? null : uploadInvoice, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Ink( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: _isLoading + ? [Colors.grey, Colors.grey] + : [primaryColor, secondaryColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + if (!_isLoading) + BoxShadow( + color: primaryColor.withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Container( + alignment: Alignment.center, + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.save_rounded, color: Colors.white), + SizedBox(width: 10), + Text( + 'حفظ الفاتورة', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildModernTextField({ + required TextEditingController controller, + required String label, + required IconData icon, + required String hint, + bool isNumber = false, + }) { + return TextFormField( + controller: controller, + keyboardType: isNumber + ? const TextInputType.numberWithOptions(decimal: true) + : TextInputType.text, + validator: (val) { + if (val == null || val.isEmpty) { + return 'هذا الحقل مطلوب'; + } + return null; + }, + decoration: InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: Icon(icon, color: primaryColor.withOpacity(0.7)), + filled: true, + fillColor: Colors.grey[50], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primaryColor, width: 1.5), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), ); } } diff --git a/lib/views/invoice/invoice_list_page.dart b/lib/views/invoice/invoice_list_page.dart index 84a8a8e..8ad5a4d 100644 --- a/lib/views/invoice/invoice_list_page.dart +++ b/lib/views/invoice/invoice_list_page.dart @@ -1,24 +1,32 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:http/http.dart' as http; import '../../constant/links.dart'; -import '../../controller/admin/get_all_invoice_controller.dart'; import '../../controller/functions/crud.dart'; import '../../print.dart'; import 'add_invoice_page.dart'; +// نفترض أن هذا الموديل موجود في مشروعك، إذا لم يكن موجوداً يرجى إضافته أو تعديل الاستيراد +// import '../../model/invoice_model.dart'; + class InvoiceListPage extends StatefulWidget { + const InvoiceListPage({super.key}); + @override _InvoiceListPageState createState() => _InvoiceListPageState(); } class _InvoiceListPageState extends State { - List invoices = []; + List invoices = []; // استخدام dynamic لتجنب مشاكل الموديل إذا اختلف int totalCount = 0; double totalAmount = 0.0; bool isLoading = true; + // الألوان "الإيجابية" للتصميم الجديد + final Color primaryColor = const Color(0xFF4F46E5); // Indigo + final Color secondaryColor = const Color(0xFF818CF8); // Lighter Indigo + final Color moneyColor = const Color(0xFF059669); // Emerald Green + final Color bgColor = const Color(0xFFF3F4F6); // Light Gray Background + @override void initState() { super.initState(); @@ -26,255 +34,423 @@ class _InvoiceListPageState extends State { } Future fetchInvoices() async { - // لإظهار مؤشر التحديث بشكل جيد - if (!isLoading) { - setState(() {}); - } + if (!mounted) return; + setState(() => isLoading = true); - final response = await CRUD().post(link: AppLink.getInvoices, payload: {}); - final data = (response); - Log.print('data: $data'); + try { + final response = + await CRUD().post(link: AppLink.getInvoices, payload: {}); - if (mounted) { - if (data != 'failure' && data['status'] == 'success') { - setState(() { - invoices = List.from(data['data']) - .map((item) => InvoiceModel.fromJson(item)) - .toList(); - totalCount = data['summary']['count']; - totalAmount = - double.tryParse(data['summary']['total'].toString()) ?? 0.0; - isLoading = false; - }); + if (response != 'failure' && response['status'] == 'success') { + final data = response; + if (mounted) { + setState(() { + invoices = data['data']; // استخدام البيانات مباشرة + totalCount = int.tryParse(data['summary']['count'].toString()) ?? 0; + totalAmount = + double.tryParse(data['summary']['total'].toString()) ?? 0.0; + isLoading = false; + }); + } } else { - setState(() { - isLoading = false; - }); - Get.snackbar("خطأ", "فشل في تحميل الفواتير. حاول التحديث مرة أخرى.", - backgroundColor: Colors.red.withOpacity(0.8), - colorText: Colors.white); + if (mounted) { + setState(() => isLoading = false); + Get.snackbar("تنبيه", "لا توجد فواتير لعرضها أو حدث خطأ", + backgroundColor: Colors.orange.withOpacity(0.2), + colorText: Colors.orange[900]); + } } + } catch (e) { + Log.print('Error fetching invoices: $e'); + if (mounted) setState(() => isLoading = false); } } - // --- دالة لعرض الصورة في نافذة منبثقة --- void _showImageDialog(BuildContext context, String imageUrl) { showDialog( context: context, - builder: (BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: GestureDetector( - // لإغلاق الصورة عند الضغط عليها - onTap: () => Navigator.of(context).pop(), - child: Container( - padding: EdgeInsets.all(12), - child: InteractiveViewer( - // لإتاحة التكبير والتصغير - panEnabled: true, - minScale: 0.5, - maxScale: 4, - child: Image.network( - imageUrl, - fit: BoxFit.contain, - // إظهار مؤشر تحميل أثناء جلب الصورة - loadingBuilder: (BuildContext context, Widget child, - ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ); - }, - // إظهار أيقونة خطأ في حال فشل تحميل الصورة - errorBuilder: (context, error, stackTrace) { - return Icon(Icons.broken_image, - size: 100, color: Colors.red); - }, - ), - ), - ), - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("قائمة الفواتير"), - centerTitle: true, - elevation: 2, - actions: [ - IconButton( - icon: Icon(Icons.add_a_photo), - onPressed: () { - // يمكنك إضافة إجراء الطباعة هنا - - Get.to(() => AddInvoicePage()); - }, - ), - ], - ), - body: isLoading - ? Center(child: CircularProgressIndicator()) - : RefreshIndicator( - onRefresh: fetchInvoices, // خاصية السحب للتحديث - child: Column( - children: [ - Expanded( - child: ListView.builder( - padding: - EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), - itemCount: invoices.length, - itemBuilder: (context, index) { - final invoice = invoices[index]; - return Card( - elevation: 4, - margin: EdgeInsets.symmetric(vertical: 8.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - child: InkWell( - borderRadius: BorderRadius.circular(15.0), - onTap: () { - // التحقق من وجود رابط للصورة قبل محاولة عرضه - if (invoice.imageLink != null && - invoice.imageLink!.isNotEmpty) { - _showImageDialog(context, invoice.imageLink!); - } else { - Get.snackbar("لا توجد صورة", - "هذه الفاتورة لا تحتوي على صورة مرفقة."); - } - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - // أيقونة الفاتورة الرئيسية - Icon(Icons.receipt_long, - color: Theme.of(context).primaryColor, - size: 40), - SizedBox(width: 16), - // تفاصيل الفاتورة - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "فاتورة رقم: ${invoice.invoiceNumber}", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - SizedBox(height: 8), - Text( - "الاسم: ${invoice.name}", - style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8), - Text( - "المبلغ: ${invoice.amount} د.أ", - style: TextStyle( - color: Colors.green.shade700, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 4), - Text( - "التاريخ: ${invoice.date}", - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - ), - ), - ], - ), - ), - // أيقونة توضح وجود صورة - if (invoice.imageLink != null && - invoice.imageLink!.isNotEmpty) - Icon(Icons.image_outlined, - color: Colors.blueAccent, size: 30), - ], - ), - ), - ), - ); - }, - ), - ), - _buildSummaryCard(), // بطاقة الملخص السفلية - ], - ), - ), - ); - } - - Widget _buildSummaryCard() { - return Card( - margin: EdgeInsets.all(0), - elevation: 8, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 25), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: (_) => Dialog( + backgroundColor: Colors.transparent, + child: Stack( + alignment: Alignment.center, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "إجمالي الفواتير", - style: TextStyle(color: Colors.grey.shade600, fontSize: 14), - ), - Text( - "$totalCount", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.all(5), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: InteractiveViewer( + panEnabled: true, + minScale: 0.5, + maxScale: 4, + child: Image.network( + imageUrl, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return SizedBox( + height: 200, + width: 200, + child: Center( + child: CircularProgressIndicator(color: primaryColor), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return const SizedBox( + height: 150, + width: 150, + child: Icon(Icons.broken_image, + size: 60, color: Colors.grey), + ); + }, ), ), - ], + ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "المبلغ الإجمالي", - style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + Positioned( + top: 0, + right: 0, + child: CircleAvatar( + backgroundColor: Colors.white, + child: IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () => Navigator.pop(context), ), - Text( - "${totalAmount.toStringAsFixed(2)} د.أ", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.green.shade800, - ), - ), - ], + ), ), ], ), ), ); } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bgColor, + // زر عائم بتصميم متدرج + floatingActionButton: Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: [primaryColor, secondaryColor]), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: FloatingActionButton.extended( + onPressed: () => Get.to(() => AddInvoicePage()), + label: const Text('إضافة فاتورة', + style: TextStyle(fontWeight: FontWeight.bold)), + icon: const Icon(Icons.add), + backgroundColor: Colors.transparent, + elevation: 0, + ), + ), + body: Column( + children: [ + // 1. رأس الصفحة (Header & Summary) + _buildHeader(), + + // 2. قائمة الفواتير + Expanded( + child: isLoading + ? Center(child: CircularProgressIndicator(color: primaryColor)) + : invoices.isEmpty + ? _buildEmptyState() + : RefreshIndicator( + onRefresh: fetchInvoices, + color: primaryColor, + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), + itemCount: invoices.length, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final invoice = invoices[index]; + return _buildInvoiceCard(invoice); + }, + ), + ), + ), + ], + ), + ); + } + + // === تصميم الهيدر (رأس الصفحة) === + Widget _buildHeader() { + return Container( + width: double.infinity, + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 20, + bottom: 30, + left: 20, + right: 20, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [primaryColor, const Color(0xFF6366F1)], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + // العنوان وزر الرجوع + Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios, + color: Colors.white, size: 20), + onPressed: () => Get.back(), + ), + const Expanded( + child: Text( + "سجل الفواتير", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 40), // للمحاذاة + ], + ), + const SizedBox(height: 25), + + // بطاقات الملخص + Row( + children: [ + Expanded( + child: _buildSummaryItem( + title: "الإجمالي", + value: "${totalAmount.toStringAsFixed(1)} د.أ", + icon: Icons.attach_money, + isMoney: true, + ), + ), + Container(width: 1, height: 40, color: Colors.white24), + Expanded( + child: _buildSummaryItem( + title: "عدد الفواتير", + value: "$totalCount", + icon: Icons.receipt_long, + isMoney: false, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSummaryItem( + {required String title, + required String value, + required IconData icon, + required bool isMoney}) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: Colors.white, size: 20), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + color: isMoney ? const Color(0xFFD1FAE5) : Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + Text( + title, + style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12), + ), + ], + ); + } + + // === تصميم بطاقة الفاتورة === + Widget _buildInvoiceCard(dynamic invoice) { + // استخراج البيانات بأمان + String name = invoice['name'] ?? 'بدون اسم'; + String amount = invoice['amount']?.toString() ?? '0'; + String date = invoice['date'] ?? ''; + String invNumber = invoice['invoiceNumber']?.toString() ?? '#'; + String? imageUrl = invoice['imageLink']; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + if (imageUrl != null && imageUrl.isNotEmpty) { + _showImageDialog(context, imageUrl); + } else { + Get.snackbar("تنبيه", "لا توجد صورة مرفقة", + backgroundColor: Colors.grey[200], colorText: Colors.black); + } + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // 1. أيقونة أو صورة مصغرة + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.08), + borderRadius: BorderRadius.circular(15), + ), + child: imageUrl != null && imageUrl.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Icon(Icons.receipt, color: primaryColor), + ), + ) + : Icon(Icons.receipt_outlined, color: primaryColor), + ), + + const SizedBox(width: 16), + + // 2. التفاصيل + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Color(0xFF1F2937), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "#$invNumber", + style: TextStyle( + fontSize: 10, + color: Colors.grey[600], + fontWeight: FontWeight.bold), + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + date, + style: TextStyle(color: Colors.grey[500], fontSize: 12), + ), + ], + ), + ), + + const SizedBox(width: 12), + + // 3. المبلغ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "$amount", + style: TextStyle( + color: moneyColor, + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + Text( + "د.أ", + style: TextStyle( + color: moneyColor.withOpacity(0.7), + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.receipt_long_rounded, size: 80, color: Colors.grey[300]), + const SizedBox(height: 16), + Text( + "لا توجد فواتير حالياً", + style: TextStyle(color: Colors.grey[500], fontSize: 16), + ), + const SizedBox(height: 8), + TextButton.icon( + onPressed: fetchInvoices, + icon: const Icon(Icons.refresh), + label: const Text("تحديث"), + ), + ], + ), + ); + } }