Files
Siro/siro_driver/lib/controller/gamification/gamification_controller.dart
2026-06-15 01:37:41 +03:00

436 lines
14 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}