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:
parent
b1437db9e0
commit
ac7a75c92f
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
147
lib/helpers/widgets/date_range_picker.dart
Normal file
147
lib/helpers/widgets/date_range_picker.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user