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';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
// Data models
|
// ------------------ Data Models ------------------
|
||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
List<EmployeeModel> employees = [];
|
List<EmployeeModel> employees = [];
|
||||||
List<AttendanceLogModel> attendanceLogs = [];
|
List<AttendanceLogModel> attendanceLogs = [];
|
||||||
List<RegularizationLogModel> regularizationLogs = [];
|
List<RegularizationLogModel> regularizationLogs = [];
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||||
|
|
||||||
// ------------------ Organizations ------------------
|
// ------------------ Organizations ------------------
|
||||||
List<Organization> organizations = [];
|
List<Organization> organizations = [];
|
||||||
Organization? selectedOrganization;
|
Organization? selectedOrganization;
|
||||||
final isLoadingOrganizations = false.obs;
|
final isLoadingOrganizations = false.obs;
|
||||||
|
|
||||||
// States
|
// ------------------ States ------------------
|
||||||
String selectedTab = 'todaysAttendance';
|
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 isLoading = true.obs;
|
||||||
final isLoadingProjects = true.obs;
|
final isLoadingProjects = true.obs;
|
||||||
@ -46,11 +51,12 @@ class AttendanceController extends GetxController {
|
|||||||
final uploadingStates = <String, RxBool>{}.obs;
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
var showPendingOnly = false.obs;
|
var showPendingOnly = false.obs;
|
||||||
|
|
||||||
|
final searchQuery = ''.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_initializeDefaults();
|
_initializeDefaults();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeDefaults() {
|
void _initializeDefaults() {
|
||||||
@ -59,14 +65,38 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
void _setDefaultDateRange() {
|
void _setDefaultDateRange() {
|
||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
startDateAttendance = today.subtract(const Duration(days: 7));
|
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
||||||
endDateAttendance = today.subtract(const Duration(days: 1));
|
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
||||||
logSafe(
|
logSafe(
|
||||||
"Default date range set: $startDateAttendance to $endDateAttendance");
|
"Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Project & Employee ------------------
|
// ------------------ Computed Filters ------------------
|
||||||
/// Called when a notification says attendance has been updated
|
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 {
|
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||||
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
@ -79,36 +109,6 @@ class AttendanceController extends GetxController {
|
|||||||
"Attendance data refreshed from notification for project $projectId");
|
"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 {
|
Future<void> fetchTodaysAttendance(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
@ -128,6 +128,7 @@ class AttendanceController extends GetxController {
|
|||||||
logSafe("Failed to fetch employees for project $projectId",
|
logSafe("Failed to fetch employees for project $projectId",
|
||||||
level: LogLevel.error);
|
level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@ -147,7 +148,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Capture ------------------
|
// ------------------ Attendance Capture ------------------
|
||||||
|
|
||||||
Future<bool> captureAndUploadAttendance(
|
Future<bool> captureAndUploadAttendance(
|
||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
@ -155,8 +155,8 @@ class AttendanceController extends GetxController {
|
|||||||
String comment = "Marked via mobile app",
|
String comment = "Marked via mobile app",
|
||||||
required int action,
|
required int action,
|
||||||
bool imageCapture = true,
|
bool imageCapture = true,
|
||||||
String? markTime, // still optional in controller
|
String? markTime,
|
||||||
String? date, // new optional param
|
String? date,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
uploadingStates[employeeId]?.value = true;
|
uploadingStates[employeeId]?.value = true;
|
||||||
@ -170,7 +170,6 @@ class AttendanceController extends GetxController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Add timestamp to the image
|
|
||||||
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||||
imageFile: File(image.path));
|
imageFile: File(image.path));
|
||||||
|
|
||||||
@ -193,29 +192,20 @@ class AttendanceController extends GetxController {
|
|||||||
? ApiService.generateImageName(employeeId, employees.length + 1)
|
? ApiService.generateImageName(employeeId, employees.length + 1)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// ---------------- DATE / TIME LOGIC ----------------
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
// Default effectiveDate = now
|
|
||||||
DateTime effectiveDate = now;
|
DateTime effectiveDate = now;
|
||||||
|
|
||||||
if (action == 1) {
|
if (action == 1) {
|
||||||
// Checkout
|
|
||||||
// Try to find today's open log for this employee
|
|
||||||
final log = attendanceLogs.firstWhereOrNull(
|
final log = attendanceLogs.firstWhereOrNull(
|
||||||
(log) => log.employeeId == employeeId && log.checkOut == null,
|
(log) => log.employeeId == employeeId && log.checkOut == null,
|
||||||
);
|
);
|
||||||
if (log?.checkIn != null) {
|
if (log?.checkIn != null) effectiveDate = log!.checkIn!;
|
||||||
effectiveDate = log!.checkIn!; // use check-in date
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
||||||
|
|
||||||
final formattedDate =
|
final formattedDate =
|
||||||
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
||||||
|
|
||||||
// ---------------- API CALL ----------------
|
|
||||||
final result = await ApiService.uploadAttendanceImage(
|
final result = await ApiService.uploadAttendanceImage(
|
||||||
id,
|
id,
|
||||||
employeeId,
|
employeeId,
|
||||||
@ -264,7 +254,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Logs ------------------
|
// ------------------ Attendance Logs ------------------
|
||||||
|
|
||||||
Future<void> fetchAttendanceLogs(String? projectId,
|
Future<void> fetchAttendanceLogs(String? projectId,
|
||||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
@ -313,7 +302,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Regularization Logs ------------------
|
// ------------------ Regularization Logs ------------------
|
||||||
|
|
||||||
Future<void> fetchRegularizationLogs(String? projectId) async {
|
Future<void> fetchRegularizationLogs(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
@ -337,7 +325,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Log View ------------------
|
// ------------------ Attendance Log View ------------------
|
||||||
|
|
||||||
Future<void> fetchLogsView(String? id) async {
|
Future<void> fetchLogsView(String? id) async {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
@ -360,7 +347,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Combined Load ------------------
|
// ------------------ Combined Load ------------------
|
||||||
|
|
||||||
Future<void> loadAttendanceData(String projectId) async {
|
Future<void> loadAttendanceData(String projectId) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
@ -372,7 +358,6 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
await fetchOrganizations(projectId);
|
await fetchOrganizations(projectId);
|
||||||
|
|
||||||
// Call APIs depending on the selected tab only
|
|
||||||
switch (selectedTab) {
|
switch (selectedTab) {
|
||||||
case 'todaysAttendance':
|
case 'todaysAttendance':
|
||||||
await fetchTodaysAttendance(projectId);
|
await fetchTodaysAttendance(projectId);
|
||||||
@ -380,8 +365,8 @@ class AttendanceController extends GetxController {
|
|||||||
case 'attendanceLogs':
|
case 'attendanceLogs':
|
||||||
await fetchAttendanceLogs(
|
await fetchAttendanceLogs(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: startDateAttendance,
|
dateFrom: startDateAttendance.value,
|
||||||
dateTo: endDateAttendance,
|
dateTo: endDateAttendance.value,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'regularizationRequests':
|
case 'regularizationRequests':
|
||||||
@ -395,7 +380,6 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ UI Interaction ------------------
|
// ------------------ UI Interaction ------------------
|
||||||
|
|
||||||
Future<void> selectDateRangeForAttendance(
|
Future<void> selectDateRangeForAttendance(
|
||||||
BuildContext context, AttendanceController controller) async {
|
BuildContext context, AttendanceController controller) async {
|
||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
@ -405,16 +389,17 @@ class AttendanceController extends GetxController {
|
|||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: today.subtract(const Duration(days: 1)),
|
lastDate: today.subtract(const Duration(days: 1)),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: DateTimeRange(
|
||||||
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
start: startDateAttendance.value,
|
||||||
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
|
end: endDateAttendance.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
startDateAttendance = picked.start;
|
startDateAttendance.value = picked.start;
|
||||||
endDateAttendance = picked.end;
|
endDateAttendance.value = picked.end;
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Date range selected: $startDateAttendance to $endDateAttendance");
|
"Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
|
|||||||
@ -34,8 +34,8 @@ class DocumentController extends GetxController {
|
|||||||
// Additional filters
|
// Additional filters
|
||||||
final isUploadedAt = true.obs;
|
final isUploadedAt = true.obs;
|
||||||
final isVerified = RxnBool();
|
final isVerified = RxnBool();
|
||||||
final startDate = Rxn<String>();
|
final startDate = Rxn<DateTime>();
|
||||||
final endDate = Rxn<String>();
|
final endDate = Rxn<DateTime>();
|
||||||
|
|
||||||
// ==================== Lifecycle ====================
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,10 @@ class DailyTaskController extends GetxController {
|
|||||||
DateTime? startDateTask;
|
DateTime? startDateTask;
|
||||||
DateTime? endDateTask;
|
DateTime? endDateTask;
|
||||||
|
|
||||||
|
// Rx fields for DateRangePickerWidget
|
||||||
|
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
|
||||||
|
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
|
||||||
|
|
||||||
List<TaskModel> dailyTasks = [];
|
List<TaskModel> dailyTasks = [];
|
||||||
final RxSet<String> expandedDates = <String>{}.obs;
|
final RxSet<String> expandedDates = <String>{}.obs;
|
||||||
|
|
||||||
@ -33,14 +37,19 @@ class DailyTaskController extends GetxController {
|
|||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
RxBool isLoadingMore = false.obs;
|
RxBool isLoadingMore = false.obs;
|
||||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
int currentPage = 1;
|
int currentPage = 1;
|
||||||
int pageSize = 20;
|
int pageSize = 20;
|
||||||
bool hasMore = true;
|
bool hasMore = true;
|
||||||
|
|
||||||
|
FilterData? taskFilterData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_initializeDefaults();
|
_initializeDefaults();
|
||||||
|
_initializeRxDates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeDefaults() {
|
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() {
|
void clearTaskFilters() {
|
||||||
selectedBuildings.clear();
|
selectedBuildings.clear();
|
||||||
selectedFloors.clear();
|
selectedFloors.clear();
|
||||||
@ -65,9 +80,26 @@ class DailyTaskController extends GetxController {
|
|||||||
selectedServices.clear();
|
selectedServices.clear();
|
||||||
startDateTask = null;
|
startDateTask = null;
|
||||||
endDateTask = null;
|
endDateTask = null;
|
||||||
|
|
||||||
|
// reset Rx dates as well
|
||||||
|
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
|
||||||
|
endDateTaskRx.value = DateTime.now();
|
||||||
|
|
||||||
update();
|
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(
|
Future<void> fetchTaskData(
|
||||||
String projectId, {
|
String projectId, {
|
||||||
int pageNumber = 1,
|
int pageNumber = 1,
|
||||||
@ -119,16 +151,13 @@ class DailyTaskController extends GetxController {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterData? taskFilterData;
|
|
||||||
|
|
||||||
Future<void> fetchTaskFilter(String projectId) async {
|
Future<void> fetchTaskFilter(String projectId) async {
|
||||||
isFilterLoading.value = true;
|
isFilterLoading.value = true;
|
||||||
try {
|
try {
|
||||||
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
|
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
|
||||||
|
|
||||||
if (filterResponse != null && filterResponse.success) {
|
if (filterResponse != null && filterResponse.success) {
|
||||||
taskFilterData =
|
taskFilterData = filterResponse.data;
|
||||||
filterResponse.data; // now taskFilterData is FilterData?
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
|
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
|
||||||
level: LogLevel.info,
|
level: LogLevel.info,
|
||||||
@ -171,12 +200,15 @@ class DailyTaskController extends GetxController {
|
|||||||
startDateTask = picked.start;
|
startDateTask = picked.start;
|
||||||
endDateTask = picked.end;
|
endDateTask = picked.end;
|
||||||
|
|
||||||
|
// update Rx fields as well
|
||||||
|
startDateTaskRx.value = picked.start;
|
||||||
|
endDateTaskRx.value = picked.end;
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Date range selected: $startDateTask to $endDateTask",
|
"Date range selected: $startDateTask to $endDateTask",
|
||||||
level: LogLevel.info,
|
level: LogLevel.info,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Add null check before calling fetchTaskData
|
|
||||||
final projectId = controller.selectedProjectId;
|
final projectId = controller.selectedProjectId;
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
if (projectId != null && projectId.isNotEmpty) {
|
||||||
await controller.fetchTaskData(projectId);
|
await controller.fetchTaskData(projectId);
|
||||||
@ -190,9 +222,7 @@ class DailyTaskController extends GetxController {
|
|||||||
required String projectId,
|
required String projectId,
|
||||||
required String taskAllocationId,
|
required String taskAllocationId,
|
||||||
}) async {
|
}) async {
|
||||||
// re-fetch tasks
|
|
||||||
await fetchTaskData(projectId);
|
await fetchTaskData(projectId);
|
||||||
|
update();
|
||||||
update(); // rebuilds UI
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
class ApiEndpoints {
|
class ApiEndpoints {
|
||||||
// static const String baseUrl = "https://stageapi.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://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||||
|
|
||||||
// Dashboard Module API Endpoints
|
// 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:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||||
|
import 'package:marco/helpers/widgets/date_range_picker.dart';
|
||||||
|
|
||||||
class AttendanceFilterBottomSheet extends StatefulWidget {
|
class AttendanceFilterBottomSheet extends StatefulWidget {
|
||||||
final AttendanceController controller;
|
final AttendanceController controller;
|
||||||
@ -35,16 +36,12 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getLabelText() {
|
String getLabelText() {
|
||||||
final startDate = widget.controller.startDateAttendance;
|
final start = DateTimeUtils.formatDate(
|
||||||
final endDate = widget.controller.endDateAttendance;
|
widget.controller.startDateAttendance.value, 'dd MMM yyyy');
|
||||||
|
final end = DateTimeUtils.formatDate(
|
||||||
if (startDate != null && endDate != null) {
|
widget.controller.endDateAttendance.value, 'dd MMM yyyy');
|
||||||
final start = DateTimeUtils.formatDate(startDate, 'dd MMM yyyy');
|
|
||||||
final end = DateTimeUtils.formatDate(endDate, 'dd MMM yyyy');
|
|
||||||
return "$start - $end";
|
return "$start - $end";
|
||||||
}
|
}
|
||||||
return "Date Range";
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _popupSelector({
|
Widget _popupSelector({
|
||||||
required String currentValue,
|
required String currentValue,
|
||||||
@ -126,6 +123,7 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final List<Widget> widgets = [
|
final List<Widget> widgets = [
|
||||||
|
// 🔹 View Section
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
@ -202,7 +200,7 @@ class _AttendanceFilterBottomSheetState
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 🔹 Date Range only for attendanceLogs
|
// 🔹 Date Range (only for Attendance Logs)
|
||||||
if (tempSelectedTab == 'attendanceLogs') {
|
if (tempSelectedTab == 'attendanceLogs') {
|
||||||
widgets.addAll([
|
widgets.addAll([
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@ -213,37 +211,16 @@ class _AttendanceFilterBottomSheetState
|
|||||||
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
InkWell(
|
// ✅ Reusable DateRangePickerWidget
|
||||||
borderRadius: BorderRadius.circular(10),
|
DateRangePickerWidget(
|
||||||
onTap: () async {
|
startDate: widget.controller.startDateAttendance,
|
||||||
await widget.controller.selectDateRangeForAttendance(
|
endDate: widget.controller.endDateAttendance,
|
||||||
context,
|
startLabel: "Start Date",
|
||||||
widget.controller,
|
endLabel: "End Date",
|
||||||
);
|
onDateRangeSelected: (start, end) {
|
||||||
|
// Optional: trigger UI updates if needed
|
||||||
setState(() {});
|
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/utils/base_bottom_sheet.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/helpers/widgets/date_range_picker.dart';
|
||||||
|
|
||||||
class DailyTaskFilterBottomSheet extends StatelessWidget {
|
class DailyTaskFilterBottomSheet extends StatelessWidget {
|
||||||
final DailyTaskController controller;
|
final DailyTaskController controller;
|
||||||
@ -217,74 +218,17 @@ class DailyTaskFilterBottomSheet extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
MyText.labelMedium("Select Date Range"),
|
MyText.labelMedium("Select Date Range"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Row(
|
DateRangePickerWidget(
|
||||||
children: [
|
startDate: controller.startDateTaskRx,
|
||||||
Expanded(
|
endDate: controller.endDateTaskRx,
|
||||||
child: _dateButton(
|
startLabel: "From Date",
|
||||||
label: controller.startDateTask != null
|
endLabel: "To Date",
|
||||||
? "${controller.startDateTask!.day}/${controller.startDateTask!.month}/${controller.startDateTask!.year}"
|
onDateRangeSelected: (start, end) {
|
||||||
: "From Date",
|
controller.updateDateRange(start, end);
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
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:get/get.dart';
|
||||||
import 'package:marco/controller/document/user_document_controller.dart';
|
import 'package:marco/controller/document/user_document_controller.dart';
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.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_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/model/document/document_filter_model.dart';
|
import 'package:marco/model/document/document_filter_model.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
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 entityId;
|
||||||
final String entityTypeId;
|
final String entityTypeId;
|
||||||
final DocumentController docController = Get.find<DocumentController>();
|
|
||||||
|
|
||||||
UserDocumentFilterBottomSheet({
|
const UserDocumentFilterBottomSheet({
|
||||||
super.key,
|
super.key,
|
||||||
required this.entityId,
|
required this.entityId,
|
||||||
required this.entityTypeId,
|
required this.entityTypeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UserDocumentFilterBottomSheet> createState() =>
|
||||||
|
_UserDocumentFilterBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserDocumentFilterBottomSheetState
|
||||||
|
extends State<UserDocumentFilterBottomSheet> with UIMixin {
|
||||||
|
final DocumentController docController = Get.find<DocumentController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final filterData = docController.filters.value;
|
final filterData = docController.filters.value;
|
||||||
@ -52,8 +60,8 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
};
|
};
|
||||||
|
|
||||||
docController.fetchDocuments(
|
docController.fetchDocuments(
|
||||||
entityTypeId: entityTypeId,
|
entityTypeId: widget.entityTypeId,
|
||||||
entityId: entityId,
|
entityId: widget.entityId,
|
||||||
filter: jsonEncode(combinedFilter),
|
filter: jsonEncode(combinedFilter),
|
||||||
reset: true,
|
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(
|
_buildField(
|
||||||
"Choose Date",
|
"Choose Date",
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Segmented Buttons
|
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Container(
|
return Row(
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
// --- Upload Date ---
|
||||||
borderRadius: BorderRadius.circular(24),
|
Expanded(
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
Radio<bool>(
|
||||||
|
value: true,
|
||||||
|
groupValue:
|
||||||
|
docController.isUploadedAt.value,
|
||||||
|
onChanged: (val) => docController
|
||||||
|
.isUploadedAt.value = val!,
|
||||||
|
activeColor: contentTheme.primary,
|
||||||
|
),
|
||||||
|
MyText("Upload Date"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// --- Update Date ---
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
onTap: () =>
|
children: [
|
||||||
docController.isUploadedAt.value = true,
|
Radio<bool>(
|
||||||
child: Container(
|
value: false,
|
||||||
padding: const EdgeInsets.symmetric(
|
groupValue:
|
||||||
vertical: 10),
|
docController.isUploadedAt.value,
|
||||||
decoration: BoxDecoration(
|
onChanged: (val) => docController
|
||||||
color: docController.isUploadedAt.value
|
.isUploadedAt.value = val!,
|
||||||
? contentTheme.primary
|
activeColor: 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
MyText("Update Date"),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
|
|
||||||
// Date Range
|
// --- Date Range Picker ---
|
||||||
Row(
|
DateRangePickerWidget(
|
||||||
children: [
|
startDate: docController.startDate,
|
||||||
Expanded(
|
endDate: docController.endDate,
|
||||||
child: Obx(() {
|
startLabel: "From Date",
|
||||||
return _dateButton(
|
endLabel: "To Date",
|
||||||
label: docController.startDate.value == null
|
onDateRangeSelected: (start, end) {
|
||||||
? 'From Date'
|
if (start != null && end != null) {
|
||||||
: DateTimeUtils.formatDate(
|
docController.startDate.value = start;
|
||||||
DateTime.parse(
|
docController.endDate.value = end;
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -252,7 +180,6 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
Obx(() {
|
Obx(() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: MySpacing.all(12),
|
padding: MySpacing.all(12),
|
||||||
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -264,8 +191,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
groupValue: docController.isVerified.value,
|
groupValue: docController.isVerified.value,
|
||||||
onChanged: (val) =>
|
onChanged: (val) =>
|
||||||
docController.isVerified.value = val,
|
docController.isVerified.value = val,
|
||||||
activeColor:
|
activeColor: contentTheme.primary,
|
||||||
contentTheme.primary,
|
|
||||||
materialTapTargetSize:
|
materialTapTargetSize:
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
@ -280,7 +206,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
groupValue: docController.isVerified.value,
|
groupValue: docController.isVerified.value,
|
||||||
onChanged: (val) =>
|
onChanged: (val) =>
|
||||||
docController.isVerified.value = val,
|
docController.isVerified.value = val,
|
||||||
activeColor: Colors.indigo,
|
activeColor: contentTheme.primary,
|
||||||
materialTapTargetSize:
|
materialTapTargetSize:
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
@ -295,7 +221,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
groupValue: docController.isVerified.value,
|
groupValue: docController.isVerified.value,
|
||||||
onChanged: (val) =>
|
onChanged: (val) =>
|
||||||
docController.isVerified.value = val,
|
docController.isVerified.value = val,
|
||||||
activeColor: Colors.indigo,
|
activeColor: contentTheme.primary,
|
||||||
materialTapTargetSize:
|
materialTapTargetSize:
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
@ -392,7 +318,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
|
|||||||
(states) {
|
(states) {
|
||||||
if (states
|
if (states
|
||||||
.contains(MaterialState.selected)) {
|
.contains(MaterialState.selected)) {
|
||||||
return Colors.indigo; // checked → Indigo
|
return contentTheme.primary;
|
||||||
}
|
}
|
||||||
return Colors.white; // unchecked → White
|
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"
|
// Filter logs if "pending only"
|
||||||
final showPendingOnly = widget.controller.showPendingOnly.value;
|
final showPendingOnly = widget.controller.showPendingOnly.value;
|
||||||
final filteredLogs = showPendingOnly
|
final filteredLogs = showPendingOnly
|
||||||
? allLogs
|
? allLogs.where((emp) => emp.activity == 1).toList()
|
||||||
.where((emp) => emp.activity == 1 )
|
|
||||||
.toList()
|
|
||||||
: allLogs;
|
: allLogs;
|
||||||
|
|
||||||
// Group logs by date string
|
// Group logs by date string
|
||||||
@ -126,11 +124,9 @@ class _AttendanceLogsTabState extends State<AttendanceLogsTab> {
|
|||||||
return db.compareTo(da);
|
return db.compareTo(da);
|
||||||
});
|
});
|
||||||
|
|
||||||
final dateRangeText = widget.controller.startDateAttendance != null &&
|
final dateRangeText =
|
||||||
widget.controller.endDateAttendance != null
|
'${DateTimeUtils.formatDate(widget.controller.startDateAttendance.value, 'dd MMM yyyy')} - '
|
||||||
? '${DateTimeUtils.formatDate(widget.controller.startDateAttendance!, 'dd MMM yyyy')} - '
|
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance.value, 'dd MMM yyyy')}';
|
||||||
'${DateTimeUtils.formatDate(widget.controller.endDateAttendance!, 'dd MMM yyyy')}'
|
|
||||||
: 'Select date range';
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@ -67,8 +67,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
case 'attendanceLogs':
|
case 'attendanceLogs':
|
||||||
await attendanceController.fetchAttendanceLogs(
|
await attendanceController.fetchAttendanceLogs(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: attendanceController.startDateAttendance,
|
dateFrom: attendanceController.startDateAttendance.value,
|
||||||
dateTo: attendanceController.endDateAttendance,
|
dateTo: attendanceController.endDateAttendance.value,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'regularizationRequests':
|
case 'regularizationRequests':
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.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_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.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/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:marco/helpers/widgets/date_range_picker.dart';
|
||||||
|
|
||||||
class ExpenseFilterBottomSheet extends StatefulWidget {
|
class ExpenseFilterBottomSheet extends StatefulWidget {
|
||||||
final ExpenseController expenseController;
|
final ExpenseController expenseController;
|
||||||
@ -29,8 +29,9 @@ class ExpenseFilterBottomSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
||||||
with UIMixin {
|
with UIMixin {
|
||||||
// FIX: create search adapter
|
/// Search employees for Paid By / Created By filters
|
||||||
Future<List<EmployeeModel>> searchEmployeesForBottomSheet(String query) async {
|
Future<List<EmployeeModel>> searchEmployeesForBottomSheet(
|
||||||
|
String query) async {
|
||||||
await widget.expenseController.searchEmployees(query);
|
await widget.expenseController.searchEmployees(query);
|
||||||
return widget.expenseController.employeeSearchResults.toList();
|
return widget.expenseController.employeeSearchResults.toList();
|
||||||
}
|
}
|
||||||
@ -67,15 +68,15 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_buildProjectFilter(context),
|
_buildProjectFilter(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildStatusFilter(context),
|
_buildStatusFilter(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildDateRangeFilter(context),
|
_buildDateRangeFilter(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildPaidByFilter(context),
|
_buildPaidByFilter(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildCreatedByFilter(context),
|
_buildCreatedByFilter(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -94,11 +95,10 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProjectFilter(BuildContext context) {
|
Widget _buildProjectFilter() {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Project",
|
"Project",
|
||||||
_popupSelector(
|
_popupSelector(
|
||||||
context,
|
|
||||||
currentValue: widget.expenseController.selectedProject.value.isEmpty
|
currentValue: widget.expenseController.selectedProject.value.isEmpty
|
||||||
? 'Select Project'
|
? 'Select Project'
|
||||||
: widget.expenseController.selectedProject.value,
|
: widget.expenseController.selectedProject.value,
|
||||||
@ -109,11 +109,10 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusFilter(BuildContext context) {
|
Widget _buildStatusFilter() {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Expense Status",
|
"Expense Status",
|
||||||
_popupSelector(
|
_popupSelector(
|
||||||
context,
|
|
||||||
currentValue: widget.expenseController.selectedStatus.value.isEmpty
|
currentValue: widget.expenseController.selectedStatus.value.isEmpty
|
||||||
? 'Select Expense Status'
|
? 'Select Expense Status'
|
||||||
: widget.expenseController.expenseStatuses
|
: widget.expenseController.expenseStatuses
|
||||||
@ -121,8 +120,9 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
e.id == widget.expenseController.selectedStatus.value)
|
e.id == widget.expenseController.selectedStatus.value)
|
||||||
?.name ??
|
?.name ??
|
||||||
'Select Expense Status',
|
'Select Expense Status',
|
||||||
items:
|
items: widget.expenseController.expenseStatuses
|
||||||
widget.expenseController.expenseStatuses.map((e) => e.name).toList(),
|
.map((e) => e.name)
|
||||||
|
.toList(),
|
||||||
onSelected: (name) {
|
onSelected: (name) {
|
||||||
final status = widget.expenseController.expenseStatuses
|
final status = widget.expenseController.expenseStatuses
|
||||||
.firstWhere((e) => e.name == name);
|
.firstWhere((e) => e.name == name);
|
||||||
@ -132,117 +132,89 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDateRangeFilter(BuildContext context) {
|
Widget _buildDateRangeFilter() {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Date Filter",
|
"Date Filter",
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// --- Radio Buttons for Transaction Date / Created At ---
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return SizedBox(
|
return Row(
|
||||||
width: double.infinity,
|
children: [
|
||||||
child: SegmentedButton<String>(
|
// --- Transaction Date ---
|
||||||
segments: widget.expenseController.dateTypes
|
Expanded(
|
||||||
.map(
|
child: Row(
|
||||||
(type) => ButtonSegment(
|
children: [
|
||||||
value: type,
|
Radio<String>(
|
||||||
label: Center(
|
value: "Transaction Date",
|
||||||
child: MyText(
|
groupValue:
|
||||||
type,
|
widget.expenseController.selectedDateType.value,
|
||||||
style: MyTextStyle.bodySmall(
|
onChanged: (val) {
|
||||||
fontWeight: 600,
|
if (val != null) {
|
||||||
fontSize: 13,
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
selected: {widget.expenseController.selectedDateType.value},
|
|
||||||
onSelectionChanged: (newSelection) {
|
|
||||||
if (newSelection.isNotEmpty) {
|
|
||||||
widget.expenseController.selectedDateType.value =
|
widget.expenseController.selectedDateType.value =
|
||||||
newSelection.first;
|
val;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
activeColor: contentTheme.primary,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
Flexible(
|
||||||
|
child: MyText(
|
||||||
|
"Transaction Date",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
// --- 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),
|
||||||
|
// --- 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(
|
return _buildField(
|
||||||
"Paid By",
|
"Paid By",
|
||||||
_employeeSelector(
|
_employeeSelector(
|
||||||
context: context,
|
|
||||||
selectedEmployees: widget.expenseController.selectedPaidByEmployees,
|
selectedEmployees: widget.expenseController.selectedPaidByEmployees,
|
||||||
searchEmployees: searchEmployeesForBottomSheet,
|
searchEmployees: searchEmployeesForBottomSheet,
|
||||||
title: 'Search Paid By',
|
title: 'Search Paid By',
|
||||||
@ -250,11 +222,10 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreatedByFilter(BuildContext context) {
|
Widget _buildCreatedByFilter() {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Created By",
|
"Created By",
|
||||||
_employeeSelector(
|
_employeeSelector(
|
||||||
context: context,
|
|
||||||
selectedEmployees: widget.expenseController.selectedCreatedByEmployees,
|
selectedEmployees: widget.expenseController.selectedCreatedByEmployees,
|
||||||
searchEmployees: searchEmployeesForBottomSheet,
|
searchEmployees: searchEmployeesForBottomSheet,
|
||||||
title: 'Search Created By',
|
title: 'Search Created By',
|
||||||
@ -262,23 +233,11 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectDate(BuildContext context, Rx<DateTime?> dateNotifier,
|
Widget _popupSelector({
|
||||||
{DateTime? firstDate, DateTime? lastDate}) async {
|
required String currentValue,
|
||||||
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 List<String> items,
|
||||||
required ValueChanged<String> onSelected}) {
|
required ValueChanged<String> onSelected,
|
||||||
|
}) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
onSelected: onSelected,
|
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({
|
Widget _employeeSelector({
|
||||||
required BuildContext context,
|
|
||||||
required RxList<EmployeeModel> selectedEmployees,
|
required RxList<EmployeeModel> selectedEmployees,
|
||||||
required Future<List<EmployeeModel>> Function(String) searchEmployees,
|
required Future<List<EmployeeModel>> Function(String) searchEmployees,
|
||||||
String title = 'Search Employee',
|
String title = 'Search Employee',
|
||||||
@ -373,9 +280,7 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (selectedEmployees.isEmpty) {
|
if (selectedEmployees.isEmpty) return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: selectedEmployees
|
children: selectedEmployees
|
||||||
@ -390,12 +295,22 @@ class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
|
|||||||
}),
|
}),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _showEmployeeSelectorBottomSheet(
|
onTap: () async {
|
||||||
|
final List<EmployeeModel>? result =
|
||||||
|
await showModalBottomSheet<List<EmployeeModel>>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (context) => EmployeeSelectorBottomSheet(
|
||||||
selectedEmployees: selectedEmployees,
|
selectedEmployees: selectedEmployees,
|
||||||
searchEmployees: searchEmployees,
|
searchEmployees: searchEmployees,
|
||||||
title: title,
|
title: title,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
if (result != null) selectedEmployees.assignAll(result);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: MySpacing.all(12),
|
padding: MySpacing.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user