diff --git a/lib/controller/attendance/attendance_screen_controller.dart b/lib/controller/attendance/attendance_screen_controller.dart index e0751e8..cab2ffd 100644 --- a/lib/controller/attendance/attendance_screen_controller.dart +++ b/lib/controller/attendance/attendance_screen_controller.dart @@ -20,22 +20,27 @@ import 'package:marco/model/attendance/organization_per_project_list_model.dart' import 'package:marco/controller/project_controller.dart'; class AttendanceController extends GetxController { - // Data models + // ------------------ Data Models ------------------ List attendances = []; List projects = []; List employees = []; List attendanceLogs = []; List regularizationLogs = []; List attendenceLogsView = []; + // ------------------ Organizations ------------------ List organizations = []; Organization? selectedOrganization; final isLoadingOrganizations = false.obs; - // States + // ------------------ States ------------------ String selectedTab = 'todaysAttendance'; - DateTime? startDateAttendance; - DateTime? endDateAttendance; + + // โœ… Reactive date range + final Rx startDateAttendance = + DateTime.now().subtract(const Duration(days: 7)).obs; + final Rx endDateAttendance = + DateTime.now().subtract(const Duration(days: 1)).obs; final isLoading = true.obs; final isLoadingProjects = true.obs; @@ -46,6 +51,8 @@ class AttendanceController extends GetxController { final uploadingStates = {}.obs; var showPendingOnly = false.obs; + final searchQuery = ''.obs; + @override void onInit() { super.onInit(); @@ -58,14 +65,38 @@ class AttendanceController extends GetxController { void _setDefaultDateRange() { final today = DateTime.now(); - startDateAttendance = today.subtract(const Duration(days: 7)); - endDateAttendance = today.subtract(const Duration(days: 1)); + startDateAttendance.value = today.subtract(const Duration(days: 7)); + endDateAttendance.value = today.subtract(const Duration(days: 1)); logSafe( - "Default date range set: $startDateAttendance to $endDateAttendance"); + "Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}"); } - // ------------------ Project & Employee ------------------ - /// Called when a notification says attendance has been updated + // ------------------ Computed Filters ------------------ + List get filteredEmployees { + if (searchQuery.value.isEmpty) return employees; + return employees + .where((e) => + e.name.toLowerCase().contains(searchQuery.value.toLowerCase())) + .toList(); + } + + List get filteredLogs { + if (searchQuery.value.isEmpty) return attendanceLogs; + return attendanceLogs + .where((log) => + log.name.toLowerCase().contains(searchQuery.value.toLowerCase())) + .toList(); + } + + List get filteredRegularizationLogs { + if (searchQuery.value.isEmpty) return regularizationLogs; + return regularizationLogs + .where((log) => + log.name.toLowerCase().contains(searchQuery.value.toLowerCase())) + .toList(); + } + + // ------------------ Project & Employee APIs ------------------ Future refreshDataFromNotification({String? projectId}) async { projectId ??= Get.find().selectedProject?.id; if (projectId == null) { @@ -78,36 +109,6 @@ class AttendanceController extends GetxController { "Attendance data refreshed from notification for project $projectId"); } - // ๐Ÿ” Search query - final searchQuery = ''.obs; - - // Computed filtered employees - List get filteredEmployees { - if (searchQuery.value.isEmpty) return employees; - return employees - .where((e) => - e.name.toLowerCase().contains(searchQuery.value.toLowerCase())) - .toList(); - } - - // Computed filtered logs - List get filteredLogs { - if (searchQuery.value.isEmpty) return attendanceLogs; - return attendanceLogs - .where((log) => - (log.name).toLowerCase().contains(searchQuery.value.toLowerCase())) - .toList(); - } - - // Computed filtered regularization logs - List get filteredRegularizationLogs { - if (searchQuery.value.isEmpty) return regularizationLogs; - return regularizationLogs - .where((log) => - log.name.toLowerCase().contains(searchQuery.value.toLowerCase())) - .toList(); - } - Future fetchTodaysAttendance(String? projectId) async { if (projectId == null) return; @@ -127,6 +128,7 @@ class AttendanceController extends GetxController { logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error); } + isLoadingEmployees.value = false; update(); } @@ -146,7 +148,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Capture ------------------ - Future captureAndUploadAttendance( String id, String employeeId, @@ -154,8 +155,8 @@ class AttendanceController extends GetxController { String comment = "Marked via mobile app", required int action, bool imageCapture = true, - String? markTime, // still optional in controller - String? date, // new optional param + String? markTime, + String? date, }) async { try { uploadingStates[employeeId]?.value = true; @@ -169,7 +170,6 @@ class AttendanceController extends GetxController { return false; } - // ๐Ÿ”น Add timestamp to the image final timestampedFile = await TimestampImageHelper.addTimestamp( imageFile: File(image.path)); @@ -192,29 +192,20 @@ class AttendanceController extends GetxController { ? ApiService.generateImageName(employeeId, employees.length + 1) : ""; - // ---------------- DATE / TIME LOGIC ---------------- final now = DateTime.now(); - - // Default effectiveDate = now DateTime effectiveDate = now; if (action == 1) { - // Checkout - // Try to find today's open log for this employee final log = attendanceLogs.firstWhereOrNull( (log) => log.employeeId == employeeId && log.checkOut == null, ); - if (log?.checkIn != null) { - effectiveDate = log!.checkIn!; // use check-in date - } + if (log?.checkIn != null) effectiveDate = log!.checkIn!; } final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now); - final formattedDate = date ?? DateFormat('yyyy-MM-dd').format(effectiveDate); - // ---------------- API CALL ---------------- final result = await ApiService.uploadAttendanceImage( id, employeeId, @@ -263,7 +254,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Logs ------------------ - Future fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { if (projectId == null) return; @@ -312,7 +302,6 @@ class AttendanceController extends GetxController { } // ------------------ Regularization Logs ------------------ - Future fetchRegularizationLogs(String? projectId) async { if (projectId == null) return; @@ -336,7 +325,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Log View ------------------ - Future fetchLogsView(String? id) async { if (id == null) return; @@ -359,7 +347,6 @@ class AttendanceController extends GetxController { } // ------------------ Combined Load ------------------ - Future loadAttendanceData(String projectId) async { isLoading.value = true; await fetchProjectData(projectId); @@ -371,7 +358,6 @@ class AttendanceController extends GetxController { await fetchOrganizations(projectId); - // Call APIs depending on the selected tab only switch (selectedTab) { case 'todaysAttendance': await fetchTodaysAttendance(projectId); @@ -379,8 +365,8 @@ class AttendanceController extends GetxController { case 'attendanceLogs': await fetchAttendanceLogs( projectId, - dateFrom: startDateAttendance, - dateTo: endDateAttendance, + dateFrom: startDateAttendance.value, + dateTo: endDateAttendance.value, ); break; case 'regularizationRequests': @@ -394,7 +380,6 @@ class AttendanceController extends GetxController { } // ------------------ UI Interaction ------------------ - Future selectDateRangeForAttendance( BuildContext context, AttendanceController controller) async { final today = DateTime.now(); @@ -404,16 +389,17 @@ class AttendanceController extends GetxController { firstDate: DateTime(2022), lastDate: today.subtract(const Duration(days: 1)), initialDateRange: DateTimeRange( - start: startDateAttendance ?? today.subtract(const Duration(days: 7)), - end: endDateAttendance ?? today.subtract(const Duration(days: 1)), + start: startDateAttendance.value, + end: endDateAttendance.value, ), ); if (picked != null) { - startDateAttendance = picked.start; - endDateAttendance = picked.end; + startDateAttendance.value = picked.start; + endDateAttendance.value = picked.end; + logSafe( - "Date range selected: $startDateAttendance to $endDateAttendance"); + "Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}"); await controller.fetchAttendanceLogs( Get.find().selectedProject?.id, diff --git a/lib/controller/document/user_document_controller.dart b/lib/controller/document/user_document_controller.dart index fb3f734..5667003 100644 --- a/lib/controller/document/user_document_controller.dart +++ b/lib/controller/document/user_document_controller.dart @@ -34,11 +34,11 @@ class DocumentController extends GetxController { // Additional filters final isUploadedAt = true.obs; final isVerified = RxnBool(); - final startDate = Rxn(); - final endDate = Rxn(); + final startDate = Rxn(); + final endDate = Rxn(); // ==================== Lifecycle ==================== - + @override void onClose() { // Don't dispose searchController here - it's managed by the page @@ -74,7 +74,7 @@ class DocumentController extends GetxController { }) async { try { isLoading.value = true; - + final success = await ApiService.deleteDocumentApi( id: id, isActive: isActive, diff --git a/lib/helpers/widgets/date_range_picker.dart b/lib/helpers/widgets/date_range_picker.dart new file mode 100644 index 0000000..6afab60 --- /dev/null +++ b/lib/helpers/widgets/date_range_picker.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/utils.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +typedef OnDateRangeSelected = void Function(DateTime? start, DateTime? end); + +class DateRangePickerWidget extends StatefulWidget { + final Rx startDate; + final Rx endDate; + final OnDateRangeSelected? onDateRangeSelected; + final String? startLabel; + final String? endLabel; + + const DateRangePickerWidget({ + Key? key, + required this.startDate, + required this.endDate, + this.onDateRangeSelected, + this.startLabel, + this.endLabel, + }); + + @override + State createState() => _DateRangePickerWidgetState(); +} + +class _DateRangePickerWidgetState extends State + with UIMixin { + Future _selectDate(BuildContext context, bool isStartDate) async { + final current = isStartDate + ? widget.startDate.value ?? DateTime.now() + : widget.endDate.value ?? DateTime.now(); + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: current, + firstDate: DateTime(2000), + lastDate: DateTime.now(), + builder: (context, child) => Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: contentTheme.primary, + onPrimary: Colors.white, + onSurface: Colors.black, + ), + ), + child: child!, + ), + ); + + if (picked != null) { + if (isStartDate) { + widget.startDate.value = picked; + } else { + widget.endDate.value = picked; + } + + if (widget.onDateRangeSelected != null) { + widget.onDateRangeSelected!( + widget.startDate.value, widget.endDate.value); + } + } + } + + Widget _dateBox({ + required BuildContext context, + required String label, + required Rx date, + required bool isStart, + }) { + return Expanded( + child: Obx(() { + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _selectDate(context, isStart), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: contentTheme.primary.withOpacity(0.08), + border: Border.all(color: contentTheme.primary.withOpacity(0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: contentTheme.primary.withOpacity(0.15), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + isStart + ? Icons.calendar_today_outlined + : Icons.event_outlined, + size: 14, + color: contentTheme.primary, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText(label, fontSize: 10, fontWeight: 500), + const SizedBox(height: 2), + MyText( + date.value != null + ? Utils.formatDate(date.value!) + : 'Not selected', + fontWeight: 600, + color: contentTheme.primary, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + }), + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _dateBox( + context: context, + label: widget.startLabel ?? 'Start Date', + date: widget.startDate, + isStart: true, + ), + const SizedBox(width: 8), + _dateBox( + context: context, + label: widget.endLabel ?? 'End Date', + date: widget.endDate, + isStart: false, + ), + ], + ); + } +} diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index 7a9eec8..8d0d10d 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -5,6 +5,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; class AttendanceFilterBottomSheet extends StatefulWidget { final AttendanceController controller; @@ -34,15 +35,11 @@ class _AttendanceFilterBottomSheetState } String getLabelText() { - final startDate = widget.controller.startDateAttendance; - final endDate = widget.controller.endDateAttendance; - - if (startDate != null && endDate != null) { - final start = DateTimeUtils.formatDate(startDate, 'dd MMM yyyy'); - final end = DateTimeUtils.formatDate(endDate, 'dd MMM yyyy'); - return "$start - $end"; - } - return "Date Range"; + final start = DateTimeUtils.formatDate( + widget.controller.startDateAttendance.value, 'dd MMM yyyy'); + final end = DateTimeUtils.formatDate( + widget.controller.endDateAttendance.value, 'dd MMM yyyy'); + return "$start - $end"; } List buildMainFilters() { @@ -61,6 +58,7 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ + // ๐Ÿ”น View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -82,8 +80,7 @@ class _AttendanceFilterBottomSheetState ); }), ]; - - // ๐Ÿ”น Date Range only for attendanceLogs + // ๐Ÿ”น Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -94,37 +91,16 @@ class _AttendanceFilterBottomSheetState child: MyText.titleSmall("Date Range", fontWeight: 600), ), ), - InkWell( - borderRadius: BorderRadius.circular(10), - onTap: () async { - await widget.controller.selectDateRangeForAttendance( - context, - widget.controller, - ); + // โœ… Reusable DateRangePickerWidget + DateRangePickerWidget( + startDate: widget.controller.startDateAttendance, + endDate: widget.controller.endDateAttendance, + startLabel: "Start Date", + endLabel: "End Date", + onDateRangeSelected: (start, end) { + // Optional: trigger UI updates if needed setState(() {}); }, - child: Ink( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(10), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - children: [ - const Icon(Icons.date_range, color: Colors.black87), - const SizedBox(width: 12), - Expanded( - child: MyText.bodyMedium( - getLabelText(), - fontWeight: 500, - color: Colors.black87, - ), - ), - const Icon(Icons.arrow_drop_down, color: Colors.black87), - ], - ), - ), ), ]); } diff --git a/lib/model/document/user_document_filter_bottom_sheet.dart b/lib/model/document/user_document_filter_bottom_sheet.dart index fa0546a..c50fb1c 100644 --- a/lib/model/document/user_document_filter_bottom_sheet.dart +++ b/lib/model/document/user_document_filter_bottom_sheet.dart @@ -2,24 +2,33 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/document/user_document_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; -import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/document/document_filter_model.dart'; import 'dart:convert'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; -class UserDocumentFilterBottomSheet extends StatelessWidget { +class UserDocumentFilterBottomSheet extends StatefulWidget { final String entityId; final String entityTypeId; - final DocumentController docController = Get.find(); - UserDocumentFilterBottomSheet({ + const UserDocumentFilterBottomSheet({ super.key, required this.entityId, required this.entityTypeId, }); + @override + State createState() => + _UserDocumentFilterBottomSheetState(); +} + +class _UserDocumentFilterBottomSheetState + extends State with UIMixin { + final DocumentController docController = Get.find(); + @override Widget build(BuildContext context) { final filterData = docController.filters.value; @@ -51,8 +60,8 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { }; docController.fetchDocuments( - entityTypeId: entityTypeId, - entityId: entityId, + entityTypeId: widget.entityTypeId, + entityId: widget.entityId, filter: jsonEncode(combinedFilter), reset: true, ); @@ -76,144 +85,64 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ), ), ), - // --- Date Filter (Uploaded On / Updated On) --- + // --- Date Range using Radio Buttons on Same Row --- _buildField( "Choose Date", Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Segmented Buttons Obx(() { - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(24), - ), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => - docController.isUploadedAt.value = true, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10), - decoration: BoxDecoration( - color: docController.isUploadedAt.value - ? Colors.indigo.shade400 - : Colors.transparent, - borderRadius: - const BorderRadius.horizontal( - left: Radius.circular(24), - ), - ), - child: Center( - child: MyText( - "Upload Date", - style: MyTextStyle.bodyMedium( - color: - docController.isUploadedAt.value - ? Colors.white - : Colors.black87, - fontWeight: 600, - ), - ), - ), + return Row( + children: [ + // --- Upload Date --- + Expanded( + child: Row( + children: [ + Radio( + value: true, + groupValue: + docController.isUploadedAt.value, + onChanged: (val) => docController + .isUploadedAt.value = val!, + activeColor: contentTheme.primary, ), - ), + MyText("Upload Date"), + ], ), - Expanded( - child: GestureDetector( - onTap: () => docController - .isUploadedAt.value = false, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10), - decoration: BoxDecoration( - color: !docController.isUploadedAt.value - ? Colors.indigo.shade400 - : Colors.transparent, - borderRadius: - const BorderRadius.horizontal( - right: Radius.circular(24), - ), - ), - child: Center( - child: MyText( - "Update Date", - style: MyTextStyle.bodyMedium( - color: !docController - .isUploadedAt.value - ? Colors.white - : Colors.black87, - fontWeight: 600, - ), - ), - ), + ), + // --- Update Date --- + Expanded( + child: Row( + children: [ + Radio( + value: false, + groupValue: + docController.isUploadedAt.value, + onChanged: (val) => docController + .isUploadedAt.value = val!, + activeColor: contentTheme.primary, ), - ), + MyText("Update Date"), + ], ), - ], - ), + ), + ], ); }), MySpacing.height(12), - // Date Range - Row( - children: [ - Expanded( - child: Obx(() { - return _dateButton( - label: docController.startDate.value == null - ? 'From Date' - : DateTimeUtils.formatDate( - DateTime.parse( - docController.startDate.value!), - 'dd MMM yyyy', - ), - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime.now(), - ); - if (picked != null) { - docController.startDate.value = - picked.toIso8601String(); - } - }, - ); - }), - ), - MySpacing.width(12), - Expanded( - child: Obx(() { - return _dateButton( - label: docController.endDate.value == null - ? 'To Date' - : DateTimeUtils.formatDate( - DateTime.parse( - docController.endDate.value!), - 'dd MMM yyyy', - ), - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime.now(), - ); - if (picked != null) { - docController.endDate.value = - picked.toIso8601String(); - } - }, - ); - }), - ), - ], + // --- Date Range Picker --- + DateRangePickerWidget( + startDate: docController.startDate, + endDate: docController.endDate, + startLabel: "From Date", + endLabel: "To Date", + onDateRangeSelected: (start, end) { + if (start != null && end != null) { + docController.startDate.value = start; + docController.endDate.value = end; + } + }, ), ], ), @@ -251,7 +180,6 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { Obx(() { return Container( padding: MySpacing.all(12), - child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -263,8 +191,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: - Colors.indigo, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -279,7 +206,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -294,7 +221,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -391,7 +318,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { (states) { if (states .contains(MaterialState.selected)) { - return Colors.indigo; // checked โ†’ Indigo + return contentTheme.primary; } return Colors.white; // unchecked โ†’ White }, @@ -454,31 +381,4 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { ], ); } - - Widget _dateButton({required String label, required VoidCallback onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: MySpacing.xy(16, 12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), - child: Row( - children: [ - const Icon(Icons.calendar_today, size: 16, color: Colors.grey), - MySpacing.width(8), - Expanded( - child: MyText( - label, - style: MyTextStyle.bodyMedium(), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - } } diff --git a/lib/view/Attendence/attendance_logs_tab.dart b/lib/view/Attendence/attendance_logs_tab.dart index fb9e5d0..61b9021 100644 --- a/lib/view/Attendence/attendance_logs_tab.dart +++ b/lib/view/Attendence/attendance_logs_tab.dart @@ -104,9 +104,7 @@ class _AttendanceLogsTabState extends State { // Filter logs if "pending only" final showPendingOnly = widget.controller.showPendingOnly.value; final filteredLogs = showPendingOnly - ? allLogs - .where((emp) => emp.activity == 1 ) - .toList() + ? allLogs.where((emp) => emp.activity == 1).toList() : allLogs; // Group logs by date string @@ -126,11 +124,9 @@ class _AttendanceLogsTabState extends State { return db.compareTo(da); }); - final dateRangeText = widget.controller.startDateAttendance != null && - widget.controller.endDateAttendance != null - ? '${DateTimeUtils.formatDate(widget.controller.startDateAttendance!, 'dd MMM yyyy')} - ' - '${DateTimeUtils.formatDate(widget.controller.endDateAttendance!, 'dd MMM yyyy')}' - : 'Select date range'; + final dateRangeText = + '${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - ' + '${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}'; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/view/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart index 32d3d8d..2a5614a 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -34,12 +34,12 @@ class _AttendanceScreenState extends State with UIMixin { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - // Listen for future project selection changes + // ๐Ÿ” Listen for project changes ever(projectController.selectedProjectId, (projectId) async { if (projectId.isNotEmpty) await _loadData(projectId); }); - // Load initial data + // ๐Ÿš€ Load initial data only once the screen is shown final projectId = projectController.selectedProjectId.value; if (projectId.isNotEmpty) _loadData(projectId); }); @@ -47,7 +47,7 @@ class _AttendanceScreenState extends State with UIMixin { Future _loadData(String projectId) async { try { - attendanceController.selectedTab = 'todaysAttendance'; + attendanceController.selectedTab = 'todaysAttendance'; await attendanceController.loadAttendanceData(projectId); attendanceController.update(['attendance_dashboard_controller']); } catch (e) { @@ -67,8 +67,8 @@ class _AttendanceScreenState extends State with UIMixin { case 'attendanceLogs': await attendanceController.fetchAttendanceLogs( projectId, - dateFrom: attendanceController.startDateAttendance, - dateTo: attendanceController.endDateAttendance, + dateFrom: attendanceController.startDateAttendance.value, + dateTo: attendanceController.endDateAttendance.value, ); break; case 'regularizationRequests': @@ -402,4 +402,13 @@ class _AttendanceScreenState extends State with UIMixin { ), ); } + + @override + void dispose() { + // ๐Ÿงน Clean up the controller when user leaves this screen + if (Get.isRegistered()) { + Get.delete(); + } + super.dispose(); + } } diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 210242b..0c6d565 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -1,15 +1,18 @@ +// ignore_for_file: must_be_immutable + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; -import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; -class ExpenseFilterBottomSheet extends StatelessWidget { +class ExpenseFilterBottomSheet extends StatefulWidget { final ExpenseController expenseController; final ScrollController scrollController; @@ -19,12 +22,18 @@ class ExpenseFilterBottomSheet extends StatelessWidget { required this.scrollController, }); - // FIX: create search adapter + @override + State createState() => + _ExpenseFilterBottomSheetState(); +} + +class _ExpenseFilterBottomSheetState extends State + with UIMixin { + /// Search employees for Paid By / Created By filters Future> searchEmployeesForBottomSheet( String query) async { - await expenseController - .searchEmployees(query); // async method, returns void - return expenseController.employeeSearchResults.toList(); + await widget.expenseController.searchEmployees(query); + return widget.expenseController.employeeSearchResults.toList(); } @override @@ -34,20 +43,21 @@ class ExpenseFilterBottomSheet extends StatelessWidget { title: 'Filter Expenses', onCancel: () => Get.back(), onSubmit: () { - expenseController.fetchExpenses(); + widget.expenseController.fetchExpenses(); Get.back(); }, submitText: 'Submit', + submitColor: contentTheme.primary, submitIcon: Icons.check_circle_outline, child: SingleChildScrollView( - controller: scrollController, + controller: widget.scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( alignment: Alignment.centerRight, child: TextButton( - onPressed: () => expenseController.clearFilters(), + onPressed: () => widget.expenseController.clearFilters(), child: MyText( "Reset Filter", style: MyTextStyle.labelMedium( @@ -58,15 +68,15 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ), ), MySpacing.height(8), - _buildProjectFilter(context), + _buildProjectFilter(), MySpacing.height(16), - _buildStatusFilter(context), + _buildStatusFilter(), MySpacing.height(16), - _buildDateRangeFilter(context), + _buildDateRangeFilter(), MySpacing.height(16), - _buildPaidByFilter(context), + _buildPaidByFilter(), MySpacing.height(16), - _buildCreatedByFilter(context), + _buildCreatedByFilter(), ], ), ), @@ -85,190 +95,145 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ); } - Widget _buildProjectFilter(BuildContext context) { + Widget _buildProjectFilter() { return _buildField( "Project", _popupSelector( - context, - currentValue: expenseController.selectedProject.value.isEmpty + currentValue: widget.expenseController.selectedProject.value.isEmpty ? 'Select Project' - : expenseController.selectedProject.value, - items: expenseController.globalProjects, - onSelected: (value) => expenseController.selectedProject.value = value, + : widget.expenseController.selectedProject.value, + items: widget.expenseController.globalProjects, + onSelected: (value) => + widget.expenseController.selectedProject.value = value, ), ); } - Widget _buildStatusFilter(BuildContext context) { + Widget _buildStatusFilter() { return _buildField( "Expense Status", _popupSelector( - context, - currentValue: expenseController.selectedStatus.value.isEmpty + currentValue: widget.expenseController.selectedStatus.value.isEmpty ? 'Select Expense Status' - : expenseController.expenseStatuses - .firstWhereOrNull( - (e) => e.id == expenseController.selectedStatus.value) + : widget.expenseController.expenseStatuses + .firstWhereOrNull((e) => + e.id == widget.expenseController.selectedStatus.value) ?.name ?? 'Select Expense Status', - items: expenseController.expenseStatuses.map((e) => e.name).toList(), + items: widget.expenseController.expenseStatuses + .map((e) => e.name) + .toList(), onSelected: (name) { - final status = expenseController.expenseStatuses + final status = widget.expenseController.expenseStatuses .firstWhere((e) => e.name == name); - expenseController.selectedStatus.value = status.id; + widget.expenseController.selectedStatus.value = status.id; }, ), ); } - Widget _buildDateRangeFilter(BuildContext context) { + Widget _buildDateRangeFilter() { return _buildField( "Date Filter", Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // --- Radio Buttons for Transaction Date / Created At --- Obx(() { - return SizedBox( - width: double.infinity, // Make it full width - child: SegmentedButton( - segments: expenseController.dateTypes - .map( - (type) => ButtonSegment( - value: type, - label: Center( - // Center label text - child: MyText( - type, - style: MyTextStyle.bodySmall( - fontWeight: 600, - fontSize: 13, - height: 1.2, - ), - ), + return Row( + children: [ + // --- Transaction Date --- + Expanded( + child: Row( + children: [ + Radio( + value: "Transaction Date", + groupValue: + widget.expenseController.selectedDateType.value, + onChanged: (val) { + if (val != null) { + widget.expenseController.selectedDateType.value = + val; + } + }, + activeColor: contentTheme.primary, + ), + Flexible( + child: MyText( + "Transaction Date", ), ), - ) - .toList(), - selected: {expenseController.selectedDateType.value}, - onSelectionChanged: (newSelection) { - if (newSelection.isNotEmpty) { - expenseController.selectedDateType.value = - newSelection.first; - } - }, - style: ButtonStyle( - visualDensity: - const VisualDensity(horizontal: -2, vertical: -2), - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - ), - backgroundColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.indigo.shade100 - : Colors.grey.shade100, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.indigo - : Colors.black87, - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - side: MaterialStateProperty.resolveWith( - (states) => BorderSide( - color: states.contains(MaterialState.selected) - ? Colors.indigo - : Colors.grey.shade300, - width: 1, - ), + ], ), ), - ), + // --- Created At --- + Expanded( + child: Row( + children: [ + Radio( + value: "Created At", + groupValue: + widget.expenseController.selectedDateType.value, + onChanged: (val) { + if (val != null) { + widget.expenseController.selectedDateType.value = + val; + } + }, + activeColor: contentTheme.primary, + ), + Flexible( + child: MyText( + "Created At", + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], ); }), MySpacing.height(16), - Row( - children: [ - Expanded( - child: _dateButton( - label: expenseController.startDate.value == null - ? 'Start Date' - : DateTimeUtils.formatDate( - expenseController.startDate.value!, 'dd MMM yyyy'), - onTap: () => _selectDate( - context, - expenseController.startDate, - lastDate: expenseController.endDate.value, - ), - ), - ), - MySpacing.width(12), - Expanded( - child: _dateButton( - label: expenseController.endDate.value == null - ? 'End Date' - : DateTimeUtils.formatDate( - expenseController.endDate.value!, 'dd MMM yyyy'), - onTap: () => _selectDate( - context, - expenseController.endDate, - firstDate: expenseController.startDate.value, - ), - ), - ), - ], + // --- Reusable Date Range Picker --- + DateRangePickerWidget( + startDate: widget.expenseController.startDate, + endDate: widget.expenseController.endDate, + startLabel: "Start Date", + endLabel: "End Date", + onDateRangeSelected: (start, end) { + widget.expenseController.startDate.value = start; + widget.expenseController.endDate.value = end; + }, ), ], ), ); } - Widget _buildPaidByFilter(BuildContext context) { + Widget _buildPaidByFilter() { return _buildField( "Paid By", _employeeSelector( - context: context, - selectedEmployees: expenseController.selectedPaidByEmployees, - searchEmployees: searchEmployeesForBottomSheet, // FIXED + selectedEmployees: widget.expenseController.selectedPaidByEmployees, + searchEmployees: searchEmployeesForBottomSheet, title: 'Search Paid By', ), ); } - Widget _buildCreatedByFilter(BuildContext context) { + Widget _buildCreatedByFilter() { return _buildField( "Created By", _employeeSelector( - context: context, - selectedEmployees: expenseController.selectedCreatedByEmployees, - searchEmployees: searchEmployeesForBottomSheet, // FIXED + selectedEmployees: widget.expenseController.selectedCreatedByEmployees, + searchEmployees: searchEmployeesForBottomSheet, title: 'Search Created By', ), ); } - Future _selectDate( - BuildContext context, - Rx dateNotifier, { - DateTime? firstDate, - DateTime? lastDate, - }) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: dateNotifier.value ?? DateTime.now(), - firstDate: firstDate ?? DateTime(2020), - lastDate: lastDate ?? DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null && picked != dateNotifier.value) { - dateNotifier.value = picked; - } - } - - Widget _popupSelector( - BuildContext context, { + Widget _popupSelector({ required String currentValue, required List items, required ValueChanged onSelected, @@ -306,59 +271,7 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ); } - Widget _dateButton({required String label, required VoidCallback onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: MySpacing.xy(16, 12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), - child: Row( - children: [ - const Icon(Icons.calendar_today, size: 16, color: Colors.grey), - MySpacing.width(8), - Expanded( - child: MyText( - label, - style: MyTextStyle.bodyMedium(), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - } - - Future _showEmployeeSelectorBottomSheet({ - required BuildContext context, - required RxList selectedEmployees, - required Future> Function(String) searchEmployees, - String title = 'Select Employee', - }) async { - final List? result = - await showModalBottomSheet>( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: searchEmployees, - title: title, - ), - ); - if (result != null) { - selectedEmployees.assignAll(result); - } - } - Widget _employeeSelector({ - required BuildContext context, required RxList selectedEmployees, required Future> Function(String) searchEmployees, String title = 'Search Employee', @@ -367,27 +280,37 @@ class ExpenseFilterBottomSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() { - if (selectedEmployees.isEmpty) { - return const SizedBox.shrink(); - } + if (selectedEmployees.isEmpty) return const SizedBox.shrink(); return Wrap( spacing: 8, children: selectedEmployees - .map((emp) => Chip( - label: MyText(emp.name), - onDeleted: () => selectedEmployees.remove(emp), - )) + .map( + (emp) => Chip( + label: MyText(emp.name), + onDeleted: () => selectedEmployees.remove(emp), + ), + ) .toList(), ); }), MySpacing.height(8), GestureDetector( - onTap: () => _showEmployeeSelectorBottomSheet( - context: context, - selectedEmployees: selectedEmployees, - searchEmployees: searchEmployees, - title: title, - ), + onTap: () async { + final List? result = + await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => EmployeeSelectorBottomSheet( + selectedEmployees: selectedEmployees, + searchEmployees: searchEmployees, + title: title, + ), + ); + if (result != null) selectedEmployees.assignAll(result); + }, child: Container( padding: MySpacing.all(12), decoration: BoxDecoration( @@ -407,5 +330,4 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ], ); } - }