Compare commits

...

357 Commits

Author SHA1 Message Date
83218166ba revert 7fb5a5217aee404e3c940f0729086460d9bd1c4c
revert fixed initial loging
2025-10-11 08:49:37 +00:00
26611d3650 reduced snaxkbar time 2025-10-11 14:16:38 +05:30
7fb5a5217a fixed initial loging 2025-10-11 11:42:40 +05:30
706881d08d Merge pull request 'Issue_10_10_2025' (#75) from Issue_10_10_2025 into main
Reviewed-on: #75
2025-10-11 04:57:46 +00:00
e8acfe10d9 enhanced the restore button logic 2025-10-10 20:01:16 +05:30
16e2f5a4f3 enhanced desabled cards 2025-10-10 19:52:12 +05:30
cd92d4d309 added multiple bucket assignment 2025-10-10 19:46:17 +05:30
5dc2db0a8b fixed the issues 2025-10-10 19:41:13 +05:30
bb5fdb27b2 fixed delete and ad comment not updating properly in contact detailsnotes 2025-10-10 10:10:46 +05:30
acb203848e fixed the bugs 2025-10-09 18:09:54 +05:30
e6238ca5b0 Merge pull request 'Tenant_Selection_Issue_Fixed' (#74) from Tenant_Selection_Issue_Fixed into main
Reviewed-on: #74
2025-10-09 09:28:43 +00:00
d5a8d08e63 added splash screen 2025-10-08 17:33:20 +05:30
041b62ca2f fixed the tennt selection process 2025-10-08 16:07:26 +05:30
d1d48b1a74 fixed auto tenant selection 2025-10-08 11:41:38 +05:30
7e75431feb fixed permissions loading issue on employee screen 2025-10-08 11:20:50 +05:30
45bc492683 fixed tenant issue 2025-10-08 11:06:39 +05:30
26675388dd corrected the prefield data while editing employee 2025-10-06 12:27:43 +05:30
d02211d389 feat: Add Flutter APK build script with dependency management and output paths 2025-09-30 14:42:06 +05:30
5086b3be98 Merge pull request 'Vaibhav_Enhancement-#1129' (#73) from Vaibhav_Enhancement-#1129 into main
Reviewed-on: #73
2025-09-30 09:09:35 +00:00
539e94fc99 feat: Rename "Comment" to "Note" in relevant UI components and dialogs 2025-09-30 13:25:31 +05:30
dbd4a42b7a feat: Improve comment action buttons layout and spacing in ContactDetailScreen 2025-09-30 11:56:32 +05:30
38ae9e3571 feat: Update GlobalProjectModel and ProjectModel to handle nullable date fields and improve JSON parsing 2025-09-30 11:03:33 +05:30
7f924ee533 feat: Add restore and delete functionality for comments with confirmation dialogs 2025-09-29 17:51:13 +05:30
d6587931fa feat: Implement restore and delete functionality for notes with confirmation dialog 2025-09-29 17:03:47 +05:30
8ad9690d89 feat: Allow multi-line input for description field in AddContactBottomSheet 2025-09-29 16:04:10 +05:30
b286ab854a feat: Add designation field to contact model and update add contact functionality 2025-09-29 16:03:03 +05:30
8576448a32 feat: Enhance AddEmployee functionality with email and organization selection support 2025-09-27 16:56:56 +05:30
075167e285 feat: Update Dashboard Overview to display total employees instead of absent count 2025-09-27 11:30:18 +05:30
fd7c338c05 refactor: Remove loading skeletons from attendance and project progress charts for improved performance 2025-09-25 18:35:39 +05:30
b5d8d41e42 feat: Redesign Dashboard Overview Widgets with enhanced metrics display and loading states 2025-09-25 17:04:22 +05:30
781a8dabaf feat: Implement recent tenant selection logic and enhance tenant loading process 2025-09-25 15:06:57 +05:30
98612db7b5 feat: Enhance AttendanceLogViewButton with state management and improved log display 2025-09-25 14:42:22 +05:30
7c21324b42 refactor: Remove unused tenant loading logic and streamline tenant selection process 2025-09-25 11:35:27 +05:30
1900e944e5 feat: Implement tenant selection logic and load saved tenant from local storage 2025-09-25 11:27:03 +05:30
53fefbba50 refactor: Update border radius for UI consistency in DocumentDetailsPage 2025-09-24 17:48:12 +05:30
9012218a44 refactor: Adjust border radius in EmployeeDetailPage and update padding in EmployeesScreen 2025-09-24 17:42:42 +05:30
fb26ba0757 feat: Add maxLines property to LabeledInput and update document tile to conditionally show date header 2025-09-24 17:38:24 +05:30
aa5ae29284 feat: Update label for visibility toggle to 'Show Deleted Contacts' 2025-09-24 17:22:09 +05:30
83d9d0689a feat: Implement tabbed navigation and enhance UI for expense and directory views 2025-09-24 17:16:38 +05:30
85d3dedbef feat: Enhance organization selection and fetching logic with reactive state management 2025-09-24 15:37:06 +05:30
1d9c416f68 refactor: Standardize border radius values across dashboard components 2025-09-24 15:04:51 +05:30
7d211e24f8 feat: Implement pagination and service filtering in daily task fetching logic 2025-09-23 15:02:37 +05:30
fc081c779e feat: Add submit button text to Attendance Filter bottom sheet 2025-09-22 17:44:02 +05:30
4cb60138c0 refactor: Simplify color assignment logic in AttendanceDashboardChart and ProjectProgressChart 2025-09-22 17:15:57 +05:30
68cfdf54d6 feat: Add service selection functionality and integrate with task fetching logic 2025-09-22 16:49:36 +05:30
83a8abbb87 feat: Implement organization selection functionality and integrate with employee fetching logic 2025-09-22 15:54:32 +05:30
17c7b9f10d feat: Update Tenant model to use Industry and TenantStatus objects and improve industry display in TenantCard 2025-09-22 14:39:48 +05:30
efb5564fcb feat: Add project refresh logic upon tenant selection to ensure updated project data 2025-09-22 14:27:25 +05:30
637426aea4 feat: Enhance organization selector to include 'All Organizations' option and improve selection logic 2025-09-22 12:14:48 +05:30
8ed67dcdf1 feat: Increase default timeout duration for API requests to enhance reliability 2025-09-22 11:21:23 +05:30
6863769b8a feat: Add organization selection and related API integration in attendance module 2025-09-21 18:17:00 +05:30
8d3c900262 feat: Implement tenant selection feature with UI and service integration 2025-09-21 16:39:22 +05:30
9362945d60 Merge pull request 'Vaibhav_Enhancement-#1253' (#72) from Vaibhav_Enhancement-#1253 into main
Reviewed-on: #72
2025-09-21 04:33:42 +00:00
b5e9c7b6a3 feat: Update base URL to stage API for development environment 2025-09-20 16:42:40 +05:30
04cbdab277 feat: Enhance daily task planning by fetching infra details and associated tasks; update API service for new endpoints 2025-09-20 16:32:11 +05:30
ae7ce851ee feat: Update User Document Filter to use radio buttons for document status selection 2025-09-19 17:49:27 +05:30
8c99ba287f Merge pull request 'feat: Add InvoiceLogs widget to display expense logs in the detail screen' (#71) from Vaibhav_Task-#1177 into main
Reviewed-on: #71
2025-09-19 06:54:28 +00:00
bbe7f4a215 feat: Add InvoiceLogs widget to display expense logs in the detail screen 2025-09-19 06:54:28 +00:00
85d776b60b Merge pull request 'Vaibhav_Task-#1177' (#70) from Vaibhav_Task-#1177 into main
Reviewed-on: #70
2025-09-19 04:39:58 +00:00
4836dd994c feat: Implement employee editing functionality; add prefill logic and update API service for createOrUpdateEmployee 2025-09-18 17:42:27 +05:30
544eb4dc79 feat: Add todaysAssigned field to WorkItem model and implement JSON parsing 2025-09-18 17:15:23 +05:30
957bae526f feat: Increase icon size in directory view for better visibility 2025-09-18 16:36:05 +05:30
a1cd212e74 style: Improve code formatting; enhance readability by adjusting line breaks and widget dimensions 2025-09-18 16:32:49 +05:30
47666c7897 feat: Enhance contact detail screen; implement reactive contact updates and improve note handling 2025-09-18 16:18:01 +05:30
e6f028d129 feat: Improve notification handling; enhance logging and ensure DocumentController registration before updates 2025-09-18 15:16:53 +05:30
1fafe77211 feat: Enhance document filtering; implement multi-select support and add date range filters 2025-09-18 12:30:44 +05:30
25b20fedda feat: Enhance dashboard refresh logic; add handling for various notification types and improve method organization 2025-09-18 11:01:03 +05:30
7d5d2b5bf4 feat: Refactor document upload UI; reposition and revalidate Document ID and Name fields 2025-09-17 17:17:57 +05:30
ef1403bec9 Merge pull request 'feat: Re-enable Firebase Messaging and FCM token handling; improve local storage initialization and logging' (#69) from Vaibhav_Task-#961 into main
Reviewed-on: #69
2025-09-17 06:17:56 +00:00
3b497fecaf feat: Update API endpoint configuration; enhance attendance logs sorting and grouping logic 2025-09-17 11:32:32 +05:30
8fb725a5cf Refactor Attendance Logs and Regularization Requests Tabs
- Changed AttendanceLogsTab from StatelessWidget to StatefulWidget to manage state for showing pending actions.
- Added a status header in AttendanceLogsTab to indicate when only pending actions are displayed.
- Updated filtering logic in AttendanceLogsTab to use filteredLogs based on the pending actions toggle.
- Refactored AttendanceScreen to include a search bar for filtering attendance logs by name.
- Introduced a new filter icon in AttendanceScreen for accessing the filter options.
- Updated RegularizationRequestsTab to use filteredRegularizationLogs for displaying requests.
- Modified TodaysAttendanceTab to utilize filteredEmployees for showing today's attendance.
- Cleaned up code formatting and improved readability across various files.
2025-09-16 18:06:19 +05:30
2517f2360e feat: Re-enable Firebase Messaging and FCM token handling; improve local storage initialization and logging 2025-09-15 18:08:19 +05:30
d2712b8288 Merge pull request 'Feature_Document' (#68) from Feature_Document into main
Reviewed-on: #68
2025-09-15 11:41:07 +00:00
fd7f108a20 feat: Add camera functionality to expense attachment; update logger configuration for improved performance 2025-09-12 15:38:42 +05:30
61acbb019b feat: Refactor DynamicMenuController to remove caching and auto-refresh; update error handling in DashboardScreen and UserProfileBar 2025-09-12 12:22:12 +05:30
be908a5251 feat: Improve document verification and rejection loading states; update permission checks for button visibility 2025-09-11 19:07:56 +05:30
5c923bb48b feat: Enhance expense payload construction with attachment change detection 2025-09-11 18:03:14 +05:30
bd6f175ca7 feat: Conditionally display Create Bucket option based on user permissions 2025-09-11 17:45:44 +05:30
229531c5bf feat: Add comprehensive validation for expense submission fields in bottom sheet 2025-09-11 17:32:13 +05:30
20365697a7 feat: Update navigation to employee profile screen in employees list 2025-09-11 17:19:30 +05:30
0cccdc6b05 feat: Add joining date functionality and enhance document upload validation 2025-09-11 17:06:26 +05:30
6d70afc779 feat: Add document verification and rejection functionality with remote logging support 2025-09-11 15:45:32 +05:30
a02887845b feat: Improve dynamic menu fetching with enhanced error handling and cache fallback 2025-09-09 10:47:22 +05:30
99bd26942c feat: Implement document editing functionality with permissions and attachment handling 2025-09-08 18:00:07 +05:30
bf84ef4786 feat: Swap colors for completed and remaining tasks in tasks overview 2025-09-05 17:37:58 +05:30
4d11a2ccf0 feat: Simplify document upload by removing initial data handling and existing file name references 2025-09-05 16:22:05 +05:30
e12e5ab13b feat: Enhance document management with delete/activate functionality, search, and inactive toggle 2025-09-05 15:14:49 +05:30
2133dedfae feat: Refactor employee detail navigation and add employee profile screen with tabbed interface 2025-09-04 17:41:11 +05:30
334023bf1b feat: Add document management features including document listing, details, and filtering
- Implemented DocumentsResponse and related models for handling document data.
- Created UserDocumentsPage for displaying user-specific documents with filtering options.
- Developed DocumentDetailsPage to show detailed information about a selected document.
- Added functionality for uploading documents with DocumentUploadBottomSheet.
- Integrated document filtering through UserDocumentFilterBottomSheet.
- Enhanced dashboard to include navigation to the document management section.
- Updated user profile right bar to provide quick access to user documents.
2025-09-04 16:56:49 +05:30
40a4a77af5 Merge pull request 'Dashboard_Charts' (#67) from Dashboard_Charts into main
Reviewed-on: #67
2025-09-01 09:41:21 +00:00
c27b226b58 feat: Format numerical values with comma separators in attendance and project progress charts 2025-09-01 15:10:28 +05:30
a0f3475c5e feat: Refactor user profile right bar for improved loading state and UI enhancements 2025-09-01 11:24:39 +05:30
3aab006bea feat: Enhance dashboard with projects, tasks, and teams overview widgets 2025-08-29 17:35:45 +05:30
f5d4ab8415 feat: Add attendance log screen and related functionalities
- Implemented AttendenceLogScreen to display employee attendance logs.
- Created RegularizationRequestsTab to manage regularization requests.
- Added TodaysAttendanceTab for viewing today's attendance.
- Removed outdated dashboard chart implementation.
- Updated dashboard screen to integrate new attendance overview and project progress charts.
- Refactored employee detail and employee screens to use updated controllers.
- Organized expense-related imports and components for better structure.
- Adjusted daily progress report to use the correct controller.
2025-08-29 15:53:19 +05:30
d62f8aa9ef feat: Add chart skeleton loader to enhance loading experience in dashboard 2025-08-28 16:28:54 +05:30
80d5fc5f21 Merge pull request 'Feature_Dynamic_Menu' (#66) from Feature_Dynamic_Menu into main
Reviewed-on: #66
2025-08-28 09:18:43 +00:00
a154872649 feat: Implement daily task planning and progress reporting features
- Added TaskListModel for managing daily tasks with JSON parsing.
- Introduced WorkStatusResponseModel and WorkStatus for handling work status data.
- Created MenuResponse and MenuItem models for dynamic menu management.
- Updated routes to reflect correct naming conventions for task planning screens.
- Enhanced DashboardScreen to include dynamic menu functionality and improved task statistics display.
- Developed DailyProgressReportScreen for displaying daily progress reports with filtering options.
- Implemented DailyTaskPlanningScreen for planning daily tasks with detailed views and actions.
- Refactored left navigation bar to align with updated task planning routes.
2025-08-28 14:48:05 +05:30
91184b48bb Refactor employee model imports and restructure employee-related files
- Updated import paths for employee model files to reflect new directory structure.
- Deleted obsolete models: JobRecentApplicationModel, LeadReportModel, Product, ProductOrderModal, ProjectSummaryModel, RecentOrderModel, TaskListModel, TimeLineModel, User, VisitorByChannelsModel.
- Introduced new AttendanceLogModel, AttendanceLogViewModel, AttendanceModel, TaskModel, TaskListModel, EmployeeInfo, and EmployeeModel with comprehensive fields and JSON serialization methods.
- Enhanced data handling in attendance and task management features.
2025-08-26 11:53:53 +05:30
c69e0d5221 Merge pull request 'Firebase_Final_Code' (#65) from Firebase_Final_Code into main
Reviewed-on: #65
2025-08-26 05:42:13 +00:00
ac6b6e6173 Merge branch 'Firebase_Final_Code' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Firebase_Final_Code 2025-08-26 11:10:57 +05:30
35a0228cbe refactor: update package name and application ID to 'com.marco.aiot'; comment out Firebase-related code 2025-08-26 11:09:51 +05:30
5977fef261 chore: update dependencies, adjust minimum SDK version, and refactor date formatting 2025-08-26 11:09:51 +05:30
311002f3ba Refactor expense deletion confirmation dialog and add validation to add expense form
- Replaced the custom delete confirmation dialog with a reusable ConfirmDialog widget for better code organization and reusability.
- Improved the add expense bottom sheet by implementing form validation using a GlobalKey and TextFormField.
- Enhanced user experience by adding validation for required fields and specific formats (e.g., GST, transaction ID).
- Updated the expense list to reflect changes in the confirmation dialog and improved the handling of attachments.
- Cleaned up code by removing unnecessary comments and ensuring consistent formatting.
2025-08-26 11:09:50 +05:30
8783e7a503 refactor: update package name and application ID to 'com.marco.aiot'; comment out Firebase-related code 2025-08-26 11:06:02 +05:30
8688e84779 chore: update dependencies, adjust minimum SDK version, and refactor date formatting 2025-08-23 17:19:20 +05:30
911cddf97c Refactor expense deletion confirmation dialog and add validation to add expense form
- Replaced the custom delete confirmation dialog with a reusable ConfirmDialog widget for better code organization and reusability.
- Improved the add expense bottom sheet by implementing form validation using a GlobalKey and TextFormField.
- Enhanced user experience by adding validation for required fields and specific formats (e.g., GST, transaction ID).
- Updated the expense list to reflect changes in the confirmation dialog and improved the handling of attachments.
- Cleaned up code by removing unnecessary comments and ensuring consistent formatting.
2025-08-22 16:31:48 +05:30
1af3ae2aa6 Merge branch 'Firebase_Final_Code' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Firebase_Final_Code 2025-08-19 15:14:08 +05:30
3ba3129b18 added firebase code 2025-08-19 15:13:13 +05:30
98d2dd4c46 chore: update version number to 1.0.0+6 2025-08-19 11:33:39 +05:30
cc0bb7aafc Merge pull request 'feat: enhance expense detail model and screen with activity logs and new dependencies' (#62) from Vaibhav_Issues_18_08_2025 into main
Reviewed-on: #62
2025-08-18 12:04:07 +00:00
6187d63ccc feat: enhance expense detail model and screen with activity logs and new dependencies 2025-08-18 12:04:07 +00:00
83804cde3f Merge pull request 'refactor: update application ID and improve attendance upload functionality' (#61) from Vaibhav_Issues_18_08_2025 into main
Reviewed-on: #61
2025-08-18 10:02:56 +00:00
1e48c686b2 refactor: update application ID and improve attendance upload functionality
- Changed application ID from "com.marco.aiotstage" to "com.marco.aiot".
- Made markTime and date parameters required in uploadAttendanceImage method.
- Added logic to handle date selection and ensure attendance logs are uploaded with the correct date.
- Enhanced UI components for better user experience in attendance and directory views.
2025-08-18 15:32:17 +05:30
f24bff4fad added firebase code 2025-08-18 11:04:55 +05:30
fa767ea201 chnaged selected colour from red to blue accent 2025-08-16 17:50:48 +05:30
c9882879ff fixed the issue when user licks on comment same multiple taps duplicate comments creating 2025-08-16 17:01:58 +05:30
a6da579f07 corrected the checkbox selection issue 2025-08-16 16:52:10 +05:30
9feb9d1b4b displayed comma seperated amount on expenses 2025-08-09 19:08:01 +05:30
2013447904 removed the extra boz coming for the dashboard chart 2025-08-08 15:33:25 +05:30
2fb3c36ba4 added pull down refresh 2025-08-08 15:19:29 +05:30
0e177e5a1f chnaged endpoint 2025-08-07 17:15:00 +05:30
7175ade940 made chnages for all employee 2025-08-07 17:14:04 +05:30
10e4a6e514 added permission for report action and repost task 2025-08-07 12:55:08 +05:30
77dcd9af8e added permission for add task 2025-08-07 12:49:02 +05:30
858fe7435d added permission based buttons 2025-08-07 12:26:50 +05:30
93f9a6e738 chnages date display as per utils 2025-08-07 11:34:33 +05:30
092fe21252 Merge pull request 'Vaibhav_Feature-#768' (#60) from Vaibhav_Feature-#768 into main
Reviewed-on: #60
2025-08-07 05:18:31 +00:00
1d17a8e109 chnaged the app init 2025-08-06 18:18:17 +05:30
7ec8b1e7bc added edge to edge in app 2025-08-06 18:14:23 +05:30
d205cc2014 added condition to accept date 2025-08-06 17:45:37 +05:30
a5058cd0bc managed the dropdown menu display 2025-08-06 16:30:34 +05:30
754f919cdc added infinite loading 2025-08-06 16:18:50 +05:30
b0c9a2c45f added add expense permission 2025-08-06 16:09:35 +05:30
3195fdd4a0 chnaged the pai by in sheet 2025-08-06 15:18:51 +05:30
06fc8a4c61 added validation 2025-08-06 11:53:03 +05:30
63e5caae24 handelled the update expense 2025-08-06 11:46:12 +05:30
aa76ec60cb made changes for employee get method 2025-08-05 20:35:22 +05:30
0401b41b3c Dynamic edit and Add expense title 2025-08-05 17:47:12 +05:30
0acd619d78 added edit functioanllity in expense 2025-08-05 17:41:36 +05:30
f1220cc018 handelled the loading 2025-08-05 10:24:42 +05:30
84811635d0 resolved the not opening detail screen o tap of non content 2025-08-05 10:14:43 +05:30
7dbc9138c6 refactor: Enhance ExpenseDetailController and ExpenseDetailScreen to support optional comments during expense status updates and improve code readability 2025-08-05 10:09:34 +05:30
f245f9accf refactor: Update API endpoints for employee retrieval to include projectId as a query parameter for improved functionality 2025-08-04 17:04:33 +05:30
0150400092 refactor: Replace Get.snackbar with showAppSnackbar for consistent error handling and improve code readability across expense-related controllers and views. 2025-08-02 17:39:08 +05:30
bba44d4d39 refactor: Update action item color handling and adjust icon size for improved UI consistency 2025-08-02 17:33:34 +05:30
7dd47ce460 refactor: Simplify ReportActionBottomSheet by removing unused imports, optimizing widget structure, and enhancing form handling for improved readability and maintainability. 2025-08-02 17:29:29 +05:30
d799093537 refactor: Enhance ForgotPasswordScreen and LoginOptionScreen for improved readability and maintainability by extracting widget methods and optimizing variable declarations. 2025-08-02 16:54:57 +05:30
9d9afe37b8 refactor: Clean up AttendanceActionButton and AttendanceFilterBottomSheet code by removing unused comment bottom sheet function and filter button properties for improved readability and maintainability. 2025-08-02 16:44:39 +05:30
fe66f35be7 Refactor attendance management: Split attendance screen into tabs, add attendance logs and regularization requests tabs, and improve filter functionality. Update attendance filter sheet and enhance UI components for better user experience. 2025-08-02 16:19:12 +05:30
70443d8e24 feat: Refactor EmployeesScreen for improved readability and structure 2025-08-02 15:34:38 +05:30
0f0eb51c15 feat: Refactor TeamBottomSheet and TeamMembersBottomSheet to use BaseBottomSheet for improved UI consistency 2025-08-02 15:22:24 +05:30
2518b65cb7 feat: Implement delete expense functionality with confirmation dialog 2025-08-02 14:57:34 +05:30
5f66c4c647 feat: Update AttachmentsSection to use RxList for reactive attachment management 2025-08-02 10:16:01 +05:30
d0cbfa987d feat: Refactor TeamMembersBottomSheet and CreateBucketBottomSheet for improved structure and readability 2025-08-01 17:26:11 +05:30
0f14fda83a feat: Refactor DirectoryFilterBottomSheet to manage state and improve filter functionality 2025-08-01 17:11:34 +05:30
7ce07c9b47 feat: Enhance AddContactController with submission state management
- Added `isSubmitting` state to prevent multiple submissions in AddContactController.
- Updated the `submitContact` method to handle submission state and validation.
- Refactored `AddContactBottomSheet` to utilize `BaseBottomSheet` for better UI consistency.
- Improved dynamic list handling for email and phone inputs in AddContactBottomSheet.
- Cleaned up controller initialization and field management in AddContactBottomSheet.
- Enhanced error handling and user feedback for required fields.
2025-08-01 16:42:29 +05:30
f5eed0a0b9 Refactor Expense Filter Bottom Sheet for improved readability and maintainability
- Extracted widget builders for Project, Expense Status, Date Range, Paid By, and Created By filters into separate methods.
- Simplified date selection logic by creating a reusable _selectDate method.
- Centralized input decoration for text fields.
- Updated Expense Screen to use local state for search query and history view toggle.
- Enhanced filtering logic for expenses based on search query and date.
- Improved UI elements in Daily Progress Report and Daily Task Planning screens, including padding and border radius adjustments.
2025-08-01 16:21:24 +05:30
6d29d444fa feat: refactor AddEmployeeBottomSheet and AssignProjectBottomSheet for improved UI and functionality 2025-07-31 18:17:48 +05:30
797df80890 feat: enhance bottom sheet components with dynamic button visibility and improved styling 2025-07-31 18:09:17 +05:30
3427c5bd26 refactor: streamline API call handling in ExpenseDetailController and enhance error logging 2025-07-31 16:57:15 +05:30
31966f4bc5 feat: add permission handling in ExpenseDetailScreen and enhance ExpenseDetailController with permission parsing 2025-07-31 16:44:05 +05:30
29f759ca9d Refactor expense reimbursement and filter UI components
- Updated ReimbursementBottomSheet to use BaseBottomSheet for consistent styling and functionality.
- Improved input field decorations and added spacing helpers for better layout.
- Simplified the employee selection process and integrated it into the new design.
- Refactored ExpenseDetailScreen to utilize controller initialization method.
- Enhanced ExpenseFilterBottomSheet with a cleaner structure and improved field handling.
- Removed unnecessary wrapper for ExpenseFilterBottomSheet and integrated it directly into the expense screen.
2025-07-31 13:13:00 +05:30
adf5e1437e feat: make statusId dynamic in reimbursement handling and update related components 2025-07-31 11:09:25 +05:30
9d49f2a92d feat: implement reimbursement functionality in ExpenseDetailController and add ReimbursementBottomSheet for expense reimbursement entry 2025-07-30 19:19:18 +05:30
cef3bd8a1e feat: add noOfPersons input field in AddExpenseBottomSheet for enhanced expense entry 2025-07-30 17:37:47 +05:30
2b34635a75 refactor: reorganize imports and enhance AddExpenseBottomSheet for improved readability and functionality 2025-07-30 17:19:46 +05:30
154cfdb471 refactor: adjust font weights and sizes in ToggleButton and ExpenseList for improved UI consistency 2025-07-30 16:52:25 +05:30
d28332b55d feat: implement ExpenseDetailModel and update ExpenseDetailController and Screen for improved expense detail handling 2025-07-30 16:45:21 +05:30
98836f8157 Refactor app_logger to remove storage permission request and improve log file handling 2025-07-30 11:37:38 +05:30
ddbc1ec1e5 Refactor ContactDetailScreen and DirectoryView for improved readability and performance
- Moved the Delta to HTML conversion logic outside of the ContactDetailScreen class for better separation of concerns.
- Simplified the handling of email and phone display in DirectoryView to show only the first entry, reducing redundancy.
- Enhanced the layout and structure of the ContactDetailScreen for better maintainability.
- Introduced a new ProjectLabel widget to encapsulate project display logic in the ContactDetailScreen.
- Cleaned up unnecessary comments and improved code formatting for consistency.
2025-07-29 18:05:37 +05:30
5bc811f91f feat: update package identifiers and versioning for staging environment 2025-07-29 16:38:13 +05:30
f4b905cd42 feat(build): enhance Gradle configuration with keystore properties and release signing setup 2025-07-29 09:57:24 +05:30
e5b3616245 Refactor expense models and detail screen for improved error handling and data validation
- Enhanced `ExpenseResponse` and `ExpenseData` models to handle null values and provide default values.
- Introduced a new `Filter` class to encapsulate filtering logic for expenses.
- Updated `ExpenseDetailScreen` to utilize a controller for fetching expense details and managing loading states.
- Improved UI responsiveness with loading skeletons and error messages.
- Refactored filter bottom sheet to streamline filter selection and reset functionality.
- Added visual indicators for filter application in the main expense screen.
- Enhanced expense detail display with better formatting and status color handling.
2025-07-28 12:09:13 +05:30
9124b815ef feat(expense): refactor expense data handling and response parsing 2025-07-25 12:00:33 +05:30
a83954c5c4 Merge branch 'Vaibhav_Feature-#768' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Vaibhav_Feature-#768 2025-07-25 10:46:32 +05:30
586d18565f feat(expense): implement expense filtering functionality with UI integration 2025-07-25 10:45:22 +05:30
e0ed35a671 feat(expense): implement update expense status functionality and UI integration 2025-07-25 10:45:22 +05:30
debc12bc1b feat(expense): add refresh functionality to expense list 2025-07-25 10:45:22 +05:30
d90673523a feat(directory): add 'Create Bucket' option to actions menu 2025-07-25 10:45:22 +05:30
d7b62323d6 fix(expense): remove unnecessary hit test behavior from expense list item 2025-07-25 10:45:22 +05:30
ff01c05a73 feat(expense): improve expense submission validation and UI feedback 2025-07-25 10:45:22 +05:30
a7bb24ee29 feat(expense): enhance expense submission process and UI feedback 2025-07-25 10:45:22 +05:30
af83d66390 feat: Add expense models and update expense detail screen
- Created ExpenseModel, Project, ExpenseType, PaymentMode, PaidBy, CreatedBy, and Status classes for expense management.
- Implemented JSON serialization and deserialization for expense models.
- Added ExpenseStatusModel and ExpenseTypeModel for handling status and type of expenses.
- Introduced PaymentModeModel for managing payment modes.
- Refactored ExpenseDetailScreen to utilize the new ExpenseModel structure.
- Enhanced UI components for better display of expense details.
- Added search and filter functionality in ExpenseMainScreen.
- Updated dependencies in pubspec.yaml to include geocoding package.
2025-07-25 10:45:21 +05:30
b40d371d43 feat(expense): add expense management screens and functionality 2025-07-25 10:41:32 +05:30
982c81f849 feat: Add connectivity handling and offline screen, update MPIN to 4-digit support 2025-07-24 17:35:59 +05:30
9e4c0378c6 feat: Enhance attendance dashboard by filtering roles with data and improving chart series mapping 2025-07-24 15:56:17 +05:30
069fa29aa7 Merge remote-tracking branch 'origin/main' into Vaibhav_Feature-#768 2025-07-24 10:49:25 +05:30
25a1331878 Merge pull request 'Assign_Project' (#56) from Assign_Project into main
Reviewed-on: #56
2025-07-24 04:52:00 +00:00
380bdf870e feat: Update MPIN functionality to support 4-digit MPIN and enhance user experience 2025-07-23 18:21:16 +05:30
008d35d576 feat: Refactor employee screen initialization and enhance loading states 2025-07-23 15:16:41 +05:30
69e64ec789 feat: Update employee creation process and API endpoints for improved functionality 2025-07-23 11:58:09 +05:30
943c7c7b50 feat: Add employee assignment functionality and improve employee detail view
- Implemented employee skeleton card for loading states in the UI.
- Created AssignedProjectsResponse and AssignedProject models for handling project assignments.
- Enhanced EmployeeDetailBottomSheet to include project assignment button.
- Developed AssignProjectBottomSheet for selecting projects to assign to employees.
- Introduced EmployeeDetailPage for displaying detailed employee information.
- Updated EmployeesScreen to support searching and filtering employees.
- Improved layout and UI elements for better user experience.
2025-07-22 20:10:57 +05:30
0e1b6e2a8c feat(expense): implement expense filtering functionality with UI integration 2025-07-22 13:01:07 +05:30
d0f42da30f feat(expense): implement update expense status functionality and UI integration 2025-07-21 18:35:27 +05:30
93cdaab2c2 feat(expense): add refresh functionality to expense list 2025-07-21 16:39:11 +05:30
4a01371fad feat(directory): add 'Create Bucket' option to actions menu 2025-07-21 16:22:18 +05:30
f7352eb3c3 fix(expense): remove unnecessary hit test behavior from expense list item 2025-07-21 16:12:13 +05:30
ee469f694e feat(expense): improve expense submission validation and UI feedback 2025-07-21 15:24:18 +05:30
6c0e73d870 feat(expense): enhance expense submission process and UI feedback 2025-07-21 09:54:13 +05:30
30318cd294 feat: Add expense models and update expense detail screen
- Created ExpenseModel, Project, ExpenseType, PaymentMode, PaidBy, CreatedBy, and Status classes for expense management.
- Implemented JSON serialization and deserialization for expense models.
- Added ExpenseStatusModel and ExpenseTypeModel for handling status and type of expenses.
- Introduced PaymentModeModel for managing payment modes.
- Refactored ExpenseDetailScreen to utilize the new ExpenseModel structure.
- Enhanced UI components for better display of expense details.
- Added search and filter functionality in ExpenseMainScreen.
- Updated dependencies in pubspec.yaml to include geocoding package.
2025-07-19 20:15:54 +05:30
8c5035d679 feat(expense): add expense management screens and functionality 2025-07-18 17:51:59 +05:30
60060eaa5e Merge pull request 'resolved directory data update issues' (#55) from Vaibhav_Issue_17_07_2025 into main
Reviewed-on: #55
2025-07-18 06:58:28 +00:00
02daa1e689 fix(api_service): enhance logout handling on token issues 2025-07-18 10:15:25 +05:30
4908db35ad resolved directory data update issues 2025-07-17 17:50:13 +05:30
990bf737dc Merge pull request 'Vaibhav_Task-#742' (#54) from Vaibhav_Task-#742 into main
Reviewed-on: #54
2025-07-16 06:25:42 +00:00
Vaibhav Surve
072bc34cde Added Delete button with permission control 2025-07-16 10:31:09 +05:30
Vaibhav Surve
befbead7ff added the edit bucket and assign fucntionallity 2025-07-15 17:34:36 +05:30
Vaibhav Surve
85966b5ebf fix(team_members): adjust spacing in Team Members Bottom Sheet for improved layout 2025-07-15 15:34:52 +05:30
Vaibhav Surve
b0c97a850c refactor(team_members): improve comments and remove unused code in Team Members Bottom Sheet 2025-07-15 15:33:13 +05:30
e96aeeb14b Merge pull request 'Vaibhav_Task-#729' (#53) from Vaibhav_Task-#729 into main
Reviewed-on: #53
2025-07-15 09:52:00 +00:00
Vaibhav Surve
9f7d6c92c5 feat(manage_bucket): add dialog for upcoming edit feature in Team Members Bottom Sheet 2025-07-15 15:06:16 +05:30
Vaibhav Surve
0335b0d3ab fix(add_contact): adjust padding in Add Contact Bottom Sheet for better layout 2025-07-15 14:56:44 +05:30
Vaibhav Surve
e624fb00a0 feat(team_members): enhance Team Members Bottom Sheet with bucket details and edit functionality 2025-07-15 14:52:25 +05:30
Vaibhav Surve
219815dd27 refactor(directory): simplify floating action button and enhance menu structure 2025-07-15 12:56:58 +05:30
Vaibhav Surve
9c28dc05dd feat(bucket): add search functionality to Manage Buckets screen 2025-07-15 10:52:36 +05:30
Vaibhav Surve
f9ab336eb0 feat(directory): implement Manage Buckets screen with employee management functionality 2025-07-14 18:56:00 +05:30
Vaibhav Surve
07bf9a93aa refactor(add_contact): improve phone list rendering and enhance layout structure 2025-07-14 10:15:11 +05:30
Vaibhav Surve
395444e8fc feat(contact): add contact picker functionality for selecting Indian phone numbers 2025-07-14 10:12:40 +05:30
Vaibhav Surve
574e7df447 feat(contact): streamline validation logic and enhance UI for adding contacts 2025-07-14 10:03:27 +05:30
Vaibhav Surve
33d267f18e feat(bucket): implement create bucket functionality with UI and API integration 2025-07-11 17:28:22 +05:30
Vaibhav Surve
2926bb7216 feat(auth): update WelcomeScreen and MPINAuthScreen to use dynamic brand color for backgrounds 2025-07-11 15:52:13 +05:30
059e7c6c8b Merge pull request 'Vaibhav_Feature-#541' (#52) from Vaibhav_Feature-#541 into main
Reviewed-on: #52
2025-07-11 07:39:25 +00:00
Vaibhav Surve
8a729f23fe _handleButtonPressed 2025-07-10 17:58:52 +05:30
Vaibhav Surve
71f9e54d58 feat(api): update daily task details endpoint to use new URL 2025-07-10 11:16:15 +05:30
Vaibhav Surve
1e1bcc3aa4 feat(otp): implement email saving and loading functionality in OTPController 2025-07-09 16:40:23 +05:30
Vaibhav Surve
5b5030ec36 feat(auth): refactor login success flow to inject controllers and load data conditionally 2025-07-09 15:48:15 +05:30
Vaibhav Surve
ffba37b767 feat(forgot-password): enhance ForgotPasswordScreen with logo animation and improved layout 2025-07-09 13:02:35 +05:30
Vaibhav Surve
efd5021ab1 feat(mpin-auth): enhance MPINAuthScreen with logo animation and improved layout 2025-07-09 12:43:19 +05:30
Vaibhav Surve
91e2bb7bc8 feat(login): enhance WelcomeScreen with animation and improved dialog layout 2025-07-09 12:27:58 +05:30
Vaibhav Surve
f4135a77d8 feat(login): refactor login option screen to include demo request button and improve layout 2025-07-09 11:53:30 +05:30
Vaibhav Surve
aac65104ab refactor(logging): remove sensitive flag from logSafe calls across multiple controllers and services 2025-07-09 11:35:35 +05:30
Vaibhav Surve
e059ee71f3 feat(user-profile): wrap content in SafeArea for improved layout on different devices 2025-07-08 17:58:08 +05:30
Vaibhav Surve
a9067bd407 feat(comment-editor): disable list buttons and adjust layout for improved usability 2025-07-08 17:57:53 +05:30
Vaibhav Surve
b3b68b6258 style(directory): format code for improved readability and consistency 2025-07-08 16:33:02 +05:30
Vaibhav Surve
6907d176da feat(directory): enhance toggle UI for Directory and Notes views with improved styling and animations 2025-07-08 16:06:11 +05:30
Vaibhav Surve
ae868bb0f6 feat(directory): reduce spacing and padding in category and bucket displays 2025-07-08 16:01:10 +05:30
Vaibhav Surve
df0dd5d560 feat(directory): adjust avatar size and alignment in contact list 2025-07-08 15:44:52 +05:30
Vaibhav Surve
1ad880a021 feat(notes): add feedback messages for note update actions 2025-07-08 15:33:35 +05:30
Vaibhav Surve
fb28439d69 feat(comment): add feedback messages for comment update actions 2025-07-08 15:32:20 +05:30
Vaibhav Surve
2fef2e508e feat(contact): support multiple project selection in AddContact functionality 2025-07-08 15:28:20 +05:30
Vaibhav Surve
a8c890a60d feat(comment): improve comment submission feedback and validation messages 2025-07-08 13:17:21 +05:30
Vaibhav Surve
77e27ff98e feat(contact): enhance AddContact functionality with validation and initialization state 2025-07-08 13:10:15 +05:30
Vaibhav Surve
445cd75e03 feat(contact): implement contact editing functionality and update API integration 2025-07-07 15:34:07 +05:30
Vaibhav Surve
5fb18a13d2 feat(directory): refactor DirectoryView layout for improved structure and readability 2025-07-07 14:11:15 +05:30
Vaibhav Surve
43aeec4c6f feat(directory): integrate NotesController in DirectoryMainScreen and improve code formatting in NotesView 2025-07-07 13:57:20 +05:30
Vaibhav Surve
5e8158a410 feat(directory): center align buttons in DirectoryMainScreen and adjust padding in Directory and Notes views 2025-07-07 13:48:57 +05:30
Vaibhav Surve
45ce53539c feat(directory): enhance search functionality in Directory and Notes views 2025-07-07 13:41:46 +05:30
Vaibhav Surve
7a2798401a feat: Add Notes functionality and integrate with Directory
- Introduced NotesController to manage notes fetching, updating, and state management.
- Created NoteListResponseModel and NoteModel to handle notes data structure.
- Implemented API service method to fetch directory notes.
- Added NotesView to display notes with editing capabilities.
- Updated DirectoryController to include a flag for toggling between Directory and Notes views.
- Refactored DirectoryMainScreen to accommodate the new NotesView and toggle functionality.
- Enhanced UI components for better user experience in both Directory and Notes views.
2025-07-07 12:55:52 +05:30
Vaibhav Surve
56b493c909 feat(directory): refactor contact card layout for improved structure and readability 2025-07-07 10:13:15 +05:30
Vaibhav Surve
087c77bbd2 feat(directory): enhance input validation and layout in AddContactBottomSheet 2025-07-05 17:38:26 +05:30
Vaibhav Surve
606c5e5971 feat(directory): refactor contact card layout for improved readability and interaction 2025-07-05 17:29:35 +05:30
Vaibhav Surve
e7940941ed feat(directory): enhance AddContact functionality to support multiple emails and phones, improve logging, and refactor contact detail display 2025-07-05 13:19:53 +05:30
Vaibhav Surve
62c49b5429 feat(directory): add form reset functionality in AddContactController and initialize fields in AddContactBottomSheet 2025-07-05 11:57:15 +05:30
Vaibhav Surve
b187f1843a chore: update gradle properties for improved performance and memory management 2025-07-05 11:30:13 +05:30
Vaibhav Surve
eabd988b32 feat(directory): change floating action button color to red for better visibility 2025-07-04 17:30:26 +05:30
Vaibhav Surve
becdec1a79 feat(directory): enhance contact card UI with improved layout and interaction elements 2025-07-04 17:29:26 +05:30
Vaibhav Surve
549d8cce3c feat(directory): add comment submission functionality and UI components 2025-07-04 16:55:50 +05:30
Vaibhav Surve
be71544ae4 feat(directory): implement comment editing functionality and enhance comment model 2025-07-04 15:09:49 +05:30
Vaibhav Surve
83ad10ffb4 feat: update UI components for improved consistency and add tab indicator styling 2025-07-03 13:22:04 +05:30
a0f1602f4e feat(directory): add contact profile and directory management features
- Implemented ContactProfileResponse and related models for handling contact details.
- Created ContactTagResponse and ContactTag models for managing contact tags.
- Added DirectoryCommentResponse and DirectoryComment models for comment management.
- Developed DirectoryFilterBottomSheet for filtering contacts.
- Introduced OrganizationListModel for organization data handling.
- Updated routes to include DirectoryMainScreen.
- Enhanced DashboardScreen to navigate to the new directory page.
- Created ContactDetailScreen for displaying detailed contact information.
- Developed DirectoryMainScreen for managing and displaying contacts.
- Added dependencies for font_awesome_flutter and flutter_html in pubspec.yaml.
2025-07-02 15:57:39 +05:30
8f87161d74 refactor: Replace MyButton with OutlinedButton and ElevatedButton in various bottom sheets for improved UI consistency 2025-06-28 13:14:22 +05:30
2c79d3eec8 fix: Remove null safety checks for selectedProjectId in Layout 2025-06-25 17:59:13 +05:30
ec6c24464e Refactor logging mechanism across services and widgets
- Introduced a new `logSafe` function for consistent logging with sensitivity handling.
- Replaced direct logger calls with `logSafe` in `api_service.dart`, `app_initializer.dart`, `auth_service.dart`, `permission_service.dart`, and `my_image_compressor.dart`.
- Enhanced error handling and logging in various service methods to capture exceptions and provide more context.
- Updated image compression logging to include quality and size metrics.
- Improved app initialization logging to capture success and error states.
- Ensured sensitive information is not logged directly.
2025-06-25 12:10:57 +05:30
e6d05e247e Refactor logging implementation across controllers and services
- Replaced instances of the Logger package with a custom appLogger for consistent logging.
- Introduced app_logger.dart to manage logging with file output and storage permissions.
- Updated all controllers (e.g., DashboardController, EmployeesScreenController, etc.) to use appLogger for logging messages.
- Ensured that logging messages are appropriately categorized (info, warning, error) throughout the application.
- Implemented a file logging mechanism to store logs in a designated directory.
- Cleaned up old log files to maintain only the most recent logs.
2025-06-24 13:11:22 +05:30
ef60677c98 feat: Update default selected range to '15D' in DashboardController 2025-06-23 17:48:49 +05:30
e71e4d633a feat: Implement JWT token expiration handling and refresh logic in ApiService 2025-06-23 17:35:50 +05:30
99a9b47a19 feat: Enhance attendance chart visibility based on project selection 2025-06-23 15:52:58 +05:30
67d78f02b7 feat: Add no data message to attendance dashboard chart 2025-06-23 15:49:06 +05:30
95c625a55b Merge pull request 'feat: Improve project ID handling and enhance attendance chart data observation' (#50) from Vaibhav_Task-#519 into main
Reviewed-on: #50
2025-06-23 10:08:08 +00:00
f7fcad2992 feat: Improve project ID handling and enhance attendance chart data observation 2025-06-23 15:36:40 +05:30
099edd3884 Merge pull request 'feat: Refactor project ID handling and enhance CommentTaskBottomSheet integration' (#49) from Vaibhav_Task-#515 into main
Reviewed-on: #49
2025-06-23 10:04:14 +00:00
f1764eed81 feat: Refactor project ID handling and enhance CommentTaskBottomSheet integration 2025-06-23 15:27:06 +05:30
0317954b02 Merge pull request 'Feature_Report_Action' (#48) from Feature_Report_Action into main
Reviewed-on: #48
2025-06-23 07:32:29 +00:00
6e11fc8c52 feat: Enable AttendanceDashboardChart in the dashboard screen 2025-06-23 13:01:46 +05:30
6c3370437d feat: Update models and API service for improved data handling and type safety 2025-06-22 18:40:10 +05:30
6cdf35374d feat: Enhance dashboard functionality with attendance overview and chart visualization 2025-06-20 19:01:58 +05:30
ca8bc26ab5 feat: Refactor showCreateTaskBottomSheet for improved structure and readability 2025-06-19 18:02:37 +05:30
f7671bc5d3 feat: Refactor AddEmployeeBottomSheet for improved readability and maintainability 2025-06-19 18:01:27 +05:30
405916bb48 feat: Add custom skeleton loaders for employee list and daily progress report screens 2025-06-19 16:42:24 +05:30
97c873167f feat: Add employee list skeleton loader for improved loading experience 2025-06-19 16:22:48 +05:30
ef6521faa2 feat: Implement loading skeletons in dashboard and layout screens for better UX 2025-06-19 15:35:09 +05:30
660bd3cdf1 feat: Enhance project selection interaction by adding tap gesture to collapse dropdown 2025-06-19 13:15:02 +05:30
4ba30145ef feat: Update loading state initialization and remove unused fetchProjects method 2025-06-19 12:32:35 +05:30
d1305e1dba feat: Increase default timeout duration for API requests from 10 to 30 seconds 2025-06-19 11:39:52 +05:30
f834422c4e feat: Increase timeout duration for API requests and update relevant calls 2025-06-19 11:26:42 +05:30
44d72b73ac feat: Add optional approvedBy field to TaskModel and update JSON parsing 2025-06-19 11:14:21 +05:30
c215c4c943 feat: Rename task management function to showCreateTaskBottomSheet and update references 2025-06-19 11:03:34 +05:30
3ede53713d feat: Refactor task management to include work area and activity IDs, update task creation logic, and enhance category selection 2025-06-18 19:15:41 +05:30
5148b41579 feat: Enhance task creation UI with title and description sections 2025-06-18 12:52:20 +05:30
58a66546e4 fix: Simplify image picking logic by removing null check for pickedFiles 2025-06-18 12:18:25 +05:30
206c84b3a1 feat: Implement task management features including task creation, assignment, and employee selection 2025-06-18 11:40:08 +05:30
93a2350858 feat: Add report action and comment functionality in daily task planning
- Implemented ReportActionBottomSheet for reporting actions on tasks.
- Created TaskActionButtons for handling report and comment actions.
- Added WorkStatusResponseModel and WorkStatus model for managing work statuses.
- Refactored DailyProgressReportScreen to utilize new action buttons for reporting and commenting.
- Enhanced task data preparation for reporting and commenting actions.
2025-06-17 15:41:23 +05:30
6f2e257f0d Merge pull request 'Feature_Global_Project_Selection' (#47) from Feature_Global_Project_Selection into main
Reviewed-on: #47
2025-06-17 10:09:58 +00:00
5275ad940b Remove Sales, Comment Task, and Report Task screens; update pubspec.yaml to clean up asset paths. 2025-06-13 11:37:08 +05:30
d765b96df4 feat: Refactor user data fetching to use base URL from ApiEndpoints for improved maintainability 2025-06-13 11:01:05 +05:30
0ad8847b94 feat: Update application ID in build.gradle and refactor imports and methods in service files for improved clarity and organization 2025-06-13 10:51:23 +05:30
658f3f26e0 feat: Enhance dashboard stats display with project selection validation and user feedback 2025-06-12 23:59:13 +05:30
602d8a8dc9 feat: Update reportTask method to return success status and improve loading state handling in ReportTaskBottomSheet 2025-06-12 23:27:50 +05:30
59e6634023 feat: Refactor project selection handling in attendance actions and controllers 2025-06-12 23:18:58 +05:30
a97fb1f541 feat: Clear selected images instead of pre-signed URLs in comment task bottom sheet 2025-06-12 23:01:39 +05:30
c7600e8e26 feat: Align app bar elements and adjust padding in attendance and employee screens 2025-06-12 22:51:44 +05:30
8e47d28005 feat: Add getGlobalProjects method and update project fetching logic 2025-06-12 22:06:59 +05:30
916cfa3af4 feat: Add support for reported pre-signed URLs in comments and daily progress report 2025-06-12 22:02:19 +05:30
5cf0202cc1 feat: Remove unused selectedProjectId from DailyTaskPlaningController and update AssignTaskBottomSheet to use ProjectController's selectedProjectId 2025-06-12 21:13:12 +05:30
4022197b7f feat: Remove automatic selection of the first project after fetching projects 2025-06-12 20:26:41 +05:30
11d9f107ad feat: Refactor attendance data loading logic to streamline project change handling and improve initial data fetch 2025-06-12 19:29:38 +05:30
daf132c3b5 feat: Update comment task bottom sheet layout and enhance comment display with improved spacing and attachment handling 2025-06-12 19:14:27 +05:30
1f784d96f4 feat: Enhance image handling in task reporting and commenting with compression and content type detection 2025-06-12 18:00:31 +05:30
3a449441fa feat: Enhance project and task management features
- Added clearProjects method in ProjectController to reset project states.
- Updated fetchProjects and updateSelectedProject methods for better state management.
- Enhanced ReportTaskController to support image uploads with base64 encoding.
- Modified ApiService to handle image data in report and comment tasks.
- Integrated ProjectController in AuthService to fetch projects upon login.
- Updated LocalStorage to clear selectedProjectId on logout.
- Introduced ImageViewerDialog for displaying images in a dialog.
- Enhanced CommentTaskBottomSheet and ReportTaskBottomSheet to support image attachments.
- Improved AttendanceScreen to handle project selection and data fetching more robustly.
- Refactored EmployeesScreen to manage employee data based on project selection.
- Updated Layout to handle project selection and display appropriate messages.
- Enhanced DailyProgressReportScreen and DailyTaskPlaningScreen to reactively fetch task data based on project changes.
- Added photo_view dependency for improved image handling.
2025-06-12 17:28:06 +05:30
b38d987eac feat: Update layout structure to use Stack for project list overlay and adjust spacing in dashboard 2025-06-12 11:55:02 +05:30
56efbe8869 feat: Refactor project selection handling and update UI across various screens 2025-06-12 11:31:36 +05:30
b81ac33b2d feat: Enhance project selection handling across various screens and controllers 2025-06-11 21:55:15 +05:30
936faae07d fix: Update initial route to redirect to /dashboard instead of /home 2025-06-11 20:25:26 +05:30
313350e1a5 feat: Add home route and update logout functionality in user profile bar 2025-06-11 20:25:01 +05:30
6a36064af7 feat: Implement project management features across controllers and views
- Added DashboardController and ProjectController for managing project data.
- Enhanced LayoutController to support project selection and loading states.
- Created UserProfileBar for user-specific actions and information.
- Refactored app initialization logic to streamline setup and error handling.
- Updated layout views to integrate project selection and improve user experience.
2025-06-11 17:11:50 +05:30
52afa7735e Merge pull request 'Feature_MPIN_OTP' (#46) from Feature_MPIN_OTP into main
Reviewed-on: #46
2025-06-11 09:38:35 +00:00
8c2d258848 feat: Implement loading state in ForgotPasswordScreen during password reset process 2025-06-10 17:26:35 +05:30
18987aa97a feat: Update navigation logic in LoginController to redirect to home page after MPIN check 2025-06-10 16:33:32 +05:30
040a8f0a2e feat: Add AttendanceLogViewButton for employees with check-in records 2025-06-10 15:44:24 +05:30
a2a7eb84b0 feat: Update password validation to require a minimum length of 6 characters 2025-06-10 15:43:44 +05:30
2ccd237329 feat: Update navigation logic for MPIN and OTP authentication to redirect to home page; add failed attempts tracking in MPIN controller 2025-06-10 15:32:15 +05:30
c253c14481 feat: Refactor OTP authentication to use email instead of phone number 2025-06-10 12:14:05 +05:30
25dfcf3e08 feat: Add MPIN authentication and OTP login screens
- Implemented MPINAuthScreen for generating and entering MPIN.
- Created MPINScreen for user interaction with MPIN input.
- Developed OTPLoginScreen for OTP verification process.
- Added request demo bottom sheet with organization form.
- Enhanced DashboardScreen to check MPIN status and prompt user to generate MPIN if not set.
2025-06-10 10:04:18 +05:30
a89346fc8a Update application label in AndroidManifest.xml to match project name 2025-06-09 18:24:06 +05:30
516a779626 fix: Replace Row with Wrap for employee name and job role display to support multiline text 2025-06-06 12:27:52 +05:30
0b8a5364ae fix: Comment out unused base URL in API service files and update login redirection path 2025-06-06 12:25:12 +05:30
905b3e32c5 fix: Update employee name and designation display to allow for multiline text 2025-06-06 12:23:12 +05:30
3926e762e5 Merge pull request 'feat: Add contact picking functionality in employee form and update dependencies' (#45) from Vaibhav_Enhancement-#454 into main
Reviewed-on: #45
2025-06-04 10:44:11 +00:00
deff7aea7d feat: Add contact picking functionality in employee form and update dependencies 2025-06-04 12:44:27 +05:30
a0761d255f Merge pull request 'fix: Update validation for completed work to allow zero as a valid input' (#44) from Vaibhav_Bug-#451 into main
Reviewed-on: #44
2025-06-04 05:49:23 +00:00
d7680df3a9 fix: Update validation for completed work to allow zero as a valid input 2025-06-04 11:18:07 +05:30
4862e53967 Merge pull request 'fix: Enhance contact number validation in organization form' (#43) from Vaibhav_Bug-#452 into main
Reviewed-on: #43
2025-06-04 05:39:53 +00:00
d99221e800 fix: Enhance contact number validation in organization form 2025-06-04 11:09:15 +05:30
cd29a478a2 Merge pull request 'fix: Add percent indicator for visual progress representation in daily task planning' (#42) from Vaibhav_Task-#428 into main
Reviewed-on: #42
2025-06-03 11:57:13 +00:00
3bd25c1172 fix: Add percent indicator for visual progress representation in daily task planning 2025-06-03 17:26:43 +05:30
eabe48e572 Merge pull request 'fix: Add validation to prevent target exceeding pending tasks in assign task functionality' (#41) from Vaibhav_Bug-#450 into main
Reviewed-on: #41
2025-06-03 11:45:19 +00:00
ba42551b25 fix: Add validation to prevent target exceeding pending tasks in assign task functionality 2025-06-03 17:14:48 +05:30
1a5a084115 Merge pull request 'fix: Refactor login controller to improve form initialization and credential handling' (#40) from Vaibhav_Bug-#437 into main
Reviewed-on: #40
2025-06-03 10:03:49 +00:00
52fbd88252 fix: Refactor login controller to improve form initialization and credential handling 2025-06-03 15:32:47 +05:30
bff814f562 Merge pull request 'fix: Enhance loading state management and error handling in organization form submission' (#39) from Vaibhav_Bug-#427 into main
Reviewed-on: #39
2025-06-03 09:49:31 +00:00
fc9fbaafa9 fix: Enhance loading state management and error handling in organization form submission 2025-06-03 15:17:49 +05:30
f7fed6fe82 Merge pull request 'fix: Add country selection and phone number validation in employee form' (#38) from Vaibhav_Bug-#431 into main
Reviewed-on: #38
2025-06-03 09:35:26 +00:00
a844c758b0 fix: Add country selection and phone number validation in employee form 2025-06-03 15:04:53 +05:30
a7f8ea16b0 Merge pull request 'fix: Improve validation for completed work and display pending work in report task bottom sheet' (#37) from Vaibhav_Bug-#435 into main
Reviewed-on: #37
2025-06-03 06:13:49 +00:00
c474aad1dc fix: Improve validation for completed work and display pending work in report task bottom sheet 2025-06-03 11:43:24 +05:30
2becfc603a Merge pull request 'Vaibhav_Enhancement-#419' (#36) from Vaibhav_Enhancement-#419 into main
Reviewed-on: #36
2025-06-03 05:36:00 +00:00
6fa8858d87 fix: Update project selection logic to fetch task data instead of projects 2025-06-03 11:05:21 +05:30
f1005af7be fix: Enhance login error handling with custom snackbar and update UI for beta environment 2025-06-03 09:46:34 +05:30
22a94a023e Merge pull request 'Vaibhav_Enhancement-#419' (#35) from Vaibhav_Enhancement-#419 into main
Reviewed-on: #35
2025-06-02 07:52:52 +00:00
aca2722ee4 refactor: Update Checkbox fill color handling and enhance agreement toggle interaction 2025-05-31 16:46:10 +05:30
706726c787 refactor: Convert AttendanceFilterBottomSheet to StatefulWidget and enhance state management 2025-05-31 13:08:30 +05:30
9ad8bdc893 fix: Update button onPressed to null and adjust background color in Layout 2025-05-31 12:02:01 +05:30
08991f2095 refactor: Improve LoginController and LoginScreen structure and readability 2025-05-31 10:43:39 +05:30
ad4b24dd78 feat: Add beta environment indicator to layout 2025-05-30 15:12:35 +05:30
d9ad7581bf Merge pull request 'feat: Enhance ReportTaskController with image picking and form field management' (#34) from Vaibhav_Bug-#411 into main
Reviewed-on: #34
2025-05-30 09:12:43 +00:00
938bf58ff6 feat: Enhance ReportTaskController with image picking and form field management
- Added image picking functionality using ImagePicker for selecting images from camera or gallery.
- Refactored form field controllers in ReportTaskController for better management and disposal.
- Updated reportTask and commentTask methods to use the new controller references.
- Implemented image removal functionality in ReportTaskController.
- Modified ReportTaskBottomSheet to clear fields upon initialization and added a callback for successful reports.
- Updated attendance and regularization logs UI for better loading states and error handling.
- Improved logout functionality in LocalStorage and integrated it into layout and left bar components.
- Adjusted initial route logic in main.dart to redirect users based on authentication status.
2025-05-30 14:41:04 +05:30
ba610bf806 Merge pull request '-- Enhance layout with floating action button and navigation improvements' (#33) from Vaibhav_Dev into main
Reviewed-on: #33
2025-05-30 05:15:02 +00:00
299 changed files with 40535 additions and 208676 deletions

View File

@ -3,42 +3,84 @@ plugins {
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
id("com.google.gms.google-services")
}
// Load keystore properties from key.properties file
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.example.marco"
// Define the namespace for your Android application
namespace = "com.marco.aiot"
// Set the compile SDK version based on Flutter's configuration
compileSdk = flutter.compileSdkVersion
// Set the NDK version based on Flutter's configuration
ndkVersion = flutter.ndkVersion
// Configure Java compatibility options
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// Enable core library desugaring for Java 8+ APIs
coreLibraryDesugaringEnabled true
}
// Configure Kotlin options for JVM target
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
// Default configuration for your application
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.marco"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
// Specify your unique Application ID. This identifies your app on Google Play.
applicationId = "com.marco.aiot"
// Set minimum and target SDK versions based on Flutter's configuration
minSdk = 23
targetSdk = flutter.targetSdkVersion
// Set version code and name based on Flutter's configuration (from pubspec.yaml)
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// Define signing configurations for different build types
signingConfigs {
release {
// Reference the key alias from key.properties
keyAlias keystoreProperties['keyAlias']
// Reference the key password from key.properties
keyPassword keystoreProperties['keyPassword']
// Reference the keystore file path from key.properties
storeFile file(keystoreProperties['storeFile'])
// Reference the keystore password from key.properties
storePassword keystoreProperties['storePassword']
}
}
// Define different build types (e.g., debug, release)
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
// Apply the 'release' signing configuration defined above to the release build
signingConfig signingConfigs.release
// Enable code minification to reduce app size
minifyEnabled true
// Enable resource shrinking to remove unused resources
shrinkResources true
// Other release specific configurations can be added here, e.g., ProGuard rules
}
}
}
// Configure Flutter specific settings, pointing to the root of your Flutter project
flutter {
source = "../.."
}
// Add required dependencies for desugaring
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "626581282477",
"project_id": "mtest-a0635",
"storage_bucket": "mtest-a0635.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
"android_client_info": {
"package_name": "com.marco.aiot"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCBkDQRpbSdR0bo6pO4Bm0ZIdXkdaE3z-A"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -6,5 +6,6 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>

View File

@ -1,9 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:label="marco"
android:label="Marco"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@ -33,6 +38,9 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@ -1,4 +1,4 @@
package com.example.marco
package com.marco.aiot
import io.flutter.embedding.android.FlutterActivity

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=false

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -18,8 +18,9 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.2.1" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include ":app"

File diff suppressed because it is too large Load Diff

View File

@ -1,192 +0,0 @@
[
{
"id": 1,
"first_name": "James Carter",
"email": "james.carter@example.com",
"messages": [
{
"message": "How is your day going?",
"send_at": "2024-11-15T10:05:10Z",
"from_me": false
},
{
"message": "Reminder about the project meeting tomorrow",
"send_at": "2023-06-20T14:23:11Z",
"from_me": true
},
{
"message": "Can we meet today for a quick chat?",
"send_at": "2023-04-19T17:30:08Z",
"from_me": false
},
{
"message": "Yes, all is good. See you tomorrow at 2 PM for the meeting",
"send_at": "2023-03-22T11:09:45Z",
"from_me": false
}
]
},
{
"id": 2,
"first_name": "Sophia Lee",
"email": "sophia.lee@example.com",
"messages": [
{
"message": "Are we meeting today for the weekly catch-up?",
"send_at": "2023-09-10T08:45:36Z",
"from_me": false
},
{
"message": "Please review these updated documents",
"send_at": "2023-11-17T11:22:33Z",
"from_me": true
},
{
"message": "Good morning, How are you? When is our next meeting?",
"send_at": "2023-05-13T09:25:18Z",
"from_me": true
}
]
},
{
"id": 3,
"first_name": "Ethan Scott",
"email": "ethan.scott@example.com",
"messages": [
{
"message": "Are you available for a quick call? Need to discuss something",
"send_at": "2023-12-05T07:40:21Z",
"from_me": false
},
{
"message": "Let's meet today, shall we?",
"send_at": "2023-03-17T14:00:12Z",
"from_me": true
}
]
},
{
"id": 4,
"first_name": "Olivia Brown",
"email": "olivia.brown@example.com",
"messages": [
{
"message": "Let's meet today for a team discussion",
"send_at": "2023-05-30T11:55:10Z",
"from_me": false
},
{
"message": "Hope you're having a great day. Let's catch up soon",
"send_at": "2023-07-12T12:36:44Z",
"from_me": true
},
{
"message": "I need to go buy some groceries this afternoon, I'll be a bit late.",
"send_at": "2024-01-10T13:20:45Z",
"from_me": true
}
]
},
{
"id": 5,
"first_name": "Charlotte Miller",
"email": "charlotte.miller@example.com",
"messages": [
{
"message": "Are you available for a quick chat?",
"send_at": "2023-11-11T16:50:09Z",
"from_me": false
},
{
"message": "I just sent you the updated contract documents for review.",
"send_at": "2023-10-04T18:22:56Z",
"from_me": true
}
]
},
{
"id": 6,
"first_name": "Jackson Harris",
"email": "jackson.harris@example.com",
"messages": [
{
"message": "How's everything going? Any updates on the project?",
"send_at": "2023-08-25T11:10:30Z",
"from_me": false
},
{
"message": "Sending over the latest draft for your review",
"send_at": "2023-12-02T10:14:50Z",
"from_me": true
}
]
},
{
"id": 7,
"first_name": "Aiden Cooper",
"email": "aiden.cooper@example.com",
"messages": [
{
"message": "Do you have time today for a discussion?",
"send_at": "2023-10-05T09:18:22Z",
"from_me": false
},
{
"message": "The new update is ready for deployment, please check it.",
"send_at": "2024-01-25T11:29:13Z",
"from_me": true
}
]
},
{
"id": 8,
"first_name": "Lily King",
"email": "lily.king@example.com",
"messages": [
{
"message": "Would you be able to meet today for a catch-up?",
"send_at": "2023-06-18T13:12:09Z",
"from_me": false
},
{
"message": "I have finished reviewing the files, please take a look.",
"send_at": "2023-12-18T16:47:02Z",
"from_me": true
}
]
},
{
"id": 9,
"first_name": "Max Taylor",
"email": "max.taylor@example.com",
"messages": [
{
"message": "Please check the attached file and confirm if everything is okay.",
"send_at": "2023-09-29T14:10:33Z",
"from_me": false
},
{
"message": "Sending over the revised schedule for the next phase.",
"send_at": "2024-01-02T17:12:11Z",
"from_me": true
}
]
},
{
"id": 10,
"first_name": "Avery Clark",
"email": "avery.clark@example.com",
"messages": [
{
"message": "Are we ready for the meeting today?",
"send_at": "2023-08-22T12:43:50Z",
"from_me": false
},
{
"message": "I updated the timeline. Let me know if you have any questions.",
"send_at": "2023-12-15T10:55:29Z",
"from_me": true
}
]
}
]

View File

@ -1,82 +0,0 @@
[
{
"id": 1,
"asset": "Alaska Air Group, Inc.",
"date": "2024-06-17T12:59:41Z",
"ip_address": "113.9.18.110",
"status": "Unpaid",
"amount": 7061
},
{
"id": 2,
"asset": "T2 Biosystems, Inc.",
"date": "2024-09-09T00:20:08Z",
"ip_address": "45.51.68.143",
"status": "Unpaid",
"amount": 5677
},
{
"id": 3,
"asset": "North American Energy Partners, Inc.",
"date": "2024-07-17T10:46:42Z",
"ip_address": "221.131.122.193",
"status": "Unpaid",
"amount": 5420
},
{
"id": 4,
"asset": "Finjan Holdings, Inc.",
"date": "2024-01-20T20:10:26Z",
"ip_address": "50.242.43.22",
"status": "Success",
"amount": 6433
},
{
"id": 5,
"asset": "Omega Healthcare Investors, Inc.",
"date": "2024-11-14T23:08:09Z",
"ip_address": "109.125.5.131",
"status": "Success",
"amount": 6317
},
{
"id": 6,
"asset": "MediciNova, Inc.",
"date": "2024-01-12T18:20:33Z",
"ip_address": "54.103.156.190",
"status": "Unpaid",
"amount": 7952
},
{
"id": 7,
"asset": "PowerShares LadderRite 0-5 Year Corporate Bond Portfolio",
"date": "2024-04-26T00:44:42Z",
"ip_address": "169.190.183.205",
"status": "Success",
"amount": 6294
},
{
"id": 8,
"asset": "VelocityShares Daily 2x VIX Medium-Term ETN",
"date": "2024-02-01T12:47:59Z",
"ip_address": "144.189.211.137",
"status": "Success",
"amount": 4419
},
{
"id": 9,
"asset": "Liberty TripAdvisor Holdings, Inc.",
"date": "2023-12-29T14:49:59Z",
"ip_address": "166.41.221.149",
"status": "Unpaid",
"amount": 4195
},
{
"id": 10,
"asset": "Scorpio Tankers Inc.",
"date": "2023-12-02T11:36:44Z",
"ip_address": "27.151.0.226",
"status": "Success",
"amount": 8395
}
]

View File

@ -1,202 +0,0 @@
[
{
"id": 1,
"first_name": "Sabina",
"last_name": "Brothwood",
"project_name": "Wunsch, DuBuque and Green",
"phone_number": "541-568-8047",
"balance": "31907",
"order_count": 7,
"last_order": "2022-10-07T03:43:16Z"
},
{
"id": 2,
"first_name": "Felic",
"last_name": "Parlor",
"project_name": "Dare LLC",
"phone_number": "866-349-3385",
"balance": "02260",
"order_count": 26,
"last_order": "2023-07-12T07:52:19Z"
},
{
"id": 3,
"first_name": "Marnie",
"last_name": "Kofax",
"project_name": "Von LLC",
"phone_number": "821-779-3766",
"balance": "663",
"order_count": 21,
"last_order": "2022-10-14T21:19:33Z"
},
{
"id": 4,
"first_name": "Tine",
"last_name": "Meron",
"project_name": "Stracke Inc",
"phone_number": "901-149-2915",
"balance": "84",
"order_count": 8,
"last_order": "2023-04-06T12:36:09Z"
},
{
"id": 5,
"first_name": "Shanon",
"last_name": "Ivashchenko",
"project_name": "Satterfield, Schultz and Jones",
"phone_number": "452-728-1072",
"balance": "0878",
"order_count": 34,
"last_order": "2023-04-03T15:07:21Z"
},
{
"id": 6,
"first_name": "Guthrey",
"last_name": "Crossland",
"project_name": "Medhurst and Sons",
"phone_number": "212-991-7314",
"balance": "0291",
"order_count": 7,
"last_order": "2022-12-03T04:24:53Z"
},
{
"id": 7,
"first_name": "Florie",
"last_name": "Chestnutt",
"project_name": "Beer-Kunze",
"phone_number": "935-525-9749",
"balance": "07984",
"order_count": 69,
"last_order": "2023-01-14T10:42:28Z"
},
{
"id": 8,
"first_name": "Wittie",
"last_name": "Damsell",
"project_name": "Daniel, Legros and Roberts",
"phone_number": "632-787-4799",
"balance": "22844",
"order_count": 41,
"last_order": "2023-01-18T09:38:50Z"
},
{
"id": 9,
"first_name": "Aimee",
"last_name": "Dibdall",
"project_name": "Schuster LLC",
"phone_number": "404-339-9261",
"balance": "460",
"order_count": 41,
"last_order": "2023-04-15T03:08:51Z"
},
{
"id": 10,
"first_name": "Inna",
"last_name": "Juggins",
"project_name": "Johnson Group",
"phone_number": "769-573-9516",
"balance": "77",
"order_count": 18,
"last_order": "2022-09-13T05:14:51Z"
},
{
"id": 11,
"first_name": "Cathyleen",
"last_name": "Went",
"project_name": "DuBuque LLC",
"phone_number": "558-736-4450",
"balance": "24",
"order_count": 98,
"last_order": "2023-07-05T05:26:12Z"
},
{
"id": 12,
"first_name": "Kora",
"last_name": "Dowderswell",
"project_name": "Harber, Daugherty and West",
"phone_number": "721-147-2917",
"balance": "32",
"order_count": 5,
"last_order": "2022-10-22T07:47:42Z"
},
{
"id": 13,
"first_name": "Loni",
"last_name": "Armin",
"project_name": "Fadel-Kerluke",
"phone_number": "251-582-9867",
"balance": "2122",
"order_count": 4,
"last_order": "2023-01-26T19:56:37Z"
},
{
"id": 14,
"first_name": "Kalle",
"last_name": "Spybey",
"project_name": "Kshlerin, Torp and Koelpin",
"phone_number": "245-661-6328",
"balance": "61034",
"order_count": 70,
"last_order": "2022-12-29T15:38:20Z"
},
{
"id": 15,
"first_name": "Verena",
"last_name": "Skerme",
"project_name": "Dach, Abshire and Crooks",
"phone_number": "227-694-0272",
"balance": "68921",
"order_count": 3,
"last_order": "2022-11-29T23:02:11Z"
},
{
"id": 16,
"first_name": "Lisle",
"last_name": "McGowan",
"project_name": "White, Murphy and Sawayn",
"phone_number": "196-817-6277",
"balance": "7250",
"order_count": 34,
"last_order": "2023-06-14T11:10:56Z"
},
{
"id": 17,
"first_name": "Bryce",
"last_name": "Pires",
"project_name": "Crooks Group",
"phone_number": "424-217-0372",
"balance": "549",
"order_count": 50,
"last_order": "2023-01-08T17:58:09Z"
},
{
"id": 18,
"first_name": "Ibrahim",
"last_name": "Battram",
"project_name": "Schmidt, Feil and Schaden",
"phone_number": "836-473-5900",
"balance": "3",
"order_count": 86,
"last_order": "2023-08-05T01:46:22Z"
},
{
"id": 19,
"first_name": "Josepha",
"last_name": "Grishkov",
"project_name": "Welch-Wisozk",
"phone_number": "928-393-5306",
"balance": "528",
"order_count": 38,
"last_order": "2023-08-18T19:01:25Z"
},
{
"id": 20,
"first_name": "Ellis",
"last_name": "Barfoot",
"project_name": "Davis, Ondricka and Schaefer",
"phone_number": "169-236-9311",
"balance": "169",
"order_count": 11,
"last_order": "2023-02-21T16:29:59Z"
}
]

View File

@ -1,72 +0,0 @@
[
{
"id": 1,
"image": "assets/dummy/dummy_1.jpg",
"name": "Meir O'Leahy",
"user_name": "moleahy0",
"contact_number": "817-666-8080"
},
{
"id": 2,
"image": "assets/dummy/dummy_2.jpg",
"name": "Ernie Ayling",
"user_name": "eayling1",
"contact_number": "890-910-3243"
},
{
"id": 3,
"image": "assets/dummy/dummy_3.jpg",
"name": "Mead Ezzle",
"user_name": "mezzle2",
"contact_number": "293-162-4468"
},
{
"id": 4,
"image": "assets/dummy/dummy_4.jpg",
"name": "Esta Norewood",
"user_name": "enorewood3",
"contact_number": "532-164-0604"
},
{
"id": 5,
"image": "assets/dummy/dummy_5.jpg",
"name": "Bartram Cottell",
"user_name": "bcottell4",
"contact_number": "940-143-2842"
},
{
"id": 6,
"image": "assets/dummy/dummy_1.jpg",
"name": "Nicola Reolfo",
"user_name": "nreolfo5",
"contact_number": "356-558-8324"
},
{
"id": 7,
"image": "assets/dummy/dummy_2.jpg",
"name": "Normy Gilhoolie",
"user_name": "ngilhoolie6",
"contact_number": "256-770-5288"
},
{
"id": 8,
"image": "assets/dummy/dummy_3.jpg",
"name": "Octavia Margerrison",
"user_name": "omargerrison7",
"contact_number": "744-595-1968"
},
{
"id": 9,
"image": "assets/dummy/dummy_4.jpg",
"name": "Stella Barriball",
"user_name": "sbarriball8",
"contact_number": "906-522-1874"
},
{
"id": 10,
"image": "assets/dummy/dummy_5.jpg",
"name": "Panchito Chase",
"user_name": "pchase9",
"contact_number": "929-922-7735"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +0,0 @@
[
{
"id": 1,
"candidate": "Patrica",
"category": "Manufacture",
"designation": "Sr.UI Developer",
"mail": "pbeedie0@ustream.tv",
"location": "Pojan",
"date": "2024-08-09T06:03:25Z",
"type": "Freelancer"
},
{
"id": 2,
"candidate": "Angelique",
"category": "Marketing",
"designation": "Team Lead",
"mail": "asamwayes1@fotki.com",
"location": "Jiangluo",
"date": "2023-11-17T10:23:18Z",
"type": "Hybride"
},
{
"id": 3,
"candidate": "Garnet",
"category": "Marketing",
"designation": "Team Lead",
"mail": "gjarrelt2@dailymail.co.uk",
"location": "Wissembourg",
"date": "2024-03-18T15:31:21Z",
"type": "Freelancer"
},
{
"id": 4,
"candidate": "Guglielmo",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "gcarlone3@ted.com",
"location": "Aoqiao",
"date": "2024-04-08T22:04:13Z",
"type": "Part Time"
},
{
"id": 5,
"candidate": "Reggie",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "rmacieiczyk4@booking.com",
"location": "Insrom",
"date": "2024-01-09T06:29:40Z",
"type": "Freelancer"
},
{
"id": 6,
"candidate": "Florri",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "fharesign5@yellowbook.com",
"location": "Itambacuri",
"date": "2024-09-05T11:09:36Z",
"type": "Part Time"
},
{
"id": 7,
"candidate": "Annabella",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "aossipenko6@ucoz.com",
"location": "Watthana Nakhon",
"date": "2024-07-27T16:17:23Z",
"type": "Full Time"
},
{
"id": 8,
"candidate": "Arlene",
"category": "Manufacture",
"designation": "Team Lead",
"mail": "agook7@google.com.hk",
"location": "Hayama",
"date": "2024-09-14T13:32:31Z",
"type": "Freelancer"
},
{
"id": 9,
"candidate": "Shurlocke",
"category": "Manufacture",
"designation": "Sales Executive",
"mail": "sgallehawk8@squidoo.com",
"location": "Bel Air Rivière Sèche",
"date": "2024-06-18T00:23:24Z",
"type": "Freelancer"
},
{
"id": 10,
"candidate": "Ricoriki",
"category": "Service",
"designation": "Sales Executive",
"mail": "rgillio9@mapy.cz",
"location": "Tawangsari",
"date": "2024-06-14T19:59:41Z",
"type": "Freelancer"
}
]

View File

@ -1,112 +0,0 @@
[
{
"id": 1,
"first_name": "Bordy",
"email": "bjeffreys0@macromedia.com",
"phone_number": "217-779-9808",
"company_name": "Voonyx",
"status": "Won Lead",
"location": "Presidencia Roque Sáenz Peña",
"date": "2024-06-20T06:26:12Z",
"amount": 52397
},
{
"id": 2,
"first_name": "Collin",
"email": "cgething1@paginegialle.it",
"phone_number": "124-897-0512",
"company_name": "Rhyzio",
"status": "New Lead",
"location": "Nombre de Jesús",
"date": "2023-11-20T12:12:32Z",
"amount": 58203
},
{
"id": 3,
"first_name": "Bear",
"email": "bfowlds2@booking.com",
"phone_number": "391-249-1041",
"company_name": "Blogtag",
"status": "Lost Lead",
"location": "Shani",
"date": "2024-11-09T07:57:49Z",
"amount": 18717
},
{
"id": 4,
"first_name": "Robers",
"email": "raujouanet3@google.cn",
"phone_number": "128-604-5632",
"company_name": "Quire",
"status": "Lost Lead",
"location": "Benghazi",
"date": "2023-12-09T17:33:39Z",
"amount": 11267
},
{
"id": 5,
"first_name": "Shirlene",
"email": "sjoiris4@theglobeandmail.com",
"phone_number": "471-884-5686",
"company_name": "Voonder",
"status": "New Lead",
"location": "Krasnaye",
"date": "2024-01-15T03:06:07Z",
"amount": 66877
},
{
"id": 6,
"first_name": "Erik",
"email": "ebudden5@zdnet.com",
"phone_number": "957-550-9950",
"company_name": "Digitube",
"status": "Won Lead",
"location": "Taouloukoult",
"date": "2024-01-21T03:05:01Z",
"amount": 55766
},
{
"id": 7,
"first_name": "Sabina",
"email": "sdenman6@ning.com",
"phone_number": "612-207-4109",
"company_name": "Kwinu",
"status": "Lost Lead",
"location": "Minneapolis",
"date": "2024-05-19T15:59:28Z",
"amount": 24691
},
{
"id": 8,
"first_name": "Andi",
"email": "aschruyer7@imdb.com",
"phone_number": "410-936-5855",
"company_name": "Photojam",
"status": "Won Lead",
"location": "Masina",
"date": "2024-09-30T18:31:07Z",
"amount": 7228
},
{
"id": 9,
"first_name": "Kathy",
"email": "kstandall8@woothemes.com",
"phone_number": "840-267-7381",
"company_name": "Quinu",
"status": "Won Lead",
"location": "Shashi",
"date": "2024-04-21T18:00:25Z",
"amount": 85726
},
{
"id": 10,
"first_name": "Lenka",
"email": "llennon9@hexun.com",
"phone_number": "962-993-3146",
"company_name": "Skaboo",
"status": "Won Lead",
"location": "Shireet",
"date": "2024-06-19T12:27:05Z",
"amount": 10069
}
]

View File

@ -1,197 +0,0 @@
[
{
"id": 1,
"name": "Mints - Striped Red",
"description": "Laceration of ulnar artery at wrs/hnd lv of unsp arm",
"price": 54,
"stock": 72,
"category": "Scallops 60/80 Iqf",
"order_counts": 10,
"created_at": "2022-07-20T09:52:34Z",
"rating": 2.49,
"rating_count": 42,
"sku": "RCII"
},
{
"id": 2,
"name": "Pasta - Ravioli",
"description": "Oth disp fx of upper end l humer, subs for fx w delay heal",
"price": 35,
"stock": 64,
"category": "Chocolate - Mi - Amere Semi",
"order_counts": 45,
"created_at": "2023-03-25T22:32:10Z",
"rating": 4.66,
"rating_count": 82,
"sku": "RDS.A"
},
{
"id": 3,
"name": "Soup - Campbells Chili",
"description": "Nondisp fx of anterior wall of left acetab, init for opn fx",
"price": 27,
"stock": 21,
"category": "Tomatoes - Cherry, Yellow",
"order_counts": 53,
"created_at": "2022-05-18T15:56:06Z",
"rating": 3.05,
"rating_count": 66,
"sku": "STNG"
},
{
"id": 4,
"name": "Fennel - Seeds",
"description": "Displ spiral fx shaft of ulna, r arm, 7thD",
"price": 124,
"stock": 56,
"category": "Squid - U - 10 Thailand",
"order_counts": 13,
"created_at": "2022-04-21T11:32:39Z",
"rating": 0.59,
"rating_count": 55,
"sku": "FEUZ"
},
{
"id": 5,
"name": "Salt - Celery",
"description": "Interstitial myositis, lower leg",
"price": 25,
"stock": 78,
"category": "Gatorade - Lemon Lime",
"order_counts": 30,
"created_at": "2023-01-01T01:15:44Z",
"rating": 1.42,
"rating_count": 10,
"sku": "VER"
},
{
"id": 6,
"name": "Flour - Chickpea",
"description": "Oth fx upr end unsp rad, 7thJ",
"price": 131,
"stock": 50,
"category": "Sweet Pea Sprouts",
"order_counts": 11,
"created_at": "2023-04-09T04:41:43Z",
"rating": 4.05,
"rating_count": 24,
"sku": "HDS"
},
{
"id": 7,
"name": "Chips - Miss Vickies",
"description": "Contusion of right hip, initial encounter",
"price": 52,
"stock": 63,
"category": "Extract - Almond",
"order_counts": 62,
"created_at": "2022-06-24T05:25:56Z",
"rating": 3.35,
"rating_count": 6,
"sku": "MACQW"
},
{
"id": 8,
"name": "Ice Cream - Super Sandwich",
"description": "Acute post-traumatic headache",
"price": 87,
"stock": 44,
"category": "Bread - Wheat Baguette",
"order_counts": 63,
"created_at": "2022-07-06T05:37:09Z",
"rating": 0.96,
"rating_count": 38,
"sku": "AA"
},
{
"id": 9,
"name": "Alize Gold Passion",
"description": "War op involving explosion of marine weapons, civilian",
"price": 113,
"stock": 72,
"category": "Lid - Translucent, 3.5 And 6 Oz",
"order_counts": 23,
"created_at": "2022-07-09T18:19:37Z",
"rating": 3.25,
"rating_count": 64,
"sku": "EVOK"
},
{
"id": 10,
"name": "Mushrooms - Honey",
"description": "Sltr-haris Type IV physl fx low end l femr, 7thP",
"price": 98,
"stock": 64,
"category": "Lemon Balm - Fresh",
"order_counts": 5,
"created_at": "2022-08-05T22:14:06Z",
"rating": 3.51,
"rating_count": 0,
"sku": "TDG"
},
{
"id": 11,
"name": "Bread Base - Goodhearth",
"description": "Unspecified injury of axillary artery",
"price": 81,
"stock": 56,
"category": "Beef - Bones, Marrow",
"order_counts": 76,
"created_at": "2023-04-22T03:11:41Z",
"rating": 4.07,
"rating_count": 22,
"sku": "GPAC"
},
{
"id": 12,
"name": "Veal - Heart",
"description": "Laceration without foreign body of left buttock, subs encntr",
"price": 93,
"stock": 13,
"category": "Peach - Fresh",
"order_counts": 12,
"created_at": "2023-02-18T16:15:16Z",
"rating": 2.08,
"rating_count": 87,
"sku": "PAH"
},
{
"id": 13,
"name": "Tomatoes - Grape",
"description": "Nondisplaced bicondylar fracture of left tibia",
"price": 132,
"stock": 13,
"category": "Veal - Striploin",
"order_counts": 24,
"created_at": "2022-06-08T20:49:59Z",
"rating": 3.32,
"rating_count": 82,
"sku": "ENZL"
},
{
"id": 14,
"name": "Tomato Paste",
"description": "Abrasion of unspecified part of neck, initial encounter",
"price": 48,
"stock": 24,
"category": "Juice - Tomato, 48 Oz",
"order_counts": 4,
"created_at": "2022-12-03T04:03:52Z",
"rating": 4.11,
"rating_count": 66,
"sku": "LTEA"
},
{
"id": 15,
"name": "Cheese - Roquefort Pappillon",
"description": "Wedge comprsn fx first thor vertebra, init for opn fx",
"price": 15,
"stock": 68,
"category": "Veal - Insides, Grains",
"order_counts": 72,
"created_at": "2022-09-22T06:22:33Z",
"rating": 1.54,
"rating_count": 40,
"sku": "AIF"
}
]

View File

@ -1,102 +0,0 @@
[
{
"order_id": "TWT76911",
"customer_name": "Robinson",
"location": "Lyubokhna",
"order_date": "2023-09-17T00:35:06Z",
"quantity": 6,
"payments": "COD",
"price": 336,
"status": "New"
},
{
"order_id": "TWT23890",
"customer_name": "Claudina",
"location": "Maracha",
"order_date": "2023-09-11T15:01:04Z",
"quantity": 8,
"payments": "American Express",
"price": 428,
"status": "Shopping"
},
{
"order_id": "TWT84616",
"customer_name": "Dewain",
"location": "Fuji",
"order_date": "2023-12-26T02:42:54Z",
"quantity": 6,
"payments": "Credit Card",
"price": 410,
"status": "Shopping"
},
{
"order_id": "TWT66711",
"customer_name": "Margette",
"location": "Chicago",
"order_date": "2024-01-09T18:24:04Z",
"quantity": 10,
"payments": "Paypal",
"price": 268,
"status": "Pending"
},
{
"order_id": "TWT50711",
"customer_name": "Brittany",
"location": "Hakha",
"order_date": "2023-09-28T14:17:02Z",
"quantity": 2,
"payments": "Visa Card",
"price": 229,
"status": "New"
},
{
"order_id": "TWT37588",
"customer_name": "Venus",
"location": "Sioguí Arriba",
"order_date": "2023-10-16T18:22:32Z",
"quantity": 5,
"payments": "Visa Card",
"price": 211,
"status": "Delivered"
},
{
"order_id": "TWT36092",
"customer_name": "Norry",
"location": "Hongqi",
"order_date": "2024-01-28T16:50:34Z",
"quantity": 7,
"payments": "American Express",
"price": 111,
"status": "Shopping"
},
{
"order_id": "TWT99659",
"customer_name": "Rabbi",
"location": "Macari",
"order_date": "2023-03-27T17:42:51Z",
"quantity": 9,
"payments": "COD",
"price": 268,
"status": "Pending"
},
{
"order_id": "TWT21952",
"customer_name": "Hesther",
"location": "København",
"order_date": "2024-02-08T00:16:01Z",
"quantity": 9,
"payments": "Credit Card",
"price": 392,
"status": "Delivered"
},
{
"order_id": "TWT66885",
"customer_name": "Sioux",
"location": "Taohua",
"order_date": "2023-10-23T16:25:29Z",
"quantity": 1,
"payments": "Paypal",
"price": 337,
"status": "New"
}
]

View File

@ -1,82 +0,0 @@
[
{
"id": 1,
"title": "Marketing Manager",
"assign_to": "Hercules",
"date": "2024-03-10T00:14:27Z",
"priority": "High",
"status": "Pending"
},
{
"id": 2,
"title": "Community Outreach Specialist",
"assign_to": "Fayre",
"date": "2024-10-07T06:24:18Z",
"priority": "High",
"status": "Pending"
},
{
"id": 3,
"title": "Senior Quality Engineer",
"assign_to": "Nancy",
"date": "2024-01-11T18:22:29Z",
"priority": "Medium",
"status": "In Progress"
},
{
"id": 4,
"title": "VP Sales",
"assign_to": "Jemimah",
"date": "2024-06-17T17:57:40Z",
"priority": "Low",
"status": "Finished"
},
{
"id": 5,
"title": "Sales Associate",
"assign_to": "Raquel",
"date": "2024-05-06T11:11:43Z",
"priority": "Medium",
"status": "Finished"
},
{
"id": 6,
"title": "Dental Hygienist",
"assign_to": "Vasili",
"date": "2024-05-31T21:16:27Z",
"priority": "High",
"status": "Finished"
},
{
"id": 7,
"title": "Occupational Therapist",
"assign_to": "Lulu",
"date": "2024-03-28T21:07:00Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 8,
"title": "Analyst Programmer",
"assign_to": "Egor",
"date": "2023-12-11T08:16:01Z",
"priority": "High",
"status": "Finished"
},
{
"id": 9,
"title": "Research Assistant III",
"assign_to": "Max",
"date": "2024-02-15T21:59:34Z",
"priority": "High",
"status": "Pending"
},
{
"id": 10,
"title": "Developer II",
"assign_to": "Kaela",
"date": "2024-06-24T07:29:47Z",
"priority": "High",
"status": "Cancelled"
}
]

View File

@ -1,47 +0,0 @@
[
{
"id": 1,
"product_name": "Theobald",
"quantity": 2,
"customer": "Theobald Southcott",
"status": "Shipped",
"price": 361,
"order_date": "2023-12-11T23:48:54Z"
},
{
"id": 2,
"product_name": "Carla",
"quantity": 3,
"customer": "Carla Grgic",
"status": "Pending",
"price": 329,
"order_date": "2024-05-25T09:37:51Z"
},
{
"id": 3,
"product_name": "Liana",
"quantity": 3,
"customer": "Liana Swannell",
"status": "Delivery",
"price": 120,
"order_date": "2024-04-06T19:02:14Z"
},
{
"id": 4,
"product_name": "Radcliffe",
"quantity": 4,
"customer": "Radcliffe Venard",
"status": "Shipped",
"price": 750,
"order_date": "2024-10-27T10:44:12Z"
},
{
"id": 5,
"product_name": "Delmer",
"quantity": 3,
"customer": "Delmer Vamplew",
"status": "Delivery",
"price": 469,
"order_date": "2024-10-16T15:55:22Z"
}
]

View File

@ -1,91 +0,0 @@
[
{
"id": 1,
"title": "Finish report",
"description": "Complete the quarterly report for the team meeting.",
"due_date": "2024-07-14T00:37:09Z",
"priority": "High",
"status": "Pending"
},
{
"id": 2,
"title": "Team meeting",
"description": "Attend the weekly project meeting and provide updates.",
"due_date": "2024-04-18T01:25:27Z",
"priority": "Medium",
"status": "Completed"
},
{
"id": 3,
"title": "Buy groceries",
"description": "Purchase ingredients for dinner and weekly supplies.",
"due_date": "2024-02-17T16:32:03Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 4,
"title": "Update website",
"description": "Update the homepage with new content and images.",
"due_date": "2024-07-16T15:49:59Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 5,
"title": "Send emails",
"description": "Send out follow-up emails to clients from last week's meeting.",
"due_date": "2024-09-24T11:08:14Z",
"priority": "High",
"status": "Completed"
},
{
"id": 6,
"title": "Organize workspace",
"description": "Declutter desk and organize office supplies.",
"due_date": "2024-10-06T11:49:14Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 7,
"title": "Prepare presentation",
"description": "Create slides for next week's client pitch.",
"due_date": "2024-05-20T04:38:51Z",
"priority": "High",
"status": "In Progress"
},
{
"id": 8,
"title": "Write blog post",
"description": "Write a blog post about industry trends for the company website.",
"due_date": "2024-01-13T07:46:34Z",
"priority": "Medium",
"status": "Pending"
},
{
"id": 9,
"title": "Schedule doctor's appointment",
"description": "Call and book a check-up appointment with the doctor.",
"due_date": "2024-09-22T10:54:21Z",
"priority": "Low",
"status": "Pending"
},
{
"id": 10,
"title": "Review budget",
"description": "Review and adjust monthly budget for expenses.",
"due_date": "2024-08-20T03:57:48Z",
"priority": "Medium",
"status": "Pending"
}
]

View File

@ -1,62 +0,0 @@
[
{
"id": 1,
"first_name": "Roobbie",
"last_name": "Ivashintsov",
"email": "rivashintsov0@symantec.com"
},
{
"id": 2,
"first_name": "Cissy",
"last_name": "Salmons",
"email": "csalmons1@unicef.org"
},
{
"id": 3,
"first_name": "Jillene",
"last_name": "Besnardeau",
"email": "jbesnardeau2@china.com.cn"
},
{
"id": 4,
"first_name": "Catriona",
"last_name": "Wrennall",
"email": "cwrennall3@godaddy.com"
},
{
"id": 5,
"first_name": "Risa",
"last_name": "Rumens",
"email": "rrumens4@un.org"
},
{
"id": 6,
"first_name": "Gianina",
"last_name": "Pavlenkov",
"email": "gpavlenkov5@ted.com"
},
{
"id": 7,
"first_name": "Tripp",
"last_name": "Blowick",
"email": "tblowick6@reuters.com"
},
{
"id": 8,
"first_name": "Ephrem",
"last_name": "Pfertner",
"email": "epfertner7@godaddy.com"
},
{
"id": 9,
"first_name": "Jacinda",
"last_name": "Tomkies",
"email": "jtomkies8@si.edu"
},
{
"id": 10,
"first_name": "Traver",
"last_name": "Poile",
"email": "tpoile9@phoca.cz"
}
]

View File

@ -1,56 +0,0 @@
[
{
"id": 1,
"session_duration": "2023-08-18T14:47:41Z",
"channel": "Organic Search",
"session": 547,
"bounce_rate": 27.2,
"target_reached": 843,
"page_per_session": 4.4
},
{
"id": 2,
"session_duration": "2023-04-20T20:13:24Z",
"channel": "Direct",
"session": 855,
"bounce_rate": 25.8,
"target_reached": 998,
"page_per_session": 6.6
},
{
"id": 3,
"session_duration": "2023-09-05T02:15:03Z",
"channel": "Referral",
"session": 337,
"bounce_rate": 12.4,
"target_reached": 509,
"page_per_session": 8.0
},
{
"id": 4,
"session_duration": "2023-06-23T13:50:55Z",
"channel": "Social",
"session": 279,
"bounce_rate": 40.4,
"target_reached": 860,
"page_per_session": 7.3
},
{
"id": 5,
"session_duration": "2023-09-14T09:01:34Z",
"channel": "Email",
"session": 118,
"bounce_rate": 46.2,
"target_reached": 168,
"page_per_session": 3.0
},
{
"id": 6,
"session_duration": "2023-07-14T01:46:51Z",
"channel": "Paid Search",
"session": 205,
"bounce_rate": 32.8,
"target_reached": 583,
"page_per_session": 2.5
}
]

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "mtest-a0635",
"private_key_id": "39a69f7d2a64234784e0d0ce6c113052296d6dc1",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCimbxktO7PeQ4h\n81Ye2ZBcZjltDhqqD0o9XyLmNdHszzM056bwpJkvgoyyTJAIvR2fcBF3YQFyuC+1\nddLHtchP48FjflZ+zZzLp7oaA/Zh28OZLbCsu+Nm8vO3WJVoIaJYgi+jEz21G128\ncOIbgkKIpLMz1wQhPPOwDTuSdQ+WajWJb04/aNrmTRH1hMreyhHiIFmalcavUgc1\nY5FvgrGs7EaKjYBevoFN3dwmEXjfyHjfBSxnt1yytl9tbtINqdrYLYAMm1l3+KqO\nCGxicQE5kjI1osI2wRjsk105RHnpxPg2GZnI4vTIOkEY5czhRSOs94g2d628H6fq\nVzf9UqwtAgMBAAECggEARluLf3AjHbdd/CbVDwhJRRIeqye9NfTjxOaTrVWAfp2x\npKTQQbSXbE1rIAOtF3rthH3zsNpSzBcS3cwb5rqr8JW2qpySRNAnlp//ER7Bz9pO\nKsvwdO3gGj3qY117WNGk8/NxNXkv7FvpFY8q54hXzdSmjjnt2YwMThOLwXXRxt2B\nFxN3FpBWqw12epqS162nW2nIRJ34Jloil4J5x61Sc79MCFyCxyhMlrBkY+Ni/xb1\nigBXBjczxNiJqqDie0mc16WB1HMEcBP9Yjtb46Hhfs3NDDWNqDkoM8QmEMSg8EHy\nyjcSlf0Wj8I9Kf+0PZo+2FB2DbuhfA8IVR9U/c00KQKBgQDd/OULx6QpmUev1Gl/\nrwwN67ZUMJ72cRuwvLFsMTIzZ+oItO0AR1uMkRZ1crOMc490XNUvSCGP6piZQAn1\nro8qNAh+0Q/UvKHM1khOj/4DxEGZRnNOhe6QLZM9QNygENuEYfdYDD9wcQI9Xs+B\nMIOBsuuqUVHlsbvYkeYNS8M8swKBgQC7g3i1dYRC/bkNMthVS4GTlFRuLscyIjTi\nhruhdaSE+fBZ5RO3XDzz6oDHYcdo/z5ySqI7EIsckNRbwFsMCOjSP3xJapadPYwU\nIhZBU7lgNlPnHJ/BIUwA5JZqRqGTNWrFINUHZFp2RK/x2bYdfoqY8bq08eWs9gmR\nc7U7i+6jnwKBgGaO3isxExD89fewBQWuk70it1vyEp785rQimT3JBM5nJeLb49sL\nHKq2pU+hrH4pLY+vC/cKNidNVS8IPRG6kf4HiB0+7Td15rLCFSnmsI6A72Wm/MK8\ncdk+lRXpj4SMBT8GG8Yb8ns6WrSLxwaCqV8UkHhhlZqvIIAP998Qr6StAoGAQTwr\n8nU/3k6G4qCdwo7SNZWVCgAcLMTZwTU+cZ2L7vdFNwELKu9cBT/ALZ1G0rB5+Skd\n546J1xZLyt/QzQ8McJjFlIUQgQO4iAiT1YZbJ62+4tiCe54p4uWjrrWD4MLkslAJ\nzNiM4DhlPa6QPRKZBTyTx/+f99xg18l5c43rJ+ECgYBkXMfjdn8SOaG6ggJrf1xx\nas49vwAscx4AJaOdVu3D8lCwoNCuAJhBHcFqsJ0wEHWpsqKAdXxqX/Nt2x8t7zL0\nPoRCvfsq5P7GdRrNhrHxLwjDqh+OS+Ow6t0esPQ5RPBgtjvthAlb7bV2nIfkpmdl\nFbjML8vkXk9iPJsbAfO2jw==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@mtest-a0635.iam.gserviceaccount.com",
"client_id": "111097905744982732087",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40mtest-a0635.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

55
build_release.sh Normal file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# ===============================
# Flutter APK Build Script (AAB Disabled)
# ===============================
# Exit immediately if a command exits with a non-zero status
set -e
# Colors for pretty output
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# App info
APP_NAME="Marco"
BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
# Step 1: Clean previous builds
echo -e "${YELLOW}🧹 Cleaning previous builds...${NC}"
flutter clean
# Step 2: Get dependencies
echo -e "${YELLOW}📦 Fetching dependencies...${NC}"
flutter pub get
# ==============================
# Step 3: Build AAB (Commented)
# ==============================
# echo -e "${CYAN}🏗 Building AAB file...${NC}"
# flutter build appbundle --release
# Step 4: Build APK
echo -e "${CYAN}🏗 Building APK file...${NC}"
flutter build apk --release
# Step 5: Show output paths
# AAB_PATH="$BUILD_DIR/bundle/release/app-release.aab"
APK_PATH="$BUILD_DIR/apk/release/app-release.apk"
echo -e "${GREEN}✅ Build completed successfully!${NC}"
# echo -e "${YELLOW}📍 AAB file: ${CYAN}$AAB_PATH${NC}"
echo -e "${YELLOW}📍 APK file: ${CYAN}$APK_PATH${NC}"
# Optional: open the folder (Mac/Linux)
if command -v xdg-open &> /dev/null
then
xdg-open "$BUILD_DIR"
elif command -v open &> /dev/null
then
open "$BUILD_DIR"
fi

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.marco;
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -0,0 +1,426 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/model/attendance/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/attendance/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance/attendance_log_view_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController {
// 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
String selectedTab = 'todaysAttendance';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
final isLoading = true.obs;
final isLoadingProjects = true.obs;
final isLoadingEmployees = true.obs;
final isLoadingAttendanceLogs = true.obs;
final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
var showPendingOnly = false.obs;
@override
void onInit() {
super.onInit();
_initializeDefaults();
// 🔹 Fetch organizations for the selected project
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
fetchOrganizations(projectId);
}
}
void _initializeDefaults() {
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1));
logSafe(
"Default date range set: $startDateAttendance to $endDateAttendance");
}
// ------------------ Project & Employee ------------------
/// Called when a notification says attendance has been updated
Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) {
logSafe("No project selected for attendance refresh from notification",
level: LogLevel.warning);
return;
}
await fetchProjectData(projectId);
logSafe(
"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;
isLoadingEmployees.value = true;
final response = await ApiService.getTodaysAttendance(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe("Employees fetched: ${employees.length} for project $projectId");
} else {
logSafe("Failed to fetch employees for project $projectId",
level: LogLevel.error);
}
isLoadingEmployees.value = false;
update();
}
Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true;
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
logSafe("Failed to fetch organizations for project $projectId",
level: LogLevel.error);
}
isLoadingOrganizations.value = false;
update();
}
// ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
String projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true,
String? markTime, // still optional in controller
String? date, // new optional param
}) async {
try {
uploadingStates[employeeId]?.value = true;
XFile? image;
if (imageCapture) {
image = await ImagePicker()
.pickImage(source: ImageSource.camera, imageQuality: 80);
if (image == null) {
logSafe("Image capture cancelled.", level: LogLevel.warning);
return false;
}
final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) {
logSafe("Image compression failed.", level: LogLevel.error);
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
}
if (!await _handleLocationPermission()) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final imageName = imageCapture
? 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
}
}
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,
image,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
markTime: formattedMarkTime,
date: formattedDate,
);
logSafe(
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
return result;
} catch (e, stacktrace) {
logSafe("Error uploading attendance",
level: LogLevel.error, error: e, stackTrace: stacktrace);
return false;
} finally {
uploadingStates[employeeId]?.value = false;
}
}
Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied',
level: LogLevel.error);
return false;
}
return true;
}
// ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
organizationId: selectedOrganization?.id,
);
if (response != null) {
attendanceLogs =
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
} else {
logSafe("Failed to fetch attendance logs for project $projectId",
level: LogLevel.error);
}
isLoadingAttendanceLogs.value = false;
update();
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
final response = await ApiService.getRegularizationLogs(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
regularizationLogs =
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
} else {
logSafe("Failed to fetch regularization logs for project $projectId",
level: LogLevel.error);
}
isLoadingRegularizationLogs.value = false;
update();
}
// ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView =
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)));
logSafe("Attendance log view fetched for ID: $id");
} else {
logSafe("Failed to fetch attendance log view for ID $id",
level: LogLevel.error);
}
isLoadingLogView.value = false;
update();
}
// ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true;
await fetchProjectData(projectId);
isLoading.value = false;
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
await fetchOrganizations(projectId);
// Call APIs depending on the selected tab only
switch (selectedTab) {
case 'todaysAttendance':
await fetchTodaysAttendance(projectId);
break;
case 'attendanceLogs':
await fetchAttendanceLogs(
projectId,
dateFrom: startDateAttendance,
dateTo: endDateAttendance,
);
break;
case 'regularizationRequests':
await fetchRegularizationLogs(projectId);
break;
}
logSafe(
"Project data fetched for project ID: $projectId, tab: $selectedTab");
update();
}
// ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async {
final today = DateTime.now();
final picked = await showDateRangePicker(
context: context,
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)),
),
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
logSafe(
"Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
}

View File

@ -5,12 +5,16 @@ import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ForgotPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
final MyFormValidator basicValidator = MyFormValidator();
final RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
basicValidator.addField(
'email',
required: true,
@ -18,49 +22,49 @@ class ForgotPasswordController extends MyController {
validators: [MyEmailValidator()],
controller: TextEditingController(text: "demo@example.com"),
);
super.onInit();
}
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
basicValidator.validateForm();
basicValidator.clearErrors();
}
Get.toNamed('/auth/reset_password');
update();
}
}
/// New: Forgot password function
Future<void> onForgotPassword() async {
if (basicValidator.validateForm()) {
update();
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
if (!basicValidator.validateForm()) return;
isLoading.value = true;
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
try {
logSafe("Forgot password requested for: $email", );
final result = await AuthService.forgotPassword(email);
if (result != null) {
if (result == null) {
showAppSnackbar(
title: "Success",
message: "Your password reset link was sent.",
message: "Password reset link has been sent.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
showAppSnackbar(
title: "Success",
message: "Your password reset link was sent.",
type: SnackbarType.success,
title: "Failed",
message: errorMessage,
type: SnackbarType.error,
);
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, );
}
update();
} catch (e, stacktrace) {
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar(
title: "Error",
message: "Something went wrong. Please try again later.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
void gotoLogIn() {
Get.toNamed('/auth/login');
Get.offAllNamed('/auth/login-option');
}
}

View File

@ -4,61 +4,115 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
class LoginController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
final MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false, isChecked = false;
RxBool isLoading = false.obs; // Add reactive loading state
final String _dummyEmail = "admin@marcoaiot.com";
final String _dummyPassword = "User@123";
final RxBool isLoading = false.obs;
final RxBool showPassword = false.obs;
final RxBool isChecked = false.obs;
final RxBool showSplash = false.obs;
@override
void onInit() {
basicValidator.addField('username', required: true, label: "User_Name", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
basicValidator.addField('password', required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
super.onInit();
_initializeForm();
_loadSavedCredentials();
}
void onChangeCheckBox(bool? value) {
isChecked = value ?? isChecked;
update();
void _initializeForm() {
basicValidator.addField(
'username',
required: true,
label: "User_Name",
validators: [MyEmailValidator()],
controller: TextEditingController(),
);
basicValidator.addField(
'password',
required: true,
label: "Password",
validators: [MyLengthValidator(min: 6)],
controller: TextEditingController(),
);
}
void onChangeShowPassword() {
showPassword = !showPassword;
update();
}
void onChangeCheckBox(bool? value) => isChecked.value = value ?? false;
void onChangeShowPassword() => showPassword.toggle();
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
// Set loading to true
isLoading.value = true;
update();
if (!basicValidator.validateForm()) return;
showSplash.value = true;
try {
final loginData = basicValidator.getData();
logSafe("Attempting login for user: ${loginData['username']}");
final errors = await AuthService.loginUser(loginData);
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
showAppSnackbar(
title: "Login Failed",
message: "Username or password is incorrect",
type: SnackbarType.error,
);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
} else {
String nextUrl = Uri.parse(ModalRoute.of(Get.context!)?.settings.name ?? "").queryParameters['next'] ?? "/home";
Get.toNamed(nextUrl);
await _handleRememberMe();
enableRemoteLogging();
logSafe("Login successful for user: ${loginData['username']}");
Get.offNamed('/select-tenant');
}
// Set loading to false after the API call is complete
isLoading.value = false;
update();
} catch (e, stacktrace) {
showAppSnackbar(
title: "Login Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
logSafe("Exception during login",
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally {
showSplash.value = false;
}
}
void goToForgotPassword() {
Get.toNamed('/auth/forgot_password');
Future<void> _handleRememberMe() async {
if (isChecked.value) {
await LocalStorage.setToken(
'username', basicValidator.getController('username')!.text);
await LocalStorage.setToken(
'password', basicValidator.getController('password')!.text);
await LocalStorage.setBool('remember_me', true);
} else {
await LocalStorage.removeToken('username');
await LocalStorage.removeToken('password');
await LocalStorage.setBool('remember_me', false);
basicValidator.clearErrors();
}
}
void gotoRegister() {
Get.offAndToNamed('/auth/register_account');
Future<void> _loadSavedCredentials() async {
final savedUsername = LocalStorage.getToken('username');
final savedPassword = LocalStorage.getToken('password');
final remember = LocalStorage.getBool('remember_me') ?? false;
isChecked.value = remember;
if (remember) {
basicValidator.getController('username')?.text = savedUsername ?? '';
basicValidator.getController('password')?.text = savedPassword ?? '';
}
}
void goToForgotPassword() => Get.toNamed('/auth/forgot_password');
void gotoRegister() => Get.offAndToNamed('/auth/register_account');
}

View File

@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/project_controller.dart';
class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator();
final isNewUser = false.obs;
final isChangeMpin = false.obs;
final RxBool isLoading = false.obs;
final formKey = GlobalKey<FormState>();
// Updated to 4-digit MPIN
final digitControllers = List.generate(4, (_) => TextEditingController());
final focusNodes = List.generate(4, (_) => FocusNode());
final retypeControllers = List.generate(4, (_) => TextEditingController());
final retypeFocusNodes = List.generate(4, (_) => FocusNode());
final RxInt failedAttempts = 0.obs;
@override
void onInit() {
super.onInit();
final bool hasMpin = LocalStorage.getIsMpin();
isNewUser.value = !hasMpin;
logSafe("onInit called. isNewUser: ${isNewUser.value}");
}
/// Enable Change MPIN mode
void setChangeMpinMode() {
isChangeMpin.value = true;
isNewUser.value = false;
clearFields();
clearRetypeFields();
logSafe("setChangeMpinMode activated");
}
/// Handle digit entry and focus movement
void onDigitChanged(String value, int index, {bool isRetype = false}) {
logSafe(
"onDigitChanged -> index: $index, value: $value, isRetype: $isRetype");
final nodes = isRetype ? retypeFocusNodes : focusNodes;
if (value.isNotEmpty && index < 3) {
nodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
nodes[index - 1].requestFocus();
}
}
/// Submit MPIN for verification or generation
Future<void> onSubmitMPIN() async {
logSafe("onSubmitMPIN triggered");
if (!formKey.currentState!.validate()) {
logSafe("Form validation failed", level: LogLevel.warning);
return;
}
final enteredMPIN = digitControllers.map((c) => c.text).join();
logSafe("Entered MPIN: $enteredMPIN");
if (enteredMPIN.length < 4) {
_showError("Please enter all 4 digits.");
return;
}
if (isNewUser.value || isChangeMpin.value) {
final retypeMPIN = retypeControllers.map((c) => c.text).join();
logSafe("Retyped MPIN: $retypeMPIN");
if (retypeMPIN.length < 4) {
_showError("Please enter all 4 digits in Retype MPIN.");
return;
}
if (enteredMPIN != retypeMPIN) {
_showError("MPIN and Retype MPIN do not match.");
clearFields();
clearRetypeFields();
return;
}
final bool success = await generateMPIN(mpin: enteredMPIN);
if (success) {
logSafe("MPIN generation/change successful.");
showAppSnackbar(
title: "Success",
message: isChangeMpin.value
? "MPIN changed successfully."
: "MPIN generated successfully. Please login again.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
logSafe("MPIN generation/change failed.", level: LogLevel.warning);
clearFields();
clearRetypeFields();
}
} else {
logSafe("Existing user. Proceeding to verify MPIN.");
await verifyMPIN();
}
}
/// Forgot MPIN
Future<void> onForgotMPIN() async {
logSafe("onForgotMPIN called");
isNewUser.value = true;
isChangeMpin.value = false;
clearFields();
clearRetypeFields();
}
/// Switch to login/enter MPIN screen
void switchToEnterMPIN() {
logSafe("switchToEnterMPIN called");
isNewUser.value = false;
isChangeMpin.value = false;
clearFields();
clearRetypeFields();
}
/// Show error snackbar
void _showError(String message) {
logSafe("ERROR: $message", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: message,
type: SnackbarType.error,
);
}
/// Navigate to dashboard
/// Navigate to tenant selection after MPIN verification
void _navigateToTenantSelection({String? message}) {
if (message != null) {
logSafe("Navigating to Tenant Selection with message: $message");
showAppSnackbar(
title: "Success",
message: message,
type: SnackbarType.success,
);
}
Get.offAllNamed('/select-tenant');
}
/// Clear the primary MPIN fields
void clearFields() {
logSafe("clearFields called");
for (final c in digitControllers) {
c.clear();
}
focusNodes.first.requestFocus();
}
/// Clear the retype MPIN fields
void clearRetypeFields() {
logSafe("clearRetypeFields called");
for (final c in retypeControllers) {
c.clear();
}
retypeFocusNodes.first.requestFocus();
}
/// Cleanup
@override
void onClose() {
logSafe("onClose called");
for (final controller in digitControllers) {
controller.dispose();
}
for (final node in focusNodes) {
node.dispose();
}
for (final controller in retypeControllers) {
controller.dispose();
}
for (final node in retypeFocusNodes) {
node.dispose();
}
super.onClose();
}
/// Generate MPIN for new user/change MPIN
Future<bool> generateMPIN({required String mpin}) async {
try {
isLoading.value = true;
logSafe("generateMPIN started");
final employeeInfo = LocalStorage.getEmployeeInfo();
final String? employeeId = employeeInfo?.id;
if (employeeId == null || employeeId.isEmpty) {
isLoading.value = false;
_showError("Missing employee ID.");
return false;
}
logSafe("Calling AuthService.generateMpin for employeeId: $employeeId");
final response = await AuthService.generateMpin(
employeeId: employeeId,
mpin: mpin,
);
isLoading.value = false;
if (response == null) {
return true;
} else {
logSafe("MPIN generation returned error: $response",
level: LogLevel.warning);
showAppSnackbar(
title: "MPIN Operation Failed",
message: "Please check your inputs.",
type: SnackbarType.error,
);
basicValidator.addErrors(response);
basicValidator.validateForm();
basicValidator.clearErrors();
return false;
}
} catch (e) {
isLoading.value = false;
logSafe("Exception in generateMPIN", level: LogLevel.error, error: e);
_showError("Failed to process MPIN.");
return false;
}
}
/// Verify MPIN for existing user
Future<void> verifyMPIN() async {
logSafe("verifyMPIN triggered");
final enteredMPIN = digitControllers.map((c) => c.text).join();
if (enteredMPIN.length < 4) {
_showError("Please enter all 4 digits.");
return;
}
final mpinToken = await LocalStorage.getMpinToken();
if (mpinToken == null || mpinToken.isEmpty) {
_showError("Missing MPIN token. Please log in again.");
return;
}
try {
isLoading.value = true;
final fcmToken = await FirebaseNotificationService().getFcmToken();
final response = await AuthService.verifyMpin(
mpin: enteredMPIN,
mpinToken: mpinToken,
fcmToken: fcmToken ?? '',
);
isLoading.value = false;
if (response == null) {
logSafe("MPIN verified successfully");
await LocalStorage.setBool('mpin_verified', true);
// 🔹 Ensure controllers are injected and loaded
final token = await LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
await Get.find<PermissionController>().loadData(token);
}
if (!Get.isRegistered<ProjectController>()) {
Get.put(ProjectController(), permanent: true);
await Get.find<ProjectController>().fetchProjects();
}
}
showAppSnackbar(
title: "Success",
message: "MPIN Verified Successfully",
type: SnackbarType.success,
);
_navigateToTenantSelection();
} else {
final errorMessage = response["error"] ?? "Invalid MPIN";
logSafe("MPIN verification failed: $errorMessage",
level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: errorMessage,
type: SnackbarType.error,
);
clearFields();
onInvalidMPIN();
}
} catch (e) {
isLoading.value = false;
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
_showError("Something went wrong. Please try again.");
}
}
/// Increment failed attempts and warn
void onInvalidMPIN() {
failedAttempts.value++;
if (failedAttempts.value >= 3) {
showAppSnackbar(
title: "Error",
message: "Too many failed attempts. Consider logging in again.",
type: SnackbarType.error,
);
}
}
}

View File

@ -0,0 +1,217 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
class OTPController extends GetxController {
final formKey = GlobalKey<FormState>();
final RxString email = ''.obs;
final RxBool isOTPSent = false.obs;
final RxBool isSending = false.obs;
final RxBool isResending = false.obs;
final RxInt timer = 0.obs;
Timer? _countdownTimer;
final TextEditingController emailController = TextEditingController();
final List<TextEditingController> otpControllers =
List.generate(4, (_) => TextEditingController());
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
@override
void onInit() {
super.onInit();
timer.value = 0;
_loadSavedEmail();
logSafe("[OTPController] Initialized");
}
@override
void onClose() {
_countdownTimer?.cancel();
emailController.dispose();
for (final controller in otpControllers) {
controller.dispose();
}
for (final node in focusNodes) {
node.dispose();
}
logSafe("[OTPController] Disposed");
super.onClose();
}
Future<bool> _sendOTP(String email) async {
logSafe("[OTPController] Sending OTP");
final result = await AuthService.generateOtp(email);
if (result == null) {
logSafe("[OTPController] OTP sent successfully");
return true;
} else {
logSafe(
"[OTPController] OTP send failed",
level: LogLevel.warning,
error: result['error'],
);
showAppSnackbar(
title: "Error",
message: result['error'] ?? "Failed to send OTP",
type: SnackbarType.error,
);
return false;
}
}
Future<void> sendOTP() async {
final userEmail = emailController.text.trim();
logSafe("[OTPController] sendOTP called");
if (!_validateEmail(userEmail)) {
logSafe("[OTPController] Invalid email format", level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: "Please enter a valid email address",
type: SnackbarType.error,
);
return;
}
if (isSending.value) return;
isSending.value = true;
final success = await _sendOTP(userEmail);
if (success) {
email.value = userEmail;
isOTPSent.value = true;
await _saveEmailIfRemembered(userEmail);
_startTimer();
_clearOTPFields();
}
isSending.value = false;
}
Future<void> onResendOTP() async {
if (isResending.value) return;
logSafe("[OTPController] Resending OTP");
isResending.value = true;
_clearOTPFields();
final success = await _sendOTP(email.value);
if (success) {
_startTimer();
}
isResending.value = false;
}
void onOTPChanged(String value, int index) {
logSafe("[OTPController] OTP field changed: index=$index",
level: LogLevel.debug);
if (value.isNotEmpty) {
if (index < otpControllers.length - 1) {
focusNodes[index + 1].requestFocus();
} else {
focusNodes[index].unfocus();
}
} else {
if (index > 0) {
focusNodes[index - 1].requestFocus();
}
}
}
Future<void> verifyOTP() async {
final enteredOTP = otpControllers.map((c) => c.text).join();
final result = await AuthService.verifyOtp(
email: email.value,
otp: enteredOTP,
);
if (result == null) {
// Handle remember-me like in LoginController
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) await LocalStorage.setToken('otp_email', email.value);
// Enable remote logging
enableRemoteLogging();
Get.offAllNamed('/select-tenant');
} else {
showAppSnackbar(
title: "Error",
message: result['error']!,
type: SnackbarType.error,
);
}
}
void _clearOTPFields() {
logSafe("[OTPController] Clearing OTP input fields", level: LogLevel.debug);
for (final controller in otpControllers) {
controller.clear();
}
focusNodes[0].requestFocus();
}
void _startTimer() {
logSafe("[OTPController] Starting resend timer");
timer.value = 60;
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (this.timer.value > 0) {
this.timer.value--;
} else {
timer.cancel();
}
});
}
void resetForChangeEmail() {
logSafe("[OTPController] Resetting OTP form for change email");
isOTPSent.value = false;
email.value = '';
emailController.clear();
_clearOTPFields();
timer.value = 0;
isSending.value = false;
isResending.value = false;
for (final node in focusNodes) {
node.unfocus();
}
// Optionally remove saved email
LocalStorage.removeToken('otp_email');
}
bool _validateEmail(String email) {
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,}$');
return regex.hasMatch(email);
}
/// Save email to local storage if "remember me" is set
Future<void> _saveEmailIfRemembered(String email) async {
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) {
await LocalStorage.setToken('otp_email', email);
}
}
/// Load email from local storage if "remember me" is true
Future<void> _loadSavedEmail() async {
final remember = LocalStorage.getBool('remember_me') ?? false;
if (remember) {
final savedEmail = LocalStorage.getToken('otp_email') ?? '';
emailController.text = savedEmail;
email.value = savedEmail;
logSafe(
"[OTPController] Loaded saved email from local storage: $savedEmail");
}
}
}

View File

@ -3,16 +3,17 @@ import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class RegisterAccountController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
@override
void onInit() {
logSafe("[RegisterAccountController] onInit called");
basicValidator.addField(
'email',
required: true,
@ -38,29 +39,40 @@ class RegisterAccountController extends MyController {
validators: [MyLengthValidator(min: 6, max: 10)],
controller: TextEditingController(),
);
super.onInit();
}
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
var errors = await AuthService.loginUser(basicValidator.getData());
final data = basicValidator.getData();
logSafe("[RegisterAccountController] Submitting registration data");
final errors = await AuthService.loginUser(data);
if (errors != null) {
logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
}
logSafe("[RegisterAccountController] Redirecting to /starter");
Get.toNamed('/starter');
update();
} else {
logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
}
}
void onChangeShowPassword() {
showPassword = !showPassword;
logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
update();
}
void gotoLogin() {
Get.toNamed('/auth/login');
logSafe("[RegisterAccountController] Navigating to /auth/login-option");
Get.toNamed('/auth/login-option');
}
}

View File

@ -4,56 +4,68 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ResetPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
bool confirmPassword = false;
@override
void onInit() {
super.onInit();
logSafe("[ResetPasswordController] onInit called");
basicValidator.addField(
'password',
required: true,
validators: [
MyLengthValidator(min: 6, max: 10),
],
validators: [MyLengthValidator(min: 6, max: 10)],
controller: TextEditingController(),
);
basicValidator.addField(
'confirm_password',
required: true,
label: "Confirm password",
validators: [
MyLengthValidator(min: 6, max: 10),
],
validators: [MyLengthValidator(min: 6, max: 10)],
controller: TextEditingController(),
);
}
Future<void> onResetPassword() async {
logSafe("[ResetPasswordController] onResetPassword triggered");
if (basicValidator.validateForm()) {
final data = basicValidator.getData();
logSafe("[ResetPasswordController] Reset password form data");
update();
var errors = await AuthService.loginUser(basicValidator.getData());
final errors = await AuthService.loginUser(data); // Consider renaming this to resetPassword() for clarity
if (errors != null) {
logSafe("[ResetPasswordController] Received errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
}
Get.toNamed('/home');
logSafe("[ResetPasswordController] Navigating to /dashboard");
Get.toNamed('/dashboard');
update();
} else {
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
}
}
void onChangeShowPassword() {
showPassword = !showPassword;
logSafe("[ResetPasswordController] showPassword toggled: $showPassword");
update();
}
void onConfirmPassword() {
confirmPassword = !confirmPassword;
logSafe("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
update();
}
}
}

View File

@ -1,126 +0,0 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
enum Gender {
male,
female,
other;
const Gender();
}
final Logger logger = Logger();
class AddEmployeeController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
Gender? selectedGender;
List<Map<String, dynamic>> roles = [];
String? selectedRoleId;
@override
void onInit() {
super.onInit();
logger.i("Initializing AddEmployeeController...");
fetchRoles();
basicValidator.addField(
'first_name',
label: "First Name",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'phone_number',
label: "Phone Number",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'last_name',
label: "Last Name",
required: true,
controller: TextEditingController(),
);
logger.i("Fields initialized for first_name, phone_number, last_name.");
}
bool showOnline = true;
final List<String> categories = [];
void onGenderSelected(Gender? gender) {
selectedGender = gender;
logger.i("Gender selected: ${gender?.name}");
update();
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
}
void onRoleSelected(String? roleId) {
selectedRoleId = roleId;
logger.i("Role selected: $roleId");
update();
}
Future<bool> createEmployees() async {
logger.i("Starting employee creation...");
if (selectedGender == null || selectedRoleId == null) {
logger.w("Missing gender or role.");
showAppSnackbar(
title: "Missing Fields",
message: "Please select both Gender and Role.",
type: SnackbarType.warning,
);
return false;
}
final firstName = basicValidator.getController("first_name")?.text.trim();
final lastName = basicValidator.getController("last_name")?.text.trim();
final phoneNumber =
basicValidator.getController("phone_number")?.text.trim();
logger.i(
"Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}");
final response = await ApiService.createEmployee(
firstName: firstName!,
lastName: lastName!,
phoneNumber: phoneNumber!,
gender: selectedGender!.name,
jobRoleId: selectedRoleId!,
);
if (response == true) {
logger.i("Employee created successfully.");
showAppSnackbar(
title: "Success",
message: "Employee created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to create employee.");
showAppSnackbar(
title: "Error",
message: "Failed to create employee.",
type: SnackbarType.error,
);
return false;
}
}
}

View File

@ -1,52 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/visitor_by_channels_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class AnalyticsController extends MyController {
String selectActivity = "Year";
List<VisitorByChannelsModel> visitorByChannel = [];
final TooltipBehavior columnChartToolTip = TooltipBehavior(enable: true, format: 'point.x : point.y', tooltipPosition: TooltipPosition.pointer);
final TooltipBehavior audienceOverview = TooltipBehavior(enable: true, format: 'point.x : point.y', tooltipPosition: TooltipPosition.pointer);
@override
void onInit() {
VisitorByChannelsModel.dummyList.then((value) {
visitorByChannel = value;
update();
});
super.onInit();
}
void onSelectedActivity(String time) {
selectActivity = time;
update();
}
void removeData(index) {
visitorByChannel.removeAt(index);
update();
}
final List<ChartSampleData> columnChart = <ChartSampleData>[
ChartSampleData(x: 2010, y: 32, yValue: 50),
ChartSampleData(x: 2011, y: 44, yValue: 40),
ChartSampleData(x: 2012, y: 40, yValue: 60),
ChartSampleData(x: 2013, y: 50, yValue: 38),
ChartSampleData(x: 2014, y: 10, yValue: 28),
ChartSampleData(x: 2015, y: 20, yValue: 16),
ChartSampleData(x: 2016, y: 30, yValue: 50),
];
final List<ChartSampleData> audienceOverviewChart = [
ChartSampleData(x: 2018, y: 50, yValue: 38),
ChartSampleData(x: 2019, y: 10, yValue: 28),
ChartSampleData(x: 2020, y: 32, yValue: 50),
ChartSampleData(x: 2020, y: 44, yValue: 40),
ChartSampleData(x: 2020, y: 40, yValue: 60),
ChartSampleData(x: 2020, y: 50, yValue: 38),
ChartSampleData(x: 2021, y: 10, yValue: 28),
ChartSampleData(x: 2022, y: 20, yValue: 16),
ChartSampleData(x: 2023, y: 30, yValue: 50)
];
}

View File

@ -1,380 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
import 'dart:io';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/model/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
final Logger log = Logger();
class AttendanceController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
String selectedTab = 'Employee List';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = true.obs; // initially true
RxBool isLoadingProjects = true.obs;
RxBool isLoadingEmployees = true.obs;
RxBool isLoadingAttendanceLogs = true.obs;
RxBool isLoadingRegularizationLogs = true.obs;
RxBool isLoadingLogView = true.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@override
void onInit() {
super.onInit();
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
fetchProjects(); // fetchProjects will set isLoading to false after loading
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1));
log.i("Default date range set: $startDateAttendance to $endDateAttendance");
}
Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
log.w('Location permissions are denied');
return false;
}
}
if (permission == LocationPermission.deniedForever) {
log.e('Location permissions are permanently denied');
return false;
}
return true;
}
Future<void> fetchProjects() async {
isLoadingProjects.value = true;
isLoading.value = true;
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
await fetchProjectData(selectedProjectId);
} else {
log.w("No project data found or API call failed.");
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['attendance_dashboard_controller']);
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
isLoading.value = false;
log.i("Project data fetched for project ID: $projectId");
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
isLoadingEmployees.value = true;
final response = await ApiService.getEmployeesByProject(projectId);
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i(
"Employees fetched: ${employees.length} employees for project $projectId");
update();
} else {
log.e("Failed to fetch employees for project $projectId");
}
isLoadingEmployees.value = false;
}
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
String projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true,
String? markTime,
}) async {
try {
uploadingStates[employeeId]?.value = true;
XFile? image;
if (imageCapture) {
image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) {
log.w("Image capture cancelled.");
uploadingStates[employeeId]?.value = false;
return false;
}
final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) {
log.e("Image compression failed.");
uploadingStates[employeeId]?.value = false;
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
}
final hasLocationPermission = await _handleLocationPermission();
if (!hasLocationPermission) {
uploadingStates[employeeId]?.value = false;
return false;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final result = await ApiService.uploadAttendanceImage(
id,
employeeId,
image,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
markTime: markTime,
);
log.i("Attendance uploaded for $employeeId, action: $action");
return result;
} catch (e, stacktrace) {
log.e("Error uploading attendance", error: e, stackTrace: stacktrace);
return false;
} finally {
uploadingStates[employeeId]?.value = false;
}
}
Future<void> selectDateRangeForAttendance(
BuildContext context,
AttendanceController controller,
) async {
final today = DateTime.now();
final todayDateOnly = DateTime(today.year, today.month, today.day);
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: todayDateOnly.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange(
start: startDateAttendance ??
DateTime.now().subtract(const Duration(days: 7)),
end: endDateAttendance ??
todayDateOnly.subtract(const Duration(days: 1)),
),
selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) {
final dayDateOnly = DateTime(day.year, day.month, day.day);
if (dayDateOnly == todayDateOnly) {
return false;
}
return true;
},
builder: (BuildContext context, Widget? child) {
return Center(
child: SizedBox(
width: 400,
height: 500,
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: const Color.fromARGB(255, 95, 132, 255),
onPrimary: Colors.white,
onSurface: Colors.teal.shade800,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.teal,
),
),
dialogTheme: DialogTheme(
backgroundColor: Colors.white,
),
),
child: child!,
),
),
);
},
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
log.i("Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
controller.selectedProjectId,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
);
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
log.i("Attendance logs fetched: ${attendanceLogs.length}");
update();
} else {
log.e("Failed to fetch attendance logs for project $projectId");
}
isLoadingAttendanceLogs.value = false;
isLoading.value = false;
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
final sortedMap =
Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
log.i("Logs grouped and sorted by check-in date.");
return sortedMap;
}
Future<void> fetchRegularizationLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
isLoading.value = true;
final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) {
regularizationLogs = response
.map((json) => RegularizationLogModel.fromJson(json))
.toList();
log.i("Regularization logs fetched: ${regularizationLogs.length}");
update();
} else {
log.e("Failed to fetch regularization logs for project $projectId");
}
isLoadingRegularizationLogs.value = false;
isLoading.value = false;
}
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView = response
.map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0;
return b.activityTime!.compareTo(a.activityTime!);
});
log.i("Attendance log view fetched for ID: $id");
update();
} else {
log.e("Failed to fetch attendance log view for ID $id");
}
isLoadingLogView.value = false;
isLoading.value = false;
}
}

