feat: redesign behavior page, add fatigue monitoring and fix map controller
This commit is contained in:
@@ -213,43 +213,109 @@ class HomeCaptainController extends GetxController {
|
||||
}
|
||||
|
||||
String stringActiveDuration = '';
|
||||
|
||||
// ==========================================
|
||||
// ====== 🛡️ Fatigue Monitoring System ======
|
||||
// ==========================================
|
||||
void _checkFatigueBeforeOnline() {
|
||||
int totalSecondsToday = box.read('fatigue_total_seconds') ?? 0;
|
||||
String? lastOfflineStr = box.read('fatigue_last_offline');
|
||||
|
||||
if (lastOfflineStr != null) {
|
||||
DateTime lastOffline = DateTime.parse(lastOfflineStr);
|
||||
// If offline for more than 6 continuous hours, reset the fatigue counter
|
||||
if (DateTime.now().difference(lastOffline).inHours >= 6) {
|
||||
totalSecondsToday = 0;
|
||||
box.write('fatigue_total_seconds', 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSecondsToday >= 12 * 3600) { // 12 Hours
|
||||
_forceOfflineDueToFatigue();
|
||||
throw Exception('Fatigue Limit Exceeded');
|
||||
}
|
||||
}
|
||||
|
||||
void _forceOfflineDueToFatigue() {
|
||||
if (isActive) {
|
||||
isActive = false;
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
update();
|
||||
}
|
||||
|
||||
Get.defaultDialog(
|
||||
title: 'Safety First 🛑'.tr,
|
||||
middleText: 'You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.'.tr,
|
||||
barrierDismissible: false,
|
||||
titleStyle: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('OK'.tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onButtonSelected() {
|
||||
// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
|
||||
if (!Get.isRegistered<CaptainWalletController>()) {
|
||||
Get.put(CaptainWalletController());
|
||||
}
|
||||
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
||||
|
||||
// Toggle Active State
|
||||
isActive = !isActive;
|
||||
|
||||
if (isActive) {
|
||||
if (double.parse(totalPoints) > -200) {
|
||||
locationController.startLocationUpdates();
|
||||
HapticFeedback.heavyImpact();
|
||||
// locationBackController.startBackLocation();
|
||||
activeStartTime = DateTime.now();
|
||||
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
activeDuration = DateTime.now().difference(activeStartTime!);
|
||||
stringActiveDuration = formatDuration(activeDuration);
|
||||
update();
|
||||
});
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
try {
|
||||
_checkFatigueBeforeOnline(); // Throws exception if tired
|
||||
|
||||
if (double.parse(totalPoints) > -200) {
|
||||
locationController.startLocationUpdates();
|
||||
HapticFeedback.heavyImpact();
|
||||
activeStartTime = DateTime.now();
|
||||
|
||||
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
activeDuration = DateTime.now().difference(activeStartTime!);
|
||||
stringActiveDuration = formatDuration(activeDuration);
|
||||
|
||||
// Increment Fatigue Counter
|
||||
int totalSeconds = box.read('fatigue_total_seconds') ?? 0;
|
||||
totalSeconds += 1;
|
||||
box.write('fatigue_total_seconds', totalSeconds);
|
||||
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
if (totalSeconds >= 12 * 3600) { // 12 hours
|
||||
_forceOfflineDueToFatigue();
|
||||
}
|
||||
|
||||
update();
|
||||
});
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
// Driver is fatigued, revert state
|
||||
isActive = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
|
||||
// Save offline time for Fatigue Monitoring reset
|
||||
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||
update();
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
// متغيرات العداد للحظر
|
||||
|
||||
106
lib/controller/home/journal/schedule_controller.dart
Normal file
106
lib/controller/home/journal/schedule_controller.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
|
||||
class WorkSlot {
|
||||
int dayOfWeek; // 1=Mon ... 7=Sun
|
||||
TimeOfDay startTime;
|
||||
TimeOfDay endTime;
|
||||
bool isActive;
|
||||
|
||||
WorkSlot({required this.dayOfWeek, required this.startTime, required this.endTime, this.isActive = true});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'day': dayOfWeek, 'startH': startTime.hour, 'startM': startTime.minute,
|
||||
'endH': endTime.hour, 'endM': endTime.minute, 'active': isActive,
|
||||
};
|
||||
|
||||
factory WorkSlot.fromJson(Map<String, dynamic> json) => WorkSlot(
|
||||
dayOfWeek: json['day'] ?? 1,
|
||||
startTime: TimeOfDay(hour: json['startH'] ?? 8, minute: json['startM'] ?? 0),
|
||||
endTime: TimeOfDay(hour: json['endH'] ?? 17, minute: json['endM'] ?? 0),
|
||||
isActive: json['active'] ?? true,
|
||||
);
|
||||
|
||||
String get dayName {
|
||||
const days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String get dayNameAr {
|
||||
const days = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String formatTime(TimeOfDay t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
String get timeRange => '${formatTime(startTime)} - ${formatTime(endTime)}';
|
||||
}
|
||||
|
||||
class ScheduleController extends GetxController {
|
||||
List<WorkSlot> schedule = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadSchedule();
|
||||
}
|
||||
|
||||
void _loadSchedule() {
|
||||
final saved = box.read('work_schedule');
|
||||
if (saved != null) {
|
||||
try {
|
||||
final list = jsonDecode(saved) as List;
|
||||
schedule = list.map((e) => WorkSlot.fromJson(e)).toList();
|
||||
} catch (_) {
|
||||
_initDefault();
|
||||
}
|
||||
} else {
|
||||
_initDefault();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _initDefault() {
|
||||
schedule = List.generate(7, (i) => WorkSlot(
|
||||
dayOfWeek: i + 1,
|
||||
startTime: const TimeOfDay(hour: 8, minute: 0),
|
||||
endTime: const TimeOfDay(hour: 18, minute: 0),
|
||||
isActive: i < 6, // الجمعة عطلة
|
||||
));
|
||||
}
|
||||
|
||||
void _save() {
|
||||
box.write('work_schedule', jsonEncode(schedule.map((s) => s.toJson()).toList()));
|
||||
update();
|
||||
}
|
||||
|
||||
void toggleDay(int dayOfWeek) {
|
||||
final slot = schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek);
|
||||
slot.isActive = !slot.isActive;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateStartTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).startTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateEndTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).endTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
double get totalWeeklyHours {
|
||||
double total = 0;
|
||||
for (var s in schedule) {
|
||||
if (!s.isActive) continue;
|
||||
final startMin = s.startTime.hour * 60 + s.startTime.minute;
|
||||
final endMin = s.endTime.hour * 60 + s.endTime.minute;
|
||||
total += (endMin - startMin) / 60;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int get activeDays => schedule.where((s) => s.isActive).length;
|
||||
}
|
||||
193
lib/controller/home/statistics/statistics_controller.dart
Normal file
193
lib/controller/home/statistics/statistics_controller.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/models/model/driver/rides_summary_model.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class StatisticsController extends GetxController {
|
||||
bool isLoading = false;
|
||||
|
||||
// ═══ Weekly Data ═══
|
||||
List<DayStat> weeklyStats = [];
|
||||
double weeklyEarnings = 0;
|
||||
int weeklyTrips = 0;
|
||||
double weeklyHours = 0;
|
||||
|
||||
// ═══ Monthly Data ═══
|
||||
List<MonthlyPriceDriverModel> monthlyEarnings = [];
|
||||
List<MonthlyRideModel> monthlyRides = [];
|
||||
List<MonthlyDataModel> monthlyDuration = [];
|
||||
double monthlyTotalEarnings = 0;
|
||||
int monthlyTotalTrips = 0;
|
||||
double monthlyTotalHours = 0;
|
||||
String bestDay = '--';
|
||||
double bestDayEarnings = 0;
|
||||
|
||||
// ═══ Tab State ═══
|
||||
int selectedTab = 0; // 0=weekly, 1=monthly
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchWeeklyData();
|
||||
fetchMonthlyData();
|
||||
}
|
||||
|
||||
void changeTab(int tab) {
|
||||
selectedTab = tab;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الأسبوعية ═══════
|
||||
Future<void> fetchWeeklyData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getWeeklyAggregate,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != null && res != 'failure') {
|
||||
var data = jsonDecode(res);
|
||||
if (data['message'] is List) {
|
||||
weeklyStats = (data['message'] as List)
|
||||
.map((e) => DayStat.fromJson(e))
|
||||
.toList();
|
||||
weeklyEarnings = weeklyStats.fold(0, (s, d) => s + d.earnings);
|
||||
weeklyTrips = weeklyStats.fold(0, (s, d) => s + d.trips);
|
||||
weeklyHours = weeklyStats.fold(0, (s, d) => s + d.hours);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Weekly fetch error: $e');
|
||||
// Fallback: generate from local data
|
||||
_generateLocalWeeklyData();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الشهرية ═══════
|
||||
Future<void> fetchMonthlyData() async {
|
||||
try {
|
||||
// 1. أرباح شهرية
|
||||
var earningsRes = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (earningsRes != null && earningsRes != 'failure') {
|
||||
var data = jsonDecode(earningsRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyEarnings = (data['message'] as List)
|
||||
.map((e) => MonthlyPriceDriverModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalEarnings = monthlyEarnings.fold(0, (s, d) => s + d.pricePerDay);
|
||||
|
||||
// أفضل يوم
|
||||
if (monthlyEarnings.isNotEmpty) {
|
||||
var best = monthlyEarnings.reduce((a, b) =>
|
||||
a.pricePerDay > b.pricePerDay ? a : b);
|
||||
bestDay = best.day.toString();
|
||||
bestDayEarnings = best.pricePerDay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. رحلات شهرية
|
||||
var ridesRes = await CRUD().get(
|
||||
link: AppLink.getRidesDriverByDay,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (ridesRes != null && ridesRes != 'failure') {
|
||||
var data = jsonDecode(ridesRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyRides = (data['message'] as List)
|
||||
.map((e) => MonthlyRideModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalTrips = monthlyRides.fold(0, (s, d) => s + d.countRide);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ساعات شهرية
|
||||
var durationRes = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDuration,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (durationRes != null && durationRes != 'failure') {
|
||||
var data = jsonDecode(durationRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyDuration = (data['message'] as List)
|
||||
.map((e) => MonthlyDataModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalHours = monthlyDuration.fold(0, (s, d) => s + d.totalDuration.toDouble());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Monthly fetch error: $e');
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void _generateLocalWeeklyData() {
|
||||
// Fallback بيانات محلية عند عدم توفر الـ API
|
||||
final now = DateTime.now();
|
||||
weeklyStats = List.generate(7, (i) {
|
||||
final day = now.subtract(Duration(days: 6 - i));
|
||||
return DayStat(
|
||||
date: day,
|
||||
dayName: _getDayName(day.weekday),
|
||||
earnings: 0,
|
||||
trips: 0,
|
||||
hours: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _getDayName(int weekday) {
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[weekday - 1];
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await fetchWeeklyData();
|
||||
await fetchMonthlyData();
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════ نموذج إحصائية اليوم ═══════
|
||||
class DayStat {
|
||||
final DateTime date;
|
||||
final String dayName;
|
||||
final double earnings;
|
||||
final int trips;
|
||||
final double hours;
|
||||
|
||||
DayStat({
|
||||
required this.date,
|
||||
required this.dayName,
|
||||
required this.earnings,
|
||||
required this.trips,
|
||||
required this.hours,
|
||||
});
|
||||
|
||||
factory DayStat.fromJson(Map<String, dynamic> json) {
|
||||
final date = DateTime.tryParse(json['day']?.toString() ?? '') ?? DateTime.now();
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return DayStat(
|
||||
date: date,
|
||||
dayName: dayNames[date.weekday - 1],
|
||||
earnings: double.tryParse(json['earnings']?.toString() ?? '0') ?? 0,
|
||||
trips: int.tryParse(json['trips']?.toString() ?? '0') ?? 0,
|
||||
hours: double.tryParse(json['hours']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user