TIF_E41222181/lib/feature_student/dashboard/presentation/dashboard_controller.dart

394 lines
13 KiB
Dart

import 'package:absensi_qr/utils/app_snackbar.dart';
import 'dart:developer';
import 'package:absensi_qr/configs/api_constant.dart';
import 'package:absensi_qr/feature_student/navigation/presentation/navigation_controller.dart';
import 'package:absensi_qr/features/others/main_controller.dart';
import 'package:absensi_qr/models/academic_period_model.dart';
import 'package:absensi_qr/models/attendance_daily.dart';
import 'package:absensi_qr/models/attendance_history.dart';
import 'package:absensi_qr/models/model_merging/attendance_report_item.dart';
import 'package:absensi_qr/models/model_merging/schedule_attendance_report.dart';
import 'package:absensi_qr/services/endpoint_service.dart';
import 'package:absensi_qr/services/geolocation_service.dart';
import 'package:absensi_qr/utils/app_util.dart';
import 'package:app_settings/app_settings.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
class DashboardController extends GetxController {
final EndpointService _httpService = Get.find<EndpointService>();
final NavigationController navigationController =
Get.find<NavigationController>();
final GeolocationService _geolocationService = Get.find<GeolocationService>();
final MainController mainController = Get.find<MainController>();
var isConnected = false.obs;
// is weekend check
bool get isWeekend {
final int weekday = dateNow.weekday;
return weekday == DateTime.sunday;
// return true;
}
DateTime dateNow = DateTime.now();
// DateTime dateNow = DateTime(2026, 7, 9, 9, 20, 0); // testing only - Thursday (selaras dengan teacher)
String get dateNowFormatted => AppUtil.formatDateIndonesia(dateNow);
/// data zone
// profile student
final RxString name = ''.obs;
final RxString firstName = ''.obs;
final RxString nisn = ''.obs;
final RxString email = ''.obs;
final RxString className = ''.obs;
final RxString major = ''.obs;
final RxString entryYear = ''.obs;
final RxString profileImageURL = ''.obs;
// attendance history
RxBool isLoadingAttendanceHistory = true.obs;
RxList<AttendanceReportItem> attendanceHistoryResult =
<AttendanceReportItem>[].obs;
// upcoming or ongoing attendance
RxBool isLoadingUpcomingOrOngoingAttendance = true.obs;
AttendanceReportItem? upcomingOrOngoingAttendanceResult;
RxBool isLoadingAttendanceByClassHistory = true.obs;
RxList<ScheduleAttendanceReport> attendanceByClassHistoryResult =
<ScheduleAttendanceReport>[].obs;
RxBool isLoadingAttendanceDaily = true.obs;
Rx<AttendanceDaily?> attendanceDailyResult = Rx<AttendanceDaily?>(null);
/// data zone END
Future<void> checkConnection() async {
isConnected.value = await _httpService.testConnection();
}
// geolocation
Future<void> getLocation() async {
try {
await _geolocationService.waitForGpsEnabled(maxRetries: 5);
} catch (e) {
log("Gagal menunggu GPS aktif: $e");
if (Get.isSnackbarOpen == false) {
AppSnackbar.snackbar(
'GPS Tidak Aktif',
'Mohon nyalakan GPS untuk menggunakan aplikasi',
duration: const Duration(seconds: 3),
);
}
AppSettings.openAppSettings(type: AppSettingsType.location);
}
}
// guard check student data
bool get hasStudentData => _httpService.studentData != null;
String get placemark => _geolocationService.outputPlacemark.value;
// kota
String get placemarkCity =>
_geolocationService.placemarkResult.value?.subAdministrativeArea ??
'(No Data)';
// jalan
String get placemarkStreet =>
_geolocationService.placemarkResult.value?.street ?? '(No Data)';
// kecamatan
String get placemarkLocality =>
_geolocationService.placemarkResult.value?.locality ?? '';
// desa
String get placemarkVillage =>
_geolocationService.placemarkResult.value?.subLocality ?? '';
@override
void onInit() {
super.onInit();
if (!hasStudentData) {
Fluttertoast.showToast(msg: 'msg_missing_student_data');
} else {
/// data student is available
_setProfileData();
// Load data asynchronously without blocking controller initialization
_loadDashboardData();
// check active academic period if not loaded yet, just fetch it once
if (mainController.activeAcademicPeriod.value == null) {
_loadActiveAcademicPeriod();
}
}
ever(mainController.refreshHomeStudent, (_) {
_loadDashboardData();
});
ever(mainController.triggerUpdateProfile, (_) {
_setProfileData();
});
}
void _loadDashboardData() async {
try {
await Future.wait([
getHistoryAttendance(),
getAttendanceHistoryDaily(),
getHistoryAttendanceByClass(),
]);
await getUpcomingOrOngoingAttendance();
} catch (e) {
log('Error loading dashboard data: $e');
}
}
// service zone
Future<void> _loadActiveAcademicPeriod() async {
try {
final activePeriod = await _httpService.getActiveAcademicPeriod(timeout: const Duration(seconds: 5));
if (activePeriod.success && activePeriod.data != null) {
mainController.activeAcademicPeriod.value =
AcademicPeriodModel.fromJson(activePeriod.data!);
log('Active academic period: ${mainController.activeAcademicPeriod.value}');
} else {
mainController.activeAcademicPeriod.value = null;
}
} catch (e) {
// log error tapi tetap lanjut ke halaman berikutnya
Fluttertoast.showToast(msg: 'Gagal memuat periode aktif, coba lagi nanti');
}
}
_setProfileData() {
final String? emailService = _httpService.userData != null
? (_httpService.userData!['email'] as String?)
: null;
name.value = _httpService.studentData?.name ?? '';
firstName.value = _httpService.studentData?.name?.split(' ').first ?? '';
nisn.value = _httpService.studentData?.nisn ?? '';
email.value = emailService ?? '';
className.value = _httpService.studentData?.classData?.name ?? '';
major.value = _httpService.studentData?.classData?.major ?? '';
entryYear.value = _httpService.studentData != null
? _httpService.studentData!.entryYear.toString()
: '';
final String? profilePicture = _httpService.userModel?.profilePicture;
if (profilePicture != null) {
profileImageURL.value =
ApiConstant.getProfilePictureURL(profilePicture, 'student');
}
}
// get attendance history by now
Future<void> getHistoryAttendance() async {
isLoadingAttendanceHistory.value = true;
// guard: ensure student data and class id available
final BigInt? idClass = _httpService.studentData?.idClass;
if (idClass == null) {
Fluttertoast.showToast(msg: 'missing_class_id');
isLoadingAttendanceHistory.value = false;
return;
}
// attendanceBySchedule expects strings: idClass and date (YYYY-MM-DD)
final String idClassStr = idClass.toString();
final String dateStr = '${dateNow.year.toString().padLeft(4, '0')}-'
'${dateNow.month.toString().padLeft(2, '0')}-'
'${dateNow.day.toString().padLeft(2, '0')}';
final result = await _httpService.attendanceBySchedule(
idClass: idClassStr,
date: dateStr,
);
if (result.success) {
final List<dynamic>? raw = result.data;
if (raw != null) {
final items = raw
.map((e) => AttendanceReportItem.fromMap(e as Map<String, dynamic>))
.toList();
attendanceHistoryResult.assignAll(items);
log('Loaded ${items.length} attendance items');
} else {
attendanceHistoryResult.clear();
}
} else {
Fluttertoast.showToast(
msg: result.message ?? 'msg_failed_fetch_attendance');
}
isLoadingAttendanceHistory.value = false;
}
// get upcoming or ongoing attendance based on history attendance
Future<void> getUpcomingOrOngoingAttendance() async {
isLoadingUpcomingOrOngoingAttendance.value = true;
log('data attendance history count: ${attendanceHistoryResult.length}');
/// load data index 0
log('data attendance history index 0: ${attendanceHistoryResult.isNotEmpty ? attendanceHistoryResult[0].schedule.toString() : 'No Data'}');
/// dummy date for testing
try {
if (attendanceHistoryResult.isNotEmpty) {
// Step 1: Try to find ONGOING attendance
final ongoing = attendanceHistoryResult.firstWhereOrNull((item) {
final timeStart = item.schedule.startTime;
final timeEnd = item.schedule.endTime;
final todayStartTime = DateTime(
dateNow.year,
dateNow.month,
dateNow.day,
timeStart.hour,
timeStart.minute,
timeStart.second,
);
final todayEndTime = DateTime(
dateNow.year,
dateNow.month,
dateNow.day,
timeEnd.hour,
timeEnd.minute,
timeEnd.second,
);
if (timeStart != null && timeEnd != null) {
return dateNow.isAfter(todayStartTime) &&
dateNow.isBefore(todayEndTime);
}
return false;
});
if (ongoing != null) {
upcomingOrOngoingAttendanceResult = ongoing;
} else {
// Step 2: No ongoing, find UPCOMING (nearest/terdekat)
final upcomingList = <AttendanceReportItem>[];
for (final item in attendanceHistoryResult) {
final timeStart = item.schedule.startTime;
if (timeStart == null) continue;
final todayStartTime = DateTime(
dateNow.year,
dateNow.month,
dateNow.day,
timeStart.hour,
timeStart.minute,
timeStart.second,
);
// Collect all upcoming items
if (dateNow.isBefore(todayStartTime)) {
upcomingList.add(item);
}
}
if (upcomingList.isNotEmpty) {
// Sort by startTime ascending to get the nearest upcoming
upcomingList.sort(
(a, b) => a.schedule.startTime.compareTo(b.schedule.startTime),
);
upcomingOrOngoingAttendanceResult = upcomingList.first;
log('Found nearest upcoming attendance at ${upcomingList.first.schedule.startTime}');
} else {
upcomingOrOngoingAttendanceResult = null;
log('No upcoming or ongoing attendance found');
}
}
} else {
upcomingOrOngoingAttendanceResult = null;
log('Attendance history is empty');
}
} catch (e) {
upcomingOrOngoingAttendanceResult = null;
log('Error getting upcoming attendance: $e');
}
isLoadingUpcomingOrOngoingAttendance.value = false;
}
// get attendance daily by class history.
Future<void> getAttendanceHistoryDaily() async {
isLoadingAttendanceDaily.value = true;
final String dateStr = '${dateNow.year.toString().padLeft(4, '0')}-'
'${dateNow.month.toString().padLeft(2, '0')}-'
'${dateNow.day.toString().padLeft(2, '0')}';
final result = await _httpService.attendanceReportDaily(dateNow);
log('attendanceReportDaily result success: ${result.success} status: ${result.statusCode}');
if (result.success) {
final Map<String, dynamic>? raw = result.data;
if (raw != null) {
final attendanceDaily = AttendanceDaily.fromMap(raw);
attendanceDailyResult.value = attendanceDaily;
log('Loaded attendance daily report for date ${dateStr}');
} else {
attendanceDailyResult.value = null;
}
} else {
Fluttertoast.showToast(
msg: result.message ?? 'msg_failed_fetch_attendance');
}
isLoadingAttendanceDaily.value = false;
}
// get attendance by class history
Future<void> getHistoryAttendanceByClass() async {
isLoadingAttendanceByClassHistory.value = true;
// guard: ensure student data and class id available
final BigInt? idClass = _httpService.studentData?.idClass;
if (idClass == null) {
Fluttertoast.showToast(msg: 'missing_class_id');
isLoadingAttendanceByClassHistory.value = false;
return;
}
// attendanceBySchedule expects strings: idClass and date (YYYY-MM-DD)
final String idClassStr = idClass.toString();
final String dateStr = '${dateNow.year.toString().padLeft(4, '0')}-'
'${dateNow.month.toString().padLeft(2, '0')}-'
'${dateNow.day.toString().padLeft(2, '0')}';
final result = await _httpService.attendanceReportByScheduleClass(
idClass: idClassStr, date: dateStr);
if (result.success) {
final List<dynamic>? raw = result.data;
if (raw != null) {
final items = raw
.map((e) =>
ScheduleAttendanceReport.fromMap(e as Map<String, dynamic>))
.toList();
attendanceByClassHistoryResult.assignAll(items);
log('Loaded ${items.length} schedule attendance items');
} else {
attendanceByClassHistoryResult.clear();
}
} else {
Fluttertoast.showToast(
msg: result.message ?? 'msg_failed_fetch_attendance_by_class');
}
isLoadingAttendanceByClassHistory.value = false;
}
}