View File

@ -1,35 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/lead_report_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class CrmController extends MyController {
List<ChartSampleData>? chartData;
List<LeadReportModel> leadReport = [];
TooltipBehavior? tooltipBehavior;
@override
void onInit() {
tooltipBehavior = TooltipBehavior(enable: true);
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 10, secondSeriesYValue: 5),
ChartSampleData(x: 'Feb', y: 12, secondSeriesYValue: 8),
ChartSampleData(x: 'Mar', y: 14, secondSeriesYValue: 9),
ChartSampleData(x: 'Apr', y: 11, secondSeriesYValue: 7),
ChartSampleData(x: 'May', y: 15, secondSeriesYValue: 10),
ChartSampleData(x: 'Jun', y: 9, secondSeriesYValue: 6),
ChartSampleData(x: 'Jul', y: 13, secondSeriesYValue: 7),
ChartSampleData(x: 'Aug', y: 12, secondSeriesYValue: 8),
ChartSampleData(x: 'Sep', y: 14, secondSeriesYValue: 10),
ChartSampleData(x: 'Oct', y: 15, secondSeriesYValue: 12),
ChartSampleData(x: 'Nov', y: 13, secondSeriesYValue: 9),
ChartSampleData(x: 'Dec', y: 11, secondSeriesYValue: 6),
];
LeadReportModel.dummyList.then((value) {
leadReport = value.sublist(0, 5);
update();
});
super.onInit();
}
}

