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

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