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

357 lines
12 KiB
Dart

import 'package:siro_driver/constant/currency.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 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;
}
// ════════════════════════════════════════════
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; // الجمعة والسبت
String country = box.read(BoxName.countryCode) ?? 'Jordan';
int dailyEarningsTarget;
int dailyEarningsReward;
int weeklyEarningsTarget;
int weeklyEarningsReward;
String dailyEarningsDescAr;
String dailyEarningsDescEn;
String weeklyEarningsDescAr;
String weeklyEarningsDescEn;
if (country == 'Syria') {
dailyEarningsTarget = 150000;
dailyEarningsReward = 100; // 100 pts -> 10,000 SYP
weeklyEarningsTarget = 1000000;
weeklyEarningsReward = 500; // 500 pts -> 50,000 SYP
dailyEarningsDescAr = 'اربح 150,000 ل.س اليوم';
dailyEarningsDescEn = 'Earn 150,000 SYP today';
weeklyEarningsDescAr = 'اربح 1,000,000 ل.س هذا الأسبوع';
weeklyEarningsDescEn = 'Earn 1,000,000 SYP this week';
} else if (country == 'Egypt') {
dailyEarningsTarget = 300;
dailyEarningsReward = 30; // 30 pts -> 30 EGP
weeklyEarningsTarget = 2000;
weeklyEarningsReward = 200; // 200 pts -> 200 EGP
dailyEarningsDescAr = 'اربح 300 ج.م اليوم';
dailyEarningsDescEn = 'Earn 300 EGP today';
weeklyEarningsDescAr = 'اربح 2000 ج.م هذا الأسبوع';
weeklyEarningsDescEn = 'Earn 2000 EGP this week';
} else {
// Jordan / default
dailyEarningsTarget = 30;
dailyEarningsReward = 60; // 60 pts -> 3 JOD (60 * 0.05 = 3 JOD)
weeklyEarningsTarget = 200;
weeklyEarningsReward = 400; // 400 pts -> 20 JOD (400 * 0.05 = 20 JOD)
dailyEarningsDescAr = 'اربح 30 د.أ اليوم';
dailyEarningsDescEn = 'Earn 30 JOD today';
weeklyEarningsDescAr = 'اربح 200 د.أ هذا الأسبوع';
weeklyEarningsDescEn = 'Earn 200 JOD this week';
}
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: country == 'Syria' ? 50 : (country == 'Egypt' ? 50 : 100),
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: country == 'Syria' ? 150 : (country == 'Egypt' ? 150 : 300),
type: 'daily',
metric: 'trips',
),
Challenge(
id: 'daily_earnings',
titleEn: 'Money Maker',
titleAr: 'صانع المال',
descriptionEn: dailyEarningsDescEn,
descriptionAr: dailyEarningsDescAr,
icon: Icons.monetization_on_rounded,
color: const Color(0xFF4CAF50),
target: dailyEarningsTarget,
reward: dailyEarningsReward,
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: country == 'Syria' ? 200 : (country == 'Egypt' ? 200 : 400),
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: country == 'Syria' ? 300 : (country == 'Egypt' ? 300 : 600),
type: 'weekly',
metric: 'trips',
),
Challenge(
id: 'weekly_earnings',
titleEn: 'Big Earner',
titleAr: 'الربح الكبير',
descriptionEn: weeklyEarningsDescEn,
descriptionAr: weeklyEarningsDescAr,
icon: Icons.account_balance_wallet_rounded,
color: const Color(0xFF9C27B0),
target: weeklyEarningsTarget,
reward: weeklyEarningsReward,
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: country == 'Syria' ? 400 : (country == 'Egypt' ? 400 : 800),
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;
}
}
// 2. Fetch weekly earnings from PAYMENT server
var weeklyEarningsRes = await CRUD().getWallet(
link: AppLink.getDriverWeekPaymentMove,
payload: {'driverID': box.read(BoxName.driverID).toString()},
);
double weeklyEarnings = 0;
if (weeklyEarningsRes != null && weeklyEarningsRes != 'failure') {
var data = jsonDecode(weeklyEarningsRes);
if (data['message'] is List && data['message'].isNotEmpty) {
weeklyEarnings = double.tryParse(
data['message'][0]['totalAmount']?.toString() ?? '0') ??
0;
}
}
// 3. Fetch weekly trips and hours from RIDES server (avoiding earnings join)
var weeklyAggregateRes = await CRUD().get(
link: AppLink.getWeeklyAggregate,
payload: {'driver_id': box.read(BoxName.driverID).toString()},
);
int weeklyTrips = 0;
double weeklyHours = 0;
if (weeklyAggregateRes != null && weeklyAggregateRes != 'failure') {
var data = jsonDecode(weeklyAggregateRes);
if (data['message'] is List) {
for (var day in data['message']) {
weeklyTrips += int.tryParse(day['trips']?.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();
}
final Set<String> _claimingChallenges = {};
bool isClaiming(String challengeId) =>
_claimingChallenges.contains(challengeId);
Future<void> claimReward(Challenge challenge) async {
if (!challenge.isCompleted ||
challenge.isClaimed ||
_claimingChallenges.contains(challenge.id)) return;
_claimingChallenges.add(challenge.id);
update();
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}');
}
} catch (e) {
debugPrint('❌ [Challenges] Claim error: $e');
} finally {
_claimingChallenges.remove(challenge.id);
update();
}
}
}