View File

@ -1,43 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/coin_growth_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class CryptoController extends MyController {
List<ChartSampleData>? chartData;
DateTimeIntervalType intervalType = DateTimeIntervalType.months;
List<CoinGrowthModel> coinGrowth = [];
bool enableSolidCandle = false;
TrackballBehavior? trackballBehavior;
@override
void onInit() {
CoinGrowthModel.dummyList.then((value) {
coinGrowth = value.sublist(0, 5);
update();
});
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 50, secondSeriesYValue: 40, thirdSeriesYValue: 45),
ChartSampleData(x: 'Feb', y: 47, secondSeriesYValue: 39, thirdSeriesYValue: 48),
ChartSampleData(x: 'Mar', y: 55, secondSeriesYValue: 42, thirdSeriesYValue: 50),
ChartSampleData(x: 'Apr', y: 60, secondSeriesYValue: 45, thirdSeriesYValue: 53),
ChartSampleData(x: 'May', y: 70, secondSeriesYValue: 50, thirdSeriesYValue: 58),
ChartSampleData(x: 'Jun', y: 75, secondSeriesYValue: 55, thirdSeriesYValue: 62),
ChartSampleData(x: 'Jul', y: 80, secondSeriesYValue: 58, thirdSeriesYValue: 65),
ChartSampleData(x: 'Aug', y: 78, secondSeriesYValue: 60, thirdSeriesYValue: 66),
ChartSampleData(x: 'Sep', y: 72, secondSeriesYValue: 55, thirdSeriesYValue: 64),
ChartSampleData(x: 'Oct', y: 65, secondSeriesYValue: 50, thirdSeriesYValue: 57),
ChartSampleData(x: 'Nov', y: 58, secondSeriesYValue: 45, thirdSeriesYValue: 53),
ChartSampleData(x: 'Dec', y: 50, secondSeriesYValue: 40, thirdSeriesYValue: 48)
];
super.onInit();
}
void onSelectIntervalType(DateTimeIntervalType interval) {
intervalType = interval;
update();
}
}

