436 lines
14 KiB
Dart
436 lines
14 KiB
Dart
import 'dart:convert';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:siro_driver/constant/box_name.dart';
|
||
import 'package:siro_driver/constant/links.dart';
|
||
import 'package:siro_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;
|
||
averageRating = (box.read('gamification_averageRating') ?? 5.0).toDouble();
|
||
totalPoints = box.read('gamification_totalPoints') ?? 0;
|
||
totalReferrals = box.read('gamification_totalReferrals') ?? 0;
|
||
dailyEarnings = (box.read('gamification_dailyEarnings') ?? 0.0).toDouble();
|
||
behaviorScore = (box.read('gamification_behaviorScore') ?? 100.0).toDouble();
|
||
hardBrakes = box.read('gamification_hardBrakes') ?? 0;
|
||
maxSpeed = (box.read('gamification_maxSpeed') ?? 0.0).toDouble();
|
||
}
|
||
|
||
// ═══════ حفظ البيانات المحلية (التخزين المؤقت) ═══════
|
||
void _saveLocalData() {
|
||
box.write('gamification_totalTrips', totalTrips);
|
||
box.write('gamification_consecutiveDays', consecutiveDays);
|
||
box.write('gamification_averageRating', averageRating);
|
||
box.write('gamification_totalPoints', totalPoints);
|
||
box.write('gamification_totalReferrals', totalReferrals);
|
||
box.write('gamification_dailyEarnings', dailyEarnings);
|
||
box.write('gamification_behaviorScore', behaviorScore);
|
||
box.write('gamification_hardBrakes', hardBrakes);
|
||
box.write('gamification_maxSpeed', maxSpeed);
|
||
box.write('gamification_last_fetch', DateTime.now().toIso8601String());
|
||
}
|
||
|
||
// ═══════ حفظ الهدف اليومي ═══════
|
||
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({bool force = false}) async {
|
||
// التحقق من التخزين المؤقت (6 ساعات)
|
||
String? lastFetchStr = box.read('gamification_last_fetch');
|
||
if (!force && lastFetchStr != null) {
|
||
try {
|
||
DateTime lastFetch = DateTime.parse(lastFetchStr);
|
||
if (DateTime.now().difference(lastFetch).inHours < 6) {
|
||
debugPrint('ℹ️ [Gamification] Loading cached data (last fetch: $lastFetchStr)');
|
||
_loadLocalData();
|
||
_calculateLevel();
|
||
_updateAchievementProgress();
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
debugPrint('⚠️ [Gamification] Failed to parse last fetch time: $e');
|
||
}
|
||
}
|
||
|
||
isLoading = true;
|
||
update();
|
||
|
||
try {
|
||
var res = await CRUD().get(
|
||
link: AppLink.getGamificationDashboard,
|
||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||
);
|
||
|
||
if (res != null && res != 'failure') {
|
||
var data = jsonDecode(res);
|
||
if (data['status'] == 'success' && data['message'] != null) {
|
||
var details = data['message'];
|
||
totalTrips = int.tryParse(details['totalTrips']?.toString() ?? '0') ?? 0;
|
||
averageRating = double.tryParse(details['averageRating']?.toString() ?? '5.0') ?? 5.0;
|
||
totalReferrals = int.tryParse(details['totalReferrals']?.toString() ?? '0') ?? 0;
|
||
dailyEarnings = double.tryParse(details['todayEarnings']?.toString() ?? '0.0') ?? 0.0;
|
||
behaviorScore = double.tryParse(details['behaviorScore']?.toString() ?? '100.0') ?? 100.0;
|
||
hardBrakes = int.tryParse(details['hardBrakes']?.toString() ?? '0') ?? 0;
|
||
maxSpeed = double.tryParse(details['maxSpeed']?.toString() ?? '0.0') ?? 0.0;
|
||
totalPoints = int.tryParse(details['totalPoints']?.toString() ?? '0') ?? 0;
|
||
}
|
||
}
|
||
|
||
// 7. حساب الأيام المتتالية (محلياً)
|
||
_calculateConsecutiveDays();
|
||
} catch (e) {
|
||
debugPrint('❌ [Gamification] Error fetching data: $e');
|
||
}
|
||
|
||
_calculateLevel();
|
||
_updateAchievementProgress();
|
||
_saveLocalData();
|
||
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;
|
||
}
|