Refactor date handling in controllers and UI components

- Updated `user_document_controller.dart` to use DateTime for start and end dates.
- Enhanced `daily_task_controller.dart` with Rx fields for date range and added methods to update date ranges.
- Reverted API base URL in `api_endpoints.dart` to stage environment.
- Introduced `date_range_picker.dart` widget for reusable date selection.
- Integrated `DateRangePickerWidget` in attendance and daily task filter bottom sheets.
- Simplified date range selection logic in `attendance_filter_sheet.dart`, `daily_progress_report_filter.dart`, and `user_document_filter_bottom_sheet.dart`.
- Updated expense filter bottom sheet to utilize the new date range picker.
This commit is contained in:
Vaibhav Surve 2025-11-04 14:15:21 +05:30
parent b1437db9e0
commit ac7a75c92f
11 changed files with 435 additions and 542 deletions

View File

@ -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<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
// ------------------ Organizations ------------------
List<Organization> organizations = [];
Organization? selectedOrganization;
final isLoadingOrganizations = false.obs;
// States
// ------------------ States ------------------
String selectedTab = 'todaysAttendance';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
// Reactive date range
final Rx<DateTime> startDateAttendance =
DateTime.now().subtract(const Duration(days: 7)).obs;
final Rx<DateTime> 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 = <String, RxBool>{}.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<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
List<AttendanceLogModel> get filteredLogs {
if (searchQuery.value.isEmpty) return attendanceLogs;
return attendanceLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
List<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// ------------------ Project & Employee APIs ------------------
Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().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<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered logs
List<AttendanceLogModel> 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<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
Future<void> 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<bool> 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<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
@ -313,7 +302,6 @@ class AttendanceController extends GetxController {
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
@ -337,7 +325,6 @@ class AttendanceController extends GetxController {
}
// ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
@ -360,7 +347,6 @@ class AttendanceController extends GetxController {
}
// ------------------ Combined Load ------------------
Future<void> 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<void> 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<ProjectController>().selectedProject?.id,

View File

@ -34,11 +34,11 @@ class DocumentController extends GetxController {
// Additional filters
final isUploadedAt = true.obs;
final isVerified = RxnBool();
final startDate = Rxn<String>();
final endDate = Rxn<String>();
final startDate = Rxn<DateTime>();
final endDate = Rxn<DateTime>();
// ==================== 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,

View File

@ -13,6 +13,10 @@ class DailyTaskController extends GetxController {
DateTime? startDateTask;
DateTime? endDateTask;
// Rx fields for DateRangePickerWidget
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
@ -33,14 +37,19 @@ class DailyTaskController extends GetxController {
RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> 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<void> 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<void> 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();
}
}

View File

@ -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

View File

@ -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<DateTime?> startDate;
final Rx<DateTime?> 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<DateRangePickerWidget> createState() => _DateRangePickerWidgetState();
}
class _DateRangePickerWidgetState extends State<DateRangePickerWidget>
with UIMixin {
Future<void> _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<DateTime?> 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,
),
],
);
}
}

View File

@ -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<Widget> 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),
],
),
),
),
]);
}

View File

@ -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),
),
],
),
),
);
}
}

View File

@ -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<DocumentController>();
UserDocumentFilterBottomSheet({
const UserDocumentFilterBottomSheet({
super.key,
required this.entityId,
required this.entityTypeId,
});
@override
State<UserDocumentFilterBottomSheet> createState() =>
_UserDocumentFilterBottomSheetState();
}
class _UserDocumentFilterBottomSheetState
extends State<UserDocumentFilterBottomSheet> with UIMixin {
final DocumentController docController = Get.find<DocumentController>();
@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<bool>(
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<bool>(
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,
),
),
],
),
),
);
}
}

View File

@ -104,9 +104,7 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
// 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<AttendanceLogsTab> {
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,

View File

@ -67,8 +67,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> 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':

View File

@ -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<ExpenseFilterBottomSheet>
with UIMixin {
// FIX: create search adapter
Future<List<EmployeeModel>> searchEmployeesForBottomSheet(String query) async {
/// Search employees for Paid By / Created By filters
Future<List<EmployeeModel>> searchEmployeesForBottomSheet(
String query) async {
await widget.expenseController.searchEmployees(query);
return widget.expenseController.employeeSearchResults.toList();
}
@ -67,15 +68,15 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
),
),
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<ExpenseFilterBottomSheet>
);
}
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<ExpenseFilterBottomSheet>
);
}
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<ExpenseFilterBottomSheet>
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<ExpenseFilterBottomSheet>
);
}
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<String>(
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<String>(
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<String>(
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<ExpenseFilterBottomSheet>
);
}
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<ExpenseFilterBottomSheet>
);
}
Future<void> _selectDate(BuildContext context, Rx<DateTime?> 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<String> items,
required ValueChanged<String> onSelected}) {
Widget _popupSelector({
required String currentValue,
required List<String> items,
required ValueChanged<String> onSelected,
}) {
return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: onSelected,
@ -312,59 +271,7 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
);
}
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<void> _showEmployeeSelectorBottomSheet({
required BuildContext context,
required RxList<EmployeeModel> selectedEmployees,
required Future<List<EmployeeModel>> Function(String) searchEmployees,
String title = 'Select Employee',
}) async {
final List<EmployeeModel>? result =
await showModalBottomSheet<List<EmployeeModel>>(
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<EmployeeModel> selectedEmployees,
required Future<List<EmployeeModel>> Function(String) searchEmployees,
String title = 'Search Employee',
@ -373,9 +280,7 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
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<ExpenseFilterBottomSheet>
}),
MySpacing.height(8),
GestureDetector(
onTap: () => _showEmployeeSelectorBottomSheet(
context: context,
selectedEmployees: selectedEmployees,
searchEmployees: searchEmployees,
title: title,
),
onTap: () async {
final List<EmployeeModel>? result =
await showModalBottomSheet<List<EmployeeModel>>(
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(