View File

@ -1,127 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/daily_task_model.dart';
final Logger log = Logger();
class DailyTaskController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
DateTime? startDateTask;
DateTime? endDateTask;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
void toggleDate(String dateKey) {
if (expandedDates.contains(dateKey)) {
expandedDates.remove(dateKey);
} else {
expandedDates.add(dateKey);
}
}
RxBool isLoading = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {};
@override
void onInit() {
super.onInit();
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
fetchProjects();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateTask = today.subtract(const Duration(days: 7));
endDateTask = today;
log.i("Default date range set: $startDateTask to $endDateTask");
}
Future<void> fetchProjects() async {
isLoading.value = true;
final response = await ApiService.getProjects();
isLoading.value = false;
if (response?.isEmpty ?? true) {
log.w("No project data found or API call failed.");
return;
}
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
update();
await fetchTaskData(selectedProjectId);
}
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
final response = await ApiService.getDailyTasks(
projectId,
dateFrom: startDateTask,
dateTo: endDateTask,
);
isLoading.value = false;
if (response != null) {
groupedDailyTasks.clear();
for (var taskJson in response) {
TaskModel task = TaskModel.fromJson(taskJson);
String assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
if (groupedDailyTasks.containsKey(assignmentDateKey)) {
groupedDailyTasks[assignmentDateKey]?.add(task);
} else {
groupedDailyTasks[assignmentDateKey] = [task];
}
}
// Flatten the grouped tasks into the existing dailyTasks list
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
log.i("Daily tasks fetched and grouped: ${dailyTasks.length}");
update();
} else {
log.e("Failed to fetch daily tasks for project $projectId");
}
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
DailyTaskController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start:
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(),
),
);
if (picked == null) return;
startDateTask = picked.start;
endDateTask = picked.end;
log.i("Date range selected: $startDateTask to $endDateTask");
await controller.fetchTaskData(controller.selectedProjectId);
}
}

