feat: redesign behavior page, add fatigue monitoring and fix map controller

This commit is contained in:
Hamza-Ayed
2026-05-08 06:10:35 +03:00
parent 129c409901
commit efbc921273
24 changed files with 3772 additions and 92 deletions

View File

@@ -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";

View File

@@ -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<Challenge> dailyChallenges = [];
List<Challenge> 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<void> 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<void> 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');
}
}
}

View File

@@ -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<String> 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<DriverLevel> 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<Achievement> 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<void> 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;
}

View File

@@ -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<LeaderboardEntry> tripLeaderboard = [];
List<LeaderboardEntry> earningsLeaderboard = [];
int myRank = 0;
@override
void onInit() {
super.onInit();
fetchLeaderboard();
}
void changeTab(int tab) {
selectedTab = tab;
update();
}
List<LeaderboardEntry> get currentLeaderboard =>
selectedTab == 0 ? tripLeaderboard : earningsLeaderboard;
Future<void> 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();
}
}

View File

@@ -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<String, dynamic> 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<ReferralRecord> driverReferrals = [];
List<ReferralRecord> 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<void> 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<bool> 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<bool> 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;
}
}

View File

@@ -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<CaptainWalletController>()) {
Get.put(CaptainWalletController());
}
totalPoints = Get.find<CaptainWalletController>().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();
}
// }
}
// متغيرات العداد للحظر

View File

@@ -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<String, dynamic> toJson() => {
'day': dayOfWeek, 'startH': startTime.hour, 'startM': startTime.minute,
'endH': endTime.hour, 'endM': endTime.minute, 'active': isActive,
};
factory WorkSlot.fromJson(Map<String, dynamic> 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<WorkSlot> 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;
}

View File

@@ -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<DayStat> weeklyStats = [];
double weeklyEarnings = 0;
int weeklyTrips = 0;
double weeklyHours = 0;
// ═══ Monthly Data ═══
List<MonthlyPriceDriverModel> monthlyEarnings = [];
List<MonthlyRideModel> monthlyRides = [];
List<MonthlyDataModel> 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<void> 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<void> 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<void> 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<String, dynamic> 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,
);
}
}

View File

@@ -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": "أحد"
}

View File

@@ -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"
}

View File

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

View File

@@ -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<LeaderboardController>(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 ? '🥈' : '🥉';
}

View File

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

View File

@@ -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,

View File

@@ -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);
});

View File

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

View File

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

View File

@@ -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<GamificationController>(
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<Color>(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,
),
],
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<HomeCaptainController>(
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')),
],
);
}
}

View File

@@ -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<StatisticsController>(
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<DayStat> 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;
}
}