diff --git a/lib/controller/attendance/attendance_screen_controller.dart b/lib/controller/attendance/attendance_screen_controller.dart index 20ef3d6..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,11 +51,12 @@ class AttendanceController extends GetxController { final uploadingStates = {}.obs; var showPendingOnly = false.obs; + final searchQuery = ''.obs; + @override void onInit() { super.onInit(); _initializeDefaults(); - } void _initializeDefaults() { @@ -59,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) { @@ -79,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; @@ -128,6 +128,7 @@ class AttendanceController extends GetxController { logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error); } + isLoadingEmployees.value = false; update(); } @@ -147,7 +148,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Capture ------------------ - Future captureAndUploadAttendance( String id, String employeeId, @@ -155,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; @@ -170,7 +170,6 @@ class AttendanceController extends GetxController { return false; } - // 🔹 Add timestamp to the image final timestampedFile = await TimestampImageHelper.addTimestamp( imageFile: File(image.path)); @@ -193,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, @@ -264,7 +254,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Logs ------------------ - Future fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { if (projectId == null) return; @@ -313,7 +302,6 @@ class AttendanceController extends GetxController { } // ------------------ Regularization Logs ------------------ - Future fetchRegularizationLogs(String? projectId) async { if (projectId == null) return; @@ -337,7 +325,6 @@ class AttendanceController extends GetxController { } // ------------------ Attendance Log View ------------------ - Future fetchLogsView(String? id) async { if (id == null) return; @@ -360,7 +347,6 @@ class AttendanceController extends GetxController { } // ------------------ Combined Load ------------------ - Future loadAttendanceData(String projectId) async { isLoading.value = true; await fetchProjectData(projectId); @@ -372,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); @@ -380,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': @@ -395,7 +380,6 @@ class AttendanceController extends GetxController { } // ------------------ UI Interaction ------------------ - Future selectDateRangeForAttendance( BuildContext context, AttendanceController controller) async { final today = DateTime.now(); @@ -405,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/controller/task_planning/daily_task_controller.dart b/lib/controller/task_planning/daily_task_controller.dart index 3d54dfc..74da88e 100644 --- a/lib/controller/task_planning/daily_task_controller.dart +++ b/lib/controller/task_planning/daily_task_controller.dart @@ -13,6 +13,10 @@ class DailyTaskController extends GetxController { DateTime? startDateTask; DateTime? endDateTask; + // Rx fields for DateRangePickerWidget + Rx startDateTaskRx = DateTime.now().obs; + Rx endDateTaskRx = DateTime.now().obs; + List dailyTasks = []; final RxSet expandedDates = {}.obs; @@ -33,14 +37,19 @@ class DailyTaskController extends GetxController { RxBool isLoading = true.obs; RxBool isLoadingMore = false.obs; Map> groupedDailyTasks = {}; + // Pagination int currentPage = 1; int pageSize = 20; bool hasMore = true; + + FilterData? taskFilterData; + @override void onInit() { super.onInit(); _initializeDefaults(); + _initializeRxDates(); } void _initializeDefaults() { @@ -58,6 +67,12 @@ class DailyTaskController extends GetxController { ); } + void _initializeRxDates() { + startDateTaskRx.value = + startDateTask ?? DateTime.now().subtract(const Duration(days: 7)); + endDateTaskRx.value = endDateTask ?? DateTime.now(); + } + void clearTaskFilters() { selectedBuildings.clear(); selectedFloors.clear(); @@ -65,9 +80,26 @@ class DailyTaskController extends GetxController { selectedServices.clear(); startDateTask = null; endDateTask = null; + + // reset Rx dates as well + startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7)); + endDateTaskRx.value = DateTime.now(); + update(); } + void updateDateRange(DateTime? start, DateTime? end) { + if (start != null && end != null) { + startDateTask = start; + endDateTask = end; + + startDateTaskRx.value = start; + endDateTaskRx.value = end; + + update(); + } + } + Future fetchTaskData( String projectId, { int pageNumber = 1, @@ -96,7 +128,7 @@ class DailyTaskController extends GetxController { final response = await ApiService.getDailyTasks( projectId, - filter: filter, + filter: filter, pageNumber: pageNumber, pageSize: pageSize, ); @@ -119,16 +151,13 @@ class DailyTaskController extends GetxController { update(); } - FilterData? taskFilterData; - Future fetchTaskFilter(String projectId) async { isFilterLoading.value = true; try { final filterResponse = await ApiService.getDailyTaskFilter(projectId); if (filterResponse != null && filterResponse.success) { - taskFilterData = - filterResponse.data; // now taskFilterData is FilterData? + taskFilterData = filterResponse.data; logSafe( "Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}", level: LogLevel.info, @@ -171,12 +200,15 @@ class DailyTaskController extends GetxController { startDateTask = picked.start; endDateTask = picked.end; + // update Rx fields as well + startDateTaskRx.value = picked.start; + endDateTaskRx.value = picked.end; + logSafe( "Date range selected: $startDateTask to $endDateTask", level: LogLevel.info, ); - // ✅ Add null check before calling fetchTaskData final projectId = controller.selectedProjectId; if (projectId != null && projectId.isNotEmpty) { await controller.fetchTaskData(projectId); @@ -190,9 +222,7 @@ class DailyTaskController extends GetxController { required String projectId, required String taskAllocationId, }) async { - // re-fetch tasks await fetchTaskData(projectId); - - update(); // rebuilds UI + update(); } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index d4ad614..906e33e 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,6 +1,6 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; - static const String baseUrl = "https://api.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // Dashboard Module API Endpoints 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 84f66ff..abeaaa7 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -6,6 +6,7 @@ import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:get/get.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; @@ -35,15 +36,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"; } Widget _popupSelector({ @@ -126,6 +123,7 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ + // 🔹 View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -202,7 +200,7 @@ class _AttendanceFilterBottomSheetState }), ]); - // 🔹 Date Range only for attendanceLogs + // 🔹 Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -213,37 +211,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/dailyTaskPlanning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart index 56ca74c..a289858 100644 --- a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart +++ b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart @@ -4,6 +4,7 @@ import 'package:marco/controller/task_planning/daily_task_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; class DailyTaskFilterBottomSheet extends StatelessWidget { final DailyTaskController controller; @@ -217,74 +218,17 @@ class DailyTaskFilterBottomSheet extends StatelessWidget { children: [ MyText.labelMedium("Select Date Range"), MySpacing.height(8), - Row( - children: [ - Expanded( - child: _dateButton( - label: controller.startDateTask != null - ? "${controller.startDateTask!.day}/${controller.startDateTask!.month}/${controller.startDateTask!.year}" - : "From Date", - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: controller.startDateTask ?? DateTime.now(), - firstDate: DateTime(2022), - lastDate: DateTime.now(), - ); - if (picked != null) { - controller.startDateTask = picked; - controller.update(); // rebuild widget - } - }, - ), - ), - MySpacing.width(12), - Expanded( - child: _dateButton( - label: controller.endDateTask != null - ? "${controller.endDateTask!.day}/${controller.endDateTask!.month}/${controller.endDateTask!.year}" - : "To Date", - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: controller.endDateTask ?? DateTime.now(), - firstDate: DateTime(2022), - lastDate: DateTime.now(), - ); - if (picked != null) { - controller.endDateTask = picked; - controller.update(); - } - }, - ), - ), - ], + DateRangePickerWidget( + startDate: controller.startDateTaskRx, + endDate: controller.endDateTaskRx, + startLabel: "From Date", + endLabel: "To Date", + onDateRangeSelected: (start, end) { + controller.updateDateRange(start, end); + }, ), MySpacing.height(16), ], ); } - - Widget _dateButton({required String label, required VoidCallback onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - const Icon(Icons.calendar_today, size: 16, color: Colors.grey), - const SizedBox(width: 8), - Expanded( - child: MyText(label), - ), - ], - ), - ), - ); - } } diff --git a/lib/model/document/user_document_filter_bottom_sheet.dart b/lib/model/document/user_document_filter_bottom_sheet.dart index 616ec46..c50fb1c 100644 --- a/lib/model/document/user_document_filter_bottom_sheet.dart +++ b/lib/model/document/user_document_filter_bottom_sheet.dart @@ -2,25 +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 with UIMixin { +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; @@ -52,8 +60,8 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { }; docController.fetchDocuments( - entityTypeId: entityTypeId, - entityId: entityId, + entityTypeId: widget.entityTypeId, + entityId: widget.entityId, filter: jsonEncode(combinedFilter), reset: true, ); @@ -77,144 +85,64 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { ), ), ), - // --- 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 - ? contentTheme.primary - : 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 - ? contentTheme.primary - : 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; + } + }, ), ], ), @@ -252,7 +180,6 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { Obx(() { return Container( padding: MySpacing.all(12), - child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -264,8 +191,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: - contentTheme.primary, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -280,7 +206,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -295,7 +221,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { groupValue: docController.isVerified.value, onChanged: (val) => docController.isVerified.value = val, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -392,7 +318,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { (states) { if (states .contains(MaterialState.selected)) { - return Colors.indigo; // checked → Indigo + return contentTheme.primary; } return Colors.white; // unchecked → White }, @@ -455,31 +381,4 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { ], ); } - - 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 11edab3..2a5614a 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -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': diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 1ddce15..0c6d565 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -4,13 +4,13 @@ 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 StatefulWidget { final ExpenseController expenseController; @@ -29,8 +29,9 @@ class ExpenseFilterBottomSheet extends StatefulWidget { class _ExpenseFilterBottomSheetState extends State with UIMixin { - // FIX: create search adapter - Future> searchEmployeesForBottomSheet(String query) async { + /// Search employees for Paid By / Created By filters + Future> searchEmployeesForBottomSheet( + String query) async { await widget.expenseController.searchEmployees(query); return widget.expenseController.employeeSearchResults.toList(); } @@ -67,15 +68,15 @@ class _ExpenseFilterBottomSheetState extends State ), ), 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(), ], ), ), @@ -94,11 +95,10 @@ class _ExpenseFilterBottomSheetState extends State ); } - Widget _buildProjectFilter(BuildContext context) { + Widget _buildProjectFilter() { return _buildField( "Project", _popupSelector( - context, currentValue: widget.expenseController.selectedProject.value.isEmpty ? 'Select Project' : widget.expenseController.selectedProject.value, @@ -109,11 +109,10 @@ class _ExpenseFilterBottomSheetState extends State ); } - Widget _buildStatusFilter(BuildContext context) { + Widget _buildStatusFilter() { return _buildField( "Expense Status", _popupSelector( - context, currentValue: widget.expenseController.selectedStatus.value.isEmpty ? 'Select Expense Status' : widget.expenseController.expenseStatuses @@ -121,8 +120,9 @@ class _ExpenseFilterBottomSheetState extends State e.id == widget.expenseController.selectedStatus.value) ?.name ?? 'Select Expense Status', - items: - widget.expenseController.expenseStatuses.map((e) => e.name).toList(), + items: widget.expenseController.expenseStatuses + .map((e) => e.name) + .toList(), onSelected: (name) { final status = widget.expenseController.expenseStatuses .firstWhere((e) => e.name == name); @@ -132,117 +132,89 @@ class _ExpenseFilterBottomSheetState extends State ); } - 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, - child: SegmentedButton( - segments: widget.expenseController.dateTypes - .map( - (type) => ButtonSegment( - value: type, - label: Center( - 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: {widget.expenseController.selectedDateType.value}, - onSelectionChanged: (newSelection) { - if (newSelection.isNotEmpty) { - widget.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: widget.expenseController.startDate.value == null - ? 'Start Date' - : DateTimeUtils.formatDate( - widget.expenseController.startDate.value!, - 'dd MMM yyyy'), - onTap: () => _selectDate( - context, - widget.expenseController.startDate, - lastDate: widget.expenseController.endDate.value, - ), - ), - ), - MySpacing.width(12), - Expanded( - child: _dateButton( - label: widget.expenseController.endDate.value == null - ? 'End Date' - : DateTimeUtils.formatDate( - widget.expenseController.endDate.value!, - 'dd MMM yyyy'), - onTap: () => _selectDate( - context, - widget.expenseController.endDate, - firstDate: widget.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: widget.expenseController.selectedPaidByEmployees, searchEmployees: searchEmployeesForBottomSheet, title: 'Search Paid By', @@ -250,11 +222,10 @@ class _ExpenseFilterBottomSheetState extends State ); } - Widget _buildCreatedByFilter(BuildContext context) { + Widget _buildCreatedByFilter() { return _buildField( "Created By", _employeeSelector( - context: context, selectedEmployees: widget.expenseController.selectedCreatedByEmployees, searchEmployees: searchEmployeesForBottomSheet, title: 'Search Created By', @@ -262,23 +233,11 @@ class _ExpenseFilterBottomSheetState extends State ); } - 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, - {required String currentValue, - required List items, - required ValueChanged onSelected}) { + Widget _popupSelector({ + required String currentValue, + required List items, + required ValueChanged onSelected, + }) { return PopupMenuButton( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onSelected: onSelected, @@ -312,59 +271,7 @@ class _ExpenseFilterBottomSheetState extends State ); } - 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', @@ -373,9 +280,7 @@ class _ExpenseFilterBottomSheetState extends State 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 @@ -390,12 +295,22 @@ class _ExpenseFilterBottomSheetState extends State }), 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(