View File

@ -0,0 +1,263 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
class DashboardController extends GetxController {
// =========================
// Attendance overview
// =========================
final RxList<Map<String, dynamic>> roleWiseData =
<Map<String, dynamic>>[].obs;
final RxString attendanceSelectedRange = '15D'.obs;
final RxBool attendanceIsChartView = true.obs;
final RxBool isAttendanceLoading = false.obs;
// =========================
// Project progress overview
// =========================
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
final RxString projectSelectedRange = '15D'.obs;
final RxBool projectIsChartView = true.obs;
final RxBool isProjectLoading = false.obs;
// =========================
// Projects overview
// =========================
final RxInt totalProjects = 0.obs;
final RxInt ongoingProjects = 0.obs;
final RxBool isProjectsLoading = false.obs;
// =========================
// Tasks overview
// =========================
final RxInt totalTasks = 0.obs;
final RxInt completedTasks = 0.obs;
final RxBool isTasksLoading = false.obs;
// =========================
// Teams overview
// =========================
final RxInt totalEmployees = 0.obs;
final RxInt inToday = 0.obs;
final RxBool isTeamsLoading = false.obs;
// Common ranges
final List<String> ranges = ['7D', '15D', '30D'];
// Inside your DashboardController
final ProjectController projectController =
Get.put(ProjectController(), permanent: true);
@override
void onInit() {
super.onInit();
logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
);
fetchAllDashboardData();
// React to project change
ever<String>(projectController.selectedProjectId, (id) {
fetchAllDashboardData();
});
// React to range changes
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress());
}
// =========================
// Helper Methods
// =========================
int _getDaysFromRange(String range) {
switch (range) {
case '7D':
return 7;
case '15D':
return 15;
case '30D':
return 30;
case '3M':
return 90;
case '6M':
return 180;
default:
return 7;
}
}
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
void updateAttendanceRange(String range) {
attendanceSelectedRange.value = range;
logSafe('Attendance range updated to $range', level: LogLevel.debug);
}
void updateProjectRange(String range) {
projectSelectedRange.value = range;
logSafe('Project range updated to $range', level: LogLevel.debug);
}
void toggleAttendanceChartView(bool isChart) {
attendanceIsChartView.value = isChart;
logSafe('Attendance chart view toggled to: $isChart',
level: LogLevel.debug);
}
void toggleProjectChartView(bool isChart) {
projectIsChartView.value = isChart;
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
}
// =========================
// Manual Refresh Methods
// =========================
Future<void> refreshDashboard() async {
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchAllDashboardData();
}
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
Future<void> refreshTasks() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
}
Future<void> refreshProjects() async => fetchProjectProgress();
// =========================
// Fetch All Dashboard Data
// =========================
Future<void> fetchAllDashboardData() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) {
logSafe('No project selected. Skipping dashboard API calls.',
level: LogLevel.warning);
return;
}
await Future.wait([
fetchRoleWiseAttendance(),
fetchProjectProgress(),
fetchDashboardTasks(projectId: projectId),
fetchDashboardTeams(projectId: projectId),
]);
}
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isAttendanceLoading.value = true;
final List<dynamic>? response =
await ApiService.getDashboardAttendanceOverview(
projectId, getAttendanceDays());
if (response != null) {
roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else {
roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.',
level: LogLevel.error);
}
} catch (e, st) {
roleWiseData.clear();
logSafe('Error fetching attendance overview',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isAttendanceLoading.value = false;
}
}
Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isProjectLoading.value = true;
final response = await ApiService.getProjectProgress(
projectId: projectId, days: getProjectDays());
if (response != null && response.success) {
projectChartData.value =
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else {
projectChartData.clear();
logSafe('Failed to fetch project progress', level: LogLevel.error);
}
} catch (e, st) {
projectChartData.clear();
logSafe('Error fetching project progress',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isProjectLoading.value = false;
}
}
Future<void> fetchDashboardTasks({required String projectId}) async {
if (projectId.isEmpty) return;
try {
isTasksLoading.value = true;
final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response != null && response.success) {
totalTasks.value = response.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Failed to fetch tasks', level: LogLevel.error);
}
} catch (e, st) {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Error fetching tasks',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTasksLoading.value = false;
}
}
Future<void> fetchDashboardTeams({required String projectId}) async {
if (projectId.isEmpty) return;
try {
isTeamsLoading.value = true;
final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response != null && response.success) {
totalEmployees.value = response.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Failed to fetch teams', level: LogLevel.error);
}
} catch (e, st) {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Error fetching teams',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTeamsLoading.value = false;
}
}
}

View File

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/product_order_modal.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class EcommerceController extends MyController {
List<ChartSampleData>? salesAnalyticsData;
List<ProductOrderModal> order = [];
String selectedTimeByLocation = "Year";
@override
void onInit() {
ProductOrderModal.dummyList.then((value) {
order = value.sublist(0, 5);
update();
});
salesAnalyticsData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 43, secondSeriesYValue: 37, thirdSeriesYValue: 41),
ChartSampleData(x: 'Feb', y: 45, secondSeriesYValue: 37, thirdSeriesYValue: 45),
ChartSampleData(x: 'Mar', y: 50, secondSeriesYValue: 39, thirdSeriesYValue: 48),
ChartSampleData(x: 'Apr', y: 55, secondSeriesYValue: 43, thirdSeriesYValue: 52),
ChartSampleData(x: 'May', y: 63, secondSeriesYValue: 48, thirdSeriesYValue: 57),
ChartSampleData(x: 'Jun', y: 68, secondSeriesYValue: 54, thirdSeriesYValue: 61),
ChartSampleData(x: 'Jul', y: 72, secondSeriesYValue: 57, thirdSeriesYValue: 66),
ChartSampleData(x: 'Aug', y: 70, secondSeriesYValue: 57, thirdSeriesYValue: 66),
ChartSampleData(x: 'Sep', y: 66, secondSeriesYValue: 54, thirdSeriesYValue: 63),
ChartSampleData(x: 'Oct', y: 57, secondSeriesYValue: 48, thirdSeriesYValue: 55),
ChartSampleData(x: 'Nov', y: 50, secondSeriesYValue: 43, thirdSeriesYValue: 50),
ChartSampleData(x: 'Dec', y: 45, secondSeriesYValue: 37, thirdSeriesYValue: 45)
];
super.onInit();
}
final List<ChartSampleData> chartData = [
ChartSampleData(x: 'Jan', y: 10, yValue: 1000),
ChartSampleData(x: 'Fab', y: 20, yValue: 2000),
ChartSampleData(x: 'Mar', y: 15, yValue: 1500),
ChartSampleData(x: 'Jun', y: 5, yValue: 500),
ChartSampleData(x: 'Jul', y: 30, yValue: 3000),
ChartSampleData(x: 'Aug', y: 20, yValue: 2000),
ChartSampleData(x: 'Sep', y: 40, yValue: 4000),
ChartSampleData(x: 'Oct', y: 60, yValue: 6000),
ChartSampleData(x: 'Nov', y: 55, yValue: 5500),
ChartSampleData(x: 'Dec', y: 38, yValue: 3000),
];
final TooltipBehavior chart = TooltipBehavior(
enable: true,
format: 'point.x : point.yValue1 : point.yValue2',
);
final List<ChartSampleData> circleChart = [
ChartSampleData(x: 'David', y: 25, pointColor: const Color.fromRGBO(9, 0, 136, 1)),
ChartSampleData(x: 'Steve', y: 38, pointColor: const Color.fromRGBO(147, 0, 119, 1)),
ChartSampleData(x: 'Jack', y: 34, pointColor: const Color.fromRGBO(228, 0, 124, 1)),
ChartSampleData(x: 'Others', y: 52, pointColor: const Color.fromRGBO(255, 189, 57, 1))
];
void onSelectedTimeByLocation(String time) {
selectedTimeByLocation = time;
update();
}
@override
void dispose() {
salesAnalyticsData!.clear();
super.dispose();
}
}

View File

@ -1,42 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/job_recent_application_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class JobController extends MyController {
int isSelectedListingPerformanceTime = 0;
List<ChartSampleData>? chartData;
TooltipBehavior? columnToolTip;
List<JobRecentApplicationModel> recentApplication = [];
List<String> dummyTexts = List.generate(12, (index) => MyTextUtils.getDummyText(60));
@override
void onInit() {
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 4, secondSeriesYValue: 8),
ChartSampleData(x: 'Feb', y: 9, secondSeriesYValue: 7),
ChartSampleData(x: 'Mar', y: 6, secondSeriesYValue: 5),
ChartSampleData(x: 'Apr', y: 8, secondSeriesYValue: 3),
ChartSampleData(x: 'May', y: 7, secondSeriesYValue: 9),
ChartSampleData(x: 'Jun', y: 10, secondSeriesYValue: 6),
ChartSampleData(x: 'Jul', y: 5, secondSeriesYValue: 4),
ChartSampleData(x: 'Aug', y: 3, secondSeriesYValue: 2),
ChartSampleData(x: 'Sep', y: 6, secondSeriesYValue: 10),
ChartSampleData(x: 'Oct', y: 4, secondSeriesYValue: 8),
ChartSampleData(x: 'Nov', y: 9, secondSeriesYValue: 6),
ChartSampleData(x: 'Dec', y: 7, secondSeriesYValue: 5),
];
columnToolTip = TooltipBehavior(enable: true);
JobRecentApplicationModel.dummyList.then((value) {
recentApplication = value.sublist(0, 5);
update();
});
super.onInit();
}
void onSelectListingPerformanceTimeToggle(index) {
isSelectedListingPerformanceTime = index;
update();
}
}

View File

@ -1,46 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/project_summary_model.dart';
import 'package:marco/model/task_list_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class ProjectController extends MyController {
TooltipBehavior? tooltipBehavior;
List<TaskListModel> task = [];
List<ProjectSummaryModel> projectSummary = [];
List<ChartSampleData>? chartData;
@override
void onInit() {
TaskListModel.dummyList.then((value) {
task = value;
update();
});
ProjectSummaryModel.dummyList.then((value) {
projectSummary = value.sublist(0, 5);
update();
});
chartData = <ChartSampleData>[
ChartSampleData(x: 'Jan', y: 10, secondSeriesYValue: 8, thirdSeriesYValue: 12),
ChartSampleData(x: 'Feb', y: 5, secondSeriesYValue: 6, thirdSeriesYValue: 7),
ChartSampleData(x: 'Mar', y: 11, secondSeriesYValue: 9, thirdSeriesYValue: 6),
ChartSampleData(x: 'Apr', y: 14, secondSeriesYValue: 10, thirdSeriesYValue: 13),
ChartSampleData(x: 'May', y: 9, secondSeriesYValue: 7, thirdSeriesYValue: 5),
ChartSampleData(x: 'Jun', y: 8, secondSeriesYValue: 12, thirdSeriesYValue: 11),
ChartSampleData(x: 'Jul', y: 12, secondSeriesYValue: 11, thirdSeriesYValue: 9),
ChartSampleData(x: 'Aug', y: 7, secondSeriesYValue: 13, thirdSeriesYValue: 10),
ChartSampleData(x: 'Sep', y: 6, secondSeriesYValue: 5, thirdSeriesYValue: 8),
ChartSampleData(x: 'Oct', y: 4, secondSeriesYValue: 14, thirdSeriesYValue: 15),
ChartSampleData(x: 'Nov', y: 13, secondSeriesYValue: 4, thirdSeriesYValue: 11),
ChartSampleData(x: 'Dec', y: 15, secondSeriesYValue: 3, thirdSeriesYValue: 4)
];
tooltipBehavior = TooltipBehavior(enable: true, format: 'point.x : point.ym');
super.onInit();
}
void onSelectTask(TaskListModel task) {
task.isSelectTask = !task.isSelectTask;
update();
}
}

View File

@ -1,56 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/model/chart_model.dart';
import 'package:marco/model/recent_order_model.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class SalesController extends MyController {
List<ChartData>? statisticsData;
List<RecentOrderModel> recentOrder = [];
@override
void onInit() {
RecentOrderModel.dummyList.then((value) {
recentOrder = value;
update();
});
statisticsData = <ChartData>[
ChartData(2005, 15, 25),
ChartData(2006, 40, 55),
ChartData(2007, 50, 70),
ChartData(2008, 55, 80),
ChartData(2009, 65, 85),
ChartData(2010, 70, 95),
ChartData(2011, 90, 110)
];
super.onInit();
}
final List<ChartSampleData> visitorChartData = [
ChartSampleData(x: 'Jan', y: 12, yValue: 1200),
ChartSampleData(x: 'Feb', y: 18, yValue: 1800),
ChartSampleData(x: 'Mar', y: 22, yValue: 2200),
ChartSampleData(x: 'Apr', y: 10, yValue: 1000),
ChartSampleData(x: 'May', y: 25, yValue: 2500),
ChartSampleData(x: 'Jun', y: 35, yValue: 3500),
ChartSampleData(x: 'Jul', y: 28, yValue: 2800),
ChartSampleData(x: 'Aug', y: 45, yValue: 4500),
ChartSampleData(x: 'Sep', y: 50, yValue: 5000),
ChartSampleData(x: 'Oct', y: 60, yValue: 6000),
ChartSampleData(x: 'Nov', y: 42, yValue: 4200),
ChartSampleData(x: 'Dec', y: 55, yValue: 5500),
];
final TooltipBehavior visitorChart = TooltipBehavior(
enable: true,
format: 'point.x : point.yValue1 : point.yValue2',
);
}
class ChartData {
ChartData(this.x, this.y, this.y2);
final double x;
final double y;
final double y2;
}

View File

@ -0,0 +1,71 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart';
class AddCommentController extends GetxController {
final String contactId;
AddCommentController({required this.contactId});
final RxString note = ''.obs;
final RxBool isSubmitting = false.obs;
Future<void> submitComment() async {
if (note.value.trim().isEmpty) {
showAppSnackbar(
title: "Missing Comment",
message: "Please enter a comment before submitting.",
type: SnackbarType.warning,
);
return;
}
isSubmitting.value = true;
try {
logSafe("Submitting comment for contactId: $contactId");
final success = await ApiService.addContactComment(
note.value.trim(),
contactId,
);
if (success) {
logSafe("Comment added successfully.");
// Refresh UI
final directoryController = Get.find<DirectoryController>();
await directoryController.fetchCommentsForContact(contactId);
final notesController = Get.find<NotesController>();
await notesController.fetchNotes(
pageSize: 1000, pageNumber: 1); // Fixed here
Get.back(result: true);
showAppSnackbar(
title: "Comment Added",
message: "Your comment has been successfully added.",
type: SnackbarType.success,
);
}
} catch (e) {
logSafe("Error while submitting comment: $e", level: LogLevel.error);
showAppSnackbar(
title: "Unexpected Error",
message: "Something went wrong while adding your comment.",
type: SnackbarType.error,
);
} finally {
isSubmitting.value = false;
}
}
void updateNote(String value) {
note.value = value;
logSafe("Note updated: ${value.trim()}");
}
}

View File

