feat: redesign behavior page, add fatigue monitoring and fix map controller
This commit is contained in:
@@ -92,6 +92,13 @@ class AppLink {
|
|||||||
"$endPoint/ride/driverWallet/driverStatistic.php";
|
"$endPoint/ride/driverWallet/driverStatistic.php";
|
||||||
static String getDriverDetails =
|
static String getDriverDetails =
|
||||||
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
|
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
|
||||||
|
|
||||||
|
// ================= Gamification Endpoints =================
|
||||||
|
static String getWeeklyAggregate = "$endPoint/ride/gamification/getWeeklyAggregate.php";
|
||||||
|
static String getLeaderboard = "$endPoint/ride/gamification/getLeaderboard.php";
|
||||||
|
static String claimChallengeReward = "$endPoint/ride/gamification/claimChallengeReward.php";
|
||||||
|
static String getReferralStats = "$endPoint/ride/gamification/getReferralStats.php";
|
||||||
|
static String getDriverBehavior = "$endPoint/ride/gamification/getDriverBehavior.php";
|
||||||
static String getDriverWeekPaymentMove =
|
static String getDriverWeekPaymentMove =
|
||||||
"$walletDriver/getDriverWeekPaymentMove.php";
|
"$walletDriver/getDriverWeekPaymentMove.php";
|
||||||
static String getDriversWallet = "$walletDriver/get.php";
|
static String getDriversWallet = "$walletDriver/get.php";
|
||||||
|
|||||||
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 = '';
|
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() {
|
void onButtonSelected() {
|
||||||
// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
|
|
||||||
if (!Get.isRegistered<CaptainWalletController>()) {
|
if (!Get.isRegistered<CaptainWalletController>()) {
|
||||||
Get.put(CaptainWalletController());
|
Get.put(CaptainWalletController());
|
||||||
}
|
}
|
||||||
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
||||||
|
|
||||||
|
// Toggle Active State
|
||||||
isActive = !isActive;
|
isActive = !isActive;
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
if (double.parse(totalPoints) > -200) {
|
try {
|
||||||
locationController.startLocationUpdates();
|
_checkFatigueBeforeOnline(); // Throws exception if tired
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
// locationBackController.startBackLocation();
|
if (double.parse(totalPoints) > -200) {
|
||||||
activeStartTime = DateTime.now();
|
locationController.startLocationUpdates();
|
||||||
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
HapticFeedback.heavyImpact();
|
||||||
activeDuration = DateTime.now().difference(activeStartTime!);
|
activeStartTime = DateTime.now();
|
||||||
stringActiveDuration = formatDuration(activeDuration);
|
|
||||||
update();
|
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
});
|
activeDuration = DateTime.now().difference(activeStartTime!);
|
||||||
} else {
|
stringActiveDuration = formatDuration(activeDuration);
|
||||||
locationController.stopLocationUpdates();
|
|
||||||
|
// Increment Fatigue Counter
|
||||||
|
int totalSeconds = box.read('fatigue_total_seconds') ?? 0;
|
||||||
|
totalSeconds += 1;
|
||||||
|
box.write('fatigue_total_seconds', totalSeconds);
|
||||||
|
|
||||||
activeStartTime = null;
|
if (totalSeconds >= 12 * 3600) { // 12 hours
|
||||||
activeTimer?.cancel();
|
_forceOfflineDueToFatigue();
|
||||||
savePeriod(activeDuration);
|
}
|
||||||
activeDuration = Duration.zero;
|
|
||||||
|
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();
|
update();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
locationController.stopLocationUpdates();
|
locationController.stopLocationUpdates();
|
||||||
|
|
||||||
activeStartTime = null;
|
activeStartTime = null;
|
||||||
activeTimer?.cancel();
|
activeTimer?.cancel();
|
||||||
savePeriod(activeDuration);
|
savePeriod(activeDuration);
|
||||||
activeDuration = Duration.zero;
|
activeDuration = Duration.zero;
|
||||||
|
|
||||||
|
// Save offline time for Fatigue Monitoring reset
|
||||||
|
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||||
update();
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -576,6 +576,16 @@
|
|||||||
"ok": "",
|
"ok": "",
|
||||||
"I will slow down": "",
|
"I will slow down": "",
|
||||||
"Ride Today : ": "",
|
"Ride Today : ": "",
|
||||||
|
"Share with Passenger": "مشاركة مع الراكب",
|
||||||
|
"Driving Behavior": "سلوك القيادة",
|
||||||
|
"Excellent": "ممتاز",
|
||||||
|
"Good": "جيد",
|
||||||
|
"Needs Improvement": "بحاجة للتحسين",
|
||||||
|
"Score": "التقييم",
|
||||||
|
"Max Speed": "أقصى سرعة",
|
||||||
|
"Hard Brakes": "الفرملة المفاجئة",
|
||||||
|
"Safety First 🛑": "الأمان أولاً 🛑",
|
||||||
|
"You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.": "لقد قمت بالقيادة لمدة 12 ساعة. من أجل سلامتك وسلامة الركاب، يرجى أخذ استراحة لمدة 6 ساعات متواصلة.",
|
||||||
"app with passenger": "",
|
"app with passenger": "",
|
||||||
"Please add contacts to your phone.": "",
|
"Please add contacts to your phone.": "",
|
||||||
"I cant register in your app in face detection ": "",
|
"I cant register in your app in face detection ": "",
|
||||||
@@ -802,5 +812,89 @@
|
|||||||
"Recharge Balance": "شحن الرصيد",
|
"Recharge Balance": "شحن الرصيد",
|
||||||
"Recharge Balance Packages": "باقات شحن الرصيد",
|
"Recharge Balance Packages": "باقات شحن الرصيد",
|
||||||
"Price:": "السعر:",
|
"Price:": "السعر:",
|
||||||
"Amount to charge:": "المبلغ المطلوب شحنه:"
|
"Amount to charge:": "المبلغ المطلوب شحنه:",
|
||||||
|
"Statistics": "الإحصائيات",
|
||||||
|
"Achievements": "الإنجازات",
|
||||||
|
"Total Trips": "إجمالي الرحلات",
|
||||||
|
"Rating": "التقييم",
|
||||||
|
"Day Streak": "أيام متتالية",
|
||||||
|
"Referrals": "الإحالات",
|
||||||
|
"Daily Goal": "الهدف اليومي",
|
||||||
|
"الهدف اليومي": "Daily Goal",
|
||||||
|
"Set Goal": "تحديد الهدف",
|
||||||
|
"Edit": "تعديل",
|
||||||
|
"Goal Achieved!": "تم تحقيق الهدف! 🎉",
|
||||||
|
"Remaining:": "المتبقي:",
|
||||||
|
"Set Daily Goal": "تحديد الهدف اليومي",
|
||||||
|
"How much do you want to earn today?": "كم تريد أن تربح اليوم؟",
|
||||||
|
"Driver Level": "مستوى السائق",
|
||||||
|
"مستوى السائق": "Driver Level",
|
||||||
|
"Next Level:": "المستوى التالي:",
|
||||||
|
"Maximum Level Reached!": "وصلت للمستوى الأعلى!",
|
||||||
|
"Today Overview": "نظرة عامة على اليوم",
|
||||||
|
"نظرة عامة على اليوم": "Today Overview",
|
||||||
|
"Earnings": "الأرباح",
|
||||||
|
"Rides": "الرحلات",
|
||||||
|
"Online Duration": "مدة الاتصال",
|
||||||
|
"Refused": "مرفوض",
|
||||||
|
"Basic features": "الميزات الأساسية",
|
||||||
|
"Standard support": "دعم عادي",
|
||||||
|
"Priority medium": "أولوية متوسطة",
|
||||||
|
"Silver badge": "شارة فضية",
|
||||||
|
"-1% commission": "-1% عمولة",
|
||||||
|
"High priority": "أولوية عالية",
|
||||||
|
"Gold badge": "شارة ذهبية",
|
||||||
|
"-2% commission": "-2% عمولة",
|
||||||
|
"VIP first": "أولوية VIP",
|
||||||
|
"Diamond badge": "شارة ألماسية",
|
||||||
|
"-5% commission": "-5% عمولة",
|
||||||
|
"Priority support": "دعم أولوية",
|
||||||
|
"Challenges": "التحديات",
|
||||||
|
"Daily Challenges": "تحديات يومية",
|
||||||
|
"Weekly Challenges": "تحديات أسبوعية",
|
||||||
|
"Claim Reward": "الحصول على المكافأة",
|
||||||
|
"Claimed": "تم الاستلام",
|
||||||
|
"Referral Center": "مركز الإحالة",
|
||||||
|
"Your Referral Code": "رمز الإحالة الخاص بك",
|
||||||
|
"Share this code to earn rewards": "شارك هذا الرمز لكسب مكافآت",
|
||||||
|
"Share via": "مشاركة عبر",
|
||||||
|
"Share": "مشاركة",
|
||||||
|
"WhatsApp": "واتساب",
|
||||||
|
"Invite Driver": "دعوة سائق",
|
||||||
|
"Invite Rider": "دعوة راكب",
|
||||||
|
"How It Works": "كيف يعمل؟",
|
||||||
|
"Share your code": "شارك رمزك",
|
||||||
|
"Send your referral code to friends": "أرسل رمز الإحالة لأصدقائك",
|
||||||
|
"Friend signs up": "صديقك يسجّل",
|
||||||
|
"They register using your code": "يسجّلون باستخدام رمزك",
|
||||||
|
"Both earn rewards": "كلاكما تربحان",
|
||||||
|
"You get 100 pts, they get 50 pts": "أنت تحصل على 100 نقطة، وهم 50 نقطة",
|
||||||
|
"Bonus at 10 trips": "مكافأة عند 10 رحلات",
|
||||||
|
"Extra 200 pts when they complete 10 trips": "200 نقطة إضافية عند إكمال 10 رحلات",
|
||||||
|
"Driver Invitations": "دعوات السائقين",
|
||||||
|
"Passenger Invitations": "دعوات الركاب",
|
||||||
|
"Active": "نشط",
|
||||||
|
"registered": "مسجّل",
|
||||||
|
"active": "نشط",
|
||||||
|
"inactive": "غير نشط",
|
||||||
|
"Start sharing your code!": "ابدأ بمشاركة رمزك!",
|
||||||
|
"Code copied!": "تم نسخ الرمز!",
|
||||||
|
"Leaderboard": "لوحة المتصدرين",
|
||||||
|
"Trips": "الرحلات",
|
||||||
|
"You": "أنت",
|
||||||
|
"My Schedule": "جدولي",
|
||||||
|
"Weekly Plan": "الخطة الأسبوعية",
|
||||||
|
"Work Days": "أيام العمل",
|
||||||
|
"Day Off": "إجازة",
|
||||||
|
"Weekly Earnings": "أرباح الأسبوع",
|
||||||
|
"Monthly Report": "التقرير الشهري",
|
||||||
|
"Best Day": "أفضل يوم",
|
||||||
|
"Hours": "ساعات",
|
||||||
|
"Mon": "إثن",
|
||||||
|
"Tue": "ثلاث",
|
||||||
|
"Wed": "أربع",
|
||||||
|
"Thu": "خميس",
|
||||||
|
"Fri": "جمعة",
|
||||||
|
"Sat": "سبت",
|
||||||
|
"Sun": "أحد"
|
||||||
}
|
}
|
||||||
@@ -576,6 +576,16 @@
|
|||||||
"ok": "",
|
"ok": "",
|
||||||
"I will slow down": "",
|
"I will slow down": "",
|
||||||
"Ride Today : ": "",
|
"Ride Today : ": "",
|
||||||
|
"Share with Passenger": "Share with Passenger",
|
||||||
|
"Driving Behavior": "Driving Behavior",
|
||||||
|
"Excellent": "Excellent",
|
||||||
|
"Good": "Good",
|
||||||
|
"Needs Improvement": "Needs Improvement",
|
||||||
|
"Score": "Score",
|
||||||
|
"Max Speed": "Max Speed",
|
||||||
|
"Hard Brakes": "Hard Brakes",
|
||||||
|
"Safety First 🛑": "Safety First 🛑",
|
||||||
|
"You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.": "You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.",
|
||||||
"app with passenger": "",
|
"app with passenger": "",
|
||||||
"Please add contacts to your phone.": "",
|
"Please add contacts to your phone.": "",
|
||||||
"I cant register in your app in face detection ": "",
|
"I cant register in your app in face detection ": "",
|
||||||
@@ -802,5 +812,89 @@
|
|||||||
"Recharge Balance": "Recharge Balance",
|
"Recharge Balance": "Recharge Balance",
|
||||||
"Recharge Balance Packages": "Recharge Balance Packages",
|
"Recharge Balance Packages": "Recharge Balance Packages",
|
||||||
"Price:": "Price:",
|
"Price:": "Price:",
|
||||||
"Amount to charge:": "Amount to charge:"
|
"Amount to charge:": "Amount to charge:",
|
||||||
|
"Statistics": "Statistics",
|
||||||
|
"Achievements": "Achievements",
|
||||||
|
"Total Trips": "Total Trips",
|
||||||
|
"Rating": "Rating",
|
||||||
|
"Day Streak": "Day Streak",
|
||||||
|
"Referrals": "Referrals",
|
||||||
|
"Daily Goal": "Daily Goal",
|
||||||
|
"الهدف اليومي": "Daily Goal",
|
||||||
|
"Set Goal": "Set Goal",
|
||||||
|
"Edit": "Edit",
|
||||||
|
"Goal Achieved!": "Goal Achieved! 🎉",
|
||||||
|
"Remaining:": "Remaining:",
|
||||||
|
"Set Daily Goal": "Set Daily Goal",
|
||||||
|
"How much do you want to earn today?": "How much do you want to earn today?",
|
||||||
|
"Driver Level": "Driver Level",
|
||||||
|
"مستوى السائق": "Driver Level",
|
||||||
|
"Next Level:": "Next Level:",
|
||||||
|
"Maximum Level Reached!": "Maximum Level Reached!",
|
||||||
|
"Today Overview": "Today Overview",
|
||||||
|
"نظرة عامة على اليوم": "Today Overview",
|
||||||
|
"Earnings": "Earnings",
|
||||||
|
"Rides": "Rides",
|
||||||
|
"Online Duration": "Online Duration",
|
||||||
|
"Refused": "Refused",
|
||||||
|
"Basic features": "Basic features",
|
||||||
|
"Standard support": "Standard support",
|
||||||
|
"Priority medium": "Priority medium",
|
||||||
|
"Silver badge": "Silver badge",
|
||||||
|
"-1% commission": "-1% commission",
|
||||||
|
"High priority": "High priority",
|
||||||
|
"Gold badge": "Gold badge",
|
||||||
|
"-2% commission": "-2% commission",
|
||||||
|
"VIP first": "VIP first",
|
||||||
|
"Diamond badge": "Diamond badge",
|
||||||
|
"-5% commission": "-5% commission",
|
||||||
|
"Priority support": "Priority support",
|
||||||
|
"Challenges": "Challenges",
|
||||||
|
"Daily Challenges": "Daily Challenges",
|
||||||
|
"Weekly Challenges": "Weekly Challenges",
|
||||||
|
"Claim Reward": "Claim Reward",
|
||||||
|
"Claimed": "Claimed",
|
||||||
|
"Referral Center": "Referral Center",
|
||||||
|
"Your Referral Code": "Your Referral Code",
|
||||||
|
"Share this code to earn rewards": "Share this code to earn rewards",
|
||||||
|
"Share via": "Share via",
|
||||||
|
"Share": "Share",
|
||||||
|
"WhatsApp": "WhatsApp",
|
||||||
|
"Invite Driver": "Invite Driver",
|
||||||
|
"Invite Rider": "Invite Rider",
|
||||||
|
"How It Works": "How It Works",
|
||||||
|
"Share your code": "Share your code",
|
||||||
|
"Send your referral code to friends": "Send your referral code to friends",
|
||||||
|
"Friend signs up": "Friend signs up",
|
||||||
|
"They register using your code": "They register using your code",
|
||||||
|
"Both earn rewards": "Both earn rewards",
|
||||||
|
"You get 100 pts, they get 50 pts": "You get 100 pts, they get 50 pts",
|
||||||
|
"Bonus at 10 trips": "Bonus at 10 trips",
|
||||||
|
"Extra 200 pts when they complete 10 trips": "Extra 200 pts when they complete 10 trips",
|
||||||
|
"Driver Invitations": "Driver Invitations",
|
||||||
|
"Passenger Invitations": "Passenger Invitations",
|
||||||
|
"Active": "Active",
|
||||||
|
"registered": "Registered",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"Start sharing your code!": "Start sharing your code!",
|
||||||
|
"Code copied!": "Code copied!",
|
||||||
|
"Leaderboard": "Leaderboard",
|
||||||
|
"Trips": "Trips",
|
||||||
|
"You": "You",
|
||||||
|
"My Schedule": "My Schedule",
|
||||||
|
"Weekly Plan": "Weekly Plan",
|
||||||
|
"Work Days": "Work Days",
|
||||||
|
"Day Off": "Day Off",
|
||||||
|
"Weekly Earnings": "Weekly Earnings",
|
||||||
|
"Monthly Report": "Monthly Report",
|
||||||
|
"Best Day": "Best Day",
|
||||||
|
"Hours": "Hours",
|
||||||
|
"Mon": "Mon",
|
||||||
|
"Tue": "Tue",
|
||||||
|
"Wed": "Wed",
|
||||||
|
"Thu": "Thu",
|
||||||
|
"Fri": "Fri",
|
||||||
|
"Sat": "Sat",
|
||||||
|
"Sun": "Sun"
|
||||||
}
|
}
|
||||||
228
lib/views/gamification/challenges_page.dart
Normal file
228
lib/views/gamification/challenges_page.dart
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../controller/gamification/challenges_controller.dart';
|
||||||
|
|
||||||
|
class ChallengesPage extends StatelessWidget {
|
||||||
|
ChallengesPage({super.key});
|
||||||
|
final ChallengesController controller = Get.put(ChallengesController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
body: GetBuilder<ChallengesController>(builder: (cc) {
|
||||||
|
if (cc.isLoading)
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: FinanceDesignSystem.primaryDark));
|
||||||
|
return CustomScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 160,
|
||||||
|
pinned: true,
|
||||||
|
backgroundColor: const Color(0xFF1A237E),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: Colors.white, size: 20),
|
||||||
|
onPressed: () => Get.back()),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh_rounded,
|
||||||
|
color: Colors.white),
|
||||||
|
onPressed: () => cc.fetchChallengeProgress())
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text('Challenges'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18)),
|
||||||
|
background: Stack(fit: StackFit.expand, children: [
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: [
|
||||||
|
Color(0xFF0A0E21),
|
||||||
|
Color(0xFF1A237E)
|
||||||
|
]))),
|
||||||
|
Positioned(
|
||||||
|
right: -30,
|
||||||
|
top: -10,
|
||||||
|
child: Icon(Icons.bolt_rounded,
|
||||||
|
size: 160, color: Colors.white.withOpacity(0.04))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
// Daily Challenges
|
||||||
|
_sectionHeader(
|
||||||
|
'Daily Challenges'.tr,
|
||||||
|
'🔥',
|
||||||
|
cc.dailyChallenges.where((c) => c.isCompleted).length,
|
||||||
|
cc.dailyChallenges.length),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...cc.dailyChallenges.map((c) => _challengeCard(c, cc)),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
// Weekly Challenges
|
||||||
|
_sectionHeader(
|
||||||
|
'Weekly Challenges'.tr,
|
||||||
|
'🏆',
|
||||||
|
cc.weeklyChallenges.where((c) => c.isCompleted).length,
|
||||||
|
cc.weeklyChallenges.length),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...cc.weeklyChallenges.map((c) => _challengeCard(c, cc)),
|
||||||
|
]))),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _sectionHeader(String title, String emoji, int done, int total) {
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Row(children: [
|
||||||
|
Text(emoji, style: const TextStyle(fontSize: 20)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: FinanceDesignSystem.headingStyle),
|
||||||
|
]),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Text('$done/$total',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.accentBlue)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _challengeCard(Challenge c, ChallengesController cc) {
|
||||||
|
final isAr = Get.locale?.languageCode == 'ar';
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
border: c.isCompleted
|
||||||
|
? Border.all(color: c.color.withOpacity(0.3), width: 1.5)
|
||||||
|
: null,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: c.color.withOpacity(0.12),
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Icon(c.icon, color: c.color, size: 22),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(isAr ? c.titleAr : c.titleEn,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.primaryDark)),
|
||||||
|
Text(isAr ? c.descriptionAr : c.descriptionEn,
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||||
|
])),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFD700).withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Text('+${c.reward}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFFFF8F00))),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
// Progress bar
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Text('${c.currentProgress}/${c.target}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade600)),
|
||||||
|
Text('${(c.progress * 100).toStringAsFixed(0)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11, fontWeight: FontWeight.bold, color: c.color)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Stack(children: [
|
||||||
|
Container(
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(4))),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (ctx, cons) => AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
height: 8,
|
||||||
|
width: cons.maxWidth * c.progress,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [c.color, c.color.withOpacity(0.6)]),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
// Claim button
|
||||||
|
if (c.isCompleted && !c.isClaimed) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => cc.claimReward(c),
|
||||||
|
icon: const Icon(Icons.card_giftcard_rounded, size: 18),
|
||||||
|
label: Text('Claim Reward'.tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: c.color,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
if (c.isClaimed) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
const Icon(Icons.check_circle_rounded,
|
||||||
|
color: FinanceDesignSystem.successGreen, size: 16),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('Claimed'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.successGreen)),
|
||||||
|
])),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
lib/views/gamification/leaderboard_page.dart
Normal file
132
lib/views/gamification/leaderboard_page.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../controller/gamification/leaderboard_controller.dart';
|
||||||
|
|
||||||
|
class LeaderboardPage extends StatelessWidget {
|
||||||
|
LeaderboardPage({super.key});
|
||||||
|
final LeaderboardController controller = Get.put(LeaderboardController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
body: GetBuilder<LeaderboardController>(builder: (lc) {
|
||||||
|
if (lc.isLoading) return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.primaryDark));
|
||||||
|
return CustomScrollView(physics: const BouncingScrollPhysics(), slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 200, pinned: true,
|
||||||
|
backgroundColor: const Color(0xFF0A0E21),
|
||||||
|
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 20), onPressed: () => Get.back()),
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text('Leaderboard'.tr, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
background: Stack(fit: StackFit.expand, children: [
|
||||||
|
Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF0A0E21), Color(0xFFB71C1C)]))),
|
||||||
|
Positioned(right: -40, top: -20, child: Icon(Icons.leaderboard_rounded, size: 180, color: Colors.white.withOpacity(0.04))),
|
||||||
|
// Top 3 podium
|
||||||
|
if (lc.currentLeaderboard.length >= 3) Positioned(bottom: 50, left: 0, right: 0, child: _buildPodium(lc)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Tab bar
|
||||||
|
SliverToBoxAdapter(child: Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(14)),
|
||||||
|
child: Row(children: [
|
||||||
|
_tab('Trips'.tr, Icons.local_taxi_rounded, 0, lc),
|
||||||
|
_tab('Earnings'.tr, Icons.monetization_on_rounded, 1, lc),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
// List
|
||||||
|
SliverPadding(padding: const EdgeInsets.fromLTRB(16, 16, 16, 40), sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, i) {
|
||||||
|
if (i >= lc.currentLeaderboard.length) return null;
|
||||||
|
return _buildRow(lc.currentLeaderboard[i], lc.selectedTab == 1);
|
||||||
|
},
|
||||||
|
childCount: lc.currentLeaderboard.length,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPodium(LeaderboardController lc) {
|
||||||
|
final top3 = lc.currentLeaderboard.take(3).toList();
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||||
|
if (top3.length > 1) _podiumItem(top3[1], 2, 50, const Color(0xFFC0C0C0)),
|
||||||
|
if (top3.isNotEmpty) _podiumItem(top3[0], 1, 70, const Color(0xFFFFD700)),
|
||||||
|
if (top3.length > 2) _podiumItem(top3[2], 3, 35, const Color(0xFFCD7F32)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _podiumItem(LeaderboardEntry e, int rank, double height, Color color) {
|
||||||
|
return Padding(padding: const EdgeInsets.symmetric(horizontal: 8), child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
CircleAvatar(radius: rank == 1 ? 22 : 18, backgroundColor: color.withOpacity(0.3),
|
||||||
|
child: Text(_getRankEmoji(rank), style: TextStyle(fontSize: rank == 1 ? 22 : 16))),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(e.name.length > 8 ? '${e.name.substring(0, 8)}...' : e.name,
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: e.isCurrentUser ? FontWeight.bold : FontWeight.normal)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _tab(String label, IconData icon, int idx, LeaderboardController lc) {
|
||||||
|
final selected = lc.selectedTab == idx;
|
||||||
|
return Expanded(child: GestureDetector(
|
||||||
|
onTap: () => lc.changeTab(idx),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(color: selected ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: selected ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] : null),
|
||||||
|
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Icon(icon, size: 16, color: selected ? FinanceDesignSystem.primaryDark : Colors.grey),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: selected ? FinanceDesignSystem.primaryDark : Colors.grey)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(LeaderboardEntry e, bool isEarnings) {
|
||||||
|
final rankColor = e.rank == 1 ? const Color(0xFFFFD700) : e.rank == 2 ? const Color(0xFFC0C0C0) : e.rank == 3 ? const Color(0xFFCD7F32) : Colors.grey.shade400;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: e.isCurrentUser ? FinanceDesignSystem.accentBlue.withOpacity(0.08) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: e.isCurrentUser ? Border.all(color: FinanceDesignSystem.accentBlue.withOpacity(0.3), width: 1.5) : null,
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 3))],
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
// Rank
|
||||||
|
Container(width: 32, height: 32, decoration: BoxDecoration(color: rankColor.withOpacity(0.15), borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Center(child: e.rank <= 3
|
||||||
|
? Text(_getRankEmoji(e.rank), style: const TextStyle(fontSize: 16))
|
||||||
|
: Text('${e.rank}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: rankColor)))),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Avatar
|
||||||
|
CircleAvatar(radius: 18, backgroundColor: Colors.grey.shade200, child: Text(e.name.isNotEmpty ? e.name[0] : '?', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey.shade600))),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Name
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
Text(e.name, style: TextStyle(fontSize: 13, fontWeight: e.isCurrentUser ? FontWeight.bold : FontWeight.w500, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
if (e.isCurrentUser) ...[const SizedBox(width: 6),
|
||||||
|
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: FinanceDesignSystem.accentBlue, borderRadius: BorderRadius.circular(6)),
|
||||||
|
child: Text('You'.tr, style: const TextStyle(fontSize: 8, fontWeight: FontWeight.bold, color: Colors.white)))],
|
||||||
|
]),
|
||||||
|
])),
|
||||||
|
// Value
|
||||||
|
Text(isEarnings ? '${e.value.toStringAsFixed(0)} ${'SYP'.tr}' : '${e.value.toInt()} ${'Rides'.tr}',
|
||||||
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w800, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getRankEmoji(int rank) => rank == 1 ? '🥇' : rank == 2 ? '🥈' : '🥉';
|
||||||
|
}
|
||||||
248
lib/views/gamification/referral_center_page.dart
Normal file
248
lib/views/gamification/referral_center_page.dart
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import '../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../controller/gamification/referral_controller.dart';
|
||||||
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
|
|
||||||
|
class ReferralCenterPage extends StatelessWidget {
|
||||||
|
ReferralCenterPage({super.key});
|
||||||
|
final ReferralController controller = Get.put(ReferralController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
body: GetBuilder<ReferralController>(builder: (rc) {
|
||||||
|
if (rc.isLoading) return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.primaryDark));
|
||||||
|
return CustomScrollView(physics: const BouncingScrollPhysics(), slivers: [
|
||||||
|
// ═══ Header ═══
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 220, pinned: true,
|
||||||
|
backgroundColor: const Color(0xFF0A0E21),
|
||||||
|
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 20), onPressed: () => Get.back()),
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text('Referral Center'.tr, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
background: Stack(fit: StackFit.expand, children: [
|
||||||
|
Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF0A0E21), Color(0xFF311B92)]))),
|
||||||
|
Positioned(right: -50, bottom: -30, child: Icon(Icons.share_rounded, size: 200, color: Colors.white.withOpacity(0.03))),
|
||||||
|
// Stats in header
|
||||||
|
Positioned(bottom: 60, left: 24, right: 24, child: Row(children: [
|
||||||
|
_headerStat('${rc.totalReferrals}', 'Total Invites'.tr),
|
||||||
|
_headerStat('${rc.activeReferrals}', 'Active'.tr),
|
||||||
|
_headerStat('${rc.totalRewardsEarned.toStringAsFixed(0)}', 'Rewards'.tr),
|
||||||
|
])),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), sliver: SliverList(delegate: SliverChildListDelegate([
|
||||||
|
// ═══ Referral Code Card ═══
|
||||||
|
_buildCodeCard(rc),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// ═══ Share Buttons ═══
|
||||||
|
_buildShareSection(rc),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// ═══ How it Works ═══
|
||||||
|
_buildHowItWorks(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// ═══ Driver Referrals ═══
|
||||||
|
if (rc.driverReferrals.isNotEmpty) ...[
|
||||||
|
Text('Driver Invitations'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...rc.driverReferrals.map((r) => _referralItem(r, const Color(0xFF2196F3))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
// ═══ Passenger Referrals ═══
|
||||||
|
if (rc.passengerReferrals.isNotEmpty) ...[
|
||||||
|
Text('Passenger Invitations'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...rc.passengerReferrals.map((r) => _referralItem(r, const Color(0xFF4CAF50))),
|
||||||
|
],
|
||||||
|
if (rc.driverReferrals.isEmpty && rc.passengerReferrals.isEmpty)
|
||||||
|
_buildEmptyState(),
|
||||||
|
]))),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _headerStat(String value, String label) {
|
||||||
|
return Expanded(child: Column(children: [
|
||||||
|
Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: Colors.white)),
|
||||||
|
Text(label, style: TextStyle(fontSize: 10, color: Colors.white.withOpacity(0.6))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCodeCard(ReferralController rc) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(colors: [Color(0xFF1A237E), Color(0xFF311B92)]),
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [BoxShadow(color: const Color(0xFF311B92).withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 8))],
|
||||||
|
),
|
||||||
|
child: Column(children: [
|
||||||
|
Text('Your Referral Code'.tr, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.2))),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Text(rc.referralCode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () { rc.copyCode(); mySnackbarSuccess('Code copied!'.tr); },
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: const Icon(Icons.copy_rounded, color: Colors.white, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Share this code to earn rewards'.tr, style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShareSection(ReferralController rc) {
|
||||||
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('Share via'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(children: [
|
||||||
|
_shareButton(Icons.share_rounded, 'Share'.tr, FinanceDesignSystem.accentBlue, () => Share.share(rc.shareMessage)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_shareButton(Icons.chat_rounded, 'WhatsApp'.tr, const Color(0xFF25D366), () => _shareWhatsApp(rc.shareMessage)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_shareButton(Icons.person_add_rounded, 'Invite Driver'.tr, const Color(0xFFFF9800), () => _showInviteDialog(rc, 'driver')),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_shareButton(Icons.hail_rounded, 'Invite Rider'.tr, const Color(0xFF9C27B0), () => _showInviteDialog(rc, 'passenger')),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _shareButton(IconData icon, String label, Color color, VoidCallback onTap) {
|
||||||
|
return Expanded(child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(14)),
|
||||||
|
child: Column(children: [
|
||||||
|
Icon(icon, color: color, size: 24),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHowItWorks() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))]),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('How It Works'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_step('1', 'Share your code'.tr, 'Send your referral code to friends'.tr, const Color(0xFF2196F3)),
|
||||||
|
_step('2', 'Friend signs up'.tr, 'They register using your code'.tr, const Color(0xFFFF9800)),
|
||||||
|
_step('3', 'Both earn rewards'.tr, 'You get 100 pts, they get 50 pts'.tr, const Color(0xFF4CAF50)),
|
||||||
|
_step('4', 'Bonus at 10 trips'.tr, 'Extra 200 pts when they complete 10 trips'.tr, const Color(0xFF9C27B0)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _step(String num, String title, String desc, Color color) {
|
||||||
|
return Padding(padding: const EdgeInsets.only(bottom: 12), child: Row(children: [
|
||||||
|
Container(width: 32, height: 32, decoration: BoxDecoration(color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Center(child: Text(num, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: color))),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
Text(desc, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||||
|
])),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _referralItem(ReferralRecord r, Color color) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 8, offset: const Offset(0, 3))]),
|
||||||
|
child: Row(children: [
|
||||||
|
Container(width: 40, height: 40, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(r.type == 'driver' ? Icons.local_taxi_rounded : Icons.person_rounded, color: color, size: 20)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(r.name.isNotEmpty ? r.name : r.phone, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||||
|
Text(r.joinDate, style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
|
||||||
|
])),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: r.status == 'active' ? FinanceDesignSystem.successGreen.withOpacity(0.1)
|
||||||
|
: r.status == 'registered' ? FinanceDesignSystem.accentBlue.withOpacity(0.1) : Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Text(r.status.tr, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600,
|
||||||
|
color: r.status == 'active' ? FinanceDesignSystem.successGreen
|
||||||
|
: r.status == 'registered' ? FinanceDesignSystem.accentBlue : Colors.grey)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Column(children: [
|
||||||
|
Icon(Icons.people_outline_rounded, size: 60, color: Colors.grey.shade300),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('No invitation found yet!'.tr, style: TextStyle(fontSize: 14, color: Colors.grey.shade400)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Start sharing your code!'.tr, style: TextStyle(fontSize: 12, color: Colors.grey.shade300)),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _shareWhatsApp(String message) async {
|
||||||
|
final url = 'https://wa.me/?text=${Uri.encodeComponent(message)}';
|
||||||
|
if (await canLaunchUrl(Uri.parse(url))) {
|
||||||
|
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showInviteDialog(ReferralController rc, String type) {
|
||||||
|
final phoneController = TextEditingController();
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: type == 'driver' ? 'Invite Driver'.tr : 'Invite Rider'.tr,
|
||||||
|
titleStyle: FinanceDesignSystem.headingStyle,
|
||||||
|
content: Column(children: [
|
||||||
|
Text('Enter phone number'.tr, style: FinanceDesignSystem.subHeadingStyle),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: phoneController, keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(hintText: '09XX XXX XXX', prefixIcon: const Icon(Icons.phone_rounded),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12))),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
confirm: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (phoneController.text.length >= 10) {
|
||||||
|
Get.back();
|
||||||
|
bool success = type == 'driver'
|
||||||
|
? await rc.inviteDriver(phoneController.text)
|
||||||
|
: await rc.invitePassenger(phoneController.text);
|
||||||
|
if (success) mySnackbarSuccess('Invite sent successfully'.tr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: FinanceDesignSystem.accentBlue, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12)),
|
||||||
|
child: Text('Send Invite'.tr),
|
||||||
|
),
|
||||||
|
cancel: TextButton(onPressed: () => Get.back(), child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@ import 'package:sefer_driver/main.dart';
|
|||||||
import 'package:sefer_driver/views/Rate/rate_app_page.dart';
|
import 'package:sefer_driver/views/Rate/rate_app_page.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
|
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/invite_driver_screen.dart';
|
import 'package:sefer_driver/views/auth/captin/invite_driver_screen.dart';
|
||||||
|
import 'package:sefer_driver/views/home/statistics/statistics_dashboard.dart';
|
||||||
|
import 'package:sefer_driver/views/gamification/challenges_page.dart';
|
||||||
|
import 'package:sefer_driver/views/gamification/leaderboard_page.dart';
|
||||||
|
import 'package:sefer_driver/views/gamification/referral_center_page.dart';
|
||||||
|
import 'package:sefer_driver/views/home/journal/schedule_page.dart';
|
||||||
import 'package:sefer_driver/views/notification/available_rides_page.dart';
|
import 'package:sefer_driver/views/notification/available_rides_page.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/logout_captain.dart';
|
import 'package:sefer_driver/views/auth/captin/logout_captain.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/history/history_captain.dart';
|
import 'package:sefer_driver/views/home/Captin/history/history_captain.dart';
|
||||||
@@ -55,6 +60,26 @@ class AppDrawer extends StatelessWidget {
|
|||||||
icon: Icons.account_balance_wallet,
|
icon: Icons.account_balance_wallet,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
onTap: () => Get.to(() => WalletCaptainRefactored())),
|
onTap: () => Get.to(() => WalletCaptainRefactored())),
|
||||||
|
DrawerItem(
|
||||||
|
title: 'Statistics'.tr,
|
||||||
|
icon: Icons.bar_chart_rounded,
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
onTap: () => Get.to(() => StatisticsDashboard())),
|
||||||
|
DrawerItem(
|
||||||
|
title: 'Challenges'.tr,
|
||||||
|
icon: Icons.bolt_rounded,
|
||||||
|
color: Colors.amber,
|
||||||
|
onTap: () => Get.to(() => ChallengesPage())),
|
||||||
|
DrawerItem(
|
||||||
|
title: 'My Schedule'.tr,
|
||||||
|
icon: Icons.calendar_today_rounded,
|
||||||
|
color: Colors.teal,
|
||||||
|
onTap: () => Get.to(() => SchedulePage())),
|
||||||
|
DrawerItem(
|
||||||
|
title: 'Leaderboard'.tr,
|
||||||
|
icon: Icons.leaderboard_rounded,
|
||||||
|
color: Colors.red,
|
||||||
|
onTap: () => Get.to(() => LeaderboardPage())),
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
title: 'Profile'.tr,
|
title: 'Profile'.tr,
|
||||||
icon: Icons.person,
|
icon: Icons.person,
|
||||||
@@ -81,10 +106,10 @@ class AppDrawer extends StatelessWidget {
|
|||||||
color: Colors.cyan,
|
color: Colors.cyan,
|
||||||
onTap: () => Get.to(() => HelpCaptain())),
|
onTap: () => Get.to(() => HelpCaptain())),
|
||||||
DrawerItem(
|
DrawerItem(
|
||||||
title: 'Share App'.tr,
|
title: 'Referral Center'.tr,
|
||||||
icon: Icons.share,
|
icon: Icons.card_giftcard_rounded,
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
onTap: () => Get.to(() => InviteScreen())),
|
onTap: () => Get.to(() => ReferralCenterPage())),
|
||||||
// DrawerItem(
|
// DrawerItem(
|
||||||
// title: 'Maintenance Center'.tr,
|
// title: 'Maintenance Center'.tr,
|
||||||
// icon: Icons.build,
|
// icon: Icons.build,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:bubble_head/bubble.dart';
|
import 'package:bubble_head/bubble.dart';
|
||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
import 'package:sefer_driver/constant/api_key.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -13,8 +12,6 @@ import 'package:sefer_driver/views/widgets/mycircular.dart';
|
|||||||
import '../../../../constant/box_name.dart';
|
import '../../../../constant/box_name.dart';
|
||||||
import '../../../../constant/colors.dart';
|
import '../../../../constant/colors.dart';
|
||||||
import '../../../../constant/info.dart';
|
import '../../../../constant/info.dart';
|
||||||
import '../../../../constant/style.dart';
|
|
||||||
import '../../../../controller/functions/location_background_controller.dart';
|
|
||||||
import '../../../../controller/functions/location_controller.dart';
|
import '../../../../controller/functions/location_controller.dart';
|
||||||
import '../../../../controller/functions/overlay_permisssion.dart';
|
import '../../../../controller/functions/overlay_permisssion.dart';
|
||||||
import '../../../../controller/functions/package_info.dart';
|
import '../../../../controller/functions/package_info.dart';
|
||||||
@@ -84,9 +81,13 @@ class HomeCaptain extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
await closeOverlayIfFound();
|
await closeOverlayIfFound();
|
||||||
|
if (!context.mounted) return;
|
||||||
await checkForUpdate(context);
|
await checkForUpdate(context);
|
||||||
|
if (!context.mounted) return;
|
||||||
await getPermissionOverlay();
|
await getPermissionOverlay();
|
||||||
|
if (!context.mounted) return;
|
||||||
await showDriverGiftClaim(context);
|
await showDriverGiftClaim(context);
|
||||||
|
if (!context.mounted) return;
|
||||||
await checkForAppliedRide(context);
|
await checkForAppliedRide(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
125
lib/views/home/journal/schedule_page.dart
Normal file
125
lib/views/home/journal/schedule_page.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../controller/home/journal/schedule_controller.dart';
|
||||||
|
|
||||||
|
class SchedulePage extends StatelessWidget {
|
||||||
|
SchedulePage({super.key});
|
||||||
|
final ScheduleController controller = Get.put(ScheduleController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('My Schedule'.tr, style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
backgroundColor: Colors.transparent, elevation: 0, centerTitle: true,
|
||||||
|
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20), onPressed: () => Get.back()),
|
||||||
|
),
|
||||||
|
body: GetBuilder<ScheduleController>(builder: (sc) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
// Summary Card
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: FinanceDesignSystem.balanceGradient,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('Weekly Plan'.tr, style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('${sc.totalWeeklyHours.toStringAsFixed(1)}h', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w900, color: Colors.white)),
|
||||||
|
Text('${sc.activeDays} ${'Days'.tr}', style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 13)),
|
||||||
|
])),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(14)),
|
||||||
|
child: const Icon(Icons.calendar_today_rounded, color: Colors.white, size: 28),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Text('Work Days'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
...sc.schedule.map((slot) => _buildDayCard(context, slot, sc)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDayCard(BuildContext context, WorkSlot slot, ScheduleController sc) {
|
||||||
|
final isAr = Get.locale?.languageCode == 'ar';
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: slot.isActive ? Colors.white : Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: slot.isActive ? [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 8, offset: const Offset(0, 3))] : null,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
// Toggle
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => sc.toggleDay(slot.dayOfWeek),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: 44, height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: slot.isActive ? FinanceDesignSystem.accentBlue.withOpacity(0.1) : Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Center(child: Text(
|
||||||
|
isAr ? slot.dayNameAr.substring(0, 2) : slot.dayName,
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold,
|
||||||
|
color: slot.isActive ? FinanceDesignSystem.accentBlue : Colors.grey.shade400),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
// Day name
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(isAr ? slot.dayNameAr : slot.dayName.tr, style: TextStyle(
|
||||||
|
fontSize: 14, fontWeight: FontWeight.w600,
|
||||||
|
color: slot.isActive ? FinanceDesignSystem.primaryDark : Colors.grey.shade400)),
|
||||||
|
if (slot.isActive)
|
||||||
|
Text(slot.timeRange, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
||||||
|
if (!slot.isActive)
|
||||||
|
Text('Day Off'.tr, style: TextStyle(fontSize: 12, color: Colors.grey.shade400, fontStyle: FontStyle.italic)),
|
||||||
|
])),
|
||||||
|
// Time pickers
|
||||||
|
if (slot.isActive) ...[
|
||||||
|
_timePicker(context, slot.startTime, (t) => sc.updateStartTime(slot.dayOfWeek, t)),
|
||||||
|
Padding(padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('-', style: TextStyle(color: Colors.grey.shade400))),
|
||||||
|
_timePicker(context, slot.endTime, (t) => sc.updateEndTime(slot.dayOfWeek, t)),
|
||||||
|
],
|
||||||
|
// Toggle switch
|
||||||
|
Switch(
|
||||||
|
value: slot.isActive,
|
||||||
|
onChanged: (_) => sc.toggleDay(slot.dayOfWeek),
|
||||||
|
activeColor: FinanceDesignSystem.accentBlue,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _timePicker(BuildContext context, TimeOfDay time, Function(TimeOfDay) onChanged) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final picked = await showTimePicker(context: context, initialTime: time);
|
||||||
|
if (picked != null) onChanged(picked);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Text('${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +1,176 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/finance_design_system.dart';
|
||||||
import '../../../controller/home/captin/behavior_controller.dart';
|
import '../../../controller/home/captin/behavior_controller.dart';
|
||||||
|
|
||||||
class BehaviorPage extends StatelessWidget {
|
class BehaviorPage extends StatelessWidget {
|
||||||
const BehaviorPage({
|
const BehaviorPage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = Get.put(DriverBehaviorController());
|
final controller = Get.put(DriverBehaviorController());
|
||||||
controller.fetchDriverBehavior();
|
controller.fetchDriverBehavior();
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Driver Behavior'.tr),
|
title: Text('Driver Behavior'.tr, style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
backgroundColor: FinanceDesignSystem.cardColor,
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: const IconThemeData(color: FinanceDesignSystem.primaryDark),
|
||||||
),
|
),
|
||||||
body: Obx(() {
|
body: Obx(() {
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator(color: FinanceDesignSystem.accentBlue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
double score = controller.overallScore.value;
|
||||||
padding: const EdgeInsets.all(16),
|
bool isExcellent = score >= 90;
|
||||||
child: Column(
|
bool isGood = score >= 75 && score < 90;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Color statusColor = isExcellent ? Colors.green : (isGood ? Colors.orange : Colors.red);
|
||||||
children: [
|
String statusText = isExcellent ? 'Excellent'.tr : (isGood ? 'Good'.tr : 'Needs Improvement'.tr);
|
||||||
Card(
|
|
||||||
shape: RoundedRectangleBorder(
|
return CustomScrollView(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
slivers: [
|
||||||
elevation: 4,
|
SliverPadding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(FinanceDesignSystem.horizontalPadding),
|
||||||
padding: const EdgeInsets.all(16),
|
sliver: SliverList(
|
||||||
child: Column(
|
delegate: SliverChildListDelegate([
|
||||||
children: [
|
// Overall Score Card
|
||||||
Text("Overall Behavior Score".tr,
|
Container(
|
||||||
style: theme.textTheme.titleLarge
|
padding: const EdgeInsets.all(24),
|
||||||
?.copyWith(fontWeight: FontWeight.bold)),
|
decoration: BoxDecoration(
|
||||||
const SizedBox(height: 10),
|
color: FinanceDesignSystem.cardColor,
|
||||||
Text(
|
borderRadius: BorderRadius.circular(24),
|
||||||
"${controller.overallScore.value.toStringAsFixed(1)} / 100",
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.shield_rounded, size: 48, color: statusColor),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Overall Behavior Score".tr,
|
||||||
|
style: FinanceDesignSystem.headingStyle,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"${score.toStringAsFixed(1)} / 100",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColor.primaryColor)),
|
color: statusColor,
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(color: statusColor, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 30),
|
||||||
|
Text("Last 10 Trips".tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
Text("Last 10 Trips".tr,
|
SliverPadding(
|
||||||
style: theme.textTheme.titleMedium
|
padding: const EdgeInsets.symmetric(horizontal: FinanceDesignSystem.horizontalPadding),
|
||||||
?.copyWith(fontWeight: FontWeight.bold)),
|
sliver: SliverList(
|
||||||
const SizedBox(height: 10),
|
delegate: SliverChildBuilderDelegate(
|
||||||
ListView.builder(
|
(context, index) {
|
||||||
shrinkWrap: true,
|
var trip = controller.lastTrips[index];
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
double tripScore = double.tryParse(trip['behavior_score'].toString()) ?? 0;
|
||||||
itemCount: controller.lastTrips.length,
|
Color tColor = tripScore >= 90 ? Colors.green : (tripScore >= 75 ? Colors.orange : Colors.red);
|
||||||
itemBuilder: (context, index) {
|
|
||||||
var trip = controller.lastTrips[index];
|
return Container(
|
||||||
return Card(
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
elevation: 3,
|
decoration: BoxDecoration(
|
||||||
child: ListTile(
|
color: FinanceDesignSystem.cardColor,
|
||||||
leading: CircleAvatar(
|
borderRadius: BorderRadius.circular(16),
|
||||||
backgroundColor: AppColor.primaryColor,
|
boxShadow: [
|
||||||
child: Text("${index + 1}",
|
BoxShadow(
|
||||||
style: const TextStyle(color: Colors.white)),
|
color: Colors.black.withValues(alpha: 0.02),
|
||||||
),
|
blurRadius: 10,
|
||||||
title: Text("Trip ID: ${trip['trip_id']}"),
|
offset: const Offset(0, 4),
|
||||||
subtitle: Column(
|
)
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"${'Behavior Score'.tr}: ${trip['behavior_score']}"),
|
|
||||||
Text("${'Max Speed'.tr}: ${trip['max_speed']} km/h"),
|
|
||||||
Text("${'Hard Brake'.tr}s: ${trip['hard_brakes']}"),
|
|
||||||
Text(
|
|
||||||
"${'Distance'.tr}: ${trip['total_distance']} km"),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
child: ListTile(
|
||||||
);
|
contentPadding: const EdgeInsets.all(16),
|
||||||
},
|
leading: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tColor.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.drive_eta_rounded, color: tColor),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Trip ID: ${trip['trip_id']}",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
_buildBadge(Icons.speed, "${trip['max_speed']} km/h", Colors.blue),
|
||||||
|
_buildBadge(Icons.warning_amber_rounded, "${trip['hard_brakes']} ${'Hard Brakes'.tr}", Colors.orange),
|
||||||
|
_buildBadge(Icons.map_rounded, "${trip['total_distance']} km", Colors.purple),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${tripScore.toStringAsFixed(0)}",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: tColor),
|
||||||
|
),
|
||||||
|
Text('Score'.tr, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: controller.lastTrips.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBadge(IconData icon, String label, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: color),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(label, style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
498
lib/views/home/statistics/statistics_dashboard.dart
Normal file
498
lib/views/home/statistics/statistics_dashboard.dart
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/constant/finance_design_system.dart';
|
||||||
|
import 'package:sefer_driver/controller/gamification/gamification_controller.dart';
|
||||||
|
import 'package:sefer_driver/controller/home/statistics/statistics_controller.dart';
|
||||||
|
|
||||||
|
import 'widgets/stat_summary_card.dart';
|
||||||
|
import 'widgets/daily_goal_widget.dart';
|
||||||
|
import 'widgets/level_progress_widget.dart';
|
||||||
|
import 'widgets/today_chart_widget.dart';
|
||||||
|
import 'widgets/weekly_chart_widget.dart';
|
||||||
|
import 'widgets/monthly_chart_widget.dart';
|
||||||
|
|
||||||
|
class StatisticsDashboard extends StatelessWidget {
|
||||||
|
StatisticsDashboard({super.key});
|
||||||
|
|
||||||
|
final GamificationController gamController =
|
||||||
|
Get.put(GamificationController());
|
||||||
|
final StatisticsController statsController =
|
||||||
|
Get.put(StatisticsController());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
body: GetBuilder<GamificationController>(
|
||||||
|
builder: (gc) {
|
||||||
|
if (gc.isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: FinanceDesignSystem.primaryDark),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
slivers: [
|
||||||
|
// ═══════ App Bar ═══════
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 200,
|
||||||
|
pinned: true,
|
||||||
|
stretch: true,
|
||||||
|
backgroundColor: FinanceDesignSystem.primaryDark,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||||
|
color: Colors.white, size: 20),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh_rounded,
|
||||||
|
color: Colors.white),
|
||||||
|
onPressed: () => gc.fetchGamificationData(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text(
|
||||||
|
'Statistics'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
background: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: FinanceDesignSystem.balanceGradient,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// أيقونة زخرفية
|
||||||
|
Positioned(
|
||||||
|
right: -40,
|
||||||
|
top: -20,
|
||||||
|
child: Icon(
|
||||||
|
Icons.bar_chart_rounded,
|
||||||
|
size: 200,
|
||||||
|
color: Colors.white.withOpacity(0.04),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: -30,
|
||||||
|
bottom: 20,
|
||||||
|
child: Icon(
|
||||||
|
Icons.emoji_events_rounded,
|
||||||
|
size: 120,
|
||||||
|
color: Colors.white.withOpacity(0.03),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// بطاقة المستوى في الهيدر
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Text(
|
||||||
|
gc.currentLevel.emoji,
|
||||||
|
style: const TextStyle(fontSize: 40),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
Get.locale?.languageCode == 'ar'
|
||||||
|
? gc.currentLevel.nameAr
|
||||||
|
: gc.currentLevel.nameEn,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${gc.unlockedCount}/${gc.totalAchievements} ${'Achievements'.tr}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.5),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ═══════ المحتوى ═══════
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
// 1. بطاقات الإحصائيات الأربعة
|
||||||
|
_buildSummaryCards(gc),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 2. الهدف اليومي
|
||||||
|
DailyGoalWidget(controller: gc),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 3. تقدم المستوى
|
||||||
|
LevelProgressWidget(controller: gc),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 4. إحصائيات اليوم
|
||||||
|
TodayChartWidget(),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 5. الرسم البياني الأسبوعي
|
||||||
|
const WeeklyChartWidget(),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 6. التقرير الشهري
|
||||||
|
const MonthlyChartWidget(),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 7. تقييم سلوك القيادة
|
||||||
|
_buildBehaviorSection(gc),
|
||||||
|
const SizedBox(
|
||||||
|
height: FinanceDesignSystem.verticalSectionPadding),
|
||||||
|
|
||||||
|
// 8. الإنجازات
|
||||||
|
_buildAchievementsSection(gc),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════ بطاقات الإحصائيات ═══════
|
||||||
|
Widget _buildSummaryCards(GamificationController gc) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.5,
|
||||||
|
children: [
|
||||||
|
StatSummaryCard(
|
||||||
|
icon: Icons.local_taxi_rounded,
|
||||||
|
label: 'Total Trips'.tr,
|
||||||
|
value: gc.totalTrips.toString(),
|
||||||
|
color: FinanceDesignSystem.accentBlue,
|
||||||
|
),
|
||||||
|
StatSummaryCard(
|
||||||
|
icon: Icons.star_rounded,
|
||||||
|
label: 'Rating'.tr,
|
||||||
|
value: gc.averageRating.toStringAsFixed(1),
|
||||||
|
color: const Color(0xFFFFD700),
|
||||||
|
),
|
||||||
|
StatSummaryCard(
|
||||||
|
icon: Icons.whatshot_rounded,
|
||||||
|
label: 'Day Streak'.tr,
|
||||||
|
value: '${gc.consecutiveDays}',
|
||||||
|
color: const Color(0xFFFF5722),
|
||||||
|
),
|
||||||
|
StatSummaryCard(
|
||||||
|
icon: Icons.people_rounded,
|
||||||
|
label: 'Referrals'.tr,
|
||||||
|
value: gc.totalReferrals.toString(),
|
||||||
|
color: const Color(0xFF9C27B0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════ قسم تقييم السلوك ═══════
|
||||||
|
Widget _buildBehaviorSection(GamificationController gc) {
|
||||||
|
bool isExcellent = gc.behaviorScore >= 90;
|
||||||
|
bool isGood = gc.behaviorScore >= 75 && gc.behaviorScore < 90;
|
||||||
|
|
||||||
|
Color statusColor = isExcellent
|
||||||
|
? Colors.green
|
||||||
|
: isGood ? Colors.orange : Colors.red;
|
||||||
|
|
||||||
|
String statusText = isExcellent
|
||||||
|
? 'Excellent'.tr
|
||||||
|
: isGood ? 'Good'.tr : 'Needs Improvement'.tr;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FinanceDesignSystem.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shield_rounded, color: FinanceDesignSystem.accentBlue),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Driving Behavior'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_behaviorStatItem(
|
||||||
|
title: 'Score'.tr,
|
||||||
|
value: '${gc.behaviorScore}%',
|
||||||
|
icon: Icons.speed_rounded,
|
||||||
|
color: FinanceDesignSystem.accentBlue,
|
||||||
|
),
|
||||||
|
_behaviorStatItem(
|
||||||
|
title: 'Max Speed'.tr,
|
||||||
|
value: '${gc.maxSpeed.toStringAsFixed(0)} km/h',
|
||||||
|
icon: Icons.directions_car_rounded,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
_behaviorStatItem(
|
||||||
|
title: 'Hard Brakes'.tr,
|
||||||
|
value: '${gc.hardBrakes}',
|
||||||
|
icon: Icons.warning_rounded,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: gc.behaviorScore / 100,
|
||||||
|
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(statusColor),
|
||||||
|
minHeight: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _behaviorStatItem({required String title, required String value, required IconData icon, required Color color}) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
Text(title, style: const TextStyle(fontSize: 12, color: FinanceDesignSystem.textSecondary)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════ قسم الإنجازات ═══════
|
||||||
|
Widget _buildAchievementsSection(GamificationController gc) {
|
||||||
|
final unlockedAch =
|
||||||
|
gc.achievements.where((a) => a.isUnlocked).toList();
|
||||||
|
final lockedAch =
|
||||||
|
gc.achievements.where((a) => !a.isUnlocked).toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Achievements'.tr, style: FinanceDesignSystem.headingStyle),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${gc.unlockedCount}/${gc.totalAchievements}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: FinanceDesignSystem.accentBlue,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// الإنجازات المفتوحة
|
||||||
|
if (unlockedAch.isNotEmpty) ...[
|
||||||
|
...unlockedAch.map((a) => _buildAchievementCard(a, true)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// الإنجازات المقفلة
|
||||||
|
...lockedAch.map((a) => _buildAchievementCard(a, false)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAchievementCard(Achievement ach, bool unlocked) {
|
||||||
|
final isAr = Get.locale?.languageCode == 'ar';
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: unlocked ? Colors.white : Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
border: unlocked
|
||||||
|
? Border.all(color: ach.color.withOpacity(0.3), width: 1.5)
|
||||||
|
: null,
|
||||||
|
boxShadow: unlocked
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: ach.color.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// أيقونة الإنجاز
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: unlocked
|
||||||
|
? ach.color.withOpacity(0.15)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
ach.icon,
|
||||||
|
color: unlocked ? ach.color : Colors.grey.shade400,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
|
||||||
|
// المعلومات
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isAr ? ach.titleAr : ach.titleEn,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
color: unlocked
|
||||||
|
? FinanceDesignSystem.primaryDark
|
||||||
|
: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
isAr ? ach.descriptionAr : ach.descriptionEn,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!unlocked) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// شريط التقدم
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: ach.progress,
|
||||||
|
child: Container(
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ach.color.withOpacity(0.6),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${ach.currentProgress}/${ach.target}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// أيقونة الحالة
|
||||||
|
if (unlocked)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ach.color.withOpacity(0.15),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_rounded,
|
||||||
|
color: ach.color,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
307
lib/views/home/statistics/widgets/daily_goal_widget.dart
Normal file
307
lib/views/home/statistics/widgets/daily_goal_widget.dart
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../../controller/gamification/gamification_controller.dart';
|
||||||
|
|
||||||
|
class DailyGoalWidget extends StatelessWidget {
|
||||||
|
final GamificationController controller;
|
||||||
|
|
||||||
|
const DailyGoalWidget({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: controller.isDailyGoalMet
|
||||||
|
? FinanceDesignSystem.successGreen.withOpacity(0.1)
|
||||||
|
: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
controller.isDailyGoalMet
|
||||||
|
? Icons.check_circle_rounded
|
||||||
|
: Icons.track_changes_rounded,
|
||||||
|
color: controller.isDailyGoalMet
|
||||||
|
? FinanceDesignSystem.successGreen
|
||||||
|
: FinanceDesignSystem.accentBlue,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Daily Goal'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.primaryDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'الهدف اليومي'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// زر تعديل الهدف
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _showSetGoalDialog(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
controller.dailyGoal > 0
|
||||||
|
? 'Edit'.tr
|
||||||
|
: 'Set Goal'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: FinanceDesignSystem.accentBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// مبلغ الأرباح والهدف
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.dailyEarnings.toStringAsFixed(0),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: controller.isDailyGoalMet
|
||||||
|
? FinanceDesignSystem.successGreen
|
||||||
|
: FinanceDesignSystem.primaryDark,
|
||||||
|
fontFamily: 'digit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'/ ${controller.dailyGoal.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// شريط التقدم
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
height: 12,
|
||||||
|
width: constraints.maxWidth * controller.dailyGoalProgress,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: controller.isDailyGoalMet
|
||||||
|
? [
|
||||||
|
FinanceDesignSystem.successGreen,
|
||||||
|
const Color(0xFF69F0AE)
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
FinanceDesignSystem.accentBlue,
|
||||||
|
const Color(0xFF82B1FF)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: (controller.isDailyGoalMet
|
||||||
|
? FinanceDesignSystem.successGreen
|
||||||
|
: FinanceDesignSystem.accentBlue)
|
||||||
|
.withOpacity(0.3),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${(controller.dailyGoalProgress * 100).toStringAsFixed(0)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: controller.isDailyGoalMet
|
||||||
|
? FinanceDesignSystem.successGreen
|
||||||
|
: FinanceDesignSystem.accentBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (controller.isDailyGoalMet)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.celebration_rounded,
|
||||||
|
color: Color(0xFFFFD700), size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Goal Achieved!'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: FinanceDesignSystem.successGreen,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'${'Remaining:'.tr} ${(controller.dailyGoal - controller.dailyEarnings).clamp(0, double.infinity).toStringAsFixed(0)} ${'SYP'.tr}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSetGoalDialog(BuildContext context) {
|
||||||
|
final textController = TextEditingController(
|
||||||
|
text: controller.dailyGoal > 0
|
||||||
|
? controller.dailyGoal.toStringAsFixed(0)
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Set Daily Goal'.tr,
|
||||||
|
titleStyle: FinanceDesignSystem.headingStyle,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'How much do you want to earn today?'.tr,
|
||||||
|
style: FinanceDesignSystem.subHeadingStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: textController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'digit',
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '5000',
|
||||||
|
suffixText: 'SYP'.tr,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide:
|
||||||
|
BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: FinanceDesignSystem.accentBlue),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// الأهداف السريعة
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: [1000, 3000, 5000, 10000].map((goal) {
|
||||||
|
return ActionChip(
|
||||||
|
label: Text('$goal'),
|
||||||
|
labelStyle: const TextStyle(fontSize: 12),
|
||||||
|
backgroundColor: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||||
|
side: BorderSide.none,
|
||||||
|
onPressed: () {
|
||||||
|
textController.text = goal.toString();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
confirm: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
double? goal = double.tryParse(textController.text);
|
||||||
|
if (goal != null && goal > 0) {
|
||||||
|
controller.setDailyGoal(goal);
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: FinanceDesignSystem.accentBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||||
|
),
|
||||||
|
child: Text('Save'.tr),
|
||||||
|
),
|
||||||
|
cancel: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: Text('Cancel'.tr,
|
||||||
|
style: const TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/views/home/statistics/widgets/level_progress_widget.dart
Normal file
93
lib/views/home/statistics/widgets/level_progress_widget.dart
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../../controller/gamification/gamification_controller.dart';
|
||||||
|
|
||||||
|
class LevelProgressWidget extends StatelessWidget {
|
||||||
|
final GamificationController controller;
|
||||||
|
const LevelProgressWidget({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final level = controller.currentLevel;
|
||||||
|
final next = controller.nextLevel;
|
||||||
|
final isAr = Get.locale?.languageCode == 'ar';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [level.color.withOpacity(0.08), level.gradientEnd.withOpacity(0.04)],
|
||||||
|
begin: Alignment.topLeft, end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
border: Border.all(color: level.color.withOpacity(0.2), width: 1.5),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Driver Level'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: [level.color, level.gradientEnd]),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Text(level.emoji, style: const TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(isAr ? level.nameAr : level.nameEn, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: List.generate(DriverLevels.all.length, (i) {
|
||||||
|
final lvl = DriverLevels.all[i];
|
||||||
|
final isCurrent = lvl.id == level.id;
|
||||||
|
final isPast = DriverLevels.all.indexOf(level) > i;
|
||||||
|
return Expanded(child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2), height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCurrent ? lvl.color : isPast ? lvl.color.withOpacity(0.6) : Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(children: DriverLevels.all.map((l) => Expanded(child: Text(l.emoji, textAlign: TextAlign.center, style: TextStyle(fontSize: l.id == level.id ? 16 : 12)))).toList()),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (next != null) ...[
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Text('${'Next Level:'.tr} ${isAr ? next.nameAr : next.nameEn}', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.grey.shade700)),
|
||||||
|
Text('${(controller.progressToNext * 100).toStringAsFixed(0)}%', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: level.color)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Stack(children: [
|
||||||
|
Container(height: 10, decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(5))),
|
||||||
|
LayoutBuilder(builder: (ctx, c) => AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 800), curve: Curves.easeOutCubic, height: 10,
|
||||||
|
width: c.maxWidth * controller.progressToNext,
|
||||||
|
decoration: BoxDecoration(gradient: LinearGradient(colors: [level.color, level.gradientEnd]), borderRadius: BorderRadius.circular(5)),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text('${controller.totalPoints} / ${next.minPoints} ${'Points'.tr}', style: TextStyle(fontSize: 11, color: Colors.grey.shade500, fontFamily: 'digit')),
|
||||||
|
] else
|
||||||
|
Center(child: Text('🏆 ${'Maximum Level Reached!'.tr}', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: level.color))),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Wrap(spacing: 6, runSpacing: 6, children: level.perks.map((p) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(color: level.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Text(p.tr, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: level.color.withOpacity(0.8))),
|
||||||
|
)).toList()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
lib/views/home/statistics/widgets/monthly_chart_widget.dart
Normal file
99
lib/views/home/statistics/widgets/monthly_chart_widget.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||||
|
|
||||||
|
class MonthlyChartWidget extends StatelessWidget {
|
||||||
|
const MonthlyChartWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<StatisticsController>(
|
||||||
|
builder: (sc) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))],
|
||||||
|
),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('Monthly Report'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Summary Row
|
||||||
|
Row(children: [
|
||||||
|
_summaryTile('Total Earnings'.tr, '${sc.monthlyTotalEarnings.toStringAsFixed(0)} ${'SYP'.tr}', FinanceDesignSystem.successGreen),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_summaryTile('Total Trips'.tr, '${sc.monthlyTotalTrips}', FinanceDesignSystem.accentBlue),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_summaryTile('Best Day'.tr, '${sc.bestDay}', const Color(0xFFFFD700)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Monthly Earnings Line Chart
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: sc.monthlyEarnings.isEmpty
|
||||||
|
? Center(child: Text('No data yet'.tr, style: TextStyle(color: Colors.grey.shade400)))
|
||||||
|
: LineChart(LineChartData(
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
enabled: true,
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
getTooltipItems: (spots) => spots.map((s) => LineTooltipItem(
|
||||||
|
'${s.y.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||||
|
const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
|
)).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
gridData: FlGridData(show: true, drawVerticalLine: false,
|
||||||
|
getDrawingHorizontalLine: (v) => FlLine(color: Colors.grey.shade100, strokeWidth: 1),
|
||||||
|
),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, interval: 5,
|
||||||
|
getTitlesWidget: (v, m) => Padding(padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Text('${v.toInt()}', style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
spots: sc.monthlyEarnings.map((e) => FlSpot(e.day.toDouble(), e.pricePerDay)).toList(),
|
||||||
|
isCurved: true, curveSmoothness: 0.3,
|
||||||
|
color: FinanceDesignSystem.accentBlue, barWidth: 3,
|
||||||
|
dotData: FlDotData(show: true, getDotPainter: (s, p, d, i) =>
|
||||||
|
FlDotCirclePainter(radius: 3, color: FinanceDesignSystem.accentBlue, strokeWidth: 1, strokeColor: Colors.white),
|
||||||
|
),
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter,
|
||||||
|
colors: [FinanceDesignSystem.accentBlue.withOpacity(0.2), FinanceDesignSystem.accentBlue.withOpacity(0.0)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _summaryTile(String label, String value, Color color) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Column(children: [
|
||||||
|
Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: color)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(label, style: TextStyle(fontSize: 9, color: Colors.grey.shade600), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/views/home/statistics/widgets/stat_summary_card.dart
Normal file
83
lib/views/home/statistics/widgets/stat_summary_card.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
|
||||||
|
class StatSummaryCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const StatSummaryCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 20),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.trending_up_rounded,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: FinanceDesignSystem.primaryDark,
|
||||||
|
fontFamily: 'digit',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/views/home/statistics/widgets/today_chart_widget.dart
Normal file
57
lib/views/home/statistics/widgets/today_chart_widget.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
|
||||||
|
class TodayChartWidget extends StatelessWidget {
|
||||||
|
TodayChartWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<HomeCaptainController>(
|
||||||
|
builder: (hc) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Today Overview'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('نظرة عامة على اليوم'.tr, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildRow(Icons.monetization_on_rounded, 'Earnings'.tr, '${hc.totalMoneyToday} ${'SYP'.tr}', FinanceDesignSystem.successGreen),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildRow(Icons.local_taxi_rounded, 'Rides'.tr, hc.countRideToday, FinanceDesignSystem.accentBlue),
|
||||||
|
const Divider(height: 24),
|
||||||
|
Obx(() => _buildRow(Icons.timer_rounded, 'Online Duration'.tr, hc.totalDurationDisplay.value, const Color(0xFFFF9800))),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildRow(Icons.cancel_outlined, 'Refused'.tr, hc.countRefuse, FinanceDesignSystem.dangerRed),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(IconData icon, String label, String value, Color color) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(icon, color: color, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(child: Text(label, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade700))),
|
||||||
|
Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: FinanceDesignSystem.primaryDark, fontFamily: 'digit')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
lib/views/home/statistics/widgets/weekly_chart_widget.dart
Normal file
114
lib/views/home/statistics/widgets/weekly_chart_widget.dart
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import '../../../../constant/finance_design_system.dart';
|
||||||
|
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class WeeklyChartWidget extends StatelessWidget {
|
||||||
|
const WeeklyChartWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<StatisticsController>(
|
||||||
|
builder: (sc) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4))],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Text('Weekly Earnings'.tr, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(color: FinanceDesignSystem.successGreen.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Text('${sc.weeklyEarnings.toStringAsFixed(0)} ${'SYP'.tr}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: FinanceDesignSystem.successGreen)),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(children: [
|
||||||
|
_miniStat(Icons.local_taxi_rounded, '${sc.weeklyTrips}', 'Rides'.tr, FinanceDesignSystem.accentBlue),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_miniStat(Icons.timer_rounded, '${sc.weeklyHours.toStringAsFixed(1)}h', 'Hours'.tr, const Color(0xFFFF9800)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
height: 180,
|
||||||
|
child: sc.weeklyStats.isEmpty
|
||||||
|
? Center(child: Text('No data yet'.tr, style: TextStyle(color: Colors.grey.shade400)))
|
||||||
|
: BarChart(
|
||||||
|
BarChartData(
|
||||||
|
alignment: BarChartAlignment.spaceAround,
|
||||||
|
maxY: _getMaxY(sc.weeklyStats),
|
||||||
|
barTouchData: BarTouchData(
|
||||||
|
enabled: true,
|
||||||
|
touchTooltipData: BarTouchTooltipData(
|
||||||
|
getTooltipItem: (group, gi, rod, ri) => BarTooltipItem(
|
||||||
|
'${rod.toY.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||||
|
const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
show: true,
|
||||||
|
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, getTitlesWidget: (v, m) {
|
||||||
|
final idx = v.toInt();
|
||||||
|
if (idx >= 0 && idx < sc.weeklyStats.length) {
|
||||||
|
return Padding(padding: const EdgeInsets.only(top: 8), child: Text(sc.weeklyStats[idx].dayName.tr, style: TextStyle(fontSize: 10, color: Colors.grey.shade500)));
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
})),
|
||||||
|
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
gridData: const FlGridData(show: false),
|
||||||
|
barGroups: List.generate(sc.weeklyStats.length, (i) {
|
||||||
|
final stat = sc.weeklyStats[i];
|
||||||
|
final isToday = i == sc.weeklyStats.length - 1;
|
||||||
|
return BarChartGroupData(x: i, barRods: [
|
||||||
|
BarChartRodData(
|
||||||
|
toY: stat.earnings,
|
||||||
|
width: 20,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter, end: Alignment.topCenter,
|
||||||
|
colors: isToday
|
||||||
|
? [FinanceDesignSystem.accentBlue, const Color(0xFF82B1FF)]
|
||||||
|
: [FinanceDesignSystem.primaryDark.withOpacity(0.6), FinanceDesignSystem.primaryDark.withOpacity(0.3)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _miniStat(IconData icon, String value, String label, Color color) {
|
||||||
|
return Row(children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(value, style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(label, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getMaxY(List<DayStat> stats) {
|
||||||
|
if (stats.isEmpty) return 100;
|
||||||
|
final max = stats.map((s) => s.earnings).reduce((a, b) => a > b ? a : b);
|
||||||
|
return max <= 0 ? 100 : max * 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user