feat: redesign behavior page, add fatigue monitoring and fix map controller
This commit is contained in:
286
lib/controller/gamification/challenges_controller.dart
Normal file
286
lib/controller/gamification/challenges_controller.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
453
lib/controller/gamification/gamification_controller.dart
Normal file
453
lib/controller/gamification/gamification_controller.dart
Normal 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;
|
||||
}
|
||||
102
lib/controller/gamification/leaderboard_controller.dart
Normal file
102
lib/controller/gamification/leaderboard_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
189
lib/controller/gamification/referral_controller.dart
Normal file
189
lib/controller/gamification/referral_controller.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// متغيرات العداد للحظر
|
||||
|
||||
106
lib/controller/home/journal/schedule_controller.dart
Normal file
106
lib/controller/home/journal/schedule_controller.dart
Normal 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;
|
||||
}
|
||||
193
lib/controller/home/statistics/statistics_controller.dart
Normal file
193
lib/controller/home/statistics/statistics_controller.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user