@ -0,0 +1,316 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class AddContactController extends GetxController {
final RxList<String> categories = <String>[].obs;
final RxList<String> buckets = <String>[].obs;
final RxList<String> globalProjects = <String>[].obs;
final RxList<String> tags = <String>[].obs;
final RxString selectedCategory = ''.obs;
final RxList<String> selectedBuckets = <String>[].obs;
final RxString selectedProject = ''.obs;
final RxList<String> enteredTags = <String>[].obs;
final RxList<String> filteredSuggestions = <String>[].obs;
final RxList<String> organizationNames = <String>[].obs;
final RxList<String> filteredOrgSuggestions = <String>[].obs;
final RxMap<String, String> categoriesMap = <String, String>{}.obs;
final RxMap<String, String> bucketsMap = <String, String>{}.obs;
final RxMap<String, String> projectsMap = <String, String>{}.obs;
final RxMap<String, String> tagsMap = <String, String>{}.obs;
final RxBool isInitialized = false.obs;
final RxList<String> selectedProjects = <String>[].obs;
final RxBool isSubmitting = false.obs;
@override
void onInit() {
super.onInit();
logSafe("AddContactController initialized", level: LogLevel.debug);
fetchInitialData();
}
Future<void> fetchInitialData() async {
logSafe("Fetching initial dropdown data", level: LogLevel.debug);
await Future.wait([
fetchBuckets(),
fetchGlobalProjects(),
fetchTags(),
fetchCategories(),
fetchOrganizationNames(),
]);
// Mark initialization as done
isInitialized.value = true;
}
void resetForm() {
selectedCategory.value = '';
selectedProject.value = '';
selectedBuckets.clear();
enteredTags.clear();
filteredSuggestions.clear();
filteredOrgSuggestions.clear();
selectedProjects.clear();
}
Future<void> fetchBuckets() async {
try {
final response = await ApiService.getContactBucketList();
if (response != null && response['data'] is List) {
final names = <String>[];
for (var item in response['data']) {
if (item['name'] != null && item['id'] != null) {
bucketsMap[item['name']] = item['id'].toString();
names.add(item['name']);
}
}
buckets.assignAll(names);
logSafe("Fetched \${names.length} buckets");
}
} catch (e) {
logSafe("Failed to fetch buckets: \$e", level: LogLevel.error);
}
}
Future<void> fetchOrganizationNames() async {
try {
final orgs = await ApiService.getOrganizationList();
organizationNames.assignAll(orgs);
logSafe("Fetched \${orgs.length} organization names");
} catch (e) {
logSafe("Failed to load organization names: \$e", level: LogLevel.error);
}
}
Future<void> submitContact({
String? id,
required String name,
required String organization,
required List<Map<String, String>> emails,
required List<Map<String, String>> phones,
required String address,
required String description,
String? designation,
}) async {
if (isSubmitting.value) return;
isSubmitting.value = true;
final categoryId = categoriesMap[selectedCategory.value];
final bucketIds = selectedBuckets
.map((name) => bucketsMap[name])
.whereType<String>()
.toList();
if (bucketIds.isEmpty) {
showAppSnackbar(
title: "Missing Buckets",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
final projectIds = selectedProjects
.map((name) => projectsMap[name])
.whereType<String>()
.toList();
if (name.trim().isEmpty) {
showAppSnackbar(
title: "Missing Name",
message: "Please enter the contact name.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
if (organization.trim().isEmpty) {
showAppSnackbar(
title: "Missing Organization",
message: "Please enter the organization name.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
if (selectedBuckets.isEmpty) {
showAppSnackbar(
title: "Missing Bucket",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
try {
final tagObjects = enteredTags.map((tagName) {
final tagId = tagsMap[tagName];
return tagId != null
? {"id": tagId, "name": tagName}
: {"name": tagName};
}).toList();
final body = {
if (id != null) "id": id,
"name": name.trim(),
"organization": organization.trim(),
if (selectedCategory.value.isNotEmpty && categoryId != null)
"contactCategoryId": categoryId,
if (projectIds.isNotEmpty) "projectIds": projectIds,
"bucketIds": bucketIds,
if (enteredTags.isNotEmpty) "tags": tagObjects,
if (emails.isNotEmpty) "contactEmails": emails,
if (phones.isNotEmpty) "contactPhones": phones,
if (address.trim().isNotEmpty) "address": address.trim(),
if (description.trim().isNotEmpty) "description": description.trim(),
if (designation != null && designation.trim().isNotEmpty)
"designation": designation.trim(),
};
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
final response = id != null
? await ApiService.updateContact(id, body)
: await ApiService.createContact(body);
if (response == true) {
Get.back(result: true);
showAppSnackbar(
title: "Success",
message: id != null
? "Contact updated successfully"
: "Contact created successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to ${id != null ? 'update' : 'create'} contact",
type: SnackbarType.error,
);
}
} catch (e) {
logSafe("Submit contact error: $e", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Something went wrong",
type: SnackbarType.error,
);
} finally {
isSubmitting.value = false;
}
}
void filterOrganizationSuggestions(String query) {
if (query.trim().isEmpty) {
filteredOrgSuggestions.clear();
return;
}
final lower = query.toLowerCase();
filteredOrgSuggestions.assignAll(
organizationNames
.where((name) => name.toLowerCase().contains(lower))
.toList(),
);
logSafe("Filtered organization suggestions for: \$query",
level: LogLevel.debug);
}
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
logSafe("Fetched \${names.length} global projects");
}
} catch (e) {
logSafe("Failed to fetch global projects: \$e", level: LogLevel.error);
}
}
Future<void> fetchTags() async {
try {
final response = await ApiService.getContactTagList();
if (response != null && response['data'] is List) {
tags.assignAll(List<String>.from(
response['data'].map((e) => e['name'] ?? '').where((e) => e != ''),
));
logSafe("Fetched \${tags.length} tags");
}
} catch (e) {
logSafe("Failed to fetch tags: \$e", level: LogLevel.error);
}
}
void filterSuggestions(String query) {
if (query.trim().isEmpty) {
filteredSuggestions.clear();
return;
}
final lower = query.toLowerCase();
filteredSuggestions.assignAll(
tags
.where((tag) =>
tag.toLowerCase().contains(lower) && !enteredTags.contains(tag))
.toList(),
);
logSafe("Filtered tag suggestions for: \$query", level: LogLevel.debug);
}
void clearSuggestions() {
filteredSuggestions.clear();
logSafe("Cleared tag suggestions", level: LogLevel.debug);
}
Future<void> fetchCategories() async {
try {
final response = await ApiService.getContactCategoryList();
if (response != null && response['data'] is List) {
final names = <String>[];
for (var item in response['data']) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
categoriesMap[name] = id;
names.add(name);
}
}
categories.assignAll(names);
logSafe("Fetched \${names.length} contact categories");
}
} catch (e) {
logSafe("Failed to fetch categories: \$e", level: LogLevel.error);
}
}
void addEnteredTag(String tag) {
if (tag.trim().isNotEmpty && !enteredTags.contains(tag.trim())) {
enteredTags.add(tag.trim());
logSafe("Added tag: \$tag", level: LogLevel.debug);
}
}
void removeEnteredTag(String tag) {
enteredTags.remove(tag);
logSafe("Removed tag: \$tag", level: LogLevel.debug);
}
}

View File

@ -0,0 +1,70 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class BucketController extends GetxController {
RxBool isCreating = false.obs;
final RxString name = ''.obs;
final RxString description = ''.obs;
Future<void> createBucket() async {
if (name.value.trim().isEmpty) {
showAppSnackbar(
title: "Missing Name",
message: "Bucket name is required.",
type: SnackbarType.warning,
);
return;
}
isCreating.value = true;
try {
logSafe("Creating bucket: ${name.value}");
final success = await ApiService.createBucket(
name: name.value.trim(),
description: description.value.trim(),
);
if (success) {
logSafe("Bucket created successfully");
Get.back(result: true); // Close bottom sheet/dialog
showAppSnackbar(
title: "Success",
message: "Bucket has been created successfully.",
type: SnackbarType.success,
);
} else {
logSafe("Bucket creation failed", level: LogLevel.error);
showAppSnackbar(
title: "Creation Failed",
message: "Unable to create bucket. Please try again later.",
type: SnackbarType.error,
);
}
} catch (e) {
logSafe("Error during bucket creation: $e", level: LogLevel.error);
showAppSnackbar(
title: "Unexpected Error",
message: "Something went wrong. Please try again.",
type: SnackbarType.error,
);
} finally {
isCreating.value = false;
}
}
void updateName(String value) {
name.value = value;
logSafe("Bucket name updated: ${value.trim()}");
}
void updateDescription(String value) {
description.value = value;
logSafe("Bucket description updated: ${value.trim()}");
}
}

View File

@ -0,0 +1,390 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/contact_model.dart';
import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:marco/model/directory/directory_comment_model.dart';
class DirectoryController extends GetxController {
// -------------------- CONTACTS --------------------
RxList<ContactModel> allContacts = <ContactModel>[].obs;
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
RxList<String> selectedCategories = <String>[].obs;
RxList<String> selectedBuckets = <String>[].obs;
RxBool isActive = true.obs;
RxBool isLoading = false.obs;
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
RxString searchQuery = ''.obs;
// -------------------- COMMENTS --------------------
final Map<String, RxList<DirectoryComment>> activeCommentsMap = {};
final Map<String, RxList<DirectoryComment>> inactiveCommentsMap = {};
final editingCommentId = Rxn<String>();
@override
void onInit() {
super.onInit();
fetchContacts();
fetchBuckets();
}
// -------------------- COMMENTS HANDLING --------------------
RxList<DirectoryComment> getCommentsForContact(String contactId,
{bool active = true}) {
return active
? activeCommentsMap[contactId] ?? <DirectoryComment>[].obs
: inactiveCommentsMap[contactId] ?? <DirectoryComment>[].obs;
}
Future<void> fetchCommentsForContact(String contactId,
{bool active = true}) async {
try {
final data =
await ApiService.getDirectoryComments(contactId, active: active);
var comments =
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
// Deduplicate by ID before storing
final Map<String, DirectoryComment> uniqueMap = {
for (var c in comments) c.id: c,
};
comments = uniqueMap.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
}
} catch (e, stack) {
logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e",
level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs;
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs;
}
}
}
List<DirectoryComment> combinedComments(String contactId) {
final activeList = getCommentsForContact(contactId, active: true);
final inactiveList = getCommentsForContact(contactId, active: false);
// Deduplicate by ID (active wins)
final Map<String, DirectoryComment> byId = {};
for (final c in inactiveList) {
byId[c.id] = c;
}
for (final c in activeList) {
byId[c.id] = c;
}
final combined = byId.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return combined;
}
Future<void> updateComment(DirectoryComment comment) async {
try {
final existing = getCommentsForContact(comment.contactId)
.firstWhereOrNull((c) => c.id == comment.id);
if (existing != null && existing.note.trim() == comment.note.trim()) {
showAppSnackbar(
title: "No Changes",
message: "No changes were made to the comment.",
type: SnackbarType.info,
);
return;
}
final success = await ApiService.updateContactComment(
comment.id, comment.note, comment.contactId);
if (success) {
await fetchCommentsForContact(comment.contactId, active: true);
await fetchCommentsForContact(comment.contactId, active: false);
showAppSnackbar(
title: "Success",
message: "Comment updated successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Update comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Failed to update comment.",
type: SnackbarType.error,
);
}
}
Future<void> deleteComment(String commentId, String contactId) async {
try {
final success = await ApiService.restoreContactComment(commentId, false);
if (success) {
if (editingCommentId.value == commentId) editingCommentId.value = null;
await fetchCommentsForContact(contactId, active: true);
await fetchCommentsForContact(contactId, active: false);
showAppSnackbar(
title: "Deleted",
message: "Comment deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to delete comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Delete comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting comment.",
type: SnackbarType.error,
);
}
}
Future<void> restoreComment(String commentId, String contactId) async {
try {
final success = await ApiService.restoreContactComment(commentId, true);
if (success) {
await fetchCommentsForContact(contactId, active: true);
await fetchCommentsForContact(contactId, active: false);
showAppSnackbar(
title: "Restored",
message: "Comment restored successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to restore comment.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Restore comment failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while restoring comment.",
type: SnackbarType.error,
);
}
}
// -------------------- CONTACTS HANDLING --------------------
Future<void> fetchBuckets() async {
try {
final response = await ApiService.getContactBucketList();
if (response != null && response['data'] is List) {
final buckets = (response['data'] as List)
.map((e) => ContactBucket.fromJson(e))
.toList();
contactBuckets.assignAll(buckets);
} else {
contactBuckets.clear();
}
} catch (e) {
logSafe("Bucket fetch error: $e", level: LogLevel.error);
}
}
// -------------------- CONTACT DELETION / RESTORE --------------------
Future<void> deleteContact(String contactId) async {
try {
final success = await ApiService.deleteDirectoryContact(contactId);
if (success) {
// Refresh contacts after deletion
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Deleted",
message: "Contact deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to delete contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Delete contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting contact.",
type: SnackbarType.error,
);
}
}
Future<void> restoreContact(String contactId) async {
try {
final success = await ApiService.restoreDirectoryContact(contactId);
if (success) {
// Refresh contacts after restore
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Restored",
message: "Contact restored successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to restore contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Restore contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while restoring contact.",
type: SnackbarType.error,
);
}
}
Future<void> fetchContacts({bool active = true}) async {
try {
isLoading.value = true;
final response = await ApiService.getDirectoryData(isActive: active);
if (response != null) {
final contacts = response.map((e) => ContactModel.fromJson(e)).toList();
allContacts.assignAll(contacts);
extractCategoriesFromContacts();
applyFilters();
} else {
allContacts.clear();
filteredContacts.clear();
}
} catch (e) {
logSafe("Directory fetch error: $e", level: LogLevel.error);
} finally {
isLoading.value = false;
}
}
void extractCategoriesFromContacts() {
final uniqueCategories = <String, ContactCategory>{};
for (final contact in allContacts) {
final category = contact.contactCategory;
if (category != null) {
uniqueCategories.putIfAbsent(category.id, () => category);
}
}
contactCategories.value = uniqueCategories.values.toList();
}
void applyFilters() {
final query = searchQuery.value.toLowerCase();
filteredContacts.value = allContacts.where((contact) {
final categoryMatch = selectedCategories.isEmpty ||
(contact.contactCategory != null &&
selectedCategories.contains(contact.contactCategory!.id));
final bucketMatch = selectedBuckets.isEmpty ||
contact.bucketIds.any((id) => selectedBuckets.contains(id));
final nameMatch = contact.name.toLowerCase().contains(query);
final orgMatch = contact.organization.toLowerCase().contains(query);
final emailMatch = contact.contactEmails
.any((e) => e.emailAddress.toLowerCase().contains(query));
final phoneMatch = contact.contactPhones
.any((p) => p.phoneNumber.toLowerCase().contains(query));
final tagMatch =
contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
final categoryNameMatch =
contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
final bucketNameMatch = contact.bucketIds.any((id) {
final bucketName = contactBuckets
.firstWhereOrNull((b) => b.id == id)
?.name
.toLowerCase() ??
'';
return bucketName.contains(query);
});
final searchMatch = query.isEmpty ||
nameMatch ||
orgMatch ||
emailMatch ||
phoneMatch ||
tagMatch ||
categoryNameMatch ||
bucketNameMatch;
return categoryMatch && bucketMatch && searchMatch;
}).toList();
filteredContacts
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
}
void toggleCategory(String categoryId) {
if (selectedCategories.contains(categoryId)) {
selectedCategories.remove(categoryId);
} else {
selectedCategories.add(categoryId);
}
}
void toggleBucket(String bucketId) {
if (selectedBuckets.contains(bucketId)) {
selectedBuckets.remove(bucketId);
} else {
selectedBuckets.add(bucketId);
}
}
void updateSearchQuery(String value) {
searchQuery.value = value;
applyFilters();
}
String getBucketNames(ContactModel contact, List<ContactBucket> allBuckets) {
return contact.bucketIds
.map((id) => allBuckets.firstWhereOrNull((b) => b.id == id)?.name ?? '')
.where((name) => name.isNotEmpty)
.join(', ');
}
bool hasActiveFilters() {
return selectedCategories.isNotEmpty ||
selectedBuckets.isNotEmpty ||
searchQuery.value.trim().isNotEmpty;
}
}

View File

@ -0,0 +1,152 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
class ManageBucketController extends GetxController {
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
final DirectoryController directoryController = Get.find();
@override
void onInit() {
super.onInit();
fetchAllEmployees();
}
Future<bool> updateBucket({
required String id,
required String name,
required String description,
required List<String> employeeIds,
required List<String> originalEmployeeIds,
}) async {
isLoading(true);
update();
try {
final updated = await ApiService.updateBucket(
id: id,
name: name,
description: description,
);
if (!updated) {
showAppSnackbar(
title: "Update Failed",
message: "Unable to update bucket details.",
type: SnackbarType.error,
);
isLoading(false);
update();
return false;
}
final allInvolvedIds = {...originalEmployeeIds, ...employeeIds}.toList();
final assignPayload = allInvolvedIds.map((empId) {
return {
"employeeId": empId,
"isActive": employeeIds.contains(empId),
};
}).toList();
final assigned = await ApiService.assignEmployeesToBucket(
bucketId: id,
employees: assignPayload,
);
if (!assigned) {
showAppSnackbar(
title: "Assignment Failed",
message: "Employees couldn't be updated.",
type: SnackbarType.warning,
);
} else {
showAppSnackbar(
title: "Success",
message: "Bucket updated successfully.",
type: SnackbarType.success,
);
}
return true;
} catch (e, stack) {
logSafe("Error in updateBucket: $e", level: LogLevel.error);
logSafe("Stack: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Unexpected Error",
message: "Please try again later.",
type: SnackbarType.error,
);
return false;
} finally {
isLoading(false);
update();
}
}
Future<void> fetchAllEmployees() async {
isLoading.value = true;
try {
final response = await ApiService.getAllEmployees();
if (response != null && response.isNotEmpty) {
allEmployees.assignAll(
response.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
level: LogLevel.info,
);
} else {
allEmployees.clear();
logSafe("No employees found for Manage Bucket.",
level: LogLevel.warning);
}
} catch (e) {
allEmployees.clear();
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
Future<void> deleteBucket(String bucketId) async {
isLoading.value = true;
update();
try {
final deleted = await ApiService.deleteBucket(bucketId);
if (deleted) {
directoryController.contactBuckets.removeWhere((b) => b.id == bucketId);
showAppSnackbar(
title: "Deleted",
message: "Bucket deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Delete Failed",
message: "Unable to delete bucket.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Error deleting bucket: $e", level: LogLevel.error);
logSafe("Stack: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Unexpected Error",
message: "Failed to delete bucket.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
update();
}
}
}

View File

@ -0,0 +1,169 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/note_list_response_model.dart';
class NotesController extends GetxController {
RxList<NoteModel> notesList = <NoteModel>[].obs;
RxBool isLoading = false.obs;
RxnString editingNoteId = RxnString();
RxString searchQuery = ''.obs;
List<NoteModel> get filteredNotesList {
if (searchQuery.isEmpty) return notesList;
final query = searchQuery.value.toLowerCase();
return notesList.where((note) {
return note.note.toLowerCase().contains(query) ||
note.contactName.toLowerCase().contains(query) ||
note.organizationName.toLowerCase().contains(query) ||
note.createdBy.firstName.toLowerCase().contains(query);
}).toList();
}
@override
void onInit() {
super.onInit();
fetchNotes();
}
Future<void> fetchNotes({int pageSize = 1000, int pageNumber = 1}) async {
isLoading.value = true;
logSafe(
"📤 Fetching directory notes with pageSize=$pageSize & pageNumber=$pageNumber");
try {
final response = await ApiService.getDirectoryNotes(
pageSize: pageSize, pageNumber: pageNumber);
logSafe("💡 Directory Notes Response: $response");
if (response == null) {
logSafe("⚠️ Response is null while fetching directory notes");
notesList.clear();
} else {
logSafe("💡 Directory Notes Response: $response");
notesList.value = NotePaginationData.fromJson(response).data;
}
} catch (e, st) {
logSafe("💥 Error occurred while fetching directory notes",
error: e, stackTrace: st);
notesList.clear();
} finally {
isLoading.value = false;
}
}
Future<void> updateNote(NoteModel updatedNote) async {
try {
logSafe(
"Attempting to update note. id: ${updatedNote.id}, contactId: ${updatedNote.contactId}");
final oldNote = notesList.firstWhereOrNull((n) => n.id == updatedNote.id);
if (oldNote != null && oldNote.note.trim() == updatedNote.note.trim()) {
logSafe("No changes detected in note. id: ${updatedNote.id}");
showAppSnackbar(
title: "No Changes",
message: "No changes were made to the note.",
type: SnackbarType.info,
);
return;
}
final success = await ApiService.updateContactComment(
updatedNote.id,
updatedNote.note,
updatedNote.contactId,
);
if (success) {
logSafe("Note updated successfully. id: ${updatedNote.id}");
final index = notesList.indexWhere((n) => n.id == updatedNote.id);
if (index != -1) {
notesList[index] = updatedNote;
notesList.refresh();
}
showAppSnackbar(
title: "Success",
message: "Note updated successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update note.",
type: SnackbarType.error,
);
}
} catch (e, stackTrace) {
logSafe("Update note failed: ${e.toString()}");
logSafe("StackTrace: ${stackTrace.toString()}");
showAppSnackbar(
title: "Error",
message: "Failed to update note.",
type: SnackbarType.error,
);
}
}
Future<void> restoreOrDeleteNote(NoteModel note,
{bool restore = true}) async {
final action = restore ? "restore" : "delete";
try {
logSafe("Attempting to $action note id: ${note.id}");
final success = await ApiService.restoreContactComment(
note.id,
restore, // true = restore, false = delete
);
if (success) {
final index = notesList.indexWhere((n) => n.id == note.id);
if (index != -1) {
notesList[index] = note.copyWith(isActive: restore);
notesList.refresh();
}
showAppSnackbar(
title: restore ? "Restored" : "Deleted",
message: restore
? "Note has been restored successfully."
: "Note has been deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message:
restore ? "Failed to restore note." : "Failed to delete note.",
type: SnackbarType.error,
);
}
} catch (e, st) {
logSafe("$action note failed: $e", error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Something went wrong while trying to $action the note.",
type: SnackbarType.error,
);
}
}
void addNote(NoteModel note) {
notesList.insert(0, note);
logSafe("Note added to list");
}
void deleteNote(int index) {
if (index >= 0 && index < notesList.length) {
notesList.removeAt(index);
logSafe("Note removed from list at index $index");
}
}
void clearAllNotes() {
notesList.clear();
logSafe("All notes cleared from list");
}
}

View File

@ -0,0 +1,82 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/document_details_model.dart';
import 'package:marco/model/document/document_version_model.dart';
class DocumentDetailsController extends GetxController {
/// Observables
var isLoading = false.obs;
var documentDetails = Rxn<DocumentDetailsResponse>();
var versions = <DocumentVersionItem>[].obs;
var isVersionsLoading = false.obs;
// Loading states for buttons
var isVerifyLoading = false.obs;
var isRejectLoading = false.obs;
/// Fetch document details by id
Future<void> fetchDocumentDetails(String documentId) async {
try {
isLoading.value = true;
final response = await ApiService.getDocumentDetailsApi(documentId);
documentDetails.value = response;
} finally {
isLoading.value = false;
}
}
/// Fetch document versions by parentAttachmentId
Future<void> fetchDocumentVersions(String parentAttachmentId) async {
try {
isVersionsLoading.value = true;
final response = await ApiService.getDocumentVersionsApi(
parentAttachmentId: parentAttachmentId,
);
if (response != null) {
versions.assignAll(response.data.data);
} else {
versions.clear();
}
} finally {
isVersionsLoading.value = false;
}
}
/// Verify document
Future<bool> verifyDocument(String documentId) async {
try {
isVerifyLoading.value = true;
final result =
await ApiService.verifyDocumentApi(id: documentId, isVerify: true);
if (result) await fetchDocumentDetails(documentId);
return result;
} finally {
isVerifyLoading.value = false;
}
}
/// Reject document
Future<bool> rejectDocument(String documentId) async {
try {
isRejectLoading.value = true;
final result =
await ApiService.verifyDocumentApi(id: documentId, isVerify: false);
if (result) await fetchDocumentDetails(documentId);
return result;
} finally {
isRejectLoading.value = false;
}
}
/// Fetch Pre-Signed URL for a given version
Future<String?> fetchPresignedUrl(String versionId) async {
return await ApiService.getPresignedUrlApi(versionId);
}
/// Clear data when leaving the screen
void clearDetails() {
documentDetails.value = null;
versions.clear();
}
}

View File

@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/master_document_type_model.dart';
import 'package:marco/model/document/master_document_tags.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class DocumentUploadController extends GetxController {
// Observables
var isLoading = false.obs;
var isUploading = false.obs;
var categories = <DocumentType>[].obs;
var tags = <TagItem>[].obs;
DocumentType? selectedCategory;
/// --- FILE HANDLING ---
String? selectedFileName;
String? selectedFileBase64;
String? selectedFileContentType;
int? selectedFileSize;
/// --- TAG HANDLING ---
final tagCtrl = TextEditingController();
final enteredTags = <String>[].obs;
final filteredSuggestions = <String>[].obs;
var documentTypes = <DocumentType>[].obs;
DocumentType? selectedType;
@override
void onInit() {
super.onInit();
fetchCategories();
fetchTags();
}
/// Fetch available document categories
Future<void> fetchCategories() async {
try {
isLoading.value = true;
final response = await ApiService.getMasterDocumentTypesApi();
if (response != null && response.data.isNotEmpty) {
categories.assignAll(response.data);
logSafe("Fetched categories: ${categories.length}");
} else {
logSafe("No categories fetched", level: LogLevel.warning);
}
} finally {
isLoading.value = false;
}
}
Future<void> fetchDocumentTypes(String categoryId) async {
try {
isLoading.value = true;
final response =
await ApiService.getDocumentTypesByCategoryApi(categoryId);
if (response != null && response.data.isNotEmpty) {
documentTypes.assignAll(response.data);
selectedType = null; // reset previous type
} else {
documentTypes.clear();
selectedType = null;
}
} finally {
isLoading.value = false;
}
}
Future<String?> fetchPresignedUrl(String versionId) async {
return await ApiService.getPresignedUrlApi(versionId);
}
/// Fetch available document tags
Future<void> fetchTags() async {
try {
isLoading.value = true;
final response = await ApiService.getMasterDocumentTagsApi();
if (response != null) {
tags.assignAll(response.data);
logSafe("Fetched tags: ${tags.length}");
} else {
logSafe("No tags fetched", level: LogLevel.warning);
}
} finally {
isLoading.value = false;
}
}
/// --- TAG LOGIC ---
void filterSuggestions(String query) {
if (query.isEmpty) {
filteredSuggestions.clear();
return;
}
filteredSuggestions.assignAll(
tags.map((t) => t.name).where(
(tag) => tag.toLowerCase().contains(query.toLowerCase()),
),
);
}
void addEnteredTag(String tag) {
if (tag.trim().isEmpty) return;
if (!enteredTags.contains(tag.trim())) {
enteredTags.add(tag.trim());
}
}
void removeEnteredTag(String tag) {
enteredTags.remove(tag);
}
void clearSuggestions() {
filteredSuggestions.clear();
}
/// Upload document
Future<bool> uploadDocument({
required String documentId,
required String name,
required String entityId,
required String documentTypeId,
required String fileName,
required String base64Data,
required String contentType,
required int fileSize,
String? description,
}) async {
try {
isUploading.value = true;
final payloadTags =
enteredTags.map((t) => {"name": t, "isActive": true}).toList();
final payload = {
"documentId": documentId,
"name": name,
"description": description,
"entityId": entityId,
"documentTypeId": documentTypeId,
"fileName": fileName,
"base64Data":
base64Data.isNotEmpty ? "<base64-string-truncated>" : null,
"contentType": contentType,
"fileSize": fileSize,
"tags": payloadTags,
};
// Log the payload (hide long base64 string for readability)
logSafe("Upload payload: $payload");
final success = await ApiService.uploadDocumentApi(
documentId: documentId,
name: name,
description: description,
entityId: entityId,
documentTypeId: documentTypeId,
fileName: fileName,
base64Data: base64Data,
contentType: contentType,
fileSize: fileSize,
tags: payloadTags,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Document uploaded successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Could not upload document",
type: SnackbarType.error,
);
}
return success;
} catch (e, stack) {
logSafe("Upload error: $e", level: LogLevel.error);
logSafe("Stacktrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
return false;
} finally {
isUploading.value = false;
}
}
Future<bool> editDocument(Map<String, dynamic> payload) async {
try {
isUploading.value = true;
final attachment = payload["attachment"];
final success = await ApiService.editDocumentApi(
id: payload["id"],
name: payload["name"],
documentId: payload["documentId"],
description: payload["description"],
tags: (payload["tags"] as List).cast<Map<String, dynamic>>(),
attachment: attachment,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Document updated successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to update document",
type: SnackbarType.error,
);
}
return success;
} catch (e, stack) {
logSafe("Edit error: $e", level: LogLevel.error);
logSafe("Stacktrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
return false;
} finally {
isUploading.value = false;
}
}
}

View File

@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/document/document_filter_model.dart';
import 'package:marco/model/document/documents_list_model.dart';
class DocumentController extends GetxController {
// ------------------ Observables ---------------------
var isLoading = false.obs;
var documents = <DocumentItem>[].obs;
var filters = Rxn<DocumentFiltersData>();
// Selected filters (multi-select support)
var selectedUploadedBy = <String>[].obs;
var selectedCategory = <String>[].obs;
var selectedType = <String>[].obs;
var selectedTag = <String>[].obs;
// Pagination state
var pageNumber = 1.obs;
final int pageSize = 20;
var hasMore = true.obs;
// Error message
var errorMessage = "".obs;
// NEW: show inactive toggle
var showInactive = false.obs;
// NEW: search
var searchQuery = ''.obs;
var searchController = TextEditingController();
// New filter fields
var isUploadedAt = true.obs;
var isVerified = RxnBool();
var startDate = Rxn<String>();
var endDate = Rxn<String>();
// ------------------ API Calls -----------------------
/// Fetch Document Filters for an Entity
Future<void> fetchFilters(String entityTypeId) async {
try {
isLoading.value = true;
final response = await ApiService.getDocumentFilters(entityTypeId);
if (response != null && response.success) {
filters.value = response.data;
} else {
errorMessage.value = response?.message ?? "Failed to fetch filters";
}
} catch (e) {
errorMessage.value = "Error fetching filters: $e";
} finally {
isLoading.value = false;
}
}
/// Toggle document active/inactive state
Future<bool> toggleDocumentActive(
String id, {
required bool isActive,
required String entityTypeId,
required String entityId,
}) async {
try {
isLoading.value = true;
final success =
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
if (success) {
// 🔥 Always fetch fresh list after toggle
await fetchDocuments(
entityTypeId: entityTypeId,
entityId: entityId,
reset: true,
);
return true;
} else {
errorMessage.value = "Failed to update document state";
return false;
}
} catch (e) {
errorMessage.value = "Error updating document: $e";
return false;
} finally {
isLoading.value = false;
}
}
/// Permanently delete a document (or deactivate depending on API)
Future<bool> deleteDocument(String id, {bool isActive = false}) async {
try {
isLoading.value = true;
final success =
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
if (success) {
// remove from local list immediately for better UX
documents.removeWhere((doc) => doc.id == id);
return true;
} else {
errorMessage.value = "Failed to delete document";
return false;
}
} catch (e) {
errorMessage.value = "Error deleting document: $e";
return false;
} finally {
isLoading.value = false;
}
}
/// Fetch Documents for an entity
Future<void> fetchDocuments({
required String entityTypeId,
required String entityId,
String? filter,
String? searchString,
bool reset = false,
}) async {
try {
if (reset) {
pageNumber.value = 1;
documents.clear();
hasMore.value = true;
}
if (!hasMore.value) return;
isLoading.value = true;
final response = await ApiService.getDocumentListApi(
entityTypeId: entityTypeId,
entityId: entityId,
filter: filter ?? "",
searchString: searchString ?? searchQuery.value,
pageNumber: pageNumber.value,
pageSize: pageSize,
isActive: !showInactive.value,
);
if (response != null && response.success) {
if (response.data.data.isNotEmpty) {
documents.addAll(response.data.data);
pageNumber.value++;
} else {
hasMore.value = false;
}
} else {
errorMessage.value = response?.message ?? "Failed to fetch documents";
}
} catch (e) {
errorMessage.value = "Error fetching documents: $e";
} finally {
isLoading.value = false;
}
}
// ------------------ Helpers -----------------------
/// Clear selected filters
void clearFilters() {
selectedUploadedBy.clear();
selectedCategory.clear();
selectedType.clear();
selectedTag.clear();
isUploadedAt.value = true;
isVerified.value = null;
startDate.value = null;
endDate.value = null;
}
/// Check if any filters are active (for red dot indicator)
bool hasActiveFilters() {
return selectedUploadedBy.isNotEmpty ||
selectedCategory.isNotEmpty ||
selectedType.isNotEmpty ||
selectedTag.isNotEmpty;
}
}

View File

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class DynamicMenuController extends GetxController {
// UI reactive states
final RxBool isLoading = false.obs;
final RxBool hasError = false.obs;
final RxString errorMessage = ''.obs;
final RxList<MenuItem> menuItems = <MenuItem>[].obs;
@override
void onInit() {
super.onInit();
// Fetch menus directly from API at startup
fetchMenu();
}
/// Fetch dynamic menu from API (no local cache)
Future<void> fetchMenu() async {
isLoading.value = true;
hasError.value = false;
errorMessage.value = '';
try {
final responseData = await ApiService.getMenuApi();
if (responseData != null) {
final menuResponse = MenuResponse.fromJson(responseData);
menuItems.assignAll(menuResponse.data);
logSafe("✅ Menu loaded from API with ${menuItems.length} items");
} else {
_handleApiFailure("Menu API returned null response");
}
} catch (e) {
_handleApiFailure("Menu fetch exception: $e");
} finally {
isLoading.value = false;
}
}
void _handleApiFailure(String logMessage) {
logSafe(logMessage, level: LogLevel.error);
// No cache available, show error state
hasError.value = true;
errorMessage.value = "❌ Unable to load menus. Please try again later.";
menuItems.clear();
}
bool isMenuAllowed(String menuName) {
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
return menu?.available ?? false;
}
@override
void onClose() {
super.onClose();
}
}

View File

@ -0,0 +1,310 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
enum Gender {
male,
female,
other;
const Gender();
}
class AddEmployeeController extends MyController {
Map<String, dynamic>? editingEmployeeData;
// State
final MyFormValidator basicValidator = MyFormValidator();
final List<PlatformFile> files = [];
final List<String> categories = [];
Gender? selectedGender;
List<Map<String, dynamic>> roles = [];
String? selectedRoleId;
String selectedCountryCode = '+91';
bool showOnline = true;
DateTime? joiningDate;
String? selectedOrganizationId;
RxString selectedOrganizationName = RxString('');
@override
void onInit() {
super.onInit();
logSafe('Initializing AddEmployeeController...');
_initializeFields();
fetchRoles();
if (editingEmployeeData != null) {
prefillFields();
}
}
void _initializeFields() {
basicValidator.addField(
'first_name',
label: 'First Name',
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'phone_number',
label: 'Phone Number',
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'last_name',
label: 'Last Name',
required: true,
controller: TextEditingController(),
);
// Email is optional in controller; UI enforces when application access is checked
basicValidator.addField(
'email',
label: 'Email',
required: false,
controller: TextEditingController(),
);
logSafe('Fields initialized for first_name, phone_number, last_name, email.');
}
// Prefill fields in edit mode
void prefillFields() {
logSafe('Prefilling data for editing...');
basicValidator.getController('first_name')?.text =
editingEmployeeData?['first_name'] ?? '';
basicValidator.getController('last_name')?.text =
editingEmployeeData?['last_name'] ?? '';
basicValidator.getController('phone_number')?.text =
editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null
? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null;
basicValidator.getController('email')?.text =
editingEmployeeData?['email'] ?? '';
selectedRoleId = editingEmployeeData?['job_role_id'];
if (editingEmployeeData?['joining_date'] != null) {
joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']);
}
update();
}
void setJoiningDate(DateTime date) {
joiningDate = date;
logSafe('Joining date selected: $date');
update();
}
void onGenderSelected(Gender? gender) {
selectedGender = gender;
logSafe('Gender selected: ${gender?.name}');
update();
}
Future<void> fetchRoles() async {
logSafe('Fetching roles...');
try {
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe('Roles fetched successfully.');
update();
} else {
logSafe('Failed to fetch roles: null result', level: LogLevel.error);
}
} catch (e, st) {
logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st);
}
}
void onRoleSelected(String? roleId) {
selectedRoleId = roleId;
logSafe('Role selected: $roleId');
update();
}
// Create or update employee
Future<Map<String, dynamic>?> createOrUpdateEmployee({
String? email,
bool hasApplicationAccess = false,
}) async {
logSafe(editingEmployeeData != null
? 'Starting employee update...'
: 'Starting employee creation...');
if (selectedGender == null || selectedRoleId == null) {
showAppSnackbar(
title: 'Missing Fields',
message: 'Please select both Gender and Role.',
type: SnackbarType.warning,
);
return null;
}
final firstName = basicValidator.getController('first_name')?.text.trim();
final lastName = basicValidator.getController('last_name')?.text.trim();
final phoneNumber = basicValidator.getController('phone_number')?.text.trim();
try {
// sanitize orgId before sending
final String? orgId = (selectedOrganizationId != null &&
selectedOrganizationId!.trim().isNotEmpty)
? selectedOrganizationId
: null;
final response = await ApiService.createEmployee(
id: editingEmployeeData?['id'],
firstName: firstName!,
lastName: lastName!,
phoneNumber: phoneNumber!,
gender: selectedGender!.name,
jobRoleId: selectedRoleId!,
joiningDate: joiningDate?.toIso8601String() ?? '',
organizationId: orgId,
email: email,
hasApplicationAccess: hasApplicationAccess,
);
logSafe('Response: $response');
if (response != null && response['success'] == true) {
showAppSnackbar(
title: 'Success',
message: editingEmployeeData != null
? 'Employee updated successfully!'
: 'Employee created successfully!',
type: SnackbarType.success,
);
return response;
} else {
logSafe('Failed operation', level: LogLevel.error);
}
} catch (e, st) {
logSafe('Error creating/updating employee',
level: LogLevel.error, error: e, stackTrace: st);
}
showAppSnackbar(
title: 'Error',
message: 'Failed to save employee.',
type: SnackbarType.error,
);
return null;
}
Future<bool> _checkAndRequestContactsPermission() async {
final status = await Permission.contacts.request();
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
await openAppSettings();
}
showAppSnackbar(
title: 'Permission Required',
message: 'Please allow Contacts permission from settings to pick a contact.',
type: SnackbarType.warning,
);
return false;
}
Future<void> pickContact(BuildContext context) async {
final permissionGranted = await _checkAndRequestContactsPermission();
if (!permissionGranted) return;
try {
final picked = await FlutterContacts.openExternalPick();
if (picked == null) return;
final contact =
await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null) {
showAppSnackbar(
title: 'Error',
message: 'Failed to load contact details.',
type: SnackbarType.error,
);
return;
}
if (contact.phones.isEmpty) {
showAppSnackbar(
title: 'No Phone Number',
message: 'Selected contact has no phone number.',
type: SnackbarType.warning,
);
return;
}
final indiaPhones = contact.phones.where((p) {
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
return normalized.startsWith('+91') ||
RegExp(r'^\d{10}$').hasMatch(normalized);
}).toList();
if (indiaPhones.isEmpty) {
showAppSnackbar(
title: 'No Indian Number',
message: 'Selected contact has no Indian (+91) phone number.',
type: SnackbarType.warning,
);
return;
}
String? selectedPhone;
if (indiaPhones.length == 1) {
selectedPhone = indiaPhones.first.number;
} else {
selectedPhone = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Choose an Indian number'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: indiaPhones
.map(
(p) => ListTile(
title: Text(p.number),
onTap: () => Navigator.of(ctx).pop(p.number),
),
)
.toList(),
),
),
);
if (selectedPhone == null) return;
}
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
final phoneWithoutCountryCode = normalizedPhone.length > 10
? normalizedPhone.substring(normalizedPhone.length - 10)
: normalizedPhone;
basicValidator.getController('phone_number')?.text =
phoneWithoutCountryCode;
update();
} catch (e, st) {
logSafe('Error fetching contacts',
level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: 'Error',
message: 'Failed to fetch contacts.',
type: SnackbarType.error,
);
}
}
}

View File

