From efbc921273600383377d494ee58cf5acd39d46ee Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 8 May 2026 06:10:35 +0300 Subject: [PATCH] feat: redesign behavior page, add fatigue monitoring and fix map controller --- lib/constant/links.dart | 7 + .../gamification/challenges_controller.dart | 286 ++++++++++ .../gamification/gamification_controller.dart | 453 ++++++++++++++++ .../gamification/leaderboard_controller.dart | 102 ++++ .../gamification/referral_controller.dart | 189 +++++++ .../home/captin/home_captain_controller.dart | 104 +++- .../home/journal/schedule_controller.dart | 106 ++++ .../statistics/statistics_controller.dart | 193 +++++++ lib/translations_ar.json | 96 +++- lib/translations_en.json | 96 +++- lib/views/gamification/challenges_page.dart | 228 ++++++++ lib/views/gamification/leaderboard_page.dart | 132 +++++ .../gamification/referral_center_page.dart | 248 +++++++++ .../Captin/home_captain/drawer_captain.dart | 31 +- .../home/Captin/home_captain/home_captin.dart | 7 +- lib/views/home/journal/schedule_page.dart | 125 +++++ lib/views/home/profile/behavior_page.dart | 210 +++++--- .../home/statistics/statistics_dashboard.dart | 498 ++++++++++++++++++ .../statistics/widgets/daily_goal_widget.dart | 307 +++++++++++ .../widgets/level_progress_widget.dart | 93 ++++ .../widgets/monthly_chart_widget.dart | 99 ++++ .../statistics/widgets/stat_summary_card.dart | 83 +++ .../widgets/today_chart_widget.dart | 57 ++ .../widgets/weekly_chart_widget.dart | 114 ++++ 24 files changed, 3772 insertions(+), 92 deletions(-) create mode 100644 lib/controller/gamification/challenges_controller.dart create mode 100644 lib/controller/gamification/gamification_controller.dart create mode 100644 lib/controller/gamification/leaderboard_controller.dart create mode 100644 lib/controller/gamification/referral_controller.dart create mode 100644 lib/controller/home/journal/schedule_controller.dart create mode 100644 lib/controller/home/statistics/statistics_controller.dart create mode 100644 lib/views/gamification/challenges_page.dart create mode 100644 lib/views/gamification/leaderboard_page.dart create mode 100644 lib/views/gamification/referral_center_page.dart create mode 100644 lib/views/home/journal/schedule_page.dart create mode 100644 lib/views/home/statistics/statistics_dashboard.dart create mode 100644 lib/views/home/statistics/widgets/daily_goal_widget.dart create mode 100644 lib/views/home/statistics/widgets/level_progress_widget.dart create mode 100644 lib/views/home/statistics/widgets/monthly_chart_widget.dart create mode 100644 lib/views/home/statistics/widgets/stat_summary_card.dart create mode 100644 lib/views/home/statistics/widgets/today_chart_widget.dart create mode 100644 lib/views/home/statistics/widgets/weekly_chart_widget.dart diff --git a/lib/constant/links.dart b/lib/constant/links.dart index 626a71e..e609559 100755 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -92,6 +92,13 @@ class AppLink { "$endPoint/ride/driverWallet/driverStatistic.php"; static String getDriverDetails = "$seferCairoServer/ride/driverWallet/getDriverDetails.php"; + + // ================= Gamification Endpoints ================= + static String getWeeklyAggregate = "$endPoint/ride/gamification/getWeeklyAggregate.php"; + static String getLeaderboard = "$endPoint/ride/gamification/getLeaderboard.php"; + static String claimChallengeReward = "$endPoint/ride/gamification/claimChallengeReward.php"; + static String getReferralStats = "$endPoint/ride/gamification/getReferralStats.php"; + static String getDriverBehavior = "$endPoint/ride/gamification/getDriverBehavior.php"; static String getDriverWeekPaymentMove = "$walletDriver/getDriverWeekPaymentMove.php"; static String getDriversWallet = "$walletDriver/get.php"; diff --git a/lib/controller/gamification/challenges_controller.dart b/lib/controller/gamification/challenges_controller.dart new file mode 100644 index 0000000..edb0fe9 --- /dev/null +++ b/lib/controller/gamification/challenges_controller.dart @@ -0,0 +1,286 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import '../../../main.dart'; + +// ════════════════════════════════════════════ +// نموذج التحدي +// ════════════════════════════════════════════ + +class Challenge { + final String id; + final String titleEn; + final String titleAr; + final String descriptionEn; + final String descriptionAr; + final IconData icon; + final Color color; + final int target; + final int reward; // نقاط + final String type; // 'daily' or 'weekly' + final String metric; // 'trips', 'earnings', 'hours', 'acceptance_rate' + int currentProgress; + bool isClaimed; + + Challenge({ + required this.id, + required this.titleEn, + required this.titleAr, + required this.descriptionEn, + required this.descriptionAr, + required this.icon, + required this.color, + required this.target, + required this.reward, + required this.type, + required this.metric, + this.currentProgress = 0, + this.isClaimed = false, + }); + + double get progress => (currentProgress / target).clamp(0.0, 1.0); + bool get isCompleted => currentProgress >= target; +} + +// ════════════════════════════════════════════ +// Controller +// ════════════════════════════════════════════ + +class ChallengesController extends GetxController { + bool isLoading = false; + List dailyChallenges = []; + List weeklyChallenges = []; + + @override + void onInit() { + super.onInit(); + _generateChallenges(); + _loadProgress(); + fetchChallengeProgress(); + } + + void _generateChallenges() { + final now = DateTime.now(); + final isWeekend = now.weekday == 5 || now.weekday == 6; // الجمعة والسبت + + dailyChallenges = [ + Challenge( + id: 'daily_trips_5', + titleEn: 'Road Runner', + titleAr: 'سائق سريع', + descriptionEn: 'Complete 5 trips today', + descriptionAr: 'أكمل 5 رحلات اليوم', + icon: Icons.local_taxi_rounded, + color: const Color(0xFF2196F3), + target: 5, + reward: 50, + type: 'daily', + metric: 'trips', + ), + Challenge( + id: 'daily_trips_10', + titleEn: 'Marathon Driver', + titleAr: 'سائق الماراثون', + descriptionEn: 'Complete 10 trips today', + descriptionAr: 'أكمل 10 رحلات اليوم', + icon: Icons.directions_car_rounded, + color: const Color(0xFFFF9800), + target: 10, + reward: 150, + type: 'daily', + metric: 'trips', + ), + Challenge( + id: 'daily_earnings', + titleEn: 'Money Maker', + titleAr: 'صانع المال', + descriptionEn: 'Earn 3000 SYP today', + descriptionAr: 'اربح 3000 ل.س اليوم', + icon: Icons.monetization_on_rounded, + color: const Color(0xFF4CAF50), + target: 3000, + reward: 100, + type: 'daily', + metric: 'earnings', + ), + if (isWeekend) + Challenge( + id: 'daily_weekend_bonus', + titleEn: 'Weekend Warrior', + titleAr: 'محارب عطلة نهاية الأسبوع', + descriptionEn: 'Complete 8 trips on the weekend', + descriptionAr: 'أكمل 8 رحلات في عطلة نهاية الأسبوع', + icon: Icons.celebration_rounded, + color: const Color(0xFFE91E63), + target: 8, + reward: 200, + type: 'daily', + metric: 'trips', + ), + ]; + + weeklyChallenges = [ + Challenge( + id: 'weekly_trips_30', + titleEn: 'Weekly Champion', + titleAr: 'بطل الأسبوع', + descriptionEn: 'Complete 30 trips this week', + descriptionAr: 'أكمل 30 رحلة هذا الأسبوع', + icon: Icons.emoji_events_rounded, + color: const Color(0xFFFFD700), + target: 30, + reward: 300, + type: 'weekly', + metric: 'trips', + ), + Challenge( + id: 'weekly_earnings', + titleEn: 'Big Earner', + titleAr: 'الربح الكبير', + descriptionEn: 'Earn 20,000 SYP this week', + descriptionAr: 'اربح 20,000 ل.س هذا الأسبوع', + icon: Icons.account_balance_wallet_rounded, + color: const Color(0xFF9C27B0), + target: 20000, + reward: 500, + type: 'weekly', + metric: 'earnings', + ), + Challenge( + id: 'weekly_hours', + titleEn: 'Time Master', + titleAr: 'سيد الوقت', + descriptionEn: 'Drive for 20 hours this week', + descriptionAr: 'اقضِ 20 ساعة في القيادة هذا الأسبوع', + icon: Icons.timer_rounded, + color: const Color(0xFF00BCD4), + target: 20, + reward: 400, + type: 'weekly', + metric: 'hours', + ), + ]; + } + + void _loadProgress() { + final today = DateTime.now().toIso8601String().split('T')[0]; + final savedDate = box.read('challenges_date'); + + if (savedDate != today) { + // يوم جديد — إعادة تعيين التحديات اليومية + box.write('challenges_date', today); + for (var c in dailyChallenges) { + box.write('challenge_${c.id}_claimed', false); + } + } + + // تحميل حالة المطالبة + for (var c in dailyChallenges) { + c.isClaimed = box.read('challenge_${c.id}_claimed') ?? false; + } + for (var c in weeklyChallenges) { + c.isClaimed = box.read('challenge_${c.id}_claimed') ?? false; + } + } + + Future fetchChallengeProgress() async { + isLoading = true; + update(); + + try { + // جلب رحلات اليوم + var todayRes = await CRUD().getWallet( + link: AppLink.getDriverPaymentToday, + payload: {'driverID': box.read(BoxName.driverID).toString()}, + ); + + int todayTrips = 0; + double todayEarnings = 0; + + if (todayRes != null && todayRes != 'failure') { + var data = jsonDecode(todayRes); + todayEarnings = double.tryParse(data['message']?[0]?['todayAmount']?.toString() ?? '0') ?? 0; + todayTrips = int.tryParse(data['message']?[0]?['todayCount']?.toString() ?? '0') ?? 0; + } + + // تحديث التحديات اليومية + for (var c in dailyChallenges) { + switch (c.metric) { + case 'trips': + c.currentProgress = todayTrips; + break; + case 'earnings': + c.currentProgress = todayEarnings.toInt(); + break; + } + } + + // Fetch weekly aggregate for weekly challenges + var weeklyRes = await CRUD().get( + link: AppLink.getWeeklyAggregate, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + + int weeklyTrips = 0; + double weeklyEarnings = 0; + double weeklyHours = 0; + + if (weeklyRes != null && weeklyRes != 'failure') { + var data = jsonDecode(weeklyRes); + if (data['message'] is List) { + for (var day in data['message']) { + weeklyTrips += int.tryParse(day['trips']?.toString() ?? '0') ?? 0; + weeklyEarnings += double.tryParse(day['earnings']?.toString() ?? '0') ?? 0; + weeklyHours += double.tryParse(day['hours']?.toString() ?? '0') ?? 0; + } + } + } + + for (var c in weeklyChallenges) { + switch (c.metric) { + case 'trips': + c.currentProgress = weeklyTrips; + break; + case 'earnings': + c.currentProgress = weeklyEarnings.toInt(); + break; + case 'hours': + c.currentProgress = weeklyHours.toInt(); + break; + } + } + } catch (e) { + debugPrint('❌ [Challenges] Error: $e'); + } + + isLoading = false; + update(); + } + + Future claimReward(Challenge challenge) async { + if (!challenge.isCompleted || challenge.isClaimed) return; + + try { + var res = await CRUD().post( + link: AppLink.claimChallengeReward, + payload: { + 'driver_id': box.read(BoxName.driverID).toString(), + 'challenge_id': challenge.id, + 'points': challenge.reward.toString(), + }, + ); + + if (res != null && res != 'failure') { + challenge.isClaimed = true; + box.write('challenge_${challenge.id}_claimed', true); + debugPrint('🎉 Claimed ${challenge.reward} points for ${challenge.id}'); + update(); + } + } catch (e) { + debugPrint('❌ [Challenges] Claim error: $e'); + } + } +} diff --git a/lib/controller/gamification/gamification_controller.dart b/lib/controller/gamification/gamification_controller.dart new file mode 100644 index 0000000..c5a1051 --- /dev/null +++ b/lib/controller/gamification/gamification_controller.dart @@ -0,0 +1,453 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import '../../main.dart'; + +// ════════════════════════════════════════════ +// نماذج البيانات +// ════════════════════════════════════════════ + +class DriverLevel { + final String id; + final String nameEn; + final String nameAr; + final String emoji; + final Color color; + final Color gradientEnd; + final int minPoints; + final int maxPoints; + final double commissionDiscount; // نسبة الخصم على العمولة + final List perks; + + const DriverLevel({ + required this.id, + required this.nameEn, + required this.nameAr, + required this.emoji, + required this.color, + required this.gradientEnd, + required this.minPoints, + required this.maxPoints, + required this.commissionDiscount, + required this.perks, + }); +} + +class Achievement { + final String id; + final String titleEn; + final String titleAr; + final String descriptionEn; + final String descriptionAr; + final IconData icon; + final Color color; + final int target; + final String type; // 'trips', 'rating', 'earnings', 'streak', 'referral' + bool isUnlocked; + int currentProgress; + final DateTime? unlockedAt; + + Achievement({ + required this.id, + required this.titleEn, + required this.titleAr, + required this.descriptionEn, + required this.descriptionAr, + required this.icon, + required this.color, + required this.target, + required this.type, + this.isUnlocked = false, + this.currentProgress = 0, + this.unlockedAt, + }); + + double get progress => (currentProgress / target).clamp(0.0, 1.0); +} + +// ════════════════════════════════════════════ +// المستويات الثابتة +// ════════════════════════════════════════════ + +class DriverLevels { + static const List all = [ + DriverLevel( + id: 'bronze', + nameEn: 'Bronze', + nameAr: 'برونزي', + emoji: '🥉', + color: Color(0xFFCD7F32), + gradientEnd: Color(0xFFE8A854), + minPoints: 0, + maxPoints: 999, + commissionDiscount: 0, + perks: ['Basic features', 'Standard support'], + ), + DriverLevel( + id: 'silver', + nameEn: 'Silver', + nameAr: 'فضي', + emoji: '🥈', + color: Color(0xFF9CA3AF), + gradientEnd: Color(0xFFC0C7D1), + minPoints: 1000, + maxPoints: 4999, + commissionDiscount: 1, + perks: ['Priority medium', 'Silver badge', '-1% commission'], + ), + DriverLevel( + id: 'gold', + nameEn: 'Gold', + nameAr: 'ذهبي', + emoji: '🥇', + color: Color(0xFFFFD700), + gradientEnd: Color(0xFFFFA500), + minPoints: 5000, + maxPoints: 14999, + commissionDiscount: 2, + perks: ['High priority', 'Gold badge', '-2% commission'], + ), + DriverLevel( + id: 'diamond', + nameEn: 'Diamond', + nameAr: 'ألماسي', + emoji: '💎', + color: Color(0xFF00BCD4), + gradientEnd: Color(0xFF3F51B5), + minPoints: 15000, + maxPoints: 999999, + commissionDiscount: 5, + perks: ['VIP first', 'Diamond badge', '-5% commission', 'Priority support'], + ), + ]; + + static DriverLevel getLevel(int points) { + for (int i = all.length - 1; i >= 0; i--) { + if (points >= all[i].minPoints) return all[i]; + } + return all.first; + } + + static DriverLevel? getNextLevel(int points) { + final current = getLevel(points); + final idx = all.indexOf(current); + if (idx < all.length - 1) return all[idx + 1]; + return null; + } + + static double getProgressToNext(int points) { + final current = getLevel(points); + final next = getNextLevel(points); + if (next == null) return 1.0; + return ((points - current.minPoints) / (next.minPoints - current.minPoints)) + .clamp(0.0, 1.0); + } +} + +// ════════════════════════════════════════════ +// الـ Controller +// ════════════════════════════════════════════ + +class GamificationController extends GetxController { + bool isLoading = false; + int totalTrips = 0; + int totalPoints = 0; + double averageRating = 5.0; + int totalReferrals = 0; + int consecutiveDays = 0; // أيام متتالية + double totalEarnings = 0; + + // === Driving Behavior === + double behaviorScore = 100.0; + int hardBrakes = 0; + double maxSpeed = 0.0; + + late DriverLevel currentLevel; + DriverLevel? nextLevel; + double progressToNext = 0; + List achievements = []; + + // === Daily Goal === + double dailyGoal = 0; + double dailyEarnings = 0; + double get dailyGoalProgress => + dailyGoal > 0 ? (dailyEarnings / dailyGoal).clamp(0.0, 1.0) : 0.0; + bool get isDailyGoalMet => dailyGoalProgress >= 1.0; + + @override + void onInit() { + super.onInit(); + _loadLocalData(); + _initializeAchievements(); + _calculateLevel(); + fetchGamificationData(); + } + + // ═══════ تحميل البيانات المحلية ═══════ + void _loadLocalData() { + dailyGoal = (box.read('dailyGoal') ?? 0).toDouble(); + totalTrips = box.read('gamification_totalTrips') ?? 0; + consecutiveDays = box.read('gamification_consecutiveDays') ?? 0; + } + + // ═══════ حفظ الهدف اليومي ═══════ + void setDailyGoal(double goal) { + dailyGoal = goal; + box.write('dailyGoal', goal); + update(); + } + + void updateDailyEarnings(double earnings) { + dailyEarnings = earnings; + update(); + } + + // ═══════ حساب المستوى ═══════ + void _calculateLevel() { + currentLevel = DriverLevels.getLevel(totalPoints); + nextLevel = DriverLevels.getNextLevel(totalPoints); + progressToNext = DriverLevels.getProgressToNext(totalPoints); + update(); + } + + // ═══════ تهيئة الإنجازات ═══════ + void _initializeAchievements() { + achievements = [ + Achievement( + id: 'first_trip', + titleEn: 'First Trip', + titleAr: 'أول رحلة', + descriptionEn: 'Complete your first trip', + descriptionAr: 'أكمل أول رحلة لك', + icon: Icons.flag_rounded, + color: const Color(0xFF4CAF50), + target: 1, + type: 'trips', + ), + Achievement( + id: 'trip_50', + titleEn: 'Road Warrior', + titleAr: 'محارب الطريق', + descriptionEn: 'Complete 50 trips', + descriptionAr: 'أكمل 50 رحلة', + icon: Icons.local_taxi_rounded, + color: const Color(0xFF2196F3), + target: 50, + type: 'trips', + ), + Achievement( + id: 'trip_100', + titleEn: 'Century Rider', + titleAr: 'سائق المئة', + descriptionEn: 'Complete 100 trips', + descriptionAr: 'أكمل 100 رحلة', + icon: Icons.emoji_events_rounded, + color: const Color(0xFFFF9800), + target: 100, + type: 'trips', + ), + Achievement( + id: 'trip_500', + titleEn: 'Road Legend', + titleAr: 'أسطورة الطريق', + descriptionEn: 'Complete 500 trips', + descriptionAr: 'أكمل 500 رحلة', + icon: Icons.stars_rounded, + color: const Color(0xFFE91E63), + target: 500, + type: 'trips', + ), + Achievement( + id: 'five_star', + titleEn: 'Five Star Driver', + titleAr: 'سائق 5 نجوم', + descriptionEn: 'Maintain 5.0 rating', + descriptionAr: 'حافظ على تقييم 5.0', + icon: Icons.star_rounded, + color: const Color(0xFFFFD700), + target: 5, + type: 'rating', + ), + Achievement( + id: 'streak_7', + titleEn: 'Weekly Streak', + titleAr: 'سلسلة أسبوعية', + descriptionEn: 'Work 7 consecutive days', + descriptionAr: 'اعمل 7 أيام متتالية', + icon: Icons.whatshot_rounded, + color: const Color(0xFFFF5722), + target: 7, + type: 'streak', + ), + Achievement( + id: 'streak_30', + titleEn: 'Monthly Streak', + titleAr: 'سلسلة شهرية', + descriptionEn: 'Work 30 consecutive days', + descriptionAr: 'اعمل 30 يوم متتالي', + icon: Icons.local_fire_department_rounded, + color: const Color(0xFFD32F2F), + target: 30, + type: 'streak', + ), + Achievement( + id: 'referral_5', + titleEn: 'Social Butterfly', + titleAr: 'الفراشة الاجتماعية', + descriptionEn: 'Refer 5 drivers', + descriptionAr: 'ادعُ 5 سائقين', + icon: Icons.people_rounded, + color: const Color(0xFF9C27B0), + target: 5, + type: 'referral', + ), + ]; + } + + // ═══════ تحديث تقدم الإنجازات ═══════ + void _updateAchievementProgress() { + for (var ach in achievements) { + switch (ach.type) { + case 'trips': + ach.currentProgress = totalTrips; + break; + case 'rating': + ach.currentProgress = averageRating >= 5 ? 5 : averageRating.floor(); + break; + case 'streak': + ach.currentProgress = consecutiveDays; + break; + case 'referral': + ach.currentProgress = totalReferrals; + break; + } + ach.isUnlocked = ach.currentProgress >= ach.target; + } + update(); + } + + // ═══════ جلب البيانات من السيرفر ═══════ + Future fetchGamificationData() async { + isLoading = true; + update(); + + try { + // 1. جلب عدد الرحلات الكلي + var tripRes = await CRUD().get( + link: AppLink.getTripCountByCaptain, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (tripRes != null && tripRes != 'failure') { + var data = jsonDecode(tripRes); + totalTrips = + int.tryParse(data['message']?[0]?['count']?.toString() ?? '0') ?? 0; + box.write('gamification_totalTrips', totalTrips); + } + + // 2. جلب التقييم + var rateRes = await CRUD().get( + link: AppLink.getDriverRate, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (rateRes != null && rateRes != 'failure') { + var data = jsonDecode(rateRes); + averageRating = + double.tryParse(data['message']?[0]?['rating']?.toString() ?? '5') ?? + 5.0; + } + + // 3. جلب النقاط (الرصيد) + var pointsRes = await CRUD().getWallet( + link: AppLink.getDriverPaymentPoints, + payload: {'driverID': box.read(BoxName.driverID).toString()}, + ); + if (pointsRes != null && pointsRes != 'failure') { + var data = jsonDecode(pointsRes); + totalPoints = double.tryParse( + data['message']?[0]?['total_amount']?.toString() ?? '0') + ?.abs() + .toInt() ?? + 0; + } + + // 4. جلب عدد الدعوات + var invRes = await CRUD().get( + link: AppLink.getInviteDriver, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (invRes != null && invRes != 'failure') { + var data = jsonDecode(invRes); + if (data['message'] is List) { + totalReferrals = (data['message'] as List).length; + } + } + + // 5. جلب أرباح اليوم + var todayRes = await CRUD().getWallet( + link: AppLink.getDriverPaymentToday, + payload: {'driverID': box.read(BoxName.driverID).toString()}, + ); + if (todayRes != null && todayRes != 'failure') { + var data = jsonDecode(todayRes); + dailyEarnings = double.tryParse( + data['message']?[0]?['todayAmount']?.toString() ?? '0') ?? + 0; + } + + // 6. جلب تقييم سلوك القيادة + var behaviorRes = await CRUD().get( + link: AppLink.getDriverBehavior, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (behaviorRes != null && behaviorRes != 'failure') { + var data = jsonDecode(behaviorRes); + if (data['message'] is List && data['message'].isNotEmpty) { + var behavior = data['message'][0]; + behaviorScore = double.tryParse(behavior['avg_score']?.toString() ?? '100') ?? 100.0; + hardBrakes = int.tryParse(behavior['total_hard_brakes']?.toString() ?? '0') ?? 0; + maxSpeed = double.tryParse(behavior['max_speed']?.toString() ?? '0') ?? 0.0; + } + } + + // 7. حساب الأيام المتتالية (محلياً) + _calculateConsecutiveDays(); + } catch (e) { + debugPrint('❌ [Gamification] Error fetching data: $e'); + } + + _calculateLevel(); + _updateAchievementProgress(); + isLoading = false; + update(); + } + + void _calculateConsecutiveDays() { + String? lastActiveDate = box.read('lastActiveDate'); + String today = + DateTime.now().toIso8601String().split('T')[0]; // 2026-05-08 + + if (lastActiveDate == null) { + consecutiveDays = 1; + } else if (lastActiveDate == today) { + // نفس اليوم — لا تغيير + } else { + DateTime last = DateTime.parse(lastActiveDate); + DateTime now = DateTime.parse(today); + if (now.difference(last).inDays == 1) { + consecutiveDays++; + } else { + consecutiveDays = 1; + } + } + + box.write('lastActiveDate', today); + box.write('gamification_consecutiveDays', consecutiveDays); + } + + // ═══════ إحصائيات سريعة ═══════ + int get unlockedCount => achievements.where((a) => a.isUnlocked).length; + int get totalAchievements => achievements.length; +} diff --git a/lib/controller/gamification/leaderboard_controller.dart b/lib/controller/gamification/leaderboard_controller.dart new file mode 100644 index 0000000..0c49d67 --- /dev/null +++ b/lib/controller/gamification/leaderboard_controller.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import '../../main.dart'; + +class LeaderboardEntry { + final String driverId; + final String name; + final String photoUrl; + final int rank; + final double value; // trips or earnings + final bool isCurrentUser; + + LeaderboardEntry( + {required this.driverId, + required this.name, + required this.photoUrl, + required this.rank, + required this.value, + this.isCurrentUser = false}); +} + +class LeaderboardController extends GetxController { + bool isLoading = false; + int selectedTab = 0; // 0=trips, 1=earnings + List tripLeaderboard = []; + List earningsLeaderboard = []; + int myRank = 0; + + @override + void onInit() { + super.onInit(); + fetchLeaderboard(); + } + + void changeTab(int tab) { + selectedTab = tab; + update(); + } + + List get currentLeaderboard => + selectedTab == 0 ? tripLeaderboard : earningsLeaderboard; + Future fetchLeaderboard() async { + isLoading = true; + update(); + + try { + final myId = box.read(BoxName.driverID)?.toString() ?? ''; + + // Fetch trips leaderboard + var resTrips = await CRUD().post( + link: AppLink.getLeaderboard, + payload: {'type': 'trips'}, + ); + if (resTrips != null && resTrips != 'failure') { + var data = jsonDecode(resTrips); + if (data['message'] is List) { + tripLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry( + driverId: e['driver_id'].toString(), + name: e['name'].toString(), + photoUrl: e['photoUrl']?.toString() ?? '', + rank: int.tryParse(e['rank']?.toString() ?? '0') ?? 0, + value: double.tryParse(e['value']?.toString() ?? '0') ?? 0, + isCurrentUser: e['driver_id'].toString() == myId, + )).toList(); + } + } + + // Fetch earnings leaderboard + var resEarnings = await CRUD().post( + link: AppLink.getLeaderboard, + payload: {'type': 'earnings'}, + ); + if (resEarnings != null && resEarnings != 'failure') { + var data = jsonDecode(resEarnings); + if (data['message'] is List) { + earningsLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry( + driverId: e['driver_id'].toString(), + name: e['name'].toString(), + photoUrl: e['photoUrl']?.toString() ?? '', + rank: int.tryParse(e['rank']?.toString() ?? '0') ?? 0, + value: double.tryParse(e['value']?.toString() ?? '0') ?? 0, + isCurrentUser: e['driver_id'].toString() == myId, + )).toList(); + } + } + + // Find my rank + final myTripEntry = tripLeaderboard.firstWhereOrNull((e) => e.isCurrentUser); + final myEarnEntry = earningsLeaderboard.firstWhereOrNull((e) => e.isCurrentUser); + myRank = selectedTab == 0 ? (myTripEntry?.rank ?? 0) : (myEarnEntry?.rank ?? 0); + } catch (e) { + debugPrint('❌ [Leaderboard] Error: $e'); + } + + isLoading = false; + update(); + } +} diff --git a/lib/controller/gamification/referral_controller.dart b/lib/controller/gamification/referral_controller.dart new file mode 100644 index 0000000..044cce3 --- /dev/null +++ b/lib/controller/gamification/referral_controller.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import '../../main.dart'; + +// ════════════════════════════════════════════ +// نموذج الإحالة +// ════════════════════════════════════════════ + +class ReferralRecord { + final String id; + final String name; + final String phone; + final String status; // 'registered', 'active', 'inactive' + final String type; // 'driver', 'passenger' + final String joinDate; + final int tripCount; + + ReferralRecord({ + required this.id, + required this.name, + required this.phone, + required this.status, + required this.type, + required this.joinDate, + required this.tripCount, + }); + + factory ReferralRecord.fromJson(Map json) { + return ReferralRecord( + id: json['id']?.toString() ?? '', + name: json['name']?.toString() ?? json['nameArabic']?.toString() ?? '', + phone: json['phone']?.toString() ?? '', + status: json['status']?.toString() ?? 'registered', + type: json['type']?.toString() ?? 'driver', + joinDate: json['created_at']?.toString() ?? '', + tripCount: int.tryParse(json['trip_count']?.toString() ?? '0') ?? 0, + ); + } +} + +// ════════════════════════════════════════════ +// Controller +// ════════════════════════════════════════════ + +class ReferralController extends GetxController { + bool isLoading = false; + List driverReferrals = []; + List passengerReferrals = []; + String referralCode = ''; + int totalDriverReferrals = 0; + int totalPassengerReferrals = 0; + int activeReferrals = 0; + double totalRewardsEarned = 0; + + @override + void onInit() { + super.onInit(); + _generateReferralCode(); + fetchReferralData(); + } + + void _generateReferralCode() { + final driverId = box.read(BoxName.driverID)?.toString() ?? ''; + final name = box.read(BoxName.nameDriver)?.toString() ?? ''; + if (driverId.isNotEmpty) { + // كود فريد: أول 3 حروف من الاسم + ID + final prefix = name.length >= 3 ? name.substring(0, 3).toUpperCase() : name.toUpperCase(); + referralCode = '$prefix$driverId'; + } + } + + Future fetchReferralData() async { + isLoading = true; + update(); + + try { + // 1. جلب دعوات السائقين + var driverRes = await CRUD().get( + link: AppLink.getInviteDriver, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (driverRes != null && driverRes != 'failure') { + var data = jsonDecode(driverRes); + if (data['message'] is List) { + driverReferrals = (data['message'] as List) + .map((e) => ReferralRecord.fromJson(e)) + .toList(); + totalDriverReferrals = driverReferrals.length; + } + } + + // 2. جلب دعوات الركاب + var passengerRes = await CRUD().get( + link: AppLink.getDriverInvitationToPassengers, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (passengerRes != null && passengerRes != 'failure') { + var data = jsonDecode(passengerRes); + if (data['message'] is List) { + passengerReferrals = (data['message'] as List) + .map((e) => ReferralRecord.fromJson(e)) + .toList(); + totalPassengerReferrals = passengerReferrals.length; + } + } + + // 3. جلب الإحصائيات الدقيقة للمكافآت + var statsRes = await CRUD().get( + link: AppLink.getReferralStats, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (statsRes != null && statsRes != 'failure') { + var data = jsonDecode(statsRes); + if (data['message'] is List && data['message'].isNotEmpty) { + var stats = data['message'][0]; + totalRewardsEarned = double.tryParse(stats['totalRewards']?.toString() ?? '0') ?? 0; + activeReferrals = (int.tryParse(stats['driverInvites']?.toString() ?? '0') ?? 0) + + (int.tryParse(stats['passengerInvites']?.toString() ?? '0') ?? 0); + } + } + } catch (e) { + debugPrint('❌ [Referral] Error: $e'); + } + + isLoading = false; + update(); + } + + void copyCode() { + Clipboard.setData(ClipboardData(text: referralCode)); + } + + String get shareMessage { + final appName = 'Intaleq'; + return 'Join $appName as a driver! Use my code: $referralCode\nDownload: https://intaleq.app/driver?ref=$referralCode'; + } + + String get shareMessagePassenger { + final appName = 'Intaleq'; + return 'Get a ride with $appName! Use my code: $referralCode for a discount.\nDownload: https://intaleq.app?ref=$referralCode'; + } + + int get totalReferrals => totalDriverReferrals + totalPassengerReferrals; + + // ═══════ إرسال دعوة سائق ═══════ + Future inviteDriver(String phone) async { + try { + var res = await CRUD().post( + link: AppLink.addInviteDriver, + payload: { + 'driver_id': box.read(BoxName.driverID).toString(), + 'phone': phone, + }, + ); + if (res != null && res != 'failure') { + await fetchReferralData(); + return true; + } + } catch (e) { + debugPrint('❌ [Referral] Invite driver error: $e'); + } + return false; + } + + // ═══════ إرسال دعوة راكب ═══════ + Future invitePassenger(String phone) async { + try { + var res = await CRUD().post( + link: AppLink.addInvitationPassenger, + payload: { + 'driver_id': box.read(BoxName.driverID).toString(), + 'phone': phone, + }, + ); + if (res != null && res != 'failure') { + await fetchReferralData(); + return true; + } + } catch (e) { + debugPrint('❌ [Referral] Invite passenger error: $e'); + } + return false; + } +} diff --git a/lib/controller/home/captin/home_captain_controller.dart b/lib/controller/home/captin/home_captain_controller.dart index c639f54..9d5b5c6 100755 --- a/lib/controller/home/captin/home_captain_controller.dart +++ b/lib/controller/home/captin/home_captain_controller.dart @@ -213,43 +213,109 @@ class HomeCaptainController extends GetxController { } String stringActiveDuration = ''; + + // ========================================== + // ====== 🛡️ Fatigue Monitoring System ====== + // ========================================== + void _checkFatigueBeforeOnline() { + int totalSecondsToday = box.read('fatigue_total_seconds') ?? 0; + String? lastOfflineStr = box.read('fatigue_last_offline'); + + if (lastOfflineStr != null) { + DateTime lastOffline = DateTime.parse(lastOfflineStr); + // If offline for more than 6 continuous hours, reset the fatigue counter + if (DateTime.now().difference(lastOffline).inHours >= 6) { + totalSecondsToday = 0; + box.write('fatigue_total_seconds', 0); + } + } + + if (totalSecondsToday >= 12 * 3600) { // 12 Hours + _forceOfflineDueToFatigue(); + throw Exception('Fatigue Limit Exceeded'); + } + } + + void _forceOfflineDueToFatigue() { + if (isActive) { + isActive = false; + locationController.stopLocationUpdates(); + activeStartTime = null; + activeTimer?.cancel(); + update(); + } + + Get.defaultDialog( + title: 'Safety First 🛑'.tr, + middleText: 'You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.'.tr, + barrierDismissible: false, + titleStyle: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + confirm: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Get.back(), + child: Text('OK'.tr, style: const TextStyle(color: Colors.white)), + ), + ); + } + void onButtonSelected() { -// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش if (!Get.isRegistered()) { Get.put(CaptainWalletController()); } totalPoints = Get.find().totalPoints; + + // Toggle Active State isActive = !isActive; + if (isActive) { - if (double.parse(totalPoints) > -200) { - locationController.startLocationUpdates(); - HapticFeedback.heavyImpact(); - // locationBackController.startBackLocation(); - activeStartTime = DateTime.now(); - activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - activeDuration = DateTime.now().difference(activeStartTime!); - stringActiveDuration = formatDuration(activeDuration); - update(); - }); - } else { - locationController.stopLocationUpdates(); + try { + _checkFatigueBeforeOnline(); // Throws exception if tired + + if (double.parse(totalPoints) > -200) { + locationController.startLocationUpdates(); + HapticFeedback.heavyImpact(); + activeStartTime = DateTime.now(); + + activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + activeDuration = DateTime.now().difference(activeStartTime!); + stringActiveDuration = formatDuration(activeDuration); + + // Increment Fatigue Counter + int totalSeconds = box.read('fatigue_total_seconds') ?? 0; + totalSeconds += 1; + box.write('fatigue_total_seconds', totalSeconds); - activeStartTime = null; - activeTimer?.cancel(); - savePeriod(activeDuration); - activeDuration = Duration.zero; + if (totalSeconds >= 12 * 3600) { // 12 hours + _forceOfflineDueToFatigue(); + } + + update(); + }); + } else { + locationController.stopLocationUpdates(); + activeStartTime = null; + activeTimer?.cancel(); + savePeriod(activeDuration); + activeDuration = Duration.zero; + box.write('fatigue_last_offline', DateTime.now().toIso8601String()); + update(); + } + } catch (e) { + // Driver is fatigued, revert state + isActive = false; update(); } } else { locationController.stopLocationUpdates(); - activeStartTime = null; activeTimer?.cancel(); savePeriod(activeDuration); activeDuration = Duration.zero; + + // Save offline time for Fatigue Monitoring reset + box.write('fatigue_last_offline', DateTime.now().toIso8601String()); update(); } - // } } // متغيرات العداد للحظر diff --git a/lib/controller/home/journal/schedule_controller.dart b/lib/controller/home/journal/schedule_controller.dart new file mode 100644 index 0000000..7a07e98 --- /dev/null +++ b/lib/controller/home/journal/schedule_controller.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/main.dart'; + +class WorkSlot { + int dayOfWeek; // 1=Mon ... 7=Sun + TimeOfDay startTime; + TimeOfDay endTime; + bool isActive; + + WorkSlot({required this.dayOfWeek, required this.startTime, required this.endTime, this.isActive = true}); + + Map toJson() => { + 'day': dayOfWeek, 'startH': startTime.hour, 'startM': startTime.minute, + 'endH': endTime.hour, 'endM': endTime.minute, 'active': isActive, + }; + + factory WorkSlot.fromJson(Map json) => WorkSlot( + dayOfWeek: json['day'] ?? 1, + startTime: TimeOfDay(hour: json['startH'] ?? 8, minute: json['startM'] ?? 0), + endTime: TimeOfDay(hour: json['endH'] ?? 17, minute: json['endM'] ?? 0), + isActive: json['active'] ?? true, + ); + + String get dayName { + const days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return days[dayOfWeek]; + } + + String get dayNameAr { + const days = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد']; + return days[dayOfWeek]; + } + + String formatTime(TimeOfDay t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; + String get timeRange => '${formatTime(startTime)} - ${formatTime(endTime)}'; +} + +class ScheduleController extends GetxController { + List schedule = []; + + @override + void onInit() { + super.onInit(); + _loadSchedule(); + } + + void _loadSchedule() { + final saved = box.read('work_schedule'); + if (saved != null) { + try { + final list = jsonDecode(saved) as List; + schedule = list.map((e) => WorkSlot.fromJson(e)).toList(); + } catch (_) { + _initDefault(); + } + } else { + _initDefault(); + } + update(); + } + + void _initDefault() { + schedule = List.generate(7, (i) => WorkSlot( + dayOfWeek: i + 1, + startTime: const TimeOfDay(hour: 8, minute: 0), + endTime: const TimeOfDay(hour: 18, minute: 0), + isActive: i < 6, // الجمعة عطلة + )); + } + + void _save() { + box.write('work_schedule', jsonEncode(schedule.map((s) => s.toJson()).toList())); + update(); + } + + void toggleDay(int dayOfWeek) { + final slot = schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek); + slot.isActive = !slot.isActive; + _save(); + } + + void updateStartTime(int dayOfWeek, TimeOfDay time) { + schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).startTime = time; + _save(); + } + + void updateEndTime(int dayOfWeek, TimeOfDay time) { + schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).endTime = time; + _save(); + } + + double get totalWeeklyHours { + double total = 0; + for (var s in schedule) { + if (!s.isActive) continue; + final startMin = s.startTime.hour * 60 + s.startTime.minute; + final endMin = s.endTime.hour * 60 + s.endTime.minute; + total += (endMin - startMin) / 60; + } + return total; + } + + int get activeDays => schedule.where((s) => s.isActive).length; +} diff --git a/lib/controller/home/statistics/statistics_controller.dart b/lib/controller/home/statistics/statistics_controller.dart new file mode 100644 index 0000000..3e63f5d --- /dev/null +++ b/lib/controller/home/statistics/statistics_controller.dart @@ -0,0 +1,193 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/box_name.dart'; +import 'package:sefer_driver/constant/links.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; +import 'package:sefer_driver/models/model/driver/rides_summary_model.dart'; +import '../../../main.dart'; + +class StatisticsController extends GetxController { + bool isLoading = false; + + // ═══ Weekly Data ═══ + List weeklyStats = []; + double weeklyEarnings = 0; + int weeklyTrips = 0; + double weeklyHours = 0; + + // ═══ Monthly Data ═══ + List monthlyEarnings = []; + List monthlyRides = []; + List monthlyDuration = []; + double monthlyTotalEarnings = 0; + int monthlyTotalTrips = 0; + double monthlyTotalHours = 0; + String bestDay = '--'; + double bestDayEarnings = 0; + + // ═══ Tab State ═══ + int selectedTab = 0; // 0=weekly, 1=monthly + + @override + void onInit() { + super.onInit(); + fetchWeeklyData(); + fetchMonthlyData(); + } + + void changeTab(int tab) { + selectedTab = tab; + update(); + } + + // ═══════ جلب البيانات الأسبوعية ═══════ + Future fetchWeeklyData() async { + isLoading = true; + update(); + + try { + var res = await CRUD().get( + link: AppLink.getWeeklyAggregate, + payload: { + 'driver_id': box.read(BoxName.driverID).toString(), + }, + ); + + if (res != null && res != 'failure') { + var data = jsonDecode(res); + if (data['message'] is List) { + weeklyStats = (data['message'] as List) + .map((e) => DayStat.fromJson(e)) + .toList(); + weeklyEarnings = weeklyStats.fold(0, (s, d) => s + d.earnings); + weeklyTrips = weeklyStats.fold(0, (s, d) => s + d.trips); + weeklyHours = weeklyStats.fold(0, (s, d) => s + d.hours); + } + } + } catch (e) { + debugPrint('❌ [Stats] Weekly fetch error: $e'); + // Fallback: generate from local data + _generateLocalWeeklyData(); + } + + isLoading = false; + update(); + } + + // ═══════ جلب البيانات الشهرية ═══════ + Future fetchMonthlyData() async { + try { + // 1. أرباح شهرية + var earningsRes = await CRUD().getWallet( + link: AppLink.getAllPaymentFromRide, + payload: {'driverID': box.read(BoxName.driverID).toString()}, + ); + if (earningsRes != null && earningsRes != 'failure') { + var data = jsonDecode(earningsRes); + if (data['message'] is List) { + monthlyEarnings = (data['message'] as List) + .map((e) => MonthlyPriceDriverModel.fromJson(e)) + .toList(); + monthlyTotalEarnings = monthlyEarnings.fold(0, (s, d) => s + d.pricePerDay); + + // أفضل يوم + if (monthlyEarnings.isNotEmpty) { + var best = monthlyEarnings.reduce((a, b) => + a.pricePerDay > b.pricePerDay ? a : b); + bestDay = best.day.toString(); + bestDayEarnings = best.pricePerDay; + } + } + } + + // 2. رحلات شهرية + var ridesRes = await CRUD().get( + link: AppLink.getRidesDriverByDay, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (ridesRes != null && ridesRes != 'failure') { + var data = jsonDecode(ridesRes); + if (data['message'] is List) { + monthlyRides = (data['message'] as List) + .map((e) => MonthlyRideModel.fromJson(e)) + .toList(); + monthlyTotalTrips = monthlyRides.fold(0, (s, d) => s + d.countRide); + } + } + + // 3. ساعات شهرية + var durationRes = await CRUD().get( + link: AppLink.getTotalDriverDuration, + payload: {'driver_id': box.read(BoxName.driverID).toString()}, + ); + if (durationRes != null && durationRes != 'failure') { + var data = jsonDecode(durationRes); + if (data['message'] is List) { + monthlyDuration = (data['message'] as List) + .map((e) => MonthlyDataModel.fromJson(e)) + .toList(); + monthlyTotalHours = monthlyDuration.fold(0, (s, d) => s + d.totalDuration.toDouble()); + } + } + } catch (e) { + debugPrint('❌ [Stats] Monthly fetch error: $e'); + } + + update(); + } + + void _generateLocalWeeklyData() { + // Fallback بيانات محلية عند عدم توفر الـ API + final now = DateTime.now(); + weeklyStats = List.generate(7, (i) { + final day = now.subtract(Duration(days: 6 - i)); + return DayStat( + date: day, + dayName: _getDayName(day.weekday), + earnings: 0, + trips: 0, + hours: 0, + ); + }); + } + + String _getDayName(int weekday) { + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return days[weekday - 1]; + } + + Future refresh() async { + await fetchWeeklyData(); + await fetchMonthlyData(); + } +} + +// ═══════ نموذج إحصائية اليوم ═══════ +class DayStat { + final DateTime date; + final String dayName; + final double earnings; + final int trips; + final double hours; + + DayStat({ + required this.date, + required this.dayName, + required this.earnings, + required this.trips, + required this.hours, + }); + + factory DayStat.fromJson(Map json) { + final date = DateTime.tryParse(json['day']?.toString() ?? '') ?? DateTime.now(); + const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return DayStat( + date: date, + dayName: dayNames[date.weekday - 1], + earnings: double.tryParse(json['earnings']?.toString() ?? '0') ?? 0, + trips: int.tryParse(json['trips']?.toString() ?? '0') ?? 0, + hours: double.tryParse(json['hours']?.toString() ?? '0') ?? 0, + ); + } +} diff --git a/lib/translations_ar.json b/lib/translations_ar.json index 6c5a788..9101d74 100644 --- a/lib/translations_ar.json +++ b/lib/translations_ar.json @@ -576,6 +576,16 @@ "ok": "", "I will slow down": "", "Ride Today : ": "", + "Share with Passenger": "مشاركة مع الراكب", + "Driving Behavior": "سلوك القيادة", + "Excellent": "ممتاز", + "Good": "جيد", + "Needs Improvement": "بحاجة للتحسين", + "Score": "التقييم", + "Max Speed": "أقصى سرعة", + "Hard Brakes": "الفرملة المفاجئة", + "Safety First 🛑": "الأمان أولاً 🛑", + "You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.": "لقد قمت بالقيادة لمدة 12 ساعة. من أجل سلامتك وسلامة الركاب، يرجى أخذ استراحة لمدة 6 ساعات متواصلة.", "app with passenger": "", "Please add contacts to your phone.": "", "I cant register in your app in face detection ": "", @@ -802,5 +812,89 @@ "Recharge Balance": "شحن الرصيد", "Recharge Balance Packages": "باقات شحن الرصيد", "Price:": "السعر:", - "Amount to charge:": "المبلغ المطلوب شحنه:" + "Amount to charge:": "المبلغ المطلوب شحنه:", + "Statistics": "الإحصائيات", + "Achievements": "الإنجازات", + "Total Trips": "إجمالي الرحلات", + "Rating": "التقييم", + "Day Streak": "أيام متتالية", + "Referrals": "الإحالات", + "Daily Goal": "الهدف اليومي", + "الهدف اليومي": "Daily Goal", + "Set Goal": "تحديد الهدف", + "Edit": "تعديل", + "Goal Achieved!": "تم تحقيق الهدف! 🎉", + "Remaining:": "المتبقي:", + "Set Daily Goal": "تحديد الهدف اليومي", + "How much do you want to earn today?": "كم تريد أن تربح اليوم؟", + "Driver Level": "مستوى السائق", + "مستوى السائق": "Driver Level", + "Next Level:": "المستوى التالي:", + "Maximum Level Reached!": "وصلت للمستوى الأعلى!", + "Today Overview": "نظرة عامة على اليوم", + "نظرة عامة على اليوم": "Today Overview", + "Earnings": "الأرباح", + "Rides": "الرحلات", + "Online Duration": "مدة الاتصال", + "Refused": "مرفوض", + "Basic features": "الميزات الأساسية", + "Standard support": "دعم عادي", + "Priority medium": "أولوية متوسطة", + "Silver badge": "شارة فضية", + "-1% commission": "-1% عمولة", + "High priority": "أولوية عالية", + "Gold badge": "شارة ذهبية", + "-2% commission": "-2% عمولة", + "VIP first": "أولوية VIP", + "Diamond badge": "شارة ألماسية", + "-5% commission": "-5% عمولة", + "Priority support": "دعم أولوية", + "Challenges": "التحديات", + "Daily Challenges": "تحديات يومية", + "Weekly Challenges": "تحديات أسبوعية", + "Claim Reward": "الحصول على المكافأة", + "Claimed": "تم الاستلام", + "Referral Center": "مركز الإحالة", + "Your Referral Code": "رمز الإحالة الخاص بك", + "Share this code to earn rewards": "شارك هذا الرمز لكسب مكافآت", + "Share via": "مشاركة عبر", + "Share": "مشاركة", + "WhatsApp": "واتساب", + "Invite Driver": "دعوة سائق", + "Invite Rider": "دعوة راكب", + "How It Works": "كيف يعمل؟", + "Share your code": "شارك رمزك", + "Send your referral code to friends": "أرسل رمز الإحالة لأصدقائك", + "Friend signs up": "صديقك يسجّل", + "They register using your code": "يسجّلون باستخدام رمزك", + "Both earn rewards": "كلاكما تربحان", + "You get 100 pts, they get 50 pts": "أنت تحصل على 100 نقطة، وهم 50 نقطة", + "Bonus at 10 trips": "مكافأة عند 10 رحلات", + "Extra 200 pts when they complete 10 trips": "200 نقطة إضافية عند إكمال 10 رحلات", + "Driver Invitations": "دعوات السائقين", + "Passenger Invitations": "دعوات الركاب", + "Active": "نشط", + "registered": "مسجّل", + "active": "نشط", + "inactive": "غير نشط", + "Start sharing your code!": "ابدأ بمشاركة رمزك!", + "Code copied!": "تم نسخ الرمز!", + "Leaderboard": "لوحة المتصدرين", + "Trips": "الرحلات", + "You": "أنت", + "My Schedule": "جدولي", + "Weekly Plan": "الخطة الأسبوعية", + "Work Days": "أيام العمل", + "Day Off": "إجازة", + "Weekly Earnings": "أرباح الأسبوع", + "Monthly Report": "التقرير الشهري", + "Best Day": "أفضل يوم", + "Hours": "ساعات", + "Mon": "إثن", + "Tue": "ثلاث", + "Wed": "أربع", + "Thu": "خميس", + "Fri": "جمعة", + "Sat": "سبت", + "Sun": "أحد" } \ No newline at end of file diff --git a/lib/translations_en.json b/lib/translations_en.json index ef679d5..c548b38 100644 --- a/lib/translations_en.json +++ b/lib/translations_en.json @@ -576,6 +576,16 @@ "ok": "", "I will slow down": "", "Ride Today : ": "", + "Share with Passenger": "Share with Passenger", + "Driving Behavior": "Driving Behavior", + "Excellent": "Excellent", + "Good": "Good", + "Needs Improvement": "Needs Improvement", + "Score": "Score", + "Max Speed": "Max Speed", + "Hard Brakes": "Hard Brakes", + "Safety First 🛑": "Safety First 🛑", + "You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.": "You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.", "app with passenger": "", "Please add contacts to your phone.": "", "I cant register in your app in face detection ": "", @@ -802,5 +812,89 @@ "Recharge Balance": "Recharge Balance", "Recharge Balance Packages": "Recharge Balance Packages", "Price:": "Price:", - "Amount to charge:": "Amount to charge:" + "Amount to charge:": "Amount to charge:", + "Statistics": "Statistics", + "Achievements": "Achievements", + "Total Trips": "Total Trips", + "Rating": "Rating", + "Day Streak": "Day Streak", + "Referrals": "Referrals", + "Daily Goal": "Daily Goal", + "الهدف اليومي": "Daily Goal", + "Set Goal": "Set Goal", + "Edit": "Edit", + "Goal Achieved!": "Goal Achieved! 🎉", + "Remaining:": "Remaining:", + "Set Daily Goal": "Set Daily Goal", + "How much do you want to earn today?": "How much do you want to earn today?", + "Driver Level": "Driver Level", + "مستوى السائق": "Driver Level", + "Next Level:": "Next Level:", + "Maximum Level Reached!": "Maximum Level Reached!", + "Today Overview": "Today Overview", + "نظرة عامة على اليوم": "Today Overview", + "Earnings": "Earnings", + "Rides": "Rides", + "Online Duration": "Online Duration", + "Refused": "Refused", + "Basic features": "Basic features", + "Standard support": "Standard support", + "Priority medium": "Priority medium", + "Silver badge": "Silver badge", + "-1% commission": "-1% commission", + "High priority": "High priority", + "Gold badge": "Gold badge", + "-2% commission": "-2% commission", + "VIP first": "VIP first", + "Diamond badge": "Diamond badge", + "-5% commission": "-5% commission", + "Priority support": "Priority support", + "Challenges": "Challenges", + "Daily Challenges": "Daily Challenges", + "Weekly Challenges": "Weekly Challenges", + "Claim Reward": "Claim Reward", + "Claimed": "Claimed", + "Referral Center": "Referral Center", + "Your Referral Code": "Your Referral Code", + "Share this code to earn rewards": "Share this code to earn rewards", + "Share via": "Share via", + "Share": "Share", + "WhatsApp": "WhatsApp", + "Invite Driver": "Invite Driver", + "Invite Rider": "Invite Rider", + "How It Works": "How It Works", + "Share your code": "Share your code", + "Send your referral code to friends": "Send your referral code to friends", + "Friend signs up": "Friend signs up", + "They register using your code": "They register using your code", + "Both earn rewards": "Both earn rewards", + "You get 100 pts, they get 50 pts": "You get 100 pts, they get 50 pts", + "Bonus at 10 trips": "Bonus at 10 trips", + "Extra 200 pts when they complete 10 trips": "Extra 200 pts when they complete 10 trips", + "Driver Invitations": "Driver Invitations", + "Passenger Invitations": "Passenger Invitations", + "Active": "Active", + "registered": "Registered", + "active": "Active", + "inactive": "Inactive", + "Start sharing your code!": "Start sharing your code!", + "Code copied!": "Code copied!", + "Leaderboard": "Leaderboard", + "Trips": "Trips", + "You": "You", + "My Schedule": "My Schedule", + "Weekly Plan": "Weekly Plan", + "Work Days": "Work Days", + "Day Off": "Day Off", + "Weekly Earnings": "Weekly Earnings", + "Monthly Report": "Monthly Report", + "Best Day": "Best Day", + "Hours": "Hours", + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Sun": "Sun" } \ No newline at end of file diff --git a/lib/views/gamification/challenges_page.dart b/lib/views/gamification/challenges_page.dart new file mode 100644 index 0000000..64e1dfb --- /dev/null +++ b/lib/views/gamification/challenges_page.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/finance_design_system.dart'; +import '../../../controller/gamification/challenges_controller.dart'; + +class ChallengesPage extends StatelessWidget { + ChallengesPage({super.key}); + final ChallengesController controller = Get.put(ChallengesController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, + body: GetBuilder(builder: (cc) { + if (cc.isLoading) + return const Center( + child: CircularProgressIndicator( + color: FinanceDesignSystem.primaryDark)); + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + expandedHeight: 160, + pinned: true, + backgroundColor: const Color(0xFF1A237E), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, + color: Colors.white, size: 20), + onPressed: () => Get.back()), + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded, + color: Colors.white), + onPressed: () => cc.fetchChallengeProgress()) + ], + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text('Challenges'.tr, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18)), + background: Stack(fit: StackFit.expand, children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient(colors: [ + Color(0xFF0A0E21), + Color(0xFF1A237E) + ]))), + Positioned( + right: -30, + top: -10, + child: Icon(Icons.bolt_rounded, + size: 160, color: Colors.white.withOpacity(0.04))), + ]), + ), + ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // Daily Challenges + _sectionHeader( + 'Daily Challenges'.tr, + '🔥', + cc.dailyChallenges.where((c) => c.isCompleted).length, + cc.dailyChallenges.length), + const SizedBox(height: 12), + ...cc.dailyChallenges.map((c) => _challengeCard(c, cc)), + const SizedBox(height: 28), + // Weekly Challenges + _sectionHeader( + 'Weekly Challenges'.tr, + '🏆', + cc.weeklyChallenges.where((c) => c.isCompleted).length, + cc.weeklyChallenges.length), + const SizedBox(height: 12), + ...cc.weeklyChallenges.map((c) => _challengeCard(c, cc)), + ]))), + ]); + }), + ); + } + + Widget _sectionHeader(String title, String emoji, int done, int total) { + return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row(children: [ + Text(emoji, style: const TextStyle(fontSize: 20)), + const SizedBox(width: 8), + Text(title, style: FinanceDesignSystem.headingStyle), + ]), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: FinanceDesignSystem.accentBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(20)), + child: Text('$done/$total', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: FinanceDesignSystem.accentBlue)), + ), + ]); + } + + Widget _challengeCard(Challenge c, ChallengesController cc) { + final isAr = Get.locale?.languageCode == 'ar'; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + border: c.isCompleted + ? Border.all(color: c.color.withOpacity(0.3), width: 1.5) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4)) + ], + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: c.color.withOpacity(0.12), + borderRadius: BorderRadius.circular(12)), + child: Icon(c.icon, color: c.color, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(isAr ? c.titleAr : c.titleEn, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: FinanceDesignSystem.primaryDark)), + Text(isAr ? c.descriptionAr : c.descriptionEn, + style: + TextStyle(fontSize: 11, color: Colors.grey.shade500)), + ])), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFFFD700).withOpacity(0.15), + borderRadius: BorderRadius.circular(12)), + child: Text('+${c.reward}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Color(0xFFFF8F00))), + ), + ]), + const SizedBox(height: 14), + // Progress bar + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('${c.currentProgress}/${c.target}', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.grey.shade600)), + Text('${(c.progress * 100).toStringAsFixed(0)}%', + style: TextStyle( + fontSize: 11, fontWeight: FontWeight.bold, color: c.color)), + ]), + const SizedBox(height: 6), + Stack(children: [ + Container( + height: 8, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4))), + LayoutBuilder( + builder: (ctx, cons) => AnimatedContainer( + duration: const Duration(milliseconds: 600), + curve: Curves.easeOutCubic, + height: 8, + width: cons.maxWidth * c.progress, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [c.color, c.color.withOpacity(0.6)]), + borderRadius: BorderRadius.circular(4), + ), + )), + ]), + // Claim button + if (c.isCompleted && !c.isClaimed) ...[ + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => cc.claimReward(c), + icon: const Icon(Icons.card_giftcard_rounded, size: 18), + label: Text('Claim Reward'.tr), + style: ElevatedButton.styleFrom( + backgroundColor: c.color, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + )), + ], + if (c.isClaimed) ...[ + const SizedBox(height: 8), + Center( + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.check_circle_rounded, + color: FinanceDesignSystem.successGreen, size: 16), + const SizedBox(width: 4), + Text('Claimed'.tr, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: FinanceDesignSystem.successGreen)), + ])), + ], + ]), + ); + } +} diff --git a/lib/views/gamification/leaderboard_page.dart b/lib/views/gamification/leaderboard_page.dart new file mode 100644 index 0000000..cc65dd3 --- /dev/null +++ b/lib/views/gamification/leaderboard_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/finance_design_system.dart'; +import '../../../controller/gamification/leaderboard_controller.dart'; + +class LeaderboardPage extends StatelessWidget { + LeaderboardPage({super.key}); + final LeaderboardController controller = Get.put(LeaderboardController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, + body: GetBuilder(builder: (lc) { + if (lc.isLoading) return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.primaryDark)); + return CustomScrollView(physics: const BouncingScrollPhysics(), slivers: [ + SliverAppBar( + expandedHeight: 200, pinned: true, + backgroundColor: const Color(0xFF0A0E21), + leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 20), onPressed: () => Get.back()), + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text('Leaderboard'.tr, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)), + background: Stack(fit: StackFit.expand, children: [ + Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF0A0E21), Color(0xFFB71C1C)]))), + Positioned(right: -40, top: -20, child: Icon(Icons.leaderboard_rounded, size: 180, color: Colors.white.withOpacity(0.04))), + // Top 3 podium + if (lc.currentLeaderboard.length >= 3) Positioned(bottom: 50, left: 0, right: 0, child: _buildPodium(lc)), + ]), + ), + ), + // Tab bar + SliverToBoxAdapter(child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(14)), + child: Row(children: [ + _tab('Trips'.tr, Icons.local_taxi_rounded, 0, lc), + _tab('Earnings'.tr, Icons.monetization_on_rounded, 1, lc), + ]), + )), + // List + SliverPadding(padding: const EdgeInsets.fromLTRB(16, 16, 16, 40), sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (ctx, i) { + if (i >= lc.currentLeaderboard.length) return null; + return _buildRow(lc.currentLeaderboard[i], lc.selectedTab == 1); + }, + childCount: lc.currentLeaderboard.length, + ), + )), + ]); + }), + ); + } + + Widget _buildPodium(LeaderboardController lc) { + final top3 = lc.currentLeaderboard.take(3).toList(); + return Row(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ + if (top3.length > 1) _podiumItem(top3[1], 2, 50, const Color(0xFFC0C0C0)), + if (top3.isNotEmpty) _podiumItem(top3[0], 1, 70, const Color(0xFFFFD700)), + if (top3.length > 2) _podiumItem(top3[2], 3, 35, const Color(0xFFCD7F32)), + ]); + } + + Widget _podiumItem(LeaderboardEntry e, int rank, double height, Color color) { + return Padding(padding: const EdgeInsets.symmetric(horizontal: 8), child: Column(mainAxisSize: MainAxisSize.min, children: [ + CircleAvatar(radius: rank == 1 ? 22 : 18, backgroundColor: color.withOpacity(0.3), + child: Text(_getRankEmoji(rank), style: TextStyle(fontSize: rank == 1 ? 22 : 16))), + const SizedBox(height: 4), + Text(e.name.length > 8 ? '${e.name.substring(0, 8)}...' : e.name, + style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: e.isCurrentUser ? FontWeight.bold : FontWeight.normal)), + ])); + } + + Widget _tab(String label, IconData icon, int idx, LeaderboardController lc) { + final selected = lc.selectedTab == idx; + return Expanded(child: GestureDetector( + onTap: () => lc.changeTab(idx), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration(color: selected ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(10), + boxShadow: selected ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] : null), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(icon, size: 16, color: selected ? FinanceDesignSystem.primaryDark : Colors.grey), + const SizedBox(width: 6), + Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: selected ? FinanceDesignSystem.primaryDark : Colors.grey)), + ]), + ), + )); + } + + Widget _buildRow(LeaderboardEntry e, bool isEarnings) { + final rankColor = e.rank == 1 ? const Color(0xFFFFD700) : e.rank == 2 ? const Color(0xFFC0C0C0) : e.rank == 3 ? const Color(0xFFCD7F32) : Colors.grey.shade400; + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: e.isCurrentUser ? FinanceDesignSystem.accentBlue.withOpacity(0.08) : Colors.white, + borderRadius: BorderRadius.circular(14), + border: e.isCurrentUser ? Border.all(color: FinanceDesignSystem.accentBlue.withOpacity(0.3), width: 1.5) : null, + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 3))], + ), + child: Row(children: [ + // Rank + Container(width: 32, height: 32, decoration: BoxDecoration(color: rankColor.withOpacity(0.15), borderRadius: BorderRadius.circular(8)), + child: Center(child: e.rank <= 3 + ? Text(_getRankEmoji(e.rank), style: const TextStyle(fontSize: 16)) + : Text('${e.rank}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: rankColor)))), + const SizedBox(width: 12), + // Avatar + CircleAvatar(radius: 18, backgroundColor: Colors.grey.shade200, child: Text(e.name.isNotEmpty ? e.name[0] : '?', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey.shade600))), + const SizedBox(width: 12), + // Name + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Text(e.name, style: TextStyle(fontSize: 13, fontWeight: e.isCurrentUser ? FontWeight.bold : FontWeight.w500, color: FinanceDesignSystem.primaryDark)), + if (e.isCurrentUser) ...[const SizedBox(width: 6), + Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: FinanceDesignSystem.accentBlue, borderRadius: BorderRadius.circular(6)), + child: Text('You'.tr, style: const TextStyle(fontSize: 8, fontWeight: FontWeight.bold, color: Colors.white)))], + ]), + ])), + // Value + Text(isEarnings ? '${e.value.toStringAsFixed(0)} ${'SYP'.tr}' : '${e.value.toInt()} ${'Rides'.tr}', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w800, color: FinanceDesignSystem.primaryDark)), + ]), + ); + } + + String _getRankEmoji(int rank) => rank == 1 ? '🥇' : rank == 2 ? '🥈' : '🥉'; +} diff --git a/lib/views/gamification/referral_center_page.dart b/lib/views/gamification/referral_center_page.dart new file mode 100644 index 0000000..5319373 --- /dev/null +++ b/lib/views/gamification/referral_center_page.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../constant/finance_design_system.dart'; +import '../../../controller/gamification/referral_controller.dart'; +import 'package:sefer_driver/views/widgets/error_snakbar.dart'; + +class ReferralCenterPage extends StatelessWidget { + ReferralCenterPage({super.key}); + final ReferralController controller = Get.put(ReferralController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, + body: GetBuilder(builder: (rc) { + if (rc.isLoading) return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.primaryDark)); + return CustomScrollView(physics: const BouncingScrollPhysics(), slivers: [ + // ═══ Header ═══ + SliverAppBar( + expandedHeight: 220, pinned: true, + backgroundColor: const Color(0xFF0A0E21), + leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 20), onPressed: () => Get.back()), + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text('Referral Center'.tr, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)), + background: Stack(fit: StackFit.expand, children: [ + Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF0A0E21), Color(0xFF311B92)]))), + Positioned(right: -50, bottom: -30, child: Icon(Icons.share_rounded, size: 200, color: Colors.white.withOpacity(0.03))), + // Stats in header + Positioned(bottom: 60, left: 24, right: 24, child: Row(children: [ + _headerStat('${rc.totalReferrals}', 'Total Invites'.tr), + _headerStat('${rc.activeReferrals}', 'Active'.tr), + _headerStat('${rc.totalRewardsEarned.toStringAsFixed(0)}', 'Rewards'.tr), + ])), + ]), + ), + ), + SliverPadding(padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), sliver: SliverList(delegate: SliverChildListDelegate([ + // ═══ Referral Code Card ═══ + _buildCodeCard(rc), + const SizedBox(height: 20), + // ═══ Share Buttons ═══ + _buildShareSection(rc), + const SizedBox(height: 24), + // ═══ How it Works ═══ + _buildHowItWorks(), + const SizedBox(height: 24), + // ═══ Driver Referrals ═══ + if (rc.driverReferrals.isNotEmpty) ...[ + Text('Driver Invitations'.tr, style: FinanceDesignSystem.headingStyle), + const SizedBox(height: 12), + ...rc.driverReferrals.map((r) => _referralItem(r, const Color(0xFF2196F3))), + const SizedBox(height: 20), + ], + // ═══ Passenger Referrals ═══ + if (rc.passengerReferrals.isNotEmpty) ...[ + Text('Passenger Invitations'.tr, style: FinanceDesignSystem.headingStyle), + const SizedBox(height: 12), + ...rc.passengerReferrals.map((r) => _referralItem(r, const Color(0xFF4CAF50))), + ], + if (rc.driverReferrals.isEmpty && rc.passengerReferrals.isEmpty) + _buildEmptyState(), + ]))), + ]); + }), + ); + } + + Widget _headerStat(String value, String label) { + return Expanded(child: Column(children: [ + Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: Colors.white)), + Text(label, style: TextStyle(fontSize: 10, color: Colors.white.withOpacity(0.6))), + ])); + } + + Widget _buildCodeCard(ReferralController rc) { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [Color(0xFF1A237E), Color(0xFF311B92)]), + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [BoxShadow(color: const Color(0xFF311B92).withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 8))], + ), + child: Column(children: [ + Text('Your Referral Code'.tr, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14)), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.2))), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(rc.referralCode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3)), + const SizedBox(width: 12), + GestureDetector( + onTap: () { rc.copyCode(); mySnackbarSuccess('Code copied!'.tr); }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8)), + child: const Icon(Icons.copy_rounded, color: Colors.white, size: 18), + ), + ), + ]), + ), + const SizedBox(height: 12), + Text('Share this code to earn rewards'.tr, style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12)), + ]), + ); + } + + Widget _buildShareSection(ReferralController rc) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Share via'.tr, style: FinanceDesignSystem.headingStyle), + const SizedBox(height: 12), + Row(children: [ + _shareButton(Icons.share_rounded, 'Share'.tr, FinanceDesignSystem.accentBlue, () => Share.share(rc.shareMessage)), + const SizedBox(width: 12), + _shareButton(Icons.chat_rounded, 'WhatsApp'.tr, const Color(0xFF25D366), () => _shareWhatsApp(rc.shareMessage)), + const SizedBox(width: 12), + _shareButton(Icons.person_add_rounded, 'Invite Driver'.tr, const Color(0xFFFF9800), () => _showInviteDialog(rc, 'driver')), + const SizedBox(width: 12), + _shareButton(Icons.hail_rounded, 'Invite Rider'.tr, const Color(0xFF9C27B0), () => _showInviteDialog(rc, 'passenger')), + ]), + ]); + } + + Widget _shareButton(IconData icon, String label, Color color, VoidCallback onTap) { + return Expanded(child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(14)), + child: Column(children: [ + Icon(icon, color: color, size: 24), + const SizedBox(height: 6), + Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis), + ]), + ), + )); + } + + Widget _buildHowItWorks() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))]), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('How It Works'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + const SizedBox(height: 16), + _step('1', 'Share your code'.tr, 'Send your referral code to friends'.tr, const Color(0xFF2196F3)), + _step('2', 'Friend signs up'.tr, 'They register using your code'.tr, const Color(0xFFFF9800)), + _step('3', 'Both earn rewards'.tr, 'You get 100 pts, they get 50 pts'.tr, const Color(0xFF4CAF50)), + _step('4', 'Bonus at 10 trips'.tr, 'Extra 200 pts when they complete 10 trips'.tr, const Color(0xFF9C27B0)), + ]), + ); + } + + Widget _step(String num, String title, String desc, Color color) { + return Padding(padding: const EdgeInsets.only(bottom: 12), child: Row(children: [ + Container(width: 32, height: 32, decoration: BoxDecoration(color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(8)), + child: Center(child: Text(num, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: color))), + ), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: FinanceDesignSystem.primaryDark)), + Text(desc, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), + ])), + ])); + } + + Widget _referralItem(ReferralRecord r, Color color) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 3))]), + child: Row(children: [ + Container(width: 40, height: 40, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), + child: Icon(r.type == 'driver' ? Icons.local_taxi_rounded : Icons.person_rounded, color: color, size: 20)), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(r.name.isNotEmpty ? r.name : r.phone, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + Text(r.joinDate, style: TextStyle(fontSize: 10, color: Colors.grey.shade500)), + ])), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: r.status == 'active' ? FinanceDesignSystem.successGreen.withOpacity(0.1) + : r.status == 'registered' ? FinanceDesignSystem.accentBlue.withOpacity(0.1) : Colors.grey.shade100, + borderRadius: BorderRadius.circular(8)), + child: Text(r.status.tr, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, + color: r.status == 'active' ? FinanceDesignSystem.successGreen + : r.status == 'registered' ? FinanceDesignSystem.accentBlue : Colors.grey)), + ), + ]), + ); + } + + Widget _buildEmptyState() { + return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Column(children: [ + Icon(Icons.people_outline_rounded, size: 60, color: Colors.grey.shade300), + const SizedBox(height: 12), + Text('No invitation found yet!'.tr, style: TextStyle(fontSize: 14, color: Colors.grey.shade400)), + const SizedBox(height: 4), + Text('Start sharing your code!'.tr, style: TextStyle(fontSize: 12, color: Colors.grey.shade300)), + ]))); + } + + void _shareWhatsApp(String message) async { + final url = 'https://wa.me/?text=${Uri.encodeComponent(message)}'; + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } + } + + void _showInviteDialog(ReferralController rc, String type) { + final phoneController = TextEditingController(); + Get.defaultDialog( + title: type == 'driver' ? 'Invite Driver'.tr : 'Invite Rider'.tr, + titleStyle: FinanceDesignSystem.headingStyle, + content: Column(children: [ + Text('Enter phone number'.tr, style: FinanceDesignSystem.subHeadingStyle), + const SizedBox(height: 12), + TextField( + controller: phoneController, keyboardType: TextInputType.phone, + decoration: InputDecoration(hintText: '09XX XXX XXX', prefixIcon: const Icon(Icons.phone_rounded), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12))), + ), + ]), + confirm: ElevatedButton( + onPressed: () async { + if (phoneController.text.length >= 10) { + Get.back(); + bool success = type == 'driver' + ? await rc.inviteDriver(phoneController.text) + : await rc.invitePassenger(phoneController.text); + if (success) mySnackbarSuccess('Invite sent successfully'.tr); + } + }, + style: ElevatedButton.styleFrom(backgroundColor: FinanceDesignSystem.accentBlue, foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12)), + child: Text('Send Invite'.tr), + ), + cancel: TextButton(onPressed: () => Get.back(), child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))), + ); + } +} diff --git a/lib/views/home/Captin/home_captain/drawer_captain.dart b/lib/views/home/Captin/home_captain/drawer_captain.dart index e26b942..abe1ab2 100755 --- a/lib/views/home/Captin/home_captain/drawer_captain.dart +++ b/lib/views/home/Captin/home_captain/drawer_captain.dart @@ -14,6 +14,11 @@ import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/views/Rate/rate_app_page.dart'; import 'package:sefer_driver/views/auth/captin/contact_us_page.dart'; import 'package:sefer_driver/views/auth/captin/invite_driver_screen.dart'; +import 'package:sefer_driver/views/home/statistics/statistics_dashboard.dart'; +import 'package:sefer_driver/views/gamification/challenges_page.dart'; +import 'package:sefer_driver/views/gamification/leaderboard_page.dart'; +import 'package:sefer_driver/views/gamification/referral_center_page.dart'; +import 'package:sefer_driver/views/home/journal/schedule_page.dart'; import 'package:sefer_driver/views/notification/available_rides_page.dart'; import 'package:sefer_driver/views/auth/captin/logout_captain.dart'; import 'package:sefer_driver/views/home/Captin/history/history_captain.dart'; @@ -55,6 +60,26 @@ class AppDrawer extends StatelessWidget { icon: Icons.account_balance_wallet, color: Colors.green, onTap: () => Get.to(() => WalletCaptainRefactored())), + DrawerItem( + title: 'Statistics'.tr, + icon: Icons.bar_chart_rounded, + color: Colors.deepPurple, + onTap: () => Get.to(() => StatisticsDashboard())), + DrawerItem( + title: 'Challenges'.tr, + icon: Icons.bolt_rounded, + color: Colors.amber, + onTap: () => Get.to(() => ChallengesPage())), + DrawerItem( + title: 'My Schedule'.tr, + icon: Icons.calendar_today_rounded, + color: Colors.teal, + onTap: () => Get.to(() => SchedulePage())), + DrawerItem( + title: 'Leaderboard'.tr, + icon: Icons.leaderboard_rounded, + color: Colors.red, + onTap: () => Get.to(() => LeaderboardPage())), DrawerItem( title: 'Profile'.tr, icon: Icons.person, @@ -81,10 +106,10 @@ class AppDrawer extends StatelessWidget { color: Colors.cyan, onTap: () => Get.to(() => HelpCaptain())), DrawerItem( - title: 'Share App'.tr, - icon: Icons.share, + title: 'Referral Center'.tr, + icon: Icons.card_giftcard_rounded, color: Colors.indigo, - onTap: () => Get.to(() => InviteScreen())), + onTap: () => Get.to(() => ReferralCenterPage())), // DrawerItem( // title: 'Maintenance Center'.tr, // icon: Icons.build, diff --git a/lib/views/home/Captin/home_captain/home_captin.dart b/lib/views/home/Captin/home_captain/home_captin.dart index db98e9f..d326fb8 100755 --- a/lib/views/home/Captin/home_captain/home_captin.dart +++ b/lib/views/home/Captin/home_captain/home_captin.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:bubble_head/bubble.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; -import 'package:sefer_driver/constant/api_key.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -13,8 +12,6 @@ import 'package:sefer_driver/views/widgets/mycircular.dart'; import '../../../../constant/box_name.dart'; import '../../../../constant/colors.dart'; import '../../../../constant/info.dart'; -import '../../../../constant/style.dart'; -import '../../../../controller/functions/location_background_controller.dart'; import '../../../../controller/functions/location_controller.dart'; import '../../../../controller/functions/overlay_permisssion.dart'; import '../../../../controller/functions/package_info.dart'; @@ -84,9 +81,13 @@ class HomeCaptain extends StatelessWidget { Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) async { await closeOverlayIfFound(); + if (!context.mounted) return; await checkForUpdate(context); + if (!context.mounted) return; await getPermissionOverlay(); + if (!context.mounted) return; await showDriverGiftClaim(context); + if (!context.mounted) return; await checkForAppliedRide(context); }); diff --git a/lib/views/home/journal/schedule_page.dart b/lib/views/home/journal/schedule_page.dart new file mode 100644 index 0000000..d8f9969 --- /dev/null +++ b/lib/views/home/journal/schedule_page.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../constant/finance_design_system.dart'; +import '../../../controller/home/journal/schedule_controller.dart'; + +class SchedulePage extends StatelessWidget { + SchedulePage({super.key}); + final ScheduleController controller = Get.put(ScheduleController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, + appBar: AppBar( + title: Text('My Schedule'.tr, style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + backgroundColor: Colors.transparent, elevation: 0, centerTitle: true, + leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20), onPressed: () => Get.back()), + ), + body: GetBuilder(builder: (sc) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Summary Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: FinanceDesignSystem.balanceGradient, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + ), + child: Row(children: [ + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Weekly Plan'.tr, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14)), + const SizedBox(height: 8), + Text('${sc.totalWeeklyHours.toStringAsFixed(1)}h', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w900, color: Colors.white)), + Text('${sc.activeDays} ${'Days'.tr}', style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 13)), + ])), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(14)), + child: const Icon(Icons.calendar_today_rounded, color: Colors.white, size: 28), + ), + ]), + ), + const SizedBox(height: 24), + + Text('Work Days'.tr, style: FinanceDesignSystem.headingStyle), + const SizedBox(height: 12), + + ...sc.schedule.map((slot) => _buildDayCard(context, slot, sc)), + ]), + ); + }), + ); + } + + Widget _buildDayCard(BuildContext context, WorkSlot slot, ScheduleController sc) { + final isAr = Get.locale?.languageCode == 'ar'; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: slot.isActive ? Colors.white : Colors.grey.shade50, + borderRadius: BorderRadius.circular(14), + boxShadow: slot.isActive ? [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 8, offset: const Offset(0, 3))] : null, + ), + child: Row(children: [ + // Toggle + GestureDetector( + onTap: () => sc.toggleDay(slot.dayOfWeek), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 44, height: 44, + decoration: BoxDecoration( + color: slot.isActive ? FinanceDesignSystem.accentBlue.withOpacity(0.1) : Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Center(child: Text( + isAr ? slot.dayNameAr.substring(0, 2) : slot.dayName, + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, + color: slot.isActive ? FinanceDesignSystem.accentBlue : Colors.grey.shade400), + )), + ), + ), + const SizedBox(width: 14), + // Day name + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(isAr ? slot.dayNameAr : slot.dayName.tr, style: TextStyle( + fontSize: 14, fontWeight: FontWeight.w600, + color: slot.isActive ? FinanceDesignSystem.primaryDark : Colors.grey.shade400)), + if (slot.isActive) + Text(slot.timeRange, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)), + if (!slot.isActive) + Text('Day Off'.tr, style: TextStyle(fontSize: 12, color: Colors.grey.shade400, fontStyle: FontStyle.italic)), + ])), + // Time pickers + if (slot.isActive) ...[ + _timePicker(context, slot.startTime, (t) => sc.updateStartTime(slot.dayOfWeek, t)), + Padding(padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('-', style: TextStyle(color: Colors.grey.shade400))), + _timePicker(context, slot.endTime, (t) => sc.updateEndTime(slot.dayOfWeek, t)), + ], + // Toggle switch + Switch( + value: slot.isActive, + onChanged: (_) => sc.toggleDay(slot.dayOfWeek), + activeColor: FinanceDesignSystem.accentBlue, + ), + ]), + ); + } + + Widget _timePicker(BuildContext context, TimeOfDay time, Function(TimeOfDay) onChanged) { + return GestureDetector( + onTap: () async { + final picked = await showTimePicker(context: context, initialTime: time); + if (picked != null) onChanged(picked); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), + child: Text('${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: FinanceDesignSystem.primaryDark)), + ), + ); + } +} diff --git a/lib/views/home/profile/behavior_page.dart b/lib/views/home/profile/behavior_page.dart index 08645e3..8d93dad 100644 --- a/lib/views/home/profile/behavior_page.dart +++ b/lib/views/home/profile/behavior_page.dart @@ -1,96 +1,176 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../constant/colors.dart'; +import '../../../constant/finance_design_system.dart'; import '../../../controller/home/captin/behavior_controller.dart'; class BehaviorPage extends StatelessWidget { - const BehaviorPage({ - super.key, - }); + const BehaviorPage({super.key}); @override Widget build(BuildContext context) { final controller = Get.put(DriverBehaviorController()); controller.fetchDriverBehavior(); - final theme = Theme.of(context); return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, appBar: AppBar( - title: Text('Driver Behavior'.tr), + title: Text('Driver Behavior'.tr, style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), centerTitle: true, + backgroundColor: FinanceDesignSystem.cardColor, + elevation: 0, + iconTheme: const IconThemeData(color: FinanceDesignSystem.primaryDark), ), body: Obx(() { if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.accentBlue)); } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text("Overall Behavior Score".tr, - style: theme.textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 10), - Text( - "${controller.overallScore.value.toStringAsFixed(1)} / 100", + double score = controller.overallScore.value; + bool isExcellent = score >= 90; + bool isGood = score >= 75 && score < 90; + Color statusColor = isExcellent ? Colors.green : (isGood ? Colors.orange : Colors.red); + String statusText = isExcellent ? 'Excellent'.tr : (isGood ? 'Good'.tr : 'Needs Improvement'.tr); + + return CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(FinanceDesignSystem.horizontalPadding), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // Overall Score Card + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: FinanceDesignSystem.cardColor, + borderRadius: BorderRadius.circular(24), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))], + ), + child: Column( + children: [ + Icon(Icons.shield_rounded, size: 48, color: statusColor), + const SizedBox(height: 16), + Text( + "Overall Behavior Score".tr, + style: FinanceDesignSystem.headingStyle, + ), + const SizedBox(height: 8), + Text( + "${score.toStringAsFixed(1)} / 100", style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: AppColor.primaryColor)), - ], + fontSize: 36, + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + statusText, + style: TextStyle(color: statusColor, fontWeight: FontWeight.bold), + ), + ), + ], + ), ), - ), + const SizedBox(height: 30), + Text("Last 10 Trips".tr, style: FinanceDesignSystem.headingStyle), + const SizedBox(height: 16), + ]), ), - const SizedBox(height: 20), - Text("Last 10 Trips".tr, - style: theme.textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 10), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: controller.lastTrips.length, - itemBuilder: (context, index) { - var trip = controller.lastTrips[index]; - return Card( - elevation: 3, - child: ListTile( - leading: CircleAvatar( - backgroundColor: AppColor.primaryColor, - child: Text("${index + 1}", - style: const TextStyle(color: Colors.white)), - ), - title: Text("Trip ID: ${trip['trip_id']}"), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${'Behavior Score'.tr}: ${trip['behavior_score']}"), - Text("${'Max Speed'.tr}: ${trip['max_speed']} km/h"), - Text("${'Hard Brake'.tr}s: ${trip['hard_brakes']}"), - Text( - "${'Distance'.tr}: ${trip['total_distance']} km"), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: FinanceDesignSystem.horizontalPadding), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + var trip = controller.lastTrips[index]; + double tripScore = double.tryParse(trip['behavior_score'].toString()) ?? 0; + Color tColor = tripScore >= 90 ? Colors.green : (tripScore >= 75 ? Colors.orange : Colors.red); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: FinanceDesignSystem.cardColor, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ) ], ), - ), - ); - }, + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: tColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon(Icons.drive_eta_rounded, color: tColor), + ), + title: Text( + "Trip ID: ${trip['trip_id']}", + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Wrap( + spacing: 12, + runSpacing: 8, + children: [ + _buildBadge(Icons.speed, "${trip['max_speed']} km/h", Colors.blue), + _buildBadge(Icons.warning_amber_rounded, "${trip['hard_brakes']} ${'Hard Brakes'.tr}", Colors.orange), + _buildBadge(Icons.map_rounded, "${trip['total_distance']} km", Colors.purple), + ], + ), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${tripScore.toStringAsFixed(0)}", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: tColor), + ), + Text('Score'.tr, style: const TextStyle(fontSize: 10, color: Colors.grey)), + ], + ), + ), + ); + }, + childCount: controller.lastTrips.length, + ), ), - ], - ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 40)), + ], ); }), ); } + + Widget _buildBadge(IconData icon, String label, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w600)), + ], + ), + ); + } } diff --git a/lib/views/home/statistics/statistics_dashboard.dart b/lib/views/home/statistics/statistics_dashboard.dart new file mode 100644 index 0000000..608042f --- /dev/null +++ b/lib/views/home/statistics/statistics_dashboard.dart @@ -0,0 +1,498 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/constant/finance_design_system.dart'; +import 'package:sefer_driver/controller/gamification/gamification_controller.dart'; +import 'package:sefer_driver/controller/home/statistics/statistics_controller.dart'; + +import 'widgets/stat_summary_card.dart'; +import 'widgets/daily_goal_widget.dart'; +import 'widgets/level_progress_widget.dart'; +import 'widgets/today_chart_widget.dart'; +import 'widgets/weekly_chart_widget.dart'; +import 'widgets/monthly_chart_widget.dart'; + +class StatisticsDashboard extends StatelessWidget { + StatisticsDashboard({super.key}); + + final GamificationController gamController = + Get.put(GamificationController()); + final StatisticsController statsController = + Get.put(StatisticsController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: FinanceDesignSystem.backgroundColor, + body: GetBuilder( + builder: (gc) { + if (gc.isLoading) { + return const Center( + child: CircularProgressIndicator( + color: FinanceDesignSystem.primaryDark), + ); + } + + return CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + // ═══════ App Bar ═══════ + SliverAppBar( + expandedHeight: 200, + pinned: true, + stretch: true, + backgroundColor: FinanceDesignSystem.primaryDark, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded, + color: Colors.white, size: 20), + onPressed: () => Get.back(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded, + color: Colors.white), + onPressed: () => gc.fetchGamificationData(), + ), + ], + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + title: Text( + 'Statistics'.tr, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + background: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: const BoxDecoration( + gradient: FinanceDesignSystem.balanceGradient, + ), + ), + // أيقونة زخرفية + Positioned( + right: -40, + top: -20, + child: Icon( + Icons.bar_chart_rounded, + size: 200, + color: Colors.white.withOpacity(0.04), + ), + ), + Positioned( + left: -30, + bottom: 20, + child: Icon( + Icons.emoji_events_rounded, + size: 120, + color: Colors.white.withOpacity(0.03), + ), + ), + // بطاقة المستوى في الهيدر + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Text( + gc.currentLevel.emoji, + style: const TextStyle(fontSize: 40), + ), + const SizedBox(height: 4), + Text( + Get.locale?.languageCode == 'ar' + ? gc.currentLevel.nameAr + : gc.currentLevel.nameEn, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + '${gc.unlockedCount}/${gc.totalAchievements} ${'Achievements'.tr}', + style: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ), + + // ═══════ المحتوى ═══════ + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // 1. بطاقات الإحصائيات الأربعة + _buildSummaryCards(gc), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 2. الهدف اليومي + DailyGoalWidget(controller: gc), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 3. تقدم المستوى + LevelProgressWidget(controller: gc), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 4. إحصائيات اليوم + TodayChartWidget(), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 5. الرسم البياني الأسبوعي + const WeeklyChartWidget(), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 6. التقرير الشهري + const MonthlyChartWidget(), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 7. تقييم سلوك القيادة + _buildBehaviorSection(gc), + const SizedBox( + height: FinanceDesignSystem.verticalSectionPadding), + + // 8. الإنجازات + _buildAchievementsSection(gc), + ]), + ), + ), + ], + ); + }, + ), + ); + } + + // ═══════ بطاقات الإحصائيات ═══════ + Widget _buildSummaryCards(GamificationController gc) { + return GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1.5, + children: [ + StatSummaryCard( + icon: Icons.local_taxi_rounded, + label: 'Total Trips'.tr, + value: gc.totalTrips.toString(), + color: FinanceDesignSystem.accentBlue, + ), + StatSummaryCard( + icon: Icons.star_rounded, + label: 'Rating'.tr, + value: gc.averageRating.toStringAsFixed(1), + color: const Color(0xFFFFD700), + ), + StatSummaryCard( + icon: Icons.whatshot_rounded, + label: 'Day Streak'.tr, + value: '${gc.consecutiveDays}', + color: const Color(0xFFFF5722), + ), + StatSummaryCard( + icon: Icons.people_rounded, + label: 'Referrals'.tr, + value: gc.totalReferrals.toString(), + color: const Color(0xFF9C27B0), + ), + ], + ); + } + + // ═══════ قسم تقييم السلوك ═══════ + Widget _buildBehaviorSection(GamificationController gc) { + bool isExcellent = gc.behaviorScore >= 90; + bool isGood = gc.behaviorScore >= 75 && gc.behaviorScore < 90; + + Color statusColor = isExcellent + ? Colors.green + : isGood ? Colors.orange : Colors.red; + + String statusText = isExcellent + ? 'Excellent'.tr + : isGood ? 'Good'.tr : 'Needs Improvement'.tr; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: FinanceDesignSystem.cardColor, + borderRadius: BorderRadius.circular(24), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.shield_rounded, color: FinanceDesignSystem.accentBlue), + const SizedBox(width: 8), + Text('Driving Behavior'.tr, style: FinanceDesignSystem.headingStyle), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _behaviorStatItem( + title: 'Score'.tr, + value: '${gc.behaviorScore}%', + icon: Icons.speed_rounded, + color: FinanceDesignSystem.accentBlue, + ), + _behaviorStatItem( + title: 'Max Speed'.tr, + value: '${gc.maxSpeed.toStringAsFixed(0)} km/h', + icon: Icons.directions_car_rounded, + color: Colors.orange, + ), + _behaviorStatItem( + title: 'Hard Brakes'.tr, + value: '${gc.hardBrakes}', + icon: Icons.warning_rounded, + color: Colors.red, + ), + ], + ), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: gc.behaviorScore / 100, + backgroundColor: FinanceDesignSystem.backgroundColor, + valueColor: AlwaysStoppedAnimation(statusColor), + minHeight: 8, + ), + ), + ], + ), + ); + } + + Widget _behaviorStatItem({required String title, required String value, required IconData icon, required Color color}) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 24), + ), + const SizedBox(height: 8), + Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: FinanceDesignSystem.primaryDark)), + Text(title, style: const TextStyle(fontSize: 12, color: FinanceDesignSystem.textSecondary)), + ], + ); + } + + // ═══════ قسم الإنجازات ═══════ + Widget _buildAchievementsSection(GamificationController gc) { + final unlockedAch = + gc.achievements.where((a) => a.isUnlocked).toList(); + final lockedAch = + gc.achievements.where((a) => !a.isUnlocked).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Achievements'.tr, style: FinanceDesignSystem.headingStyle), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: FinanceDesignSystem.accentBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${gc.unlockedCount}/${gc.totalAchievements}', + style: const TextStyle( + color: FinanceDesignSystem.accentBlue, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // الإنجازات المفتوحة + if (unlockedAch.isNotEmpty) ...[ + ...unlockedAch.map((a) => _buildAchievementCard(a, true)), + const SizedBox(height: 16), + ], + + // الإنجازات المقفلة + ...lockedAch.map((a) => _buildAchievementCard(a, false)), + ], + ); + } + + Widget _buildAchievementCard(Achievement ach, bool unlocked) { + final isAr = Get.locale?.languageCode == 'ar'; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: unlocked ? Colors.white : Colors.grey.shade50, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + border: unlocked + ? Border.all(color: ach.color.withOpacity(0.3), width: 1.5) + : null, + boxShadow: unlocked + ? [ + BoxShadow( + color: ach.color.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: Row( + children: [ + // أيقونة الإنجاز + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: unlocked + ? ach.color.withOpacity(0.15) + : Colors.grey.shade200, + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + ach.icon, + color: unlocked ? ach.color : Colors.grey.shade400, + size: 26, + ), + ), + const SizedBox(width: 14), + + // المعلومات + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAr ? ach.titleAr : ach.titleEn, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: unlocked + ? FinanceDesignSystem.primaryDark + : Colors.grey.shade500, + ), + ), + const SizedBox(height: 2), + Text( + isAr ? ach.descriptionAr : ach.descriptionEn, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), + ), + if (!unlocked) ...[ + const SizedBox(height: 8), + // شريط التقدم + Stack( + children: [ + Container( + height: 6, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(3), + ), + ), + FractionallySizedBox( + widthFactor: ach.progress, + child: Container( + height: 6, + decoration: BoxDecoration( + color: ach.color.withOpacity(0.6), + borderRadius: BorderRadius.circular(3), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + '${ach.currentProgress}/${ach.target}', + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade400, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ), + + // أيقونة الحالة + if (unlocked) + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: ach.color.withOpacity(0.15), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check_rounded, + color: ach.color, + size: 18, + ), + ) + else + Icon( + Icons.lock_outline_rounded, + color: Colors.grey.shade300, + size: 20, + ), + ], + ), + ); + } +} diff --git a/lib/views/home/statistics/widgets/daily_goal_widget.dart b/lib/views/home/statistics/widgets/daily_goal_widget.dart new file mode 100644 index 0000000..56da026 --- /dev/null +++ b/lib/views/home/statistics/widgets/daily_goal_widget.dart @@ -0,0 +1,307 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../../constant/finance_design_system.dart'; +import '../../../../controller/gamification/gamification_controller.dart'; + +class DailyGoalWidget extends StatelessWidget { + final GamificationController controller; + + const DailyGoalWidget({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: controller.isDailyGoalMet + ? FinanceDesignSystem.successGreen.withOpacity(0.1) + : FinanceDesignSystem.accentBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + controller.isDailyGoalMet + ? Icons.check_circle_rounded + : Icons.track_changes_rounded, + color: controller.isDailyGoalMet + ? FinanceDesignSystem.successGreen + : FinanceDesignSystem.accentBlue, + size: 20, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Daily Goal'.tr, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: FinanceDesignSystem.primaryDark, + ), + ), + Text( + 'الهدف اليومي'.tr, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), + ), + ], + ), + ], + ), + // زر تعديل الهدف + InkWell( + onTap: () => _showSetGoalDialog(context), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + controller.dailyGoal > 0 + ? 'Edit'.tr + : 'Set Goal'.tr, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: FinanceDesignSystem.accentBlue, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 20), + + // مبلغ الأرباح والهدف + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + controller.dailyEarnings.toStringAsFixed(0), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w900, + color: controller.isDailyGoalMet + ? FinanceDesignSystem.successGreen + : FinanceDesignSystem.primaryDark, + fontFamily: 'digit', + ), + ), + const SizedBox(width: 6), + Text( + '/ ${controller.dailyGoal.toStringAsFixed(0)} ${'SYP'.tr}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.grey.shade400, + ), + ), + ], + ), + const SizedBox(height: 14), + + // شريط التقدم + Stack( + children: [ + Container( + height: 12, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(6), + ), + ), + LayoutBuilder( + builder: (context, constraints) { + return AnimatedContainer( + duration: const Duration(milliseconds: 800), + curve: Curves.easeOutCubic, + height: 12, + width: constraints.maxWidth * controller.dailyGoalProgress, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: controller.isDailyGoalMet + ? [ + FinanceDesignSystem.successGreen, + const Color(0xFF69F0AE) + ] + : [ + FinanceDesignSystem.accentBlue, + const Color(0xFF82B1FF) + ], + ), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: (controller.isDailyGoalMet + ? FinanceDesignSystem.successGreen + : FinanceDesignSystem.accentBlue) + .withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + ); + }, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${(controller.dailyGoalProgress * 100).toStringAsFixed(0)}%', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: controller.isDailyGoalMet + ? FinanceDesignSystem.successGreen + : FinanceDesignSystem.accentBlue, + ), + ), + if (controller.isDailyGoalMet) + Row( + children: [ + const Icon(Icons.celebration_rounded, + color: Color(0xFFFFD700), size: 14), + const SizedBox(width: 4), + Text( + 'Goal Achieved!'.tr, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: FinanceDesignSystem.successGreen, + ), + ), + ], + ) + else + Text( + '${'Remaining:'.tr} ${(controller.dailyGoal - controller.dailyEarnings).clamp(0, double.infinity).toStringAsFixed(0)} ${'SYP'.tr}', + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), + ), + ], + ), + ], + ), + ); + } + + void _showSetGoalDialog(BuildContext context) { + final textController = TextEditingController( + text: controller.dailyGoal > 0 + ? controller.dailyGoal.toStringAsFixed(0) + : '', + ); + + Get.defaultDialog( + title: 'Set Daily Goal'.tr, + titleStyle: FinanceDesignSystem.headingStyle, + content: Column( + children: [ + Text( + 'How much do you want to earn today?'.tr, + style: FinanceDesignSystem.subHeadingStyle, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + TextField( + controller: textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + fontFamily: 'digit', + ), + decoration: InputDecoration( + hintText: '5000', + suffixText: 'SYP'.tr, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + const BorderSide(color: FinanceDesignSystem.accentBlue), + ), + ), + ), + const SizedBox(height: 12), + // الأهداف السريعة + Wrap( + spacing: 8, + children: [1000, 3000, 5000, 10000].map((goal) { + return ActionChip( + label: Text('$goal'), + labelStyle: const TextStyle(fontSize: 12), + backgroundColor: FinanceDesignSystem.accentBlue.withOpacity(0.1), + side: BorderSide.none, + onPressed: () { + textController.text = goal.toString(); + }, + ); + }).toList(), + ), + ], + ), + confirm: ElevatedButton( + onPressed: () { + double? goal = double.tryParse(textController.text); + if (goal != null && goal > 0) { + controller.setDailyGoal(goal); + Get.back(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: FinanceDesignSystem.accentBlue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + ), + child: Text('Save'.tr), + ), + cancel: TextButton( + onPressed: () => Get.back(), + child: Text('Cancel'.tr, + style: const TextStyle(color: Colors.grey)), + ), + ); + } +} diff --git a/lib/views/home/statistics/widgets/level_progress_widget.dart b/lib/views/home/statistics/widgets/level_progress_widget.dart new file mode 100644 index 0000000..75af64a --- /dev/null +++ b/lib/views/home/statistics/widgets/level_progress_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../../constant/finance_design_system.dart'; +import '../../../../controller/gamification/gamification_controller.dart'; + +class LevelProgressWidget extends StatelessWidget { + final GamificationController controller; + const LevelProgressWidget({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final level = controller.currentLevel; + final next = controller.nextLevel; + final isAr = Get.locale?.languageCode == 'ar'; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [level.color.withOpacity(0.08), level.gradientEnd.withOpacity(0.04)], + begin: Alignment.topLeft, end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + border: Border.all(color: level.color.withOpacity(0.2), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Driver Level'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + gradient: LinearGradient(colors: [level.color, level.gradientEnd]), + borderRadius: BorderRadius.circular(20), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(level.emoji, style: const TextStyle(fontSize: 16)), + const SizedBox(width: 6), + Text(isAr ? level.nameAr : level.nameEn, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)), + ]), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: List.generate(DriverLevels.all.length, (i) { + final lvl = DriverLevels.all[i]; + final isCurrent = lvl.id == level.id; + final isPast = DriverLevels.all.indexOf(level) > i; + return Expanded(child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2), height: 8, + decoration: BoxDecoration( + color: isCurrent ? lvl.color : isPast ? lvl.color.withOpacity(0.6) : Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + )); + }), + ), + const SizedBox(height: 6), + Row(children: DriverLevels.all.map((l) => Expanded(child: Text(l.emoji, textAlign: TextAlign.center, style: TextStyle(fontSize: l.id == level.id ? 16 : 12)))).toList()), + const SizedBox(height: 16), + if (next != null) ...[ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('${'Next Level:'.tr} ${isAr ? next.nameAr : next.nameEn}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.grey.shade700)), + Text('${(controller.progressToNext * 100).toStringAsFixed(0)}%', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: level.color)), + ]), + const SizedBox(height: 8), + Stack(children: [ + Container(height: 10, decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(5))), + LayoutBuilder(builder: (ctx, c) => AnimatedContainer( + duration: const Duration(milliseconds: 800), curve: Curves.easeOutCubic, height: 10, + width: c.maxWidth * controller.progressToNext, + decoration: BoxDecoration(gradient: LinearGradient(colors: [level.color, level.gradientEnd]), borderRadius: BorderRadius.circular(5)), + )), + ]), + const SizedBox(height: 6), + Text('${controller.totalPoints} / ${next.minPoints} ${'Points'.tr}', style: TextStyle(fontSize: 11, color: Colors.grey.shade500, fontFamily: 'digit')), + ] else + Center(child: Text('🏆 ${'Maximum Level Reached!'.tr}', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: level.color))), + const SizedBox(height: 16), + Wrap(spacing: 6, runSpacing: 6, children: level.perks.map((p) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: level.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), + child: Text(p.tr, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: level.color.withOpacity(0.8))), + )).toList()), + ], + ), + ); + } +} diff --git a/lib/views/home/statistics/widgets/monthly_chart_widget.dart b/lib/views/home/statistics/widgets/monthly_chart_widget.dart new file mode 100644 index 0000000..679611d --- /dev/null +++ b/lib/views/home/statistics/widgets/monthly_chart_widget.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../../../../constant/finance_design_system.dart'; +import '../../../../controller/home/statistics/statistics_controller.dart'; + +class MonthlyChartWidget extends StatelessWidget { + const MonthlyChartWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GetBuilder( + builder: (sc) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))], + ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Monthly Report'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + const SizedBox(height: 16), + // Summary Row + Row(children: [ + _summaryTile('Total Earnings'.tr, '${sc.monthlyTotalEarnings.toStringAsFixed(0)} ${'SYP'.tr}', FinanceDesignSystem.successGreen), + const SizedBox(width: 12), + _summaryTile('Total Trips'.tr, '${sc.monthlyTotalTrips}', FinanceDesignSystem.accentBlue), + const SizedBox(width: 12), + _summaryTile('Best Day'.tr, '${sc.bestDay}', const Color(0xFFFFD700)), + ]), + const SizedBox(height: 20), + // Monthly Earnings Line Chart + SizedBox( + height: 200, + child: sc.monthlyEarnings.isEmpty + ? Center(child: Text('No data yet'.tr, style: TextStyle(color: Colors.grey.shade400))) + : LineChart(LineChartData( + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (spots) => spots.map((s) => LineTooltipItem( + '${s.y.toStringAsFixed(0)} ${'SYP'.tr}', + const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12), + )).toList(), + ), + ), + gridData: FlGridData(show: true, drawVerticalLine: false, + getDrawingHorizontalLine: (v) => FlLine(color: Colors.grey.shade100, strokeWidth: 1), + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, interval: 5, + getTitlesWidget: (v, m) => Padding(padding: const EdgeInsets.only(top: 8), + child: Text('${v.toInt()}', style: TextStyle(fontSize: 10, color: Colors.grey.shade500)), + ), + )), + leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: sc.monthlyEarnings.map((e) => FlSpot(e.day.toDouble(), e.pricePerDay)).toList(), + isCurved: true, curveSmoothness: 0.3, + color: FinanceDesignSystem.accentBlue, barWidth: 3, + dotData: FlDotData(show: true, getDotPainter: (s, p, d, i) => + FlDotCirclePainter(radius: 3, color: FinanceDesignSystem.accentBlue, strokeWidth: 1, strokeColor: Colors.white), + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, + colors: [FinanceDesignSystem.accentBlue.withOpacity(0.2), FinanceDesignSystem.accentBlue.withOpacity(0.0)], + ), + ), + ), + ], + )), + ), + ]), + ); + }, + ); + } + + Widget _summaryTile(String label, String value, Color color) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12)), + child: Column(children: [ + Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: color)), + const SizedBox(height: 2), + Text(label, style: TextStyle(fontSize: 9, color: Colors.grey.shade600), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis), + ]), + ), + ); + } +} diff --git a/lib/views/home/statistics/widgets/stat_summary_card.dart b/lib/views/home/statistics/widgets/stat_summary_card.dart new file mode 100644 index 0000000..e7fedd4 --- /dev/null +++ b/lib/views/home/statistics/widgets/stat_summary_card.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../../../../constant/finance_design_system.dart'; + +class StatSummaryCard extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final Color color; + + const StatSummaryCard({ + super.key, + required this.icon, + required this.label, + required this.value, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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), + ), + child: Icon(icon, color: color, size: 20), + ), + Icon( + Icons.trending_up_rounded, + color: Colors.grey.shade300, + size: 16, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: FinanceDesignSystem.primaryDark, + fontFamily: 'digit', + ), + ), + Text( + label, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/views/home/statistics/widgets/today_chart_widget.dart b/lib/views/home/statistics/widgets/today_chart_widget.dart new file mode 100644 index 0000000..ae6b8ce --- /dev/null +++ b/lib/views/home/statistics/widgets/today_chart_widget.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; +import '../../../../constant/finance_design_system.dart'; + +class TodayChartWidget extends StatelessWidget { + TodayChartWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GetBuilder( + builder: (hc) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Today Overview'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + const SizedBox(height: 4), + Text('نظرة عامة على اليوم'.tr, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), + const SizedBox(height: 20), + _buildRow(Icons.monetization_on_rounded, 'Earnings'.tr, '${hc.totalMoneyToday} ${'SYP'.tr}', FinanceDesignSystem.successGreen), + const Divider(height: 24), + _buildRow(Icons.local_taxi_rounded, 'Rides'.tr, hc.countRideToday, FinanceDesignSystem.accentBlue), + const Divider(height: 24), + Obx(() => _buildRow(Icons.timer_rounded, 'Online Duration'.tr, hc.totalDurationDisplay.value, const Color(0xFFFF9800))), + const Divider(height: 24), + _buildRow(Icons.cancel_outlined, 'Refused'.tr, hc.countRefuse, FinanceDesignSystem.dangerRed), + ], + ), + ); + }, + ); + } + + Widget _buildRow(IconData icon, String label, String value, Color color) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 14), + Expanded(child: Text(label, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade700))), + Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: FinanceDesignSystem.primaryDark, fontFamily: 'digit')), + ], + ); + } +} diff --git a/lib/views/home/statistics/widgets/weekly_chart_widget.dart b/lib/views/home/statistics/widgets/weekly_chart_widget.dart new file mode 100644 index 0000000..874cbb8 --- /dev/null +++ b/lib/views/home/statistics/widgets/weekly_chart_widget.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../../../../constant/finance_design_system.dart'; +import '../../../../controller/home/statistics/statistics_controller.dart'; + + +class WeeklyChartWidget extends StatelessWidget { + const WeeklyChartWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GetBuilder( + builder: (sc) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('Weekly Earnings'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: FinanceDesignSystem.successGreen.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), + child: Text('${sc.weeklyEarnings.toStringAsFixed(0)} ${'SYP'.tr}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: FinanceDesignSystem.successGreen)), + ), + ]), + const SizedBox(height: 8), + Row(children: [ + _miniStat(Icons.local_taxi_rounded, '${sc.weeklyTrips}', 'Rides'.tr, FinanceDesignSystem.accentBlue), + const SizedBox(width: 16), + _miniStat(Icons.timer_rounded, '${sc.weeklyHours.toStringAsFixed(1)}h', 'Hours'.tr, const Color(0xFFFF9800)), + ]), + const SizedBox(height: 20), + SizedBox( + height: 180, + child: sc.weeklyStats.isEmpty + ? Center(child: Text('No data yet'.tr, style: TextStyle(color: Colors.grey.shade400))) + : BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: _getMaxY(sc.weeklyStats), + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, gi, rod, ri) => BarTooltipItem( + '${rod.toY.toStringAsFixed(0)} ${'SYP'.tr}', + const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12), + ), + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, getTitlesWidget: (v, m) { + final idx = v.toInt(); + if (idx >= 0 && idx < sc.weeklyStats.length) { + return Padding(padding: const EdgeInsets.only(top: 8), child: Text(sc.weeklyStats[idx].dayName.tr, style: TextStyle(fontSize: 10, color: Colors.grey.shade500))); + } + return const SizedBox.shrink(); + })), + leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData(show: false), + gridData: const FlGridData(show: false), + barGroups: List.generate(sc.weeklyStats.length, (i) { + final stat = sc.weeklyStats[i]; + final isToday = i == sc.weeklyStats.length - 1; + return BarChartGroupData(x: i, barRods: [ + BarChartRodData( + toY: stat.earnings, + width: 20, + borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), + gradient: LinearGradient( + begin: Alignment.bottomCenter, end: Alignment.topCenter, + colors: isToday + ? [FinanceDesignSystem.accentBlue, const Color(0xFF82B1FF)] + : [FinanceDesignSystem.primaryDark.withOpacity(0.6), FinanceDesignSystem.primaryDark.withOpacity(0.3)], + ), + ), + ]); + }), + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _miniStat(IconData icon, String value, String label, Color color) { + return Row(children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 4), + Text(value, style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)), + const SizedBox(width: 4), + Text(label, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), + ]); + } + + double _getMaxY(List stats) { + if (stats.isEmpty) return 100; + final max = stats.map((s) => s.earnings).reduce((a, b) => a > b ? a : b); + return max <= 0 ? 100 : max * 1.2; + } +}