@ -0,0 +1,145 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/model/employees/assigned_projects_model.dart';
import 'package:marco/controller/project_controller.dart';
class AssignProjectController extends GetxController {
final String employeeId;
final String jobRoleId;
AssignProjectController({
required this.employeeId,
required this.jobRoleId,
});
final ProjectController projectController = Get.put(ProjectController());
RxBool isLoading = false.obs;
RxBool isAssigning = false.obs;
RxList<String> assignedProjectIds = <String>[].obs;
RxList<String> selectedProjects = <String>[].obs;
RxList<GlobalProjectModel> allProjects = <GlobalProjectModel>[].obs;
RxList<GlobalProjectModel> filteredProjects = <GlobalProjectModel>[].obs;
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchAllProjectsAndAssignments();
});
}
/// Fetch all projects and assigned projects
Future<void> fetchAllProjectsAndAssignments() async {
isLoading.value = true;
try {
await projectController.fetchProjects();
allProjects.assignAll(projectController.projects);
filteredProjects.assignAll(allProjects); // initially show all
final responseList = await ApiService.getAssignedProjects(employeeId);
if (responseList != null) {
final assignedProjects =
responseList.map((e) => AssignedProject.fromJson(e)).toList();
assignedProjectIds.assignAll(
assignedProjects.map((p) => p.id).toList(),
);
selectedProjects.assignAll(assignedProjectIds);
}
logSafe("All Projects: ${allProjects.map((e) => e.id)}");
logSafe("Assigned Project IDs: $assignedProjectIds");
} catch (e, stack) {
logSafe("Error fetching projects or assignments: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isLoading.value = false;
}
}
/// Assign selected projects
Future<bool> assignProjectsToEmployee() async {
if (selectedProjects.isEmpty) {
logSafe("No projects selected for assignment.", level: LogLevel.warning);
return false;
}
final List<Map<String, dynamic>> projectPayload =
selectedProjects.map((id) {
return {"projectId": id, "jobRoleId": jobRoleId, "status": true};
}).toList();
isAssigning.value = true;
try {
final success = await ApiService.assignProjects(
employeeId: employeeId,
projects: projectPayload,
);
if (success) {
logSafe("Projects assigned successfully.");
assignedProjectIds.assignAll(selectedProjects);
return true;
} else {
logSafe("Failed to assign projects.", level: LogLevel.error);
return false;
}
} catch (e, stack) {
logSafe("Error assigning projects: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return false;
} finally {
isAssigning.value = false;
}
}
/// Toggle project selection
void toggleProjectSelection(String projectId, bool isSelected) {
if (isSelected) {
if (!selectedProjects.contains(projectId)) {
selectedProjects.add(projectId);
}
} else {
selectedProjects.remove(projectId);
}
}
/// Check if project is selected
bool isProjectSelected(String projectId) {
return selectedProjects.contains(projectId);
}
/// Select all / deselect all
void toggleSelectAll() {
if (areAllSelected()) {
selectedProjects.clear();
} else {
selectedProjects.assignAll(allProjects.map((p) => p.id.toString()));
}
}
/// Are all selected?
bool areAllSelected() {
return selectedProjects.length == allProjects.length &&
allProjects.isNotEmpty;
}
/// Filter projects by search text
void filterProjects(String query) {
if (query.isEmpty) {
filteredProjects.assignAll(allProjects);
} else {
filteredProjects.assignAll(
allProjects
.where((p) => p.name.toLowerCase().contains(query.toLowerCase()))
.toList(),
);
}
}
}

View File

@ -1,86 +1,152 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/attendance/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart';
final Logger log = Logger();
import 'package:marco/controller/project_controller.dart';
class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>();
RxBool isLoadingEmployeeDetails = false.obs;
@override
void onInit() {
super.onInit();
fetchAllProjects();
fetchAllEmployees();
isLoading.value = true;
fetchAllProjects().then((_) {
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
selectedProjectId = projectId;
fetchEmployeesByProject(projectId);
} else if (isAllEmployeeSelected.value) {
fetchAllEmployees();
} else {
clearEmployees();
}
});
}
Future<void> fetchAllProjects() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getProjects,
onSuccess: (data) {
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
log.i("Projects fetched: ${projects.length} projects loaded.");
logSafe(
"Projects fetched: ${projects.length} projects loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
logSafe("No project data found or API call failed.",
level: LogLevel.warning);
},
onEmpty: () => log.w("No project data found or API call failed."),
);
isLoading.value = false;
update();
}
Future<void> fetchAllEmployees() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getAllEmployees,
onSuccess: (data) {
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
log.i("All Employees fetched: ${employees.length} employees loaded.");
},
onEmpty: () => log.w("No Employee data found or API call failed."),
);
isLoading.value = false;
update();
void clearEmployees() {
employees.clear();
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
return;
}
Future<void> fetchAllEmployees({String? organizationId}) async {
isLoading.value = true;
update(['employee_screen_controller']);
await _handleApiCall(
() => ApiService.getAllEmployees(
organizationId: organizationId), // pass orgId to API
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
employees.clear();
logSafe(
"No Employee data found or API call failed",
level: LogLevel.warning,
);
},
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeesByProject(String projectId,
{String? organizationId}) async {
if (projectId.isEmpty) return;
isLoading.value = true;
await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId),
() => ApiService.getAllEmployeesByProject(projectId,
organizationId: organizationId),
onSuccess: (data) {
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
update();
},
onEmpty: () => employees.clear(),
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe(
"Employee details loaded for $employeeId",
level: LogLevel.info,
);
},
onEmpty: () {
log.w("No employees found for project $projectId.");
employees = [];
update();
selectedEmployeeDetails.value = null;
logSafe(
"No employee details found for $employeeId",
level: LogLevel.warning,
);
},
onError: (e) {
selectedEmployeeDetails.value = null;
logSafe(
"Error fetching employee details for $employeeId",
level: LogLevel.error,
error: e,
);
},
onError: (e) =>
log.e("Error fetching employees for project $projectId: $e"),
);
isLoading.value = false;
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleApiCall(
@ -100,32 +166,11 @@ class EmployeesScreenController extends GetxController {
if (onError != null) {
onError(e);
} else {
log.e("API call error: $e");
logSafe("API call error", level: LogLevel.error, error: e);
}
}
}
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
},
onError: (e) {
selectedEmployeeDetails.value = null;
},
);
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess,
@ -143,7 +188,7 @@ class EmployeesScreenController extends GetxController {
if (onError != null) {
onError(e);
} else {
log.e("API call error: $e");
logSafe("API call error", level: LogLevel.error, error: e);
}
}
}

View File

@ -0,0 +1,497 @@
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
class AddExpenseController extends GetxController {
// --- Text Controllers ---
final controllers = <TextEditingController>[
TextEditingController(), // amount
TextEditingController(), // description
TextEditingController(), // supplier
TextEditingController(), // transactionId
TextEditingController(), // gst
TextEditingController(), // location
TextEditingController(), // transactionDate
TextEditingController(), // noOfPersons
TextEditingController(), // employeeSearch
];
TextEditingController get amountController => controllers[0];
TextEditingController get descriptionController => controllers[1];
TextEditingController get supplierController => controllers[2];
TextEditingController get transactionIdController => controllers[3];
TextEditingController get gstController => controllers[4];
TextEditingController get locationController => controllers[5];
TextEditingController get transactionDateController => controllers[6];
TextEditingController get noOfPersonsController => controllers[7];
TextEditingController get employeeSearchController => controllers[8];
// --- Reactive State ---
final isLoading = false.obs;
final isSubmitting = false.obs;
final isFetchingLocation = false.obs;
final isEditMode = false.obs;
final isSearchingEmployees = false.obs;
// --- Dropdown Selections & Data ---
final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>();
final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>();
final attachments = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].obs;
final globalProjects = <String>[].obs;
final projectsMap = <String, String>{}.obs;
final expenseTypes = <ExpenseTypeModel>[].obs;
final paymentModes = <PaymentModeModel>[].obs;
final allEmployees = <EmployeeModel>[].obs;
final employeeSearchResults = <EmployeeModel>[].obs;
String? editingExpenseId;
final expenseController = Get.find<ExpenseController>();
final ImagePicker _picker = ImagePicker();
@override
void onInit() {
super.onInit();
loadMasterData();
employeeSearchController.addListener(
() => searchEmployees(employeeSearchController.text),
);
}
@override
void onClose() {
for (var c in controllers) {
c.dispose();
}
super.onClose();
}
// --- Employee Search ---
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) return employeeSearchResults.clear();
isSearchingEmployees.value = true;
try {
final data = await ApiService.searchEmployeesBasic(
searchString: query.trim(),
);
if (data is List) {
employeeSearchResults.assignAll(
data
.map((e) => EmployeeModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
} else {
employeeSearchResults.clear();
}
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
// --- Form Population (Edit) ---
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
isEditMode.value = true;
editingExpenseId = '${data['id']}';
selectedProject.value = data['projectName'] ?? '';
amountController.text = '${data['amount'] ?? ''}';
supplierController.text = data['supplerName'] ?? '';
descriptionController.text = data['description'] ?? '';
transactionIdController.text = data['transactionId'] ?? '';
locationController.text = data['location'] ?? '';
noOfPersonsController.text = '${data['noOfPersons'] ?? 0}';
_setTransactionDate(data['transactionDate']);
_setDropdowns(data);
await _setPaidBy(data);
_setAttachments(data['attachments']);
_logPrefilledData();
}
void _setTransactionDate(dynamic dateStr) {
if (dateStr == null) {
selectedTransactionDate.value = null;
transactionDateController.clear();
return;
}
try {
final parsed = DateTime.parse(dateStr);
selectedTransactionDate.value = parsed;
transactionDateController.text = DateFormat('dd-MM-yyyy').format(parsed);
} catch (_) {
selectedTransactionDate.value = null;
transactionDateController.clear();
}
}
void _setDropdowns(Map<String, dynamic> data) {
selectedExpenseType.value =
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
selectedPaymentMode.value =
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
}
Future<void> _setPaidBy(Map<String, dynamic> data) async {
final paidById = '${data['paidById']}';
selectedPaidBy.value =
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
if (selectedPaidBy.value == null && data['paidByFirstName'] != null) {
await searchEmployees(
'${data['paidByFirstName']} ${data['paidByLastName']}',
);
selectedPaidBy.value = employeeSearchResults
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
}
}
void _setAttachments(dynamic attachmentsData) {
existingAttachments.clear();
if (attachmentsData is List) {
existingAttachments.addAll(
List<Map<String, dynamic>>.from(attachmentsData).map(
(e) => {...e, 'isActive': true},
),
);
}
}
void _logPrefilledData() {
final info = [
'ID: $editingExpenseId',
'Project: ${selectedProject.value}',
'Amount: ${amountController.text}',
'Supplier: ${supplierController.text}',
'Description: ${descriptionController.text}',
'Transaction ID: ${transactionIdController.text}',
'Location: ${locationController.text}',
'Transaction Date: ${transactionDateController.text}',
'No. of Persons: ${noOfPersonsController.text}',
'Expense Type: ${selectedExpenseType.value?.name}',
'Payment Mode: ${selectedPaymentMode.value?.name}',
'Paid By: ${selectedPaidBy.value?.name}',
'Attachments: ${attachments.length}',
'Existing Attachments: ${existingAttachments.length}',
];
for (var line in info) {
logSafe(line, level: LogLevel.info);
}
}
// --- Pickers ---
Future<void> pickTransactionDate(BuildContext context) async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedTransactionDate.value ?? DateTime.now(),
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
final now = DateTime.now();
final finalDateTime = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
now.hour,
now.minute,
now.second,
);
selectedTransactionDate.value = finalDateTime;
transactionDateController.text =
DateFormat('dd MMM yyyy').format(finalDateTime);
}
}
Future<void> pickAttachments() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
allowMultiple: true,
);
if (result != null) {
attachments.addAll(
result.paths.whereType<String>().map(File.new),
);
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
void removeAttachment(File file) => attachments.remove(file);
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) attachments.add(File(pickedFile.path));
} catch (e) {
_errorSnackbar("Camera error: $e");
}
}
// --- Location ---
Future<void> fetchCurrentLocation() async {
isFetchingLocation.value = true;
try {
if (!await _ensureLocationPermission()) return;
final position = await Geolocator.getCurrentPosition();
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
locationController.text = placemarks.isNotEmpty
? [
placemarks.first.name,
placemarks.first.street,
placemarks.first.locality,
placemarks.first.administrativeArea,
placemarks.first.country,
].where((e) => e?.isNotEmpty == true).join(", ")
: "${position.latitude}, ${position.longitude}";
} catch (e) {
_errorSnackbar("Location error: $e");
} finally {
isFetchingLocation.value = false;
}
}
Future<bool> _ensureLocationPermission() async {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
_errorSnackbar("Location permission denied.");
return false;
}
}
if (!await Geolocator.isLocationServiceEnabled()) {
_errorSnackbar("Location service disabled.");
return false;
}
return true;
}
// --- Data Fetching ---
Future<void> loadMasterData() async =>
Future.wait([fetchMasterData(), fetchGlobalProjects()]);
Future<void> fetchMasterData() async {
try {
final types = await ApiService.getMasterExpenseTypes();
if (types is List) {
expenseTypes.value = types
.map((e) => ExpenseTypeModel.fromJson(e as Map<String, dynamic>))
.toList();
}
final modes = await ApiService.getMasterPaymentModes();
if (modes is List) {
paymentModes.value = modes
.map((e) => PaymentModeModel.fromJson(e as Map<String, dynamic>))
.toList();
}
} catch (_) {
_errorSnackbar("Failed to fetch master data");
}
}
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
}
} catch (e) {
logSafe("Error fetching projects: $e", level: LogLevel.error);
}
}
// --- Submission ---
Future<void> submitOrUpdateExpense() async {
if (isSubmitting.value) return;
isSubmitting.value = true;
try {
final validationMsg = validateForm();
if (validationMsg.isNotEmpty) {
_errorSnackbar(validationMsg, "Missing Fields");
return;
}
final payload = await _buildExpensePayload();
final success = await _submitToApi(payload);
if (success) {
await expenseController.fetchExpenses();
Get.back();
showAppSnackbar(
title: "Success",
message:
"Expense ${isEditMode.value ? 'updated' : 'created'} successfully!",
type: SnackbarType.success,
);
} else {
_errorSnackbar("Operation failed. Try again.");
}
} catch (e) {
_errorSnackbar("Unexpected error: $e");
} finally {
isSubmitting.value = false;
}
}
Future<bool> _submitToApi(Map<String, dynamic> payload) async {
if (isEditMode.value && editingExpenseId != null) {
return ApiService.editExpenseApi(
expenseId: editingExpenseId!,
payload: payload,
);
}
return ApiService.createExpenseApi(
projectId: payload['projectId'],
expensesTypeId: payload['expensesTypeId'],
paymentModeId: payload['paymentModeId'],
paidById: payload['paidById'],
transactionDate: DateTime.parse(payload['transactionDate']),
transactionId: payload['transactionId'],
description: payload['description'],
location: payload['location'],
supplerName: payload['supplerName'],
amount: payload['amount'],
noOfPersons: payload['noOfPersons'],
billAttachments: payload['billAttachments'],
);
}
Future<Map<String, dynamic>> _buildExpensePayload() async {
final now = DateTime.now();
final existingPayload = isEditMode.value
? existingAttachments
.map((e) => {
"documentId": e['documentId'],
"fileName": e['fileName'],
"contentType": e['contentType'],
"fileSize": 0,
"description": "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
"base64Data": "",
})
.toList()
: <Map<String, dynamic>>[];
final newPayload = await Future.wait(
attachments.map((file) async {
final bytes = await file.readAsBytes();
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType":
lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": await file.length(),
"description": "",
};
}),
);
final type = selectedExpenseType.value!;
return {
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectsMap[selectedProject.value]!,
"expensesTypeId": type.id,
"paymentModeId": selectedPaymentMode.value!.id,
"paidById": selectedPaidBy.value!.id,
"transactionDate":
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
"transactionId": transactionIdController.text,
"description": descriptionController.text,
"location": locationController.text,
"supplerName": supplierController.text,
"amount": double.parse(amountController.text.trim()),
"noOfPersons": type.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0,
"billAttachments": [
...existingPayload,
...newPayload,
].isEmpty
? null
: [...existingPayload, ...newPayload],
};
}
String validateForm() {
final missing = <String>[];
if (selectedProject.value.isEmpty) missing.add("Project");
if (selectedExpenseType.value == null) missing.add("Expense Type");
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
if (selectedPaidBy.value == null) missing.add("Paid By");
if (amountController.text.trim().isEmpty) missing.add("Amount");
if (descriptionController.text.trim().isEmpty) missing.add("Description");
if (selectedTransactionDate.value == null) {
missing.add("Transaction Date");
} else if (selectedTransactionDate.value!.isAfter(DateTime.now())) {
missing.add("Valid Transaction Date");
}
if (double.tryParse(amountController.text.trim()) == null) {
missing.add("Valid Amount");
}
final hasActiveExisting =
existingAttachments.any((e) => e['isActive'] != false);
if (attachments.isEmpty && !hasActiveExisting) {
missing.add("Attachment");
}
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
}
// --- Snackbar Helper ---
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
}

View File

@ -0,0 +1,187 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_detail_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:flutter/material.dart';
class ExpenseDetailController extends GetxController {
final Rx<ExpenseDetailModel?> expense = Rx<ExpenseDetailModel?>(null);
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
late String _expenseId;
bool _isInitialized = false;
final employeeSearchController = TextEditingController();
final isSearchingEmployees = false.obs;
/// Call this once from the screen (NOT inside build) to initialize
void init(String expenseId) {
if (_isInitialized) return;
_isInitialized = true;
_expenseId = expenseId;
// Use Future.wait to fetch details and employees concurrently
Future.wait([
fetchExpenseDetails(),
fetchAllEmployees(),
]);
}
/// Generic method to handle API calls with loading and error states
Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async {
isLoading.value = true;
errorMessage.value = ''; // Clear previous errors
try {
logSafe("Initiating $operationName...");
final result = await apiCall();
logSafe("$operationName completed successfully.");
return result;
} catch (e, stack) {
errorMessage.value =
'An unexpected error occurred during $operationName.';
logSafe("Exception in $operationName: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return null;
} finally {
isLoading.value = false;
}
}
/// Fetch expense details by stored ID
Future<void> fetchExpenseDetails() async {
final result = await _apiCallWrapper(
() => ApiService.getExpenseDetailsApi(expenseId: _expenseId),
"fetch expense details");
if (result != null) {
try {
expense.value = ExpenseDetailModel.fromJson(result);
logSafe("Expense details loaded successfully: ${expense.value?.id}");
} catch (e) {
errorMessage.value = 'Failed to parse expense details: $e';
logSafe("Parse error in fetchExpenseDetails: $e",
level: LogLevel.error);
}
} else {
errorMessage.value = 'Failed to fetch expense details from server.';
logSafe("fetchExpenseDetails failed: null response",
level: LogLevel.error);
}
}
// This method seems like a utility and might be better placed in a helper or utility class
// if it's used across multiple controllers. Keeping it here for now as per original code.
List<String> parsePermissionIds(dynamic permissionData) {
if (permissionData == null) return [];
if (permissionData is List) {
return permissionData
.map((e) => e.toString().trim())
.where((e) => e.isNotEmpty)
.toList();
}
if (permissionData is String) {
final clean = permissionData.replaceAll(RegExp(r'[\[\]]'), '');
return clean
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
}
return [];
}
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) return employeeSearchResults.clear();
isSearchingEmployees.value = true;
try {
final data =
await ApiService.searchEmployeesBasic(searchString: query.trim());
employeeSearchResults.assignAll(
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
);
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
final response = await _apiCallWrapper(
() => ApiService.getAllEmployees(), "fetch all employees");
if (response != null && response.isNotEmpty) {
try {
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
logSafe("All Employees fetched: ${allEmployees.length}",
level: LogLevel.info);
} catch (e) {
errorMessage.value = 'Failed to parse employee data: $e';
logSafe("Parse error in fetchAllEmployees: $e", level: LogLevel.error);
}
} else {
allEmployees.clear();
logSafe("No employees found.", level: LogLevel.warning);
}
// `update()` is typically not needed for RxList directly unless you have specific GetBuilder/Obx usage that requires it
// If you are using Obx widgets, `allEmployees.assignAll` will automatically trigger a rebuild.
}
/// Update expense with reimbursement info and status
Future<bool> updateExpenseStatusWithReimbursement({
required String comment,
required String reimburseTransactionId,
required String reimburseDate,
required String reimburseById,
required String statusId,
}) async {
final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi(
expenseId: _expenseId,
statusId: statusId,
comment: comment,
reimburseTransactionId: reimburseTransactionId,
reimburseDate: reimburseDate,
reimbursedById: reimburseById,
),
"submit reimbursement",
);
if (success == true) {
// Explicitly check for true as _apiCallWrapper returns T?
await fetchExpenseDetails(); // Refresh details after successful update
return true;
} else {
errorMessage.value = "Failed to submit reimbursement.";
return false;
}
}
/// Update status for this specific expense
Future<bool> updateExpenseStatus(String statusId, {String? comment}) async {
final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi(
expenseId: _expenseId,
statusId: statusId,
comment: comment,
),
"update expense status",
);
if (success == true) {
await fetchExpenseDetails();
return true;
} else {
errorMessage.value = "Failed to update expense status.";
return false;
}
}
}

View File

@ -0,0 +1,357 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_list_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/expense_status_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter/material.dart';
class ExpenseController extends GetxController {
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
// Master data
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
final RxList<String> globalProjects = <String>[].obs;
final RxMap<String, String> projectsMap = <String, String>{}.obs;
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
// Persistent Filter States
final RxString selectedProject = ''.obs;
final RxString selectedStatus = ''.obs;
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> selectedCreatedByEmployees =
<EmployeeModel>[].obs;
final RxString selectedDateType = 'Transaction Date'.obs;
final employeeSearchController = TextEditingController();
final isSearchingEmployees = false.obs;
final employeeSearchResults = <EmployeeModel>[].obs;
final List<String> dateTypes = [
'Transaction Date',
'Created At',
];
int _pageSize = 20;
int _pageNumber = 1;
@override
void onInit() {
super.onInit();
loadInitialMasterData();
fetchAllEmployees();
employeeSearchController.addListener(() {
searchEmployees(employeeSearchController.text);
});
}
bool get isFilterApplied {
return selectedProject.value.isNotEmpty ||
selectedStatus.value.isNotEmpty ||
startDate.value != null ||
endDate.value != null ||
selectedPaidByEmployees.isNotEmpty ||
selectedCreatedByEmployees.isNotEmpty;
}
/// Load master data
Future<void> loadInitialMasterData() async {
await fetchGlobalProjects();
await fetchMasterData();
}
Future<void> deleteExpense(String expenseId) async {
try {
logSafe("Attempting to delete expense: $expenseId");
final success = await ApiService.deleteExpense(expenseId);
if (success) {
expenses.removeWhere((e) => e.id == expenseId);
logSafe("Expense deleted successfully.");
showAppSnackbar(
title: "Deleted",
message: "Expense has been deleted successfully.",
type: SnackbarType.success,
);
} else {
logSafe("Failed to delete expense: $expenseId", level: LogLevel.error);
showAppSnackbar(
title: "Failed",
message: "Failed to delete expense.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Exception in deleteExpense: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting.",
type: SnackbarType.error,
);
}
}
Future<void> searchEmployees(String searchQuery) async {
if (searchQuery.trim().isEmpty) {
employeeSearchResults.clear();
return;
}
isSearchingEmployees.value = true;
try {
final results = await ApiService.searchEmployeesBasic(
searchString: searchQuery.trim(),
);
if (results != null) {
employeeSearchResults.assignAll(
results.map((e) => EmployeeModel.fromJson(e)),
);
} else {
employeeSearchResults.clear();
}
} catch (e) {
logSafe("Error searching employees: $e", level: LogLevel.error);
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Fetch expenses using filters
Future<void> fetchExpenses({
List<String>? projectIds,
List<String>? statusIds,
List<String>? createdByIds,
List<String>? paidByIds,
DateTime? startDate,
DateTime? endDate,
int pageSize = 20,
int pageNumber = 1,
}) async {
isLoading.value = true;
errorMessage.value = '';
expenses.clear();
_pageSize = pageSize;
_pageNumber = pageNumber;
final Map<String, dynamic> filterMap = {
"projectIds": projectIds ??
(selectedProject.value.isEmpty
? []
: [projectsMap[selectedProject.value] ?? '']),
"statusIds": statusIds ??
(selectedStatus.value.isEmpty ? [] : [selectedStatus.value]),
"createdByIds":
createdByIds ?? selectedCreatedByEmployees.map((e) => e.id).toList(),
"paidByIds":
paidByIds ?? selectedPaidByEmployees.map((e) => e.id).toList(),
"startDate": (startDate ?? this.startDate.value)?.toIso8601String(),
"endDate": (endDate ?? this.endDate.value)?.toIso8601String(),
"isTransactionDate": selectedDateType.value == 'Transaction Date',
};
try {
logSafe("Fetching expenses with filter: ${jsonEncode(filterMap)}");
final result = await ApiService.getExpenseListApi(
filter: jsonEncode(filterMap),
pageSize: _pageSize,
pageNumber: _pageNumber,
);
if (result != null) {
try {
final expenseResponse = ExpenseResponse.fromJson(result);
// If the backend returns no data, treat it as empty list
if (expenseResponse.data.data.isEmpty) {
expenses.clear();
errorMessage.value = ''; // no error
logSafe("Expense list is empty.");
} else {
expenses.assignAll(expenseResponse.data.data);
logSafe("Expenses loaded: ${expenses.length}");
logSafe(
"Pagination Info: Page ${expenseResponse.data.currentPage} of ${expenseResponse.data.totalPages} | Total: ${expenseResponse.data.totalEntites}");
}
} catch (e) {
errorMessage.value = 'Failed to parse expenses: $e';
logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error);
}
} else {
// Only treat as error if this means a network or server failure
errorMessage.value = 'Unable to connect to the server.';
logSafe("fetchExpenses failed: null response", level: LogLevel.error);
}
} catch (e, stack) {
errorMessage.value = 'An unexpected error occurred.';
logSafe("Exception in fetchExpenses: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isLoading.value = false;
}
}
/// Clear all filters
void clearFilters() {
selectedProject.value = '';
selectedStatus.value = '';
startDate.value = null;
endDate.value = null;
selectedPaidByEmployees.clear();
selectedCreatedByEmployees.clear();
}
/// Fetch master data: expense types, payment modes, and expense status
Future<void> fetchMasterData() async {
try {
final expenseTypesData = await ApiService.getMasterExpenseTypes();
if (expenseTypesData is List) {
expenseTypes.value =
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
}
final expenseStatusData = await ApiService.getMasterExpenseStatus();
if (expenseStatusData is List) {
expenseStatuses.value = expenseStatusData
.map((e) => ExpenseStatusModel.fromJson(e))
.toList();
}
} catch (e) {
showAppSnackbar(
title: "Error",
message: "Failed to fetch master data: $e",
type: SnackbarType.error,
);
}
}
/// Fetch global projects
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
logSafe("Fetched ${names.length} global projects");
}
} catch (e) {
logSafe("Failed to fetch global projects: $e", level: LogLevel.error);
}
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
isLoading.value = true;
try {
final response = await ApiService.getAllEmployees();
if (response != null && response.isNotEmpty) {
allEmployees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
level: LogLevel.info,
);
} else {
allEmployees.clear();
logSafe("No employees found for Manage Bucket.",
level: LogLevel.warning);
}
} catch (e) {
allEmployees.clear();
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
Future<void> loadMoreExpenses() async {
if (isLoading.value) return;
_pageNumber += 1;
isLoading.value = true;
final Map<String, dynamic> filterMap = {
"projectIds": selectedProject.value.isEmpty
? []
: [projectsMap[selectedProject.value] ?? ''],
"statusIds": selectedStatus.value.isEmpty ? [] : [selectedStatus.value],
"createdByIds": selectedCreatedByEmployees.map((e) => e.id).toList(),
"paidByIds": selectedPaidByEmployees.map((e) => e.id).toList(),
"startDate": startDate.value?.toIso8601String(),
"endDate": endDate.value?.toIso8601String(),
"isTransactionDate": selectedDateType.value == 'Transaction Date',
};
try {
final result = await ApiService.getExpenseListApi(
filter: jsonEncode(filterMap),
pageSize: _pageSize,
pageNumber: _pageNumber,
);
if (result != null) {
final expenseResponse = ExpenseResponse.fromJson(result);
expenses.addAll(expenseResponse.data.data);
}
} catch (e) {
logSafe("Error in loadMoreExpenses: $e", level: LogLevel.error);
} finally {
isLoading.value = false;
}
}
/// Update expense status
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
isLoading.value = true;
errorMessage.value = '';
try {
logSafe("Updating status for expense: $expenseId -> $statusId");
final success = await ApiService.updateExpenseStatusApi(
expenseId: expenseId,
statusId: statusId,
);
if (success) {
logSafe("Expense status updated successfully.");
await fetchExpenses();
return true;
} else {
errorMessage.value = "Failed to update expense status.";
return false;
}
} catch (e, stack) {
errorMessage.value = 'An unexpected error occurred.';
logSafe("Exception in updateExpenseStatus: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
return false;
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,17 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:marco/model/time_line.dart';
class TimeLineController extends MyController {
List<TimeLineModel> timeline = [];
List<String> dummyTexts = List.generate(12, (index) => MyTextUtils.getDummyText(60));
@override
void onInit() {
TimeLineModel.dummyList.then((value) {
timeline = value.sublist(0, 6);
update();
});
super.onInit();
}
}

View File

@ -1,46 +1,112 @@
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/model/project_model.dart';
class LayoutController extends GetxController {
// Theme Customization
ThemeCustomizer themeCustomizer = ThemeCustomizer();
// Global Keys
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
ScrollController scrollController = ScrollController();
// Scroll
final ScrollController scrollController = ScrollController();
// Reactive State
final RxBool isLoading = true.obs;
final RxBool isLoadingProjects = true.obs;
final RxBool isProjectSelectionExpanded = true.obs;
final RxBool isProjectListExpanded = false.obs;
final RxBool isProjectDropdownExpanded = false.obs;
final RxList<ProjectModel> projects = <ProjectModel>[].obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
// Selected Project
RxString? selectedProjectId;
bool isLastIndex = false;
@override
void onInit() {
super.onInit();
fetchProjects();
}
@override
void onReady() {
super.onReady();
ThemeCustomizer.addListener(onChangeTheme);
}
@override
void dispose() {
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
super.dispose();
}
/// Fetch project list from API and initialize the selection.
Future<void> fetchProjects() async {
isLoading.value = true;
isLoadingProjects.value = true;
try {
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) {
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
projects.assignAll(fetchedProjects);
selectedProjectId = RxString(fetchedProjects.first.id.toString());
logSafe("Projects fetched: ${fetchedProjects.length}", level: LogLevel.info);
} else {
logSafe("No projects found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: st);
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['dashboard_controller']);
}
/// Update selected project ID
void updateSelectedProject(String projectId) {
selectedProjectId?.value = projectId;
logSafe("Selected project updated", level: LogLevel.info);
}
/// Toggle expansion of the project list section
void toggleProjectListExpanded() {
isProjectListExpanded.toggle();
logSafe("Project list expanded: ${isProjectListExpanded.value}", level: LogLevel.debug);
}
/// Handle theme changes (light/dark, drawer toggles)
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
themeCustomizer = newVal;
update();
if (newVal.rightBarOpen) {
scaffoldKey.currentState?.openEndDrawer();
logSafe("Theme changed — end drawer opened", level: LogLevel.debug);
} else {
scaffoldKey.currentState?.closeEndDrawer();
logSafe("Theme changed — end drawer closed", level: LogLevel.debug);
}
}
enableNotificationShade() {
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
/// Optional notification toggles (placeholder)
void enableNotificationShade() {
logSafe("Notification shade enabled (not implemented)", level: LogLevel.verbose);
}
disableNotificationShade() {
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
}
@override
void dispose() {
super.dispose();
ThemeCustomizer.removeListener(onChangeTheme);
scrollController.dispose();
void disableNotificationShade() {
logSafe("Notification shade disabled (not implemented)", level: LogLevel.verbose);
}
}

View File

@ -2,26 +2,74 @@ import 'dart:async';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/permission_service.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/model/projects_model.dart';
final log = Logger();
class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs;
var employeeInfo = Rxn<EmployeeInfo>();
var projectsInfo = <ProjectInfo>[].obs;
Timer? _refreshTimer;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
_loadDataFromAPI();
_startAutoRefresh();
_initialize();
}
Future<void> _initialize() async {
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
_startAutoRefresh();
} else {
logSafe("Token is null or empty. Skipping API load and auto-refresh.",
level: LogLevel.warning);
}
}
Future<String?> _getAuthToken() async {
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('jwt_token');
logSafe("Auth token retrieved: $token", level: LogLevel.debug);
return token;
} catch (e, stacktrace) {
logSafe("Error retrieving auth token",
level: LogLevel.error, error: e, stackTrace: stacktrace);
return null;
}
}
Future<void> loadData(String token) async {
try {
isLoading.value = true;
final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData);
await _storeData();
logSafe("Data loaded and state updated successfully.");
} catch (e, stacktrace) {
logSafe("Error loading data from API",
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally {
isLoading.value = false;
}
}
void _updateState(Map<String, dynamic> userData) {
try {
permissions.assignAll(userData['permissions']);
employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']);
logSafe("State updated with user data.");
} catch (e, stacktrace) {
logSafe("Error updating state",
level: LogLevel.error, error: e, stackTrace: stacktrace);
}
}
Future<void> _storeData() async {
@ -47,76 +95,61 @@ class PermissionController extends GetxController {
);
}
log.i("User data successfully stored in SharedPreferences.");
logSafe("User data successfully stored in SharedPreferences.");
} catch (e, stacktrace) {
log.e("Error storing data", error: e, stackTrace: stacktrace);
}
}
Future<void> _loadDataFromAPI() async {
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
} else {
log.w("No token found for loading API data.");
}
}
Future<void> loadData(String token) async {
try {
final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData);
await _storeData();
log.i("Data loaded and state updated successfully.");
} catch (e, stacktrace) {
log.e("Error loading data from API", error: e, stackTrace: stacktrace);
}
}
void _updateState(Map<String, dynamic> userData) {
try {
permissions.assignAll(userData['permissions']);
employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']);
log.i("State updated with new user data.");
} catch (e, stacktrace) {
log.e("Error updating state", error: e, stackTrace: stacktrace);
}
}
Future<String?> _getAuthToken() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('jwt_token');
} catch (e, stacktrace) {
log.e("Error retrieving auth token", error: e, stackTrace: stacktrace);
return null;
logSafe("Error storing data",
level: LogLevel.error, error: e, stackTrace: stacktrace);
}
}
void _startAutoRefresh() {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
log.i("Auto-refresh triggered.");
await _loadDataFromAPI();
logSafe("Auto-refresh triggered.");
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
await loadData(token!);
} else {
logSafe("Token missing during auto-refresh. Skipping.",
level: LogLevel.warning);
}
});
}
bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId);
log.d("Checking permission $permissionId: $hasPerm");
logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug);
return hasPerm;
}
bool isUserAssignedToProject(String projectId) {
final assigned = projectsInfo.any((project) => project.id == projectId);
log.d("Checking project assignment for $projectId: $assigned");
logSafe("Checking project assignment for $projectId: $assigned",
level: LogLevel.debug);
return assigned;
}
List<String> get allowedPermissionIds {
final ids = permissions.map((p) => p.id).toList();
logSafe("[PermissionController] Allowed Permission IDs: $ids",
level: LogLevel.debug);
return ids;
}
bool hasAnyPermission(List<String> ids) {
logSafe("[PermissionController] Checking if any of these are allowed: $ids",
level: LogLevel.debug);
final allowed = allowedPermissionIds;
final result = ids.any((id) => allowed.contains(id));
logSafe("[PermissionController] Permission match result: $result",
level: LogLevel.debug);
return result;
}
@override
void onClose() {
_refreshTimer?.cancel();
log.i("PermissionController disposed and timer cancelled.");
logSafe("PermissionController disposed and auto-refresh timer cancelled.");
super.onClose();
}
}

View File

@ -0,0 +1,83 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ProjectController extends GetxController {
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
RxString selectedProjectId = ''.obs;
RxBool isProjectListExpanded = false.obs;
RxBool isProjectSelectionExpanded = false.obs;
RxBool isProjectDropdownExpanded = false.obs;
RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
GlobalProjectModel? get selectedProject {
if (selectedProjectId.value.isEmpty) return null;
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
}
@override
void onInit() {
super.onInit();
fetchProjects();
}
void clearProjects() {
projects.clear();
selectedProjectId.value = '';
isProjectSelectionExpanded.value = false;
isProjectListExpanded.value = false;
isProjectDropdownExpanded.value = false;
isLoadingProjects.value = false;
isLoading.value = false;
uploadingStates.clear();
LocalStorage.saveString('selectedProjectId', '');
logSafe("Projects cleared and UI states reset.");
update();
}
/// Fetches projects and initializes selected project.
Future<void> fetchProjects() async {
isLoadingProjects.value = true;
isLoading.value = true;
final response = await ApiService.getGlobalProjects();
if (response != null && response.isNotEmpty) {
projects.assignAll(
response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
);
String? savedId = LocalStorage.getString('selectedProjectId');
if (savedId != null && projects.any((p) => p.id == savedId)) {
selectedProjectId.value = savedId;
} else {
selectedProjectId.value = projects.first.id.toString();
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
}
isProjectSelectionExpanded.value = false;
logSafe("Projects fetched: ${projects.length}");
} else {
logSafe("No Global projects found or API call failed.", level: LogLevel.warning);
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['dashboard_controller']);
}
Future<void> updateSelectedProject(String projectId) async {
selectedProjectId.value = projectId;
await LocalStorage.saveString('selectedProjectId', projectId);
logSafe("Selected project updated to $projectId");
update(['selected_project']);
}
}

View File

@ -1,190 +0,0 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
class DailyTaskPlaningController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
void updateSelectedEmployees() {
final selected =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
}
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
_initializeDefaults();
}
void _initializeDefaults() {
fetchProjects();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target") {
if (int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
}
if (fieldType == "description") {
if (value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
}
return null;
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logger.i("Role selected: $roleId");
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logger.i("Starting assign task...");
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logger.i("Task assigned successfully.");
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to assign task.");
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchProjects() async {
try {
isLoading.value = true;
final response = await ApiService.getProjects();
if (response?.isEmpty ?? true) {
log.w("No project data found or API call failed.");
return;
}
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
update();
await fetchTaskData(selectedProjectId);
} catch (e, stack) {
log.e("Error fetching projects", error: e, stackTrace: stack);
} finally {
isLoading.value = false;
}
}
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
try {
isLoading.value = true;
final response = await ApiService.getDailyTasksDetails(projectId);
if (response != null) {
final data = response['data'];
if (data != null) {
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
log.i("Daily task Planning Details fetched.");
} else {
log.e("Data field is null");
}
} else {
log.e(
"Failed to fetch daily task planning Details for project $projectId");
}
} catch (e, stack) {
log.e("Error fetching daily task data", error: e, stackTrace: stack);
} finally {
isLoading.value = false;
update();
}
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
return;
}
isLoading.value = true;
try {
final response = await ApiService.getAllEmployeesByProject(projectId);
if (response != null && response.isNotEmpty) {
employees =
response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
} else {
log.w("No employees found for project $projectId.");
employees = [];
}
} catch (e) {
log.e("Error fetching employees for project $projectId: $e");
}
update();
isLoading.value = false;
}
}

View File

@ -1,216 +0,0 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
final Logger logger = Logger();
enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
class ReportTaskController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
RxBool isLoading = false.obs;
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
@override
void onInit() {
super.onInit();
logger.i("Initializing ReportTaskController...");
// Add form fields to the validator
basicValidator.addField(
'assigned_date',
label: "Assigned Date",
controller: TextEditingController(),
);
basicValidator.addField(
'work_area',
label: "Work Area",
controller: TextEditingController(),
);
basicValidator.addField(
'activity',
label: "Activity",
controller: TextEditingController(),
);
basicValidator.addField(
'team_size',
label: "Team Size",
controller: TextEditingController(),
);
basicValidator.addField(
'task_id',
label: "Task Id",
controller: TextEditingController(),
);
basicValidator.addField(
'assigned',
label: "Assigned",
controller: TextEditingController(),
);
basicValidator.addField(
'completed_work',
label: "Completed Work",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'comment',
label: "Comment",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'assigned_by',
label: "Assigned By",
controller: TextEditingController(),
);
basicValidator.addField(
'team_members',
label: "Team Members",
controller: TextEditingController(),
);
basicValidator.addField(
'planned_work',
label: "Planned Work",
controller: TextEditingController(),
);
logger.i(
"Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment.");
}
Future<void> reportTask({
required String projectId,
required String comment,
required int completedTask,
required List<Map<String, dynamic>> checklist,
required DateTime reportedDate,
}) async {
logger.i("Starting task report...");
final completedWork =
basicValidator.getController('completed_work')?.text.trim();
if (completedWork == null || completedWork.isEmpty) {
showAppSnackbar(
title: "Error",
message: "Completed work is required.",
type: SnackbarType.error,
);
return;
}
final completedWorkInt = int.tryParse(completedWork);
if (completedWorkInt == null || completedWorkInt <= 0) {
showAppSnackbar(
title: "Error",
message: "Completed work must be a positive integer.",
type: SnackbarType.error,
);
return;
}
final commentField = basicValidator.getController('comment')?.text.trim();
if (commentField == null || commentField.isEmpty) {
showAppSnackbar(
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return;
}
try {
isLoading.value = true;
final success = await ApiService.reportTask(
id: projectId,
comment: commentField,
completedTask: completedTask,
checkList: checklist,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Task reported successfully!",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to report task.",
type: SnackbarType.error,
);
}
} catch (e) {
logger.e("Error reporting task: $e");
showAppSnackbar(
title: "Error",
message: "An error occurred while reporting the task.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
Future<void> commentTask({
required String projectId,
required String comment,
}) async {
logger.i("Starting task comment...");
final commentField = basicValidator.getController('comment')?.text.trim();
if (commentField == null || commentField.isEmpty) {
showAppSnackbar(
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return;
}
try {
isLoading.value = true;
final success = await ApiService.commentTask(
id: projectId,
comment: commentField,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Task commented successfully!",
type: SnackbarType.success,
);
await taskController.fetchTaskData(projectId);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to comment task.",
type: SnackbarType.error,
);
}
} catch (e) {
logger.e("Error commenting task: $e");
showAppSnackbar(
title: "Error",
message: "An error occurred while commenting the task.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,152 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchWorkMasterCategories();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logSafe("Starting task assignment...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logSafe("Task assigned successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<bool> createTask({
required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask,
required String comment,
required String categoryId,
DateTime? assignmentDate,
}) async {
logSafe("Creating new task...", level: LogLevel.info);
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
assignmentDate: assignmentDate,
categoryId: categoryId,
);
if (response == true) {
logSafe("Task created successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to create task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to create task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true;
try {
final response = await ApiService.getMasterWorkCategories();
if (response != null) {
final dataList = response['data'] ?? [];
final parsedList = List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
);
workMasterCategories.assignAll(parsedList);
final mapped = {for (var item in parsedList) item.id: item.name};
categoryIdNameMap.assignAll(mapped);
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
} else {
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
workMasterCategories.clear();
categoryIdNameMap.clear();
}
isLoadingWorkMasterCategories.value = false;
update();
}
void selectCategory(String id) {
selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, );
}
}

View File

@ -0,0 +1,198 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
class DailyTaskController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
DateTime? startDateTask;
DateTime? endDateTask;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
void toggleDate(String dateKey) {
if (expandedDates.contains(dateKey)) {
expandedDates.remove(dateKey);
} else {
expandedDates.add(dateKey);
}
}
RxSet<String> selectedBuildings = <String>{}.obs;
RxSet<String> selectedFloors = <String>{}.obs;
RxSet<String> selectedActivities = <String>{}.obs;
RxSet<String> selectedServices = <String>{}.obs;
RxBool isFilterLoading = false.obs;
RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination
int currentPage = 1;
int pageSize = 20;
bool hasMore = true;
@override
void onInit() {
super.onInit();
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateTask = today.subtract(const Duration(days: 7));
endDateTask = today;
logSafe(
"Default date range set: $startDateTask to $endDateTask",
level: LogLevel.info,
);
}
void clearTaskFilters() {
selectedBuildings.clear();
selectedFloors.clear();
selectedActivities.clear();
selectedServices.clear();
startDateTask = null;
endDateTask = null;
update();
}
Future<void> fetchTaskData(
String projectId, {
int pageNumber = 1,
int pageSize = 20,
bool isLoadMore = false,
}) async {
if (!isLoadMore) {
isLoading.value = true;
currentPage = 1;
hasMore = true;
groupedDailyTasks.clear();
dailyTasks.clear();
} else {
isLoadingMore.value = true;
}
// Create the filter object
final filter = {
"buildingIds": selectedBuildings.toList(),
"floorIds": selectedFloors.toList(),
"activityIds": selectedActivities.toList(),
"serviceIds": selectedServices.toList(),
"dateFrom": startDateTask?.toIso8601String(),
"dateTo": endDateTask?.toIso8601String(),
};
final response = await ApiService.getDailyTasks(
projectId,
filter: filter,
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.isNotEmpty) {
for (var task in response) {
final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
}
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber;
} else {
hasMore = false;
}
isLoading.value = false;
isLoadingMore.value = false;
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?
logSafe(
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
level: LogLevel.info,
);
} else {
logSafe(
"Failed to fetch task filter for projectId: $projectId",
level: LogLevel.warning,
);
}
} catch (e, stack) {
logSafe("Exception in fetchTaskFilter: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isFilterLoading.value = false;
update();
}
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
DailyTaskController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start:
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(),
),
);
if (picked == null) {
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
return;
}
startDateTask = picked.start;
endDateTask = 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);
} else {
logSafe("Project ID is null or empty, skipping fetchTaskData",
level: LogLevel.warning);
}
}
void refreshTasksFromNotification({
required String projectId,
required String taskAllocationId,
}) async {
// re-fetch tasks
await fetchTaskData(projectId);
update(); // rebuilds UI
}
}

View File

@ -0,0 +1,264 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:marco/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = [];
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
List<EmployeeModel> allEmployeesCache = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxBool isAssigningTask = false.obs;
RxnString selectedRoleId = RxnString();
RxBool isFetchingTasks = true.obs;
RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = true.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) return 'This field is required';
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
void updateSelectedEmployees() {
selectedEmployees.value =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
logSafe("Updated selected employees", level: LogLevel.debug);
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logSafe("Role selected", level: LogLevel.info);
}
Future<void> fetchRoles() async {
logSafe("Fetching roles...", level: LogLevel.info);
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe("Roles fetched successfully", level: LogLevel.info);
update();
} else {
logSafe("Failed to fetch roles", level: LogLevel.error);
}
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
String? organizationId,
String? serviceId,
}) async {
isAssigningTask.value = true;
logSafe("Starting assign task...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
organizationId: organizationId,
serviceId: serviceId,
);
isAssigningTask.value = false;
if (response == true) {
logSafe("Task assigned successfully", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
/// Fetch Infra details and then tasks per work area
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) return;
isFetchingTasks.value = true;
try {
final infraResponse = await ApiService.getInfraDetails(
projectId,
serviceId: serviceId,
);
final infraData = infraResponse?['data'] as List<dynamic>?;
if (infraData == null || infraData.isEmpty) {
dailyTasks = [];
return;
}
dailyTasks = infraData.map((buildingJson) {
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors: (buildingJson['floors'] as List<dynamic>)
.map((floorJson) => Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>)
.map((areaJson) => WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [],
))
.toList(),
))
.toList(),
);
return TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
);
}).toList();
await Future.wait(dailyTasks
.expand((task) => task.buildings)
.expand((b) => b.floors)
.expand((f) => f.workAreas)
.map((area) async {
try {
final taskResponse =
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
workItemId: taskJson['id'],
workItem: WorkItem(
id: taskJson['id'],
activityMaster: taskJson['activityMaster'] != null
? ActivityMaster.fromJson(taskJson['activityMaster'])
: null,
workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster'])
: null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate'])
: null,
),
)));
} catch (e, stack) {
logSafe("Error fetching tasks for work area ${area.id}",
level: LogLevel.error, error: e, stackTrace: stack);
}
}));
} catch (e, stack) {
logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isFetchingTasks.value = false;
update();
}
}
Future<void> fetchEmployeesByProjectService({
required String projectId,
String? serviceId,
String? organizationId,
}) async {
isFetchingEmployees.value = true;
try {
final response = await ApiService.getEmployeesByProjectService(
projectId,
serviceId: serviceId ?? '',
organizationId: organizationId ?? '',
);
if (response != null && response.isNotEmpty) {
employees.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
if (serviceId == null && organizationId == null) {
allEmployeesCache = List.from(employees);
}
final currentEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates.removeWhere((key, _) => !currentEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});
selectedEmployees.removeWhere((e) => !currentEmployeeIds.contains(e.id));
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
} else {
employees.clear();
uploadingStates.clear();
selectedEmployees.clear();
logSafe(
serviceId != null || organizationId != null
? "Filtered employees empty"
: "No employees found",
level: LogLevel.warning,
);
}
} catch (e, stack) {
logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack);
if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) {
employees.assignAll(allEmployeesCache);
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});
selectedEmployees.removeWhere((e) => !cachedEmployeeIds.contains(e.id));
} else {
employees.clear();
uploadingStates.clear();
selectedEmployees.clear();
}
} finally {
isFetchingEmployees.value = false;
update();
}
}
}

View File

@ -0,0 +1,296 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
enum ApiStatus { idle, loading, success, failure }
class ReportTaskActionController extends MyController {
final RxBool isLoading = false.obs;
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
final RxList<File> selectedImages = <File>[].obs;
final RxList<WorkStatus> workStatus = <WorkStatus>[].obs;
final RxList<WorkStatus> workStatuses = <WorkStatus>[].obs;
final RxBool showAddTaskCheckbox = false.obs;
final RxBool isAddTaskChecked = false.obs;
final RxBool isLoadingWorkStatus = false.obs;
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
final RxString selectedWorkStatusName = ''.obs;
final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker();
final assignedDateController = TextEditingController();
final workAreaController = TextEditingController();
final activityController = TextEditingController();
final teamSizeController = TextEditingController();
final taskIdController = TextEditingController();
final assignedController = TextEditingController();
final completedWorkController = TextEditingController();
final commentController = TextEditingController();
final assignedByController = TextEditingController();
final teamMembersController = TextEditingController();
final plannedWorkController = TextEditingController();
final approvedTaskController = TextEditingController();
List<TextEditingController> get _allControllers => [
assignedDateController,
workAreaController,
activityController,
teamSizeController,
taskIdController,
assignedController,
completedWorkController,
commentController,
assignedByController,
teamMembersController,
plannedWorkController,
approvedTaskController,
];
@override
void onInit() {
super.onInit();
logSafe("Initializing ReportTaskController...");
_initializeFormFields();
}
@override
void onClose() {
for (final controller in _allControllers) {
controller.dispose();
}
logSafe("Disposed all text controllers in ReportTaskActionController.");
super.onClose();
}
void _initializeFormFields() {
basicValidator
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
..addField('work_area', label: "Work Area", controller: workAreaController)
..addField('activity', label: "Activity", controller: activityController)
..addField('team_size', label: "Team Size", controller: teamSizeController)
..addField('task_id', label: "Task Id", controller: taskIdController)
..addField('assigned', label: "Assigned", controller: assignedController)
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
..addField('comment', label: "Comment", required: true, controller: commentController)
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
..addField('team_members', label: "Team Members", controller: teamMembersController)
..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
}
Future<bool> approveTask({
required String projectId,
required String comment,
required String reportActionId,
required String approvedTaskCount,
List<File>? images,
}) async {
logSafe("approveTask() started", sensitive: false);
if (projectId.isEmpty || reportActionId.isEmpty) {
_showError("Project ID and Report Action ID are required.");
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
return false;
}
final approvedTaskInt = int.tryParse(approvedTaskCount);
final completedWorkInt = int.tryParse(completedWorkController.text.trim());
if (approvedTaskInt == null) {
_showError("Invalid approved task count.");
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
return false;
}
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
_showError("Approved task count cannot exceed completed work.");
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
return false;
}
if (comment.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return false;
}
try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true;
logSafe("Calling _prepareImages() for approval...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.approveTask()");
final success = await ApiService.approveTask(
id: projectId,
workStatus: reportActionId,
approvedTask: approvedTaskInt,
comment: comment,
images: imageData,
);
if (success) {
logSafe("Task approved successfully");
_showSuccess("Task approved successfully!");
await taskController.fetchTaskData(projectId);
return true;
} else {
logSafe("API returned failure on approveTask", level: LogLevel.error);
_showError("Failed to approve task.");
return false;
}
} catch (e, st) {
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred.");
return false;
} finally {
isLoading.value = false;
Future.delayed(const Duration(milliseconds: 500), () {
reportStatus.value = ApiStatus.idle;
});
}
}
Future<void> commentTask({
required String projectId,
required String comment,
List<File>? images,
}) async {
logSafe("commentTask() started", sensitive: false);
if (commentController.text.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return;
}
try {
isLoading.value = true;
logSafe("Calling _prepareImages() for comment...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.commentTask()");
final success = await ApiService.commentTask(
id: projectId,
comment: commentController.text.trim(),
images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
throw Exception("Request timed out.");
});
if (success) {
logSafe("Comment added successfully");
_showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId);
} else {
logSafe("API returned failure on commentTask", level: LogLevel.error);
_showError("Failed to comment task.");
}
} catch (e, st) {
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred while commenting the task.");
} finally {
isLoading.value = false;
}
}
Future<void> fetchWorkStatuses() async {
logSafe("Fetching work statuses...");
isLoadingWorkStatus.value = true;
final response = await ApiService.getWorkStatus();
if (response != null) {
final model = WorkStatusResponseModel.fromJson(response);
workStatus.assignAll(model.data);
logSafe("Fetched ${model.data.length} work statuses");
} else {
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
}
isLoadingWorkStatus.value = false;
update(['dashboard_controller']);
}
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
if (images == null || images.isEmpty) {
logSafe("_prepareImages: No images selected.");
return null;
}
logSafe("_prepareImages: Compressing and encoding images...");
final results = await Future.wait(images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressedBytes),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressedBytes.lengthInBytes,
"description": "Image uploaded for task",
};
}));
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
return results.whereType<Map<String, dynamic>>().toList();
}
String _getContentTypeFromFileName(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
return switch (ext) {
'jpg' || 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif',
_ => 'application/octet-stream',
};
}
Future<void> pickImages({required bool fromCamera}) async {
logSafe("Opening image picker...");
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
logSafe("Image added from camera: ${pickedFile.path}", );
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
logSafe("${pickedFiles.length} images added from gallery.", );
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
logSafe("Removing image at index $index", );
selectedImages.removeAt(index);
}
}
void _showError(String message) => showAppSnackbar(
title: "Error", message: message, type: SnackbarType.error);
void _showSuccess(String message) => showAppSnackbar(
title: "Success", message: message, type: SnackbarType.success);
}

View File

@ -0,0 +1,248 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:convert';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker();
class ReportTaskController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
RxBool isLoading = false.obs;
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
RxList<File> selectedImages = <File>[].obs;
final assignedDateController = TextEditingController();
final workAreaController = TextEditingController();
final activityController = TextEditingController();
final teamSizeController = TextEditingController();
final taskIdController = TextEditingController();
final assignedController = TextEditingController();
final completedWorkController = TextEditingController();
final commentController = TextEditingController();
final assignedByController = TextEditingController();
final teamMembersController = TextEditingController();
final plannedWorkController = TextEditingController();
@override
void onInit() {
super.onInit();
logSafe("Initializing ReportTaskController...");
basicValidator
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
..addField('work_area', label: "Work Area", controller: workAreaController)
..addField('activity', label: "Activity", controller: activityController)
..addField('team_size', label: "Team Size", controller: teamSizeController)
..addField('task_id', label: "Task Id", controller: taskIdController)
..addField('assigned', label: "Assigned", controller: assignedController)
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
..addField('comment', label: "Comment", required: true, controller: commentController)
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
..addField('team_members', label: "Team Members", controller: teamMembersController)
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
logSafe("Form fields initialized.");
}
@override
void onClose() {
[
assignedDateController,
workAreaController,
activityController,
teamSizeController,
taskIdController,
assignedController,
completedWorkController,
commentController,
assignedByController,
teamMembersController,
plannedWorkController,
].forEach((controller) => controller.dispose());
super.onClose();
}
Future<bool> reportTask({
required String projectId,
required String comment,
required int completedTask,
required List<Map<String, dynamic>> checklist,
required DateTime reportedDate,
List<File>? images,
}) async {
logSafe("Reporting task for projectId", );
final completedWork = completedWorkController.text.trim();
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
_showError("Completed work must be a positive number.");
return false;
}
final commentField = commentController.text.trim();
if (commentField.isEmpty) {
_showError("Comment is required.");
return false;
}
try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true;
final imageData = await _prepareImages(images, "task report");
final success = await ApiService.reportTask(
id: projectId,
comment: commentField,
completedTask: int.parse(completedWork),
checkList: checklist,
images: imageData,
);
if (success) {
reportStatus.value = ApiStatus.success;
_showSuccess("Task reported successfully!");
await taskController.fetchTaskData(projectId);
return true;
} else {
reportStatus.value = ApiStatus.failure;
_showError("Failed to report task.");
return false;
}
} catch (e, s) {
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
reportStatus.value = ApiStatus.failure;
_showError("An error occurred while reporting the task.");
return false;
} finally {
isLoading.value = false;
Future.delayed(const Duration(milliseconds: 500), () {
reportStatus.value = ApiStatus.idle;
});
}
}
Future<void> commentTask({
required String projectId,
required String comment,
List<File>? images,
}) async {
logSafe("Submitting comment for project", );
final commentField = commentController.text.trim();
if (commentField.isEmpty) {
_showError("Comment is required.");
return;
}
try {
isLoading.value = true;
final imageData = await _prepareImages(images, "task comment");
final success = await ApiService.commentTask(
id: projectId,
comment: commentField,
images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
logSafe("Task comment request timed out.", level: LogLevel.error);
throw Exception("Request timed out.");
});
if (success) {
_showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId);
} else {
_showError("Failed to comment task.");
}
} catch (e, s) {
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
_showError("An error occurred while commenting the task.");
} finally {
isLoading.value = false;
}
}
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
if (images == null || images.isEmpty) return null;
logSafe("Preparing images for $context upload...");
final results = await Future.wait(images.map((file) async {
try {
final compressed = await compressImageToUnder100KB(file);
if (compressed == null) return null;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressed),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressed.lengthInBytes,
"description": "Image uploaded for $context",
};
} catch (e) {
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
return null;
}
}));
return results.whereType<Map<String, dynamic>>().toList();
}
String _getContentTypeFromFileName(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
return switch (ext) {
'jpg' || 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif',
_ => 'application/octet-stream',
};
}
Future<void> pickImages({required bool fromCamera}) async {
try {
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
}
logSafe("Images picked: ${selectedImages.length}", );
} catch (e) {
logSafe("Error picking images", level: LogLevel.warning, error: e);
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index);
logSafe("Removed image at index $index");
}
}
void _showError(String message) => showAppSnackbar(
title: "Error",
message: message,
type: SnackbarType.error,
);
void _showSuccess(String message) => showAppSnackbar(
title: "Success",
message: message,
type: SnackbarType.success,
);
}

View File

@ -0,0 +1,66 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/all_organization_model.dart';
class AllOrganizationController extends GetxController {
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
Rxn<AllOrganization> selectedOrganization = Rxn<AllOrganization>();
final isLoadingOrganizations = false.obs;
String? passedOrgId;
AllOrganizationController({this.passedOrgId});
@override
void onInit() {
super.onInit();
fetchAllOrganizations();
}
Future<void> fetchAllOrganizations() async {
try {
isLoadingOrganizations.value = true;
final response = await ApiService.getAllOrganizations();
if (response != null && response.data.data.isNotEmpty) {
organizations.value = response.data.data;
// Select organization based on passed ID, or fallback to first
if (passedOrgId != null) {
selectedOrganization.value =
organizations.firstWhere(
(org) => org.id == passedOrgId,
orElse: () => organizations.first,
);
} else {
selectedOrganization.value ??= organizations.first;
}
} else {
organizations.clear();
selectedOrganization.value = null;
}
} catch (e, stackTrace) {
logSafe(
"Failed to fetch organizations: $e",
level: LogLevel.error,
error: e,
stackTrace: stackTrace,
);
organizations.clear();
selectedOrganization.value = null;
} finally {
isLoadingOrganizations.value = false;
}
}
void selectOrganization(AllOrganization? org) {
selectedOrganization.value = org;
}
void clearSelection() {
selectedOrganization.value = null;
}
String get currentSelection => selectedOrganization.value?.name ?? "All Organizations";
}

View File

@ -0,0 +1,52 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project
List<Organization> organizations = [];
/// Currently selected organization (reactive)
Rxn<Organization> selectedOrganization = Rxn<Organization>();
/// Loading state for fetching organizations
final isLoadingOrganizations = false.obs;
/// Fetch organizations assigned to a given project
Future<void> fetchOrganizations(String projectId) async {
try {
isLoadingOrganizations.value = true;
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null && response.data.isNotEmpty) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
organizations = [];
logSafe("No organizations found for project $projectId",
level: LogLevel.warning);
}
} catch (e, stackTrace) {
logSafe("Failed to fetch organizations: $e",
level: LogLevel.error, error: e, stackTrace: stackTrace);
organizations = [];
} finally {
isLoadingOrganizations.value = false;
}
}
/// Select an organization
void selectOrganization(Organization? org) {
selectedOrganization.value = org;
}
/// Clear the selection (set to "All Organizations")
void clearSelection() {
selectedOrganization.value = null;
}
/// Current selection name for UI
String get currentSelection =>
selectedOrganization.value?.name ?? "All Organizations";
}

View File

@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController {
List<Service> services = [];
Service? selectedService;
final isLoadingServices = false.obs;
/// Fetch services assigned to a project
Future<void> fetchServices(String projectId) async {
try {
isLoadingServices.value = true;
final response = await ApiService.getAssignedServices(projectId);
if (response != null) {
services = response.data;
logSafe("Services fetched: ${services.length}");
} else {
logSafe("Failed to fetch services for project $projectId",
level: LogLevel.error);
}
} finally {
isLoadingServices.value = false;
update();
}
}
/// Select a service
void selectService(Service? service) {
selectedService = service;
update();
}
/// Clear selection
void clearSelection() {
selectedService = null;
update();
}
/// Current selected name
String get currentSelection => selectedService?.name ?? "All Services";
}

View File

@ -0,0 +1,136 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart';
class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService();
// Tenant list
final tenants = <Tenant>[].obs;
// Loading state
final isLoading = false.obs;
// Selected tenant ID
final selectedTenantId = RxnString();
// Flag to indicate auto-selection (for splash screen)
final isAutoSelecting = false.obs;
@override
void onInit() {
super.onInit();
loadTenants();
}
/// Load tenants and handle auto-selection
Future<void> loadTenants() async {
isLoading.value = true;
isAutoSelecting.value = true; // show splash during auto-selection
try {
final data = await _tenantService.getTenants();
if (data == null || data.isEmpty) {
tenants.clear();
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
return;
}
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
final recentTenantId = LocalStorage.getRecentTenantId();
// Auto-select if only one tenant
if (tenants.length == 1) {
await _selectTenant(tenants.first.id);
}
// Auto-select recent tenant if available
else if (recentTenantId != null) {
final recentTenant =
tenants.firstWhereOrNull((t) => t.id == recentTenantId);
if (recentTenant != null) {
await _selectTenant(recentTenant.id);
} else {
_clearSelection();
}
}
// No auto-selection
else {
_clearSelection();
}
} catch (e, st) {
logSafe("❌ Exception in loadTenants",
level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Failed to load organizations. Please try again.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
isAutoSelecting.value = false; // hide splash
}
}
/// User manually selects a tenant
Future<void> onTenantSelected(String tenantId) async {
isAutoSelecting.value = true;
await _selectTenant(tenantId);
isAutoSelecting.value = false;
}
/// Internal tenant selection logic
Future<void> _selectTenant(String tenantId) async {
try {
isLoading.value = true;
final success = await _tenantService.selectTenant(tenantId);
if (!success) {
showAppSnackbar(
title: "Error",
message: "Unable to select organization. Please try again.",
type: SnackbarType.error,
);
return;
}
// Update tenant & persist
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
TenantService.setSelectedTenant(selectedTenant);
selectedTenantId.value = tenantId;
await LocalStorage.setRecentTenantId(tenantId);
// Load permissions if token exists
final token = LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
}
await Get.find<PermissionController>().loadData(token);
}
// Navigate **before changing isAutoSelecting**
await Get.offAllNamed('/dashboard');
// Then hide splash
isAutoSelecting.value = false;
} catch (e) {
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred while selecting organization.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Clear tenant selection
void _clearSelection() {
selectedTenantId.value = null;
TenantService.currentTenant = null;
}
}

View File

@ -0,0 +1,106 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart';
class TenantSwitchController extends GetxController {
final TenantService _tenantService = TenantService();
final tenants = <Tenant>[].obs;
final isLoading = false.obs;
final selectedTenantId = RxnString();
@override
void onInit() {
super.onInit();
loadTenants();
}
/// Load all tenants for switching (does not auto-select)
Future<void> loadTenants() async {
isLoading.value = true;
try {
final data = await _tenantService.getTenants();
if (data == null || data.isEmpty) {
tenants.clear();
logSafe("⚠️ No tenants available for switching.", level: LogLevel.warning);
return;
}
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
// Keep current tenant as selected
selectedTenantId.value = TenantService.currentTenant?.id;
} catch (e, st) {
logSafe("❌ Exception in loadTenants", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Failed to load organizations for switching.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Switch to a different tenant and navigate fully
Future<void> switchTenant(String tenantId) async {
if (TenantService.currentTenant?.id == tenantId) return;
isLoading.value = true;
try {
final success = await _tenantService.selectTenant(tenantId);
if (!success) {
logSafe("❌ Tenant switch failed: $tenantId", level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: "Unable to switch organization. Try again.",
type: SnackbarType.error,
);
return;
}
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
TenantService.setSelectedTenant(selectedTenant);
selectedTenantId.value = tenantId;
// Persist recent tenant
await LocalStorage.setRecentTenantId(tenantId);
logSafe("✅ Tenant switched successfully: $tenantId");
// 🔹 Load permissions after tenant switch (null-safe)
final token = await LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
logSafe("✅ PermissionController injected after tenant switch.");
}
await Get.find<PermissionController>().loadData(token);
} else {
logSafe("⚠️ JWT token is null. Cannot load permissions.", level: LogLevel.warning);
}
// FULL NAVIGATION: reload app/dashboard
Get.offAllNamed('/dashboard');
showAppSnackbar(
title: "Success",
message: "Switched to organization: ${selectedTenant.name}",
type: SnackbarType.success,
);
} catch (e, st) {
logSafe("❌ Exception in switchTenant", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred while switching organization.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,26 +0,0 @@
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart';
import 'package:marco/model/drag_n_drop_model.dart';
class DragNDropController extends MyController {
List<DragNDropModel> dragNDrop = [];
final scrollController = ScrollController();
final gridViewKey = GlobalKey();
List<String> dummyTexts = List.generate(12, (index) => MyTextUtils.getDummyText(60));
@override
void onInit() {
DragNDropModel.dummyList.then((value) {
dragNDrop = value;
update();
});
super.onInit();
}
void onReorder(int oldIndex, int newIndex) {
final item = dragNDrop.removeAt(oldIndex);
dragNDrop.insert(newIndex, item);
update();
}
}

Some files were not shown because too many files have changed in this diff Show More