Compare commits

...

265 Commits

Author SHA1 Message Date
9ec7dee0f1 Merge pull request 'Collection_Purchase_Widget' (#87) from Collection_Purchase_Widget into main
Reviewed-on: #87
2025-12-06 05:29:39 +00:00
48a96a703b addedapi for purchase invoice 2025-12-05 17:24:57 +05:30
8686d696f0 improved dashboard controller 2025-12-05 17:00:22 +05:30
5d73fd6f4f added api for dashboard for collection widget 2025-12-05 16:52:24 +05:30
1717cd5e2b added mytext 2025-12-05 10:53:36 +05:30
717f0c92af feat: Add CompactPurchaseInvoiceDashboard widget and integrate into dashboard screen
- Implemented a new widget for displaying purchase invoice metrics with internal dummy data.
- Integrated the CompactPurchaseInvoiceDashboard into the dashboard screen layout.
- Updated imports in dashboard_screen.dart to include the new purchase invoice dashboard widget.
2025-12-04 17:54:48 +05:30
633a75fe92 fix: Update base URL in ApiEndpoints and increment app version to 1.0.0+18 2025-12-03 17:38:20 +05:30
8fb32c7c8e Refactor dashboard screen layout and improve loading state handling
- Simplified initialization of DynamicMenuController.
- Added loading skeleton for employee quick action cards.
- Removed daily task planning and daily progress report from card order.
- Adjusted grid layout parameters for better responsiveness.
- Cleaned up code formatting for improved readability.
2025-12-03 17:08:25 +05:30
3dfa6e5877 feat: Add infrastructure project details and list models
- Implemented ProjectDetailsResponse and ProjectData models for handling project details.
- Created ProjectsResponse and ProjectsPageData models for listing infrastructure projects.
- Added InfraProjectScreen and InfraProjectDetailsScreen for displaying project information.
- Integrated search functionality in InfraProjectScreen to filter projects.
- Updated DailyTaskPlanningScreen and DailyProgressReportScreen to accept projectId as a parameter.
- Removed unnecessary dependencies and cleaned up code for better maintainability.
2025-12-03 16:49:46 +05:30
03e3e7b5db feat: Enhance Dashboard with Attendance and Infra Projects
- Added employee attendance fetching in DashboardController.
- Introduced loading state for employees in the dashboard.
- Updated API endpoints to include attendance for the dashboard.
- Created a new InfraProjectsMainScreen with tab navigation for task planning and progress reporting.
- Improved UI components for better user experience in the dashboard.
- Refactored project selection and quick actions in the dashboard.
- Added permission constants for infrastructure projects.
2025-12-03 13:09:48 +05:30
cf85c17d75 Update base URL in ApiEndpoints to point to the correct API endpoint 2025-12-01 15:13:44 +05:30
66445b1e54 Add dynamic app version display in WelcomeScreen 2025-12-01 15:08:22 +05:30
7c86d0c5c2 Refactor connectivity handling in MainWrapper and enhance offline experience in MyApp 2025-12-01 15:04:59 +05:30
012d40cd57 Implement job comments feature: add comment widget, API endpoints, and controller methods for fetching and posting comments 2025-12-01 14:38:58 +05:30
d4d678d98a Remove padding from card wrapper and comment out quick actions in dashboard for layout adjustments 2025-11-29 15:33:49 +05:30
6ed069d924 Replace CircularProgressIndicator with skeleton loaders in dashboard; reintroduce project dropdown list with search functionality 2025-11-29 15:09:10 +05:30
c9e73771b0 Refactor CustomAppBar to StatefulWidget; implement project selection dropdown and improve UI interactions 2025-11-29 15:02:03 +05:30
ed2eb014d8 Refactor attendance management screens for improved readability and g 2025-11-29 14:36:44 +05:30
3ad48016f3 Enhance splash screen with improved animations and loading indicator; update logo size and message for better visibility 2025-11-29 12:34:30 +05:30
37ce612fca Enhance attendance management with tabbed navigation and permission handling; improve UI consistency and loading states 2025-11-29 12:34:22 +05:30
7bef2e9d89 Add job status management to service project details screen 2025-11-28 18:30:08 +05:30
341d779499 Refactor service project job handling and improve tag management 2025-11-28 15:34:13 +05:30
65fbef3441 Enhance UI and Navigation
- Added navigation to the dashboard after applying the theme in ThemeController.
- Introduced a new PillTabBar widget for a modern tab design across multiple screens.
- Updated dashboard screen to improve button actions and UI consistency.
- Refactored contact detail screen to streamline layout and enhance gradient effects.
- Implemented PillTabBar in directory main screen, expense screen, and payment request screen for consistent tab navigation.
- Improved layout structure in user document screen and employee profile screen for better user experience.
- Enhanced service project details screen with a modern tab bar implementation.
2025-11-28 14:48:39 +05:30
259f2aa928 Refactor UI components to use CustomAppBar and improve layout consistency
- Replaced existing AppBar implementations with CustomAppBar in multiple screens including PaymentRequestDetailScreen, PaymentRequestMainScreen, ServiceProjectDetailsScreen, JobDetailsScreen, DailyProgressReportScreen, DailyTaskPlanningScreen, and ServiceProjectScreen.
- Enhanced visual hierarchy by adding gradient backgrounds behind app bars for better aesthetics.
- Streamlined SafeArea usage to ensure proper content display across different devices.
- Improved code readability and maintainability by removing redundant code and consolidating UI elements.
2025-11-27 19:07:24 +05:30
84156167ea Merge pull request 'Dev_Manish_Bug' (#85) from Dev_Manish_Bug into main
Reviewed-on: #85
2025-11-27 05:31:48 +00:00
260b762724 Merge branch 'Dev_Manish_Bug' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Dev_Manish_Bug 2025-11-27 10:55:29 +05:30
951dd22ecc bug fixed for 1776, 1775, 1736, 1626 2025-11-27 10:55:12 +05:30
33ae5c0333 bug fixed for 1776, 1775, 1736, 1626 2025-11-26 15:20:20 +05:30
60bc53afef refactor: remove project selection from layout and update dashboard layout 2025-11-25 18:13:22 +05:30
90a3a85753 Merge pull request 'Dev_Manish_24/11' (#84) from Dev_Manish_24/11 into main
Reviewed-on: #84
2025-11-25 10:13:59 +00:00
71a48750bd Merge branch 'Dev_Manish_24/11' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Dev_Manish_24/11 2025-11-25 15:32:10 +05:30
e2bee52820 update screen with employee should place at same place after selecting 2025-11-25 15:32:01 +05:30
bb723b91e5 height change to 0.85 2025-11-25 15:32:01 +05:30
55122b5b13 update for tag should submit after space 2025-11-25 15:32:00 +05:30
2700864adf added safe area to support mobile screen horizontally 2025-11-25 15:32:00 +05:30
18fbfaa42d enhacement of UI for mobile screen responsiveness 2025-11-25 15:32:00 +05:30
3e8bd1c41d tag can submit after space 2025-11-25 15:32:00 +05:30
4d2b05cdc2 multiple tags can add using space 2025-11-25 15:32:00 +05:30
ddb1440211 implemented new multi select role bottom sheet 2025-11-25 15:32:00 +05:30
e4f55d82f7 reenhacenment of employee selector 2025-11-25 15:32:00 +05:30
081849f964 added needed vaiables for employee selector 2025-11-25 15:32:00 +05:30
a3b95b4d07 reenhacement of employee selector 2025-11-25 15:32:00 +05:30
ed4a558894 reenhancement of employee selector 2025-11-25 15:32:00 +05:30
e2897e4dde reenhancement of employee selector 2025-11-25 15:32:00 +05:30
08777176df update screen with employee should place at same place after selecting 2025-11-25 15:30:06 +05:30
81f74004b8 height change to 0.85 2025-11-25 13:58:47 +05:30
3fa578f1b4 feat: update Kotlin plugin version and modify API endpoints for improved functionality 2025-11-25 13:19:00 +05:30
28c1c36e07 update for tag should submit after space 2025-11-25 13:18:15 +05:30
24bfccfdf6 added safe area to support mobile screen horizontally 2025-11-25 12:45:52 +05:30
5bed5bd2f4 enhacement of UI for mobile screen responsiveness 2025-11-25 12:17:45 +05:30
41112a3eea tag can submit after space 2025-11-24 16:44:09 +05:30
9eb72a60ac multiple tags can add using space 2025-11-24 15:42:38 +05:30
b401e98658 implemented new multi select role bottom sheet 2025-11-24 15:28:31 +05:30
56602328ca reenhacenment of employee selector 2025-11-24 15:15:35 +05:30
aece165c38 added needed vaiables for employee selector 2025-11-24 15:10:31 +05:30
38626ebef0 reenhacement of employee selector 2025-11-24 15:08:03 +05:30
6b58085434 reenhancement of employee selector 2025-11-24 15:00:52 +05:30
5e1379b74b reenhancement of employee selector 2025-11-24 14:56:09 +05:30
c69e4280ef Merge pull request 'Service_Project_Branching' (#82) from Service_Project_Branching into main
Reviewed-on: #82
2025-11-24 06:32:57 +00:00
761cd1f7e8 chore: update version number to 1.0.0+16 in pubspec.yaml 2025-11-24 10:28:02 +05:30
516d6b0489 feat: enhance notification action handling and add refresh indicators in ServiceProjectDetailsScreen 2025-11-22 16:17:17 +05:30
e9075dcdf5 feat: add project name support in CustomAppBar and related screens for improved context 2025-11-22 15:15:38 +05:30
5c53a3f4be Refactor project structure and rename from 'marco' to 'on field work'
- Updated import paths across multiple files to reflect the new package name.
- Changed application name and identifiers in CMakeLists.txt, Runner.rc, and other configuration files.
- Modified web index.html and manifest.json to update the app title and name.
- Adjusted macOS and Windows project settings to align with the new application name.
- Ensured consistency in naming across all relevant files and directories.
2025-11-22 14:20:37 +05:30
603e7ee7e5 feat: add advance payment indicator in payment request screens and improve code formatting 2025-11-21 16:57:37 +05:30
2c98ac359c feat: enhance payment request data models for null safety and improve filter options handling in PaymentRequestController 2025-11-21 16:42:33 +05:30
fc78806af2 feat: implement job search functionality and archived job toggle in JobsTab; refactor ServiceProjectDetailsScreen to integrate JobsTab 2025-11-21 16:33:09 +05:30
cf7021a982 feat: refactor ExpenseDetailScreen to use tagged controller for improved state management 2025-11-20 18:02:03 +05:30
efefb1c34b feat: add app version display to user profile bar and update version number in pubspec.yaml 2025-11-20 17:19:48 +05:30
846ca64402 feat: update project fetching method to use getGlobalProjects for improved data retrieval 2025-11-20 16:52:51 +05:30
765f537cf9 feat: enhance tenant fetching logic to handle empty responses and invalid JSON gracefully 2025-11-20 16:33:08 +05:30
87a7d19672 feat: enhance job detail screen to display project branch information and improve payment request handling 2025-11-20 16:21:41 +05:30
d0b40a9822 feat: enhance JSON parsing in ServiceProjectAllocationResponse and related models to handle null values gracefully 2025-11-20 11:55:15 +05:30
c44d10d35a feat: update JobDetailsResponse and JobData models to support nullable fields and improve JSON parsing 2025-11-20 11:12:59 +05:30
02ef996753 feat: update projectBranchId field in API service and increment version to 1.0.0+14 2025-11-20 09:58:36 +05:30
ec6a45ed43 feat: integrate snackbar notifications for document and job updates in user document controller and job detail screen 2025-11-19 17:29:10 +05:30
92c739045c feat: rename Expense Type to Expense Category across controllers, models, and views for consistency 2025-11-19 17:20:44 +05:30
5dd09869ad feat: enhance ExpenseDetailModel and ExpenseDetailScreen with additional fields and improved UI structure 2025-11-19 17:17:04 +05:30
8edd189479 feat: add branch selection functionality and API integration for service project jobs 2025-11-19 16:36:32 +05:30
bbadcc4139 feat: update API endpoints and enhance attendance log handling in job detail screen 2025-11-19 15:44:55 +05:30
d05e26bc87 feat: enhance date range picker logic to restrict date selection and improve validation 2025-11-19 12:19:47 +05:30
49326e9929 feat: enhance employee selection and validation in service project job form 2025-11-19 12:00:26 +05:30
0e575c393d feat: enhance job fetching logic and add auto-refresh after job addition 2025-11-19 11:28:39 +05:30
804f0eba7b enhansed ui 2025-11-18 17:50:47 +05:30
253aa55a80 added manage team 2025-11-18 17:44:12 +05:30
17fc04f3ee feat: add cached_network_image dependency and improve image handling in attendance logs 2025-11-18 12:11:30 +05:30
474ecac53c refactor: improve null safety in employee details and enhance UI handling 2025-11-18 11:17:33 +05:30
dc4ea7979c resoled payment request model crash issue 2025-11-18 10:57:46 +05:30
618ac6f27a chore: update package name and bundle identifier to com.marcoonfieldwork.aiot 2025-11-17 20:17:14 +05:30
2260382990 refactor: enhance job attendance handling and improve null safety in models 2025-11-17 18:03:59 +05:30
919310644d added tag in tag out api and code 2025-11-17 17:36:11 +05:30
605617695e refactor: improve code readability and consistency in JobDetailsScreen 2025-11-17 16:02:03 +05:30
4d47ee3002 fixed issues 2025-11-17 15:23:51 +05:30
9dd66bd297 Merge pull request 'OH_Dev_Manish' (#81) from OH_Dev_Manish into Service_Project_Feature
Reviewed-on: #81
2025-11-17 09:48:29 +00:00
f506bd1bfc Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-17 15:16:01 +05:30
39abfa2260 Rebase issues solved 2025-11-17 15:15:13 +05:30
ed88e0dfc9 implementation of Manage reporting 2025-11-17 15:14:48 +05:30
a910d37f22 All Employees fetching task done in advance payment screen 2025-11-17 15:14:13 +05:30
2df4afcea7 implementation of manage reporting inside employee profile 2025-11-17 15:14:13 +05:30
ceee12df33 implementation of Manage reporting 2025-11-17 15:14:12 +05:30
58ed2676e3 implementation of Manage reporting 2025-11-17 15:14:12 +05:30
e1b6794e15 made chnages in details screen and list screen 2025-11-17 15:14:12 +05:30
01f3cd24da added service projet screen 2025-11-17 15:14:12 +05:30
a8b31b3a95 All Employees fetching task done in advance payment screen 2025-11-17 15:13:51 +05:30
b9195c7fdd .. 2025-11-17 15:13:31 +05:30
be835bd2bd implementation of manage reporting inside employee profile 2025-11-17 15:13:31 +05:30
2d35addeeb implementation of Manage reporting 2025-11-17 15:13:31 +05:30
e3fe3c0d86 implementation of manage reporting inside employee profile 2025-11-17 15:13:31 +05:30
b8aefc544f implementation of Manage reporting 2025-11-17 15:13:30 +05:30
58c6a3f9af All Employees fetching task done in advance payment screen 2025-11-17 15:13:30 +05:30
1f694381a1 .. 2025-11-17 15:13:30 +05:30
ba6d58fd0a implementation of manage reporting inside employee profile 2025-11-17 15:13:30 +05:30
e9a43af350 implementation of Manage reporting 2025-11-17 15:13:15 +05:30
fdd2d51ed0 implementation of manage reporting inside employee profile 2025-11-17 15:12:52 +05:30
cddc2990fb created new emploee selector bottomsheet 2025-11-17 15:12:22 +05:30
3974ae8a4c Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-17 15:09:21 +05:30
68ed97b64a Rebase issues solved 2025-11-17 15:08:18 +05:30
793eeec7fa Rebase issues solved 2025-11-17 15:07:46 +05:30
29af4e53a9 implementation of Manage reporting 2025-11-17 15:07:13 +05:30
0b894a5091 All Employees fetching task done in advance payment screen 2025-11-17 15:06:39 +05:30
8153d315b7 implementation of manage reporting inside employee profile 2025-11-17 15:06:39 +05:30
bcfa29c0ed implementation of Manage reporting 2025-11-17 15:06:39 +05:30
7f7d73c790 implementation of Manage reporting 2025-11-17 15:06:38 +05:30
7fdeb103cf made chnages in details screen and list screen 2025-11-17 15:06:38 +05:30
3bae3429c6 added service projet screen 2025-11-17 15:06:38 +05:30
79b727aeb1 implementation of Manage reporting 2025-11-17 15:05:39 +05:30
52da540271 Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-17 15:00:04 +05:30
b2619eabe9 added edit job fucntioanllity 2025-11-17 14:59:01 +05:30
b21c00faac added service project job details screen 2025-11-17 14:58:27 +05:30
083eece75b added edit job api 2025-11-17 14:56:29 +05:30
bc62bc903d added service project job details screen 2025-11-17 14:56:29 +05:30
a39af6519a added edit job api 2025-11-17 14:56:29 +05:30
d530077444 added service project job details screen 2025-11-17 14:56:28 +05:30
a2aef3355f fixed the model crashing 2025-11-17 14:56:05 +05:30
de657a4bce All Employees fetching task done in advance payment screen 2025-11-17 14:56:04 +05:30
4c9fecdfcf UI Enhancements in Finance Module – Payment Request & Expense Screens 2025-11-17 14:50:59 +05:30
52782098f8 All Employees fetching task done in advance payment screen 2025-11-17 14:50:59 +05:30
fc09673cd0 .. 2025-11-17 14:50:59 +05:30
bd6fc9f81b implementation of manage reporting inside employee profile 2025-11-17 14:50:59 +05:30
3660291482 implementation of Manage reporting 2025-11-17 14:50:59 +05:30
81692a1930 implementation of Manage reporting 2025-11-17 14:50:59 +05:30
5c386cb8ff implementation of manage reporting inside employee profile 2025-11-17 14:50:58 +05:30
0334a6b7a8 implementation of Manage reporting 2025-11-17 14:50:58 +05:30
38528f1ade removed unused code 2025-11-17 14:50:58 +05:30
8619a9e43e created new emploee selector bottomsheet 2025-11-17 14:50:58 +05:30
5a3e9c2977 corrected the payment proceed validation 2025-11-17 14:50:58 +05:30
9f72f689c5 made chnages into dynamic menus 2025-11-17 14:50:58 +05:30
c8a8d45c66 made chnages in details screen and list screen 2025-11-17 14:50:57 +05:30
10e9f4a315 added edit job fucntioanllity 2025-11-17 14:49:44 +05:30
5ce37379cb Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-17 14:47:42 +05:30
d5868d213a Rebase issues solved 2025-11-17 14:46:33 +05:30
14ed2a8417 implementation of Manage reporting 2025-11-17 14:41:46 +05:30
55fffbbe78 implementation of Manage reporting 2025-11-17 14:40:51 +05:30
6f8f1f1856 All Employees fetching task done in advance payment screen 2025-11-17 14:39:22 +05:30
548ecb33a6 implementation of manage reporting inside employee profile 2025-11-17 14:39:22 +05:30
01d7a2fd3b implementation of Manage reporting 2025-11-17 14:39:09 +05:30
8dabac2eff implementation of Manage reporting 2025-11-17 14:38:26 +05:30
b476986807 made chnages in details screen and list screen 2025-11-17 14:36:14 +05:30
2149fd6bc5 added service projet screen 2025-11-17 14:36:00 +05:30
30405f7de4 Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-17 14:30:24 +05:30
ffb02027cc Rebase issues solved 2025-11-17 13:02:16 +05:30
d3b8ce17b8 All Employees fetching task done in advance payment screen 2025-11-17 12:58:43 +05:30
e024cf4576 .. 2025-11-17 12:54:15 +05:30
8edf3e89a5 implementation of manage reporting inside employee profile 2025-11-17 12:54:15 +05:30
a61b9c50af implementation of Manage reporting 2025-11-17 12:48:51 +05:30
509813ca2d implementation of manage reporting inside employee profile 2025-11-17 12:46:27 +05:30
61ca8f4250 implementation of Manage reporting 2025-11-17 12:46:27 +05:30
36a3d586a0 UI Enhancements in Finance Module – Payment Request & Expense Screens 2025-11-17 12:44:59 +05:30
899252f215 All Employees fetching task done in advance payment screen 2025-11-17 12:44:59 +05:30
411e9afcc9 .. 2025-11-17 12:44:59 +05:30
f3f1f02282 implementation of manage reporting inside employee profile 2025-11-17 12:44:59 +05:30
477667c06a implementation of Manage reporting 2025-11-17 12:44:59 +05:30
575fcc200c implementation of Manage reporting 2025-11-17 12:44:59 +05:30
1b883ac524 implementation of manage reporting inside employee profile 2025-11-17 12:44:59 +05:30
c631f8b092 implementation of Manage reporting 2025-11-17 12:44:59 +05:30
bdfc492e65 removed unused code 2025-11-17 12:44:59 +05:30
674a9c691b created new emploee selector bottomsheet 2025-11-17 12:44:58 +05:30
02c10d8115 corrected the payment proceed validation 2025-11-17 12:44:58 +05:30
456df30c8e made chnages into dynamic menus 2025-11-17 12:44:58 +05:30
d229facfba made chnages in details screen and list screen 2025-11-17 12:44:58 +05:30
4900ce87f7 Rebase issues solved 2025-11-17 11:32:07 +05:30
3ebc9ab602 Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-17 11:25:21 +05:30
7e3d2ac2b9 added edit job api 2025-11-17 11:25:16 +05:30
7ee65115be added service project job details screen 2025-11-17 11:24:37 +05:30
e1952d505b modified api service 2025-11-17 11:22:31 +05:30
2b42393741 Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-17 11:17:21 +05:30
ffd18a9e40 added edit job api 2025-11-17 11:17:12 +05:30
9ef1f57ca4 added service project job details screen 2025-11-17 11:17:12 +05:30
6e37e0dd04 fixed the model crashing 2025-11-17 11:16:16 +05:30
ee1e5014b4 added edit job api 2025-11-17 11:15:17 +05:30
31a27da85d added service project job details screen 2025-11-17 11:08:50 +05:30
2e750ad19c Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-17 11:03:29 +05:30
5f99827b23 UI Enhancements in Finance Module – Payment Request & Expense Screens 2025-11-17 10:51:18 +05:30
70c8160dce All Employees fetching task done in advance payment screen 2025-11-17 10:51:18 +05:30
831aacc202 .. 2025-11-17 10:49:11 +05:30
fe4a64e4d2 implementation of manage reporting inside employee profile 2025-11-17 10:49:11 +05:30
2d4d71b847 implementation of Manage reporting 2025-11-17 10:49:11 +05:30
03b16e0ad9 implementation of Manage reporting 2025-11-17 10:39:28 +05:30
c0b4a74f87 implementation of manage reporting inside employee profile 2025-11-17 10:07:32 +05:30
b0472503d1 implementation of Manage reporting 2025-11-17 10:07:32 +05:30
9666d39d5e fixed the model crashing 2025-11-15 15:35:56 +05:30
befeef3c02 removed unused code 2025-11-14 15:36:32 +05:30
1f47a55d9c created new emploee selector bottomsheet 2025-11-14 15:35:41 +05:30
3bc7b5092f UI Enhancements in Finance Module – Payment Request & Expense Screens 2025-11-13 17:59:51 +05:30
2255ae29fa All Employees fetching task done in advance payment screen 2025-11-13 15:27:13 +05:30
214816ac0f corrected the payment proceed validation 2025-11-13 12:26:39 +05:30
c4bb8331c9 .. 2025-11-13 11:30:36 +05:30
00aa53d496 Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-13 10:54:25 +05:30
818e0a144a implementation of manage reporting inside employee profile 2025-11-13 10:51:32 +05:30
0b60276e03 implementation of Manage reporting 2025-11-13 10:50:05 +05:30
68f17214fd made chnages into dynamic menus 2025-11-12 17:58:33 +05:30
cdba511d43 implementation of manage reporting inside employee profile 2025-11-12 17:17:01 +05:30
6c14fc1507 made chnages in details screen and list screen 2025-11-12 16:48:53 +05:30
feb12862fa Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-12 16:00:25 +05:30
1de5e7fae7 added service projet screen 2025-11-12 16:00:20 +05:30
7b6520597e added routes 2025-11-12 15:58:48 +05:30
11393922e2 Chnaged the Expense Naming and Dashboard menu handeling and chart modifications 2025-11-12 15:58:29 +05:30
cb00911983 added service projet screen 2025-11-12 15:36:10 +05:30
0414a109be Merge branch 'OH_Dev_Manish' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into OH_Dev_Manish 2025-11-12 12:16:27 +05:30
340d0b8a1e implementation of Manage reporting 2025-11-12 12:16:05 +05:30
8fb65d31e2 Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-12 12:07:10 +05:30
7f756f3d4c implementation of Manage reporting 2025-11-12 12:04:35 +05:30
d2c7f92a02 added routes 2025-11-12 12:04:12 +05:30
6b56351a49 displayed possible filds in details screen 2025-11-12 11:53:43 +05:30
5db53c29df added validation for payment request in and expense reumbersemrnt bottomsheet 2025-11-12 11:23:13 +05:30
be2f97cc0e fixed the make expense button display issue on creaying expense also 2025-11-11 19:34:54 +05:30
44bbbf9bfb fixed the assign project to eployeee issue after creating employee and added fabicon on expense details screen for assign to project 2025-11-11 19:21:53 +05:30
f62b7d924b fixed add and edit expense issue 2025-11-11 18:50:13 +05:30
ee9c94dc86 fixed the wrong time display 2025-11-11 18:07:20 +05:30
3e99fc67a3 added widgets in finance screen 2025-11-11 17:25:42 +05:30
cffa4456b9 fixed wrong list display due to date format , and added tds calculation in the payment proceed sheet 2025-11-11 17:03:06 +05:30
af5bfcaa59 replaced fab icon with icon on page 2025-11-11 16:12:13 +05:30
7f0c4d075d fixed the no data building are dispalyed 2025-11-11 15:27:19 +05:30
fe950c5b07 fixed pegination and for rejected status no data displayed 2025-11-11 14:55:37 +05:30
034de5a601 added permission for create payment request button 2025-11-11 14:38:05 +05:30
77a6d50571 added dynamic menu for finance cards as per expense title 2025-11-11 14:31:16 +05:30
077dbf35ee added routes 2025-11-11 14:21:54 +05:30
f232ab42b0 Merge branch 'Service_Project_Feature' of https://git.marcoaiot.com/admin/marco.pms.mobileapp into Service_Project_Feature 2025-11-11 14:21:01 +05:30
3543770654 made chnages to expense category of type 2025-11-11 12:53:55 +05:30
1dc68033df fixed category issue while updating and adding expense 2025-11-11 12:48:57 +05:30
b1f5fb8d78 chnaged expense type to category 2025-11-10 16:39:25 +05:30
8d688f2575 fixed date pickering issue 2025-11-10 16:22:44 +05:30
502bb1e2d9 fixed add remove doscument 2025-11-10 16:10:45 +05:30
a88d085001 fixed the loading issue 2025-11-10 14:38:10 +05:30
fd57686c8a corrected the attachment issue 2025-11-10 12:57:32 +05:30
fd861b3adb chnaged make expense flow 2025-11-10 12:08:07 +05:30
1c253df5f9 fixed the expense screen neigation build issue 2025-11-10 10:26:03 +05:30
92f7fec083 added payment request screens 2025-11-08 16:18:26 +05:30
1070f04d1a added finance code 2025-11-08 15:46:28 +05:30
eb46194679 made chnages in employee details screen 2025-11-08 12:17:17 +05:30
910bb5e6b4 removed employees by project 2025-11-08 11:53:34 +05:30
80d7ef96cb feat: Add daily progress planning skeleton loader and improve UI formatting 2025-11-04 17:41:29 +05:30
99166801da feat: Implement lazy loading for building infrastructure and enhance task fetching logic 2025-11-04 17:07:54 +05:30
87520d7664 fix: Adjust spacing and add today's planned tasks display in daily task planning 2025-11-04 16:26:33 +05:30
e8d4931016 fix: Enhance employee sorting by adding latest entry comparison after action priority 2025-11-04 15:45:53 +05:30
f0d42edcc1 fix: Update regularization time picker to use reference time and allow past times for regularization 2025-11-04 15:18:42 +05:30
82c2cc3c58 feat: Improve task data fetching by clearing grouped tasks on new load and preventing duplicates 2025-11-04 14:52:17 +05:30
ac7a75c92f Refactor date handling in controllers and UI components
- Updated `user_document_controller.dart` to use DateTime for start and end dates.
- Enhanced `daily_task_controller.dart` with Rx fields for date range and added methods to update date ranges.
- Reverted API base URL in `api_endpoints.dart` to stage environment.
- Introduced `date_range_picker.dart` widget for reusable date selection.
- Integrated `DateRangePickerWidget` in attendance and daily task filter bottom sheets.
- Simplified date range selection logic in `attendance_filter_sheet.dart`, `daily_progress_report_filter.dart`, and `user_document_filter_bottom_sheet.dart`.
- Updated expense filter bottom sheet to utilize the new date range picker.
2025-11-04 14:15:21 +05:30
b1437db9e0 fix: Update text label in attendance filter bottom sheet for clarity 2025-11-03 17:46:33 +05:30
4d3a42e851 feat: Update image picker section to use dynamic primary color for icons and buttons 2025-11-03 17:07:15 +05:30
c78c14e409 refactor: Clean up attendance controller initialization and enhance project change handling 2025-11-03 16:43:51 +05:30
ebc8996f71 feat: Enhance monthly expense dashboard with expense type filtering functionality 2025-11-03 15:00:59 +05:30
3c95583a23 feat: Add monthly expense reporting functionality with dashboard integration 2025-11-03 14:24:39 +05:30
91174dd960 fix: Adjust padding and inner radius for expense donut chart based on mobile layout 2025-11-03 10:25:58 +05:30
9e52e50cc9 fix: Change ProjectController injection method from Get.find to Get.put for proper dependency management 2025-11-03 10:14:49 +05:30
177f8c32e2 feat: Add Expense By Status Widget and related models
- Implemented ExpenseByStatusWidget to display expenses categorized by status.
- Added ExpenseReportResponse, ExpenseTypeReportResponse, and related models for handling expense data.
- Introduced Skeleton loaders for expense status and charts for better UI experience during data loading.
- Updated DashboardScreen to include the new ExpenseByStatusWidget and ensure proper integration with existing components.
2025-11-01 17:29:16 +05:30
303 changed files with 30374 additions and 7751 deletions

View File

@ -1,4 +1,4 @@
# marco # On Field Work
A new Flutter project. A new Flutter project.

View File

@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
// Define the namespace for your Android application // Define the namespace for your Android application
namespace = "com.marco.aiot" namespace = "com.marcoonfieldwork.aiot"
// Set the compile SDK version based on Flutter's configuration // Set the compile SDK version based on Flutter's configuration
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
// Set the NDK version based on Flutter's configuration // Set the NDK version based on Flutter's configuration
@ -37,7 +37,7 @@ android {
// Default configuration for your application // Default configuration for your application
defaultConfig { defaultConfig {
// Specify your unique Application ID. This identifies your app on Google Play. // Specify your unique Application ID. This identifies your app on Google Play.
applicationId = "com.marco.aiot" applicationId = "com.marcoonfieldwork.aiot"
// Set minimum and target SDK versions based on Flutter's configuration // Set minimum and target SDK versions based on Flutter's configuration
minSdk = 23 minSdk = 23
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion

View File

@ -9,7 +9,7 @@
"client_info": { "client_info": {
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024", "mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
"android_client_info": { "android_client_info": {
"package_name": "com.marco.aiot" "package_name": "com.marcoonfieldwork.aiot"
} }
}, },
"oauth_client": [], "oauth_client": [],

View File

@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:label="Marco" android:label="On Field Work"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

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

View File

@ -19,7 +19,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.0" apply false id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false id "org.jetbrains.kotlin.android" version "2.2.21" apply false
id("com.google.gms.google-services") version "4.4.2" apply false id("com.google.gms.google-services") version "4.4.2" apply false
} }

View File

@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# App info # App info
APP_NAME="Marco" APP_NAME="On Field Work"
BUILD_DIR="build/app/outputs" BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}" echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"

View File

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

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Marco</string> <string>On Field Work</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>marco</string> <string>on field work</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -8,5 +8,5 @@ class AppConstant {
static int iOSAppVersion = 1; static int iOSAppVersion = 1;
static String version = "1.0.0"; static String version = "1.0.0";
static String get appName => 'Marco'; static String get appName => 'On Field Work';
} }

View File

@ -1,61 +1,67 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:marco/model/attendance/attendance_model.dart'; import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:marco/model/project_model.dart'; import 'package:on_field_work/model/attendance/attendance_log_model.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
import 'package:marco/model/attendance/attendance_log_model.dart'; import 'package:on_field_work/model/attendance/attendance_model.dart';
import 'package:marco/model/regularization_log_model.dart'; import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/model/attendance/attendance_log_view_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:on_field_work/model/project_model.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/model/regularization_log_model.dart';
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
// Data models // ------------------ Data Models ------------------
List<AttendanceModel> attendances = []; final List<AttendanceModel> attendances = <AttendanceModel>[];
List<ProjectModel> projects = []; final List<ProjectModel> projects = <ProjectModel>[];
List<EmployeeModel> employees = []; final List<EmployeeModel> employees = <EmployeeModel>[];
List<AttendanceLogModel> attendanceLogs = []; final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
List<RegularizationLogModel> regularizationLogs = []; final List<RegularizationLogModel> regularizationLogs =
List<AttendanceLogViewModel> attendenceLogsView = []; <RegularizationLogModel>[];
final List<AttendanceLogViewModel> attendenceLogsView =
<AttendanceLogViewModel>[];
// ------------------ Organizations ------------------ // ------------------ Organizations ------------------
List<Organization> organizations = []; final List<Organization> organizations = <Organization>[];
Organization? selectedOrganization; Organization? selectedOrganization;
final isLoadingOrganizations = false.obs; final RxBool isLoadingOrganizations = false.obs;
// States // ------------------ States ------------------
String selectedTab = 'todaysAttendance'; String selectedTab = 'todaysAttendance';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
final isLoading = true.obs; // Reactive date range
final isLoadingProjects = true.obs; final Rx<DateTime> startDateAttendance =
final isLoadingEmployees = true.obs; DateTime.now().subtract(const Duration(days: 7)).obs;
final isLoadingAttendanceLogs = true.obs; final Rx<DateTime> endDateAttendance =
final isLoadingRegularizationLogs = true.obs; DateTime.now().subtract(const Duration(days: 1)).obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs; final RxBool isLoading = true.obs;
var showPendingOnly = false.obs; final RxBool isLoadingProjects = true.obs;
final RxBool isLoadingEmployees = true.obs;
final RxBool isLoadingAttendanceLogs = true.obs;
final RxBool isLoadingRegularizationLogs = true.obs;
final RxBool isLoadingLogView = true.obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
final RxBool showPendingOnly = false.obs;
final RxString searchQuery = ''.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeDefaults(); _initializeDefaults();
// 🔹 Fetch organizations for the selected project
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
fetchOrganizations(projectId);
}
} }
void _initializeDefaults() { void _initializeDefaults() {
@ -63,55 +69,60 @@ class AttendanceController extends GetxController {
} }
void _setDefaultDateRange() { void _setDefaultDateRange() {
final today = DateTime.now(); final DateTime today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7)); startDateAttendance.value = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1)); endDateAttendance.value = today.subtract(const Duration(days: 1));
logSafe( logSafe(
"Default date range set: $startDateAttendance to $endDateAttendance"); 'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
} }
// ------------------ Project & Employee ------------------ // ------------------ Computed Filters ------------------
/// Called when a notification says attendance has been updated List<EmployeeModel> get filteredEmployees {
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return employees;
return employees
.where(
(EmployeeModel e) => e.name.toLowerCase().contains(query),
)
.toList();
}
List<AttendanceLogModel> get filteredLogs {
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return attendanceLogs;
return attendanceLogs
.where(
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
)
.toList();
}
List<RegularizationLogModel> get filteredRegularizationLogs {
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return regularizationLogs;
return regularizationLogs
.where(
(RegularizationLogModel log) =>
log.name.toLowerCase().contains(query),
)
.toList();
}
// ------------------ Project & Employee APIs ------------------
Future<void> refreshDataFromNotification({String? projectId}) async { Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id; projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) { if (projectId == null) {
logSafe("No project selected for attendance refresh from notification", logSafe(
level: LogLevel.warning); 'No project selected for attendance refresh from notification',
level: LogLevel.warning,
);
return; return;
} }
await fetchProjectData(projectId); await fetchProjectData(projectId);
logSafe( logSafe(
"Attendance data refreshed from notification for project $projectId"); 'Attendance data refreshed from notification for project $projectId',
} );
// 🔍 Search query
final searchQuery = ''.obs;
// Computed filtered employees
List<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered logs
List<AttendanceLogModel> get filteredLogs {
if (searchQuery.value.isEmpty) return attendanceLogs;
return attendanceLogs
.where((log) =>
(log.name).toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered regularization logs
List<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
} }
Future<void> fetchTodaysAttendance(String? projectId) async { Future<void> fetchTodaysAttendance(String? projectId) async {
@ -119,109 +130,105 @@ class AttendanceController extends GetxController {
isLoadingEmployees.value = true; isLoadingEmployees.value = true;
final response = await ApiService.getTodaysAttendance( final List<dynamic>? response = await ApiService.getTodaysAttendance(
projectId, projectId,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
employees = response.map((e) => EmployeeModel.fromJson(e)).toList(); employees
for (var emp in employees) { ..clear()
..addAll(
response
.map<EmployeeModel>(
(dynamic e) => EmployeeModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
for (final EmployeeModel emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
logSafe("Employees fetched: ${employees.length} for project $projectId");
logSafe(
'Employees fetched: ${employees.length} for project $projectId',
);
} else { } else {
logSafe("Failed to fetch employees for project $projectId", logSafe(
level: LogLevel.error); 'Failed to fetch employees for project $projectId',
level: LogLevel.error,
);
} }
isLoadingEmployees.value = false; isLoadingEmployees.value = false;
update(); update();
} }
Future<void> fetchOrganizations(String projectId) async { Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true; isLoadingOrganizations.value = true;
// Keep original return type inference from your ApiService
final response = await ApiService.getAssignedOrganizations(projectId); final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) { if (response != null) {
organizations = response.data; organizations
logSafe("Organizations fetched: ${organizations.length}"); ..clear()
..addAll(response.data);
logSafe('Organizations fetched: ${organizations.length}');
} else { } else {
logSafe("Failed to fetch organizations for project $projectId", logSafe(
level: LogLevel.error); 'Failed to fetch organizations for project $projectId',
level: LogLevel.error,
);
} }
isLoadingOrganizations.value = false; isLoadingOrganizations.value = false;
update(); update();
} }
// ------------------ Attendance Capture ------------------ // ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance( Future<bool> captureAndUploadAttendance(
String id, String id,
String employeeId, String employeeId,
String projectId, { String projectId, {
String comment = "Marked via mobile app", String comment = 'Marked via mobile app',
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime, // still optional in controller String? markTime,
String? date, // new optional param String? date,
}) async { }) async {
try { try {
uploadingStates[employeeId]?.value = true; _setUploading(employeeId, true);
XFile? image; final XFile? image = await _captureAndPrepareImage(
if (imageCapture) { employeeId: employeeId,
image = await ImagePicker() imageCapture: imageCapture,
.pickImage(source: ImageSource.camera, imageQuality: 80); );
if (image == null) { if (imageCapture && image == null) {
logSafe("Image capture cancelled.", level: LogLevel.warning); return false;
return false;
}
// 🔹 Add timestamp to the image
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(image.path));
final compressedBytes =
await compressImageToUnder100KB(timestampedFile);
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? position = await _getCurrentPositionSafely();
final position = await Geolocator.getCurrentPosition( if (position == null) return false;
desiredAccuracy: LocationAccuracy.high);
final imageName = imageCapture final String imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1) ? ApiService.generateImageName(
: ""; employeeId,
employees.length + 1,
)
: '';
// ---------------- DATE / TIME LOGIC ---------------- final DateTime effectiveDate =
final now = DateTime.now(); _resolveEffectiveDateForAction(action, employeeId);
// Default effectiveDate = now final DateTime now = DateTime.now();
DateTime effectiveDate = now; final String formattedMarkTime =
markTime ?? DateFormat('hh:mm a').format(now);
if (action == 1) { final String formattedDate =
// 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); date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
// ---------------- API CALL ---------------- final bool result = await ApiService.uploadAttendanceImage(
final result = await ApiService.uploadAttendanceImage(
id, id,
employeeId, employeeId,
image, image,
@ -236,15 +243,99 @@ class AttendanceController extends GetxController {
date: formattedDate, date: formattedDate,
); );
logSafe( if (result) {
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate"); logSafe(
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
);
if (Get.isRegistered<DashboardController>()) {
final DashboardController dashboardController =
Get.find<DashboardController>();
await dashboardController.fetchTodaysAttendance(projectId);
}
}
return result; return result;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error uploading attendance", logSafe(
level: LogLevel.error, error: e, stackTrace: stacktrace); 'Error uploading attendance',
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
return false; return false;
} finally { } finally {
uploadingStates[employeeId]?.value = false; _setUploading(employeeId, false);
}
}
Future<XFile?> _captureAndPrepareImage({
required String employeeId,
required bool imageCapture,
}) async {
if (!imageCapture) return null;
final XFile? rawImage = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (rawImage == null) {
logSafe(
'Image capture cancelled.',
level: LogLevel.warning,
);
return null;
}
final File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(rawImage.path),
);
final List<int>? compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe(
'Image compression failed.',
level: LogLevel.error,
);
return null;
}
// FIX: convert List<int> -> Uint8List
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
final File compressedFile =
await saveCompressedImageToFile(compressedUint8List);
return XFile(compressedFile.path);
}
Future<Position?> _getCurrentPositionSafely() async {
final bool permissionGranted = await _handleLocationPermission();
if (!permissionGranted) return null;
return Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
}
DateTime _resolveEffectiveDateForAction(int action, String employeeId) {
final DateTime now = DateTime.now();
if (action != 1) return now;
final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull(
(AttendanceLogModel log) =>
log.employeeId == employeeId && log.checkOut == null,
);
return log?.checkIn ?? now;
}
void _setUploading(String employeeId, bool value) {
final RxBool? state = uploadingStates[employeeId];
if (state != null) {
state.value = value;
} else {
uploadingStates[employeeId] = value.obs;
} }
} }
@ -254,14 +345,19 @@ class AttendanceController extends GetxController {
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning); logSafe(
'Location permissions are denied',
level: LogLevel.warning,
);
return false; return false;
} }
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied', logSafe(
level: LogLevel.error); 'Location permissions are permanently denied',
level: LogLevel.error,
);
return false; return false;
} }
@ -269,26 +365,40 @@ class AttendanceController extends GetxController {
} }
// ------------------ Attendance Logs ------------------ // ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(
Future<void> fetchAttendanceLogs(String? projectId, String? projectId, {
{DateTime? dateFrom, DateTime? dateTo}) async { DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingAttendanceLogs.value = true; isLoadingAttendanceLogs.value = true;
final response = await ApiService.getAttendanceLogs( final List<dynamic>? response = await ApiService.getAttendanceLogs(
projectId, projectId,
dateFrom: dateFrom, dateFrom: dateFrom,
dateTo: dateTo, dateTo: dateTo,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
attendanceLogs = attendanceLogs
response.map((e) => AttendanceLogModel.fromJson(e)).toList(); ..clear()
logSafe("Attendance logs fetched: ${attendanceLogs.length}"); ..addAll(
response
.map<AttendanceLogModel>(
(dynamic e) => AttendanceLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
} else { } else {
logSafe("Failed to fetch attendance logs for project $projectId", logSafe(
level: LogLevel.error); 'Failed to fetch attendance logs for project $projectId',
level: LogLevel.error,
);
} }
isLoadingAttendanceLogs.value = false; isLoadingAttendanceLogs.value = false;
@ -296,45 +406,70 @@ class AttendanceController extends GetxController {
} }
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() { Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{}; final Map<String, List<AttendanceLogModel>> groupedLogs =
<String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) { for (final AttendanceLogModel logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null final String checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!) ? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown'; : 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
groupedLogs.putIfAbsent(
checkInDate,
() => <AttendanceLogModel>[],
)..add(logItem);
} }
final sortedEntries = groupedLogs.entries.toList() final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
..sort((a, b) { groupedLogs.entries.toList()
if (a.key == 'Unknown') return 1; ..sort(
if (b.key == 'Unknown') return -1; (MapEntry<String, List<AttendanceLogModel>> a,
final dateA = DateFormat('dd MMM yyyy').parse(a.key); MapEntry<String, List<AttendanceLogModel>> b) {
final dateB = DateFormat('dd MMM yyyy').parse(b.key); if (a.key == 'Unknown') return 1;
return dateB.compareTo(dateA); if (b.key == 'Unknown') return -1;
});
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries); final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key);
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
},
);
return Map<String, List<AttendanceLogModel>>.fromEntries(
sortedEntries,
);
} }
// ------------------ Regularization Logs ------------------ // ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async { Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingRegularizationLogs.value = true; isLoadingRegularizationLogs.value = true;
final response = await ApiService.getRegularizationLogs( final List<dynamic>? response = await ApiService.getRegularizationLogs(
projectId, projectId,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
regularizationLogs = regularizationLogs
response.map((e) => RegularizationLogModel.fromJson(e)).toList(); ..clear()
logSafe("Regularization logs fetched: ${regularizationLogs.length}"); ..addAll(
response
.map<RegularizationLogModel>(
(dynamic e) => RegularizationLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe(
'Regularization logs fetched: ${regularizationLogs.length}',
);
} else { } else {
logSafe("Failed to fetch regularization logs for project $projectId", logSafe(
level: LogLevel.error); 'Failed to fetch regularization logs for project $projectId',
level: LogLevel.error,
);
} }
isLoadingRegularizationLogs.value = false; isLoadingRegularizationLogs.value = false;
@ -342,22 +477,38 @@ class AttendanceController extends GetxController {
} }
// ------------------ Attendance Log View ------------------ // ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async { Future<void> fetchLogsView(String? id) async {
if (id == null) return; if (id == null) return;
isLoadingLogView.value = true; isLoadingLogView.value = true;
final response = await ApiService.getAttendanceLogView(id); final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
if (response != null) { if (response != null) {
attendenceLogsView = attendenceLogsView
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList(); ..clear()
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000)) ..addAll(
.compareTo(a.activityTime ?? DateTime(2000))); response
logSafe("Attendance log view fetched for ID: $id"); .map<AttendanceLogViewModel>(
(dynamic e) => AttendanceLogViewModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
attendenceLogsView.sort(
(AttendanceLogViewModel a, AttendanceLogViewModel b) =>
(b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)),
);
logSafe('Attendance log view fetched for ID: $id');
} else { } else {
logSafe("Failed to fetch attendance log view for ID $id", logSafe(
level: LogLevel.error); 'Failed to fetch attendance log view for ID $id',
level: LogLevel.error,
);
} }
isLoadingLogView.value = false; isLoadingLogView.value = false;
@ -365,7 +516,6 @@ class AttendanceController extends GetxController {
} }
// ------------------ Combined Load ------------------ // ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async { Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true; isLoading.value = true;
await fetchProjectData(projectId); await fetchProjectData(projectId);
@ -377,7 +527,6 @@ class AttendanceController extends GetxController {
await fetchOrganizations(projectId); await fetchOrganizations(projectId);
// Call APIs depending on the selected tab only
switch (selectedTab) { switch (selectedTab) {
case 'todaysAttendance': case 'todaysAttendance':
await fetchTodaysAttendance(projectId); await fetchTodaysAttendance(projectId);
@ -385,8 +534,8 @@ class AttendanceController extends GetxController {
case 'attendanceLogs': case 'attendanceLogs':
await fetchAttendanceLogs( await fetchAttendanceLogs(
projectId, projectId,
dateFrom: startDateAttendance, dateFrom: startDateAttendance.value,
dateTo: endDateAttendance, dateTo: endDateAttendance.value,
); );
break; break;
case 'regularizationRequests': case 'regularizationRequests':
@ -395,31 +544,35 @@ class AttendanceController extends GetxController {
} }
logSafe( logSafe(
"Project data fetched for project ID: $projectId, tab: $selectedTab"); 'Project data fetched for project ID: $projectId, tab: $selectedTab',
);
update(); update();
} }
// ------------------ UI Interaction ------------------ // ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance( Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async { BuildContext context,
final today = DateTime.now(); AttendanceController controller,
) async {
final DateTime today = DateTime.now();
final picked = await showDateRangePicker( final DateTimeRange? picked = await showDateRangePicker(
context: context, context: context,
firstDate: DateTime(2022), firstDate: DateTime(2022),
lastDate: today.subtract(const Duration(days: 1)), lastDate: today.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: startDateAttendance ?? today.subtract(const Duration(days: 7)), start: startDateAttendance.value,
end: endDateAttendance ?? today.subtract(const Duration(days: 1)), end: endDateAttendance.value,
), ),
); );
if (picked != null) { if (picked != null) {
startDateAttendance = picked.start; startDateAttendance.value = picked.start;
endDateAttendance = picked.end; endDateAttendance.value = picked.end;
logSafe( logSafe(
"Date range selected: $startDateAttendance to $endDateAttendance"); 'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
await controller.fetchAttendanceLogs( await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id, Get.find<ProjectController>().selectedProject?.id,

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:on_field_work/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
class ForgotPasswordController extends MyController { class ForgotPasswordController extends MyController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:on_field_work/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class LoginController extends MyController { class LoginController extends MyController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
class MPINController extends GetxController { class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class OTPController extends GetxController { class OTPController extends GetxController {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:on_field_work/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class RegisterAccountController extends MyController { class RegisterAccountController extends MyController {
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:on_field_work/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class ResetPasswordController extends MyController { class ResetPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,263 +1,370 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:on_field_work/model/dashboard/project_progress_model.dart';
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
class DashboardController extends GetxController { class DashboardController extends GetxController {
// ========================= // Dependencies
// Attendance overview final ProjectController projectController = Get.put(ProjectController());
// =========================
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 // 1. STATE VARIABLES
// ========================= // =========================
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
final RxString projectSelectedRange = '15D'.obs;
final RxBool projectIsChartView = true.obs;
final RxBool isProjectLoading = false.obs;
// ========================= // Attendance
// Projects overview final roleWiseData = <Map<String, dynamic>>[].obs;
// ========================= final attendanceSelectedRange = '15D'.obs;
final RxInt totalProjects = 0.obs; final attendanceIsChartView = true.obs;
final RxInt ongoingProjects = 0.obs; final isAttendanceLoading = false.obs;
final RxBool isProjectsLoading = false.obs;
// ========================= // Project Progress
// Tasks overview final projectChartData = <ChartTaskData>[].obs;
// ========================= final projectSelectedRange = '15D'.obs;
final RxInt totalTasks = 0.obs; final projectIsChartView = true.obs;
final RxInt completedTasks = 0.obs; final isProjectLoading = false.obs;
final RxBool isTasksLoading = false.obs;
// ========================= // Overview Counts
// Teams overview final totalProjects = 0.obs;
// ========================= final ongoingProjects = 0.obs;
final RxInt totalEmployees = 0.obs; final isProjectsLoading = false.obs;
final RxInt inToday = 0.obs;
final RxBool isTeamsLoading = false.obs;
// Common ranges final totalTasks = 0.obs;
final completedTasks = 0.obs;
final isTasksLoading = false.obs;
final totalEmployees = 0.obs;
final inToday = 0.obs;
final isTeamsLoading = false.obs;
// Expenses & Reports
final isPendingExpensesLoading = false.obs;
final pendingExpensesData = Rx<PendingExpensesData?>(null);
final isExpenseTypeReportLoading = false.obs;
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
final expenseReportStartDate =
DateTime.now().subtract(const Duration(days: 15)).obs;
final expenseReportEndDate = DateTime.now().obs;
final isMonthlyExpenseLoading = false.obs;
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
final selectedMonthlyExpenseDuration =
MonthlyExpenseDuration.twelveMonths.obs;
final selectedMonthsCount = 12.obs;
final expenseTypes = <ExpenseTypeModel>[].obs;
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
// Teams/Employees
final isLoadingEmployees = true.obs;
final employees = <EmployeeModel>[].obs;
final uploadingStates = <String, RxBool>{}.obs;
// Collection
final isCollectionOverviewLoading = true.obs;
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
// =========================
// Purchase Invoice Overview
// =========================
final isPurchaseInvoiceLoading = true.obs;
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
// Constants
final List<String> ranges = ['7D', '15D', '30D']; final List<String> ranges = ['7D', '15D', '30D'];
static const _rangeDaysMap = {
'7D': 7,
'15D': 15,
'30D': 30,
'3M': 90,
'6M': 180
};
// Inside your DashboardController // =========================
final ProjectController projectController = // 2. COMPUTED PROPERTIES
Get.put(ProjectController(), permanent: true); // =========================
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
// DSO Calculation Constants
static const double _w0_30 = 15.0;
static const double _w30_60 = 45.0;
static const double _w60_90 = 75.0;
static const double _w90_plus = 105.0;
double get calculatedDSO {
final data = collectionOverviewData.value;
if (data == null || data.totalDueAmount == 0) return 0.0;
final double weightedDue = (data.bucket0To30Amount * _w0_30) +
(data.bucket30To60Amount * _w30_60) +
(data.bucket60To90Amount * _w60_90) +
(data.bucket90PlusAmount * _w90_plus);
return weightedDue / data.totalDueAmount;
}
// =========================
// 3. LIFECYCLE
// =========================
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logSafe('DashboardController initialized', level: LogLevel.info);
logSafe( // Project Selection Listener
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
);
fetchAllDashboardData();
// React to project change
ever<String>(projectController.selectedProjectId, (id) { ever<String>(projectController.selectedProjectId, (id) {
fetchAllDashboardData(); if (id.isNotEmpty) {
fetchAllDashboardData();
fetchTodaysAttendance(id);
}
}); });
// React to range changes // Expense Report Date Listener
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
}
});
// Chart Range Listeners
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress()); ever(projectSelectedRange, (_) => fetchProjectProgress());
} }
// ========================= // =========================
// Helper Methods // 4. USER ACTIONS
// ========================= // =========================
int _getDaysFromRange(String range) {
switch (range) { void updateAttendanceRange(String range) =>
case '7D': attendanceSelectedRange.value = range;
return 7; void updateProjectRange(String range) => projectSelectedRange.value = range;
case '15D': void toggleAttendanceChartView(bool isChart) =>
return 15; attendanceIsChartView.value = isChart;
case '30D': void toggleProjectChartView(bool isChart) =>
return 30; projectIsChartView.value = isChart;
case '3M':
return 90; void updateSelectedExpenseType(ExpenseTypeModel? type) {
case '6M': selectedExpenseType.value = type;
return 180; fetchMonthlyExpenses(categoryId: type?.id);
default: }
return 7;
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
selectedMonthlyExpenseDuration.value = duration;
// Efficient Map lookup instead of Switch
const durationMap = {
MonthlyExpenseDuration.oneMonth: 1,
MonthlyExpenseDuration.threeMonths: 3,
MonthlyExpenseDuration.sixMonths: 6,
MonthlyExpenseDuration.twelveMonths: 12,
MonthlyExpenseDuration.all: 0,
};
selectedMonthsCount.value = durationMap[duration] ?? 12;
fetchMonthlyExpenses();
}
Future<void> refreshDashboard() => fetchAllDashboardData();
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
Future<void> refreshProjects() => fetchProjectProgress();
Future<void> refreshTasks() async {
final id = projectController.selectedProjectId.value;
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
}
// =========================
// 5. DATA FETCHING (API)
// =========================
/// Wrapper to reduce try-finally boilerplate for loading states
Future<void> _executeApiCall(
RxBool loader, Future<void> Function() apiLogic) async {
loader.value = true;
try {
await apiLogic();
} finally {
loader.value = false;
} }
} }
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 { Future<void> fetchAllDashboardData() async {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
if (projectId.isEmpty) {
logSafe('No project selected. Skipping dashboard API calls.',
level: LogLevel.warning);
return;
}
await Future.wait([ await Future.wait([
fetchRoleWiseAttendance(), fetchRoleWiseAttendance(),
fetchProjectProgress(), fetchProjectProgress(),
fetchDashboardTasks(projectId: projectId), fetchDashboardTasks(projectId: projectId),
fetchDashboardTeams(projectId: projectId), fetchDashboardTeams(projectId: projectId),
fetchPendingExpenses(),
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
),
fetchMonthlyExpenses(),
fetchMasterData(),
fetchCollectionOverview(),
fetchPurchaseInvoiceOverview(),
]); ]);
} }
// ========================= Future<void> fetchCollectionOverview() async {
// API Calls final projectId = projectController.selectedProjectId.value;
// =========================
Future<void> fetchRoleWiseAttendance() async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (projectId.isEmpty) return;
try { await _executeApiCall(isCollectionOverviewLoading, () async {
isAttendanceLoading.value = true; final response =
final List<dynamic>? response = await ApiService.getCollectionOverview(projectId: projectId);
await ApiService.getDashboardAttendanceOverview( collectionOverviewData.value =
projectId, getAttendanceDays()); (response?.success == true) ? response!.data : null;
});
}
Future<void> fetchTodaysAttendance(String projectId) async {
await _executeApiCall(isLoadingEmployees, () async {
final response = await ApiService.getAttendanceForDashboard(projectId);
if (response != null) { if (response != null) {
roleWiseData.value = employees.value = response;
response.map((e) => Map<String, dynamic>.from(e)).toList(); for (var emp in employees) {
logSafe('Attendance overview fetched successfully.', uploadingStates.putIfAbsent(emp.id, () => false.obs);
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); Future<void> fetchMasterData() async {
} finally { try {
isAttendanceLoading.value = false; final data = await ApiService.getMasterExpenseTypes();
} if (data is List) {
expenseTypes.value =
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
} catch (_) {}
}
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
await _executeApiCall(isMonthlyExpenseLoading, () async {
final response = await ApiService.getDashboardMonthlyExpensesApi(
categoryId: categoryId,
months: selectedMonthsCount.value,
);
monthlyExpenseList.value =
(response?.success == true) ? response!.data : [];
});
}
Future<void> fetchPurchaseInvoiceOverview() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
await _executeApiCall(isPurchaseInvoiceLoading, () async {
final response = await ApiService.getPurchaseInvoiceOverview(
projectId: projectId,
);
purchaseInvoiceOverviewData.value =
(response?.success == true) ? response!.data : null;
});
}
Future<void> fetchPendingExpenses() async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
await _executeApiCall(isPendingExpensesLoading, () async {
final response = await ApiService.getPendingExpensesApi(projectId: id);
pendingExpensesData.value =
(response?.success == true) ? response!.data : null;
});
}
Future<void> fetchRoleWiseAttendance() async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
await _executeApiCall(isAttendanceLoading, () async {
final response = await ApiService.getDashboardAttendanceOverview(
id, getAttendanceDays());
roleWiseData.value =
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
});
}
Future<void> fetchExpenseTypeReport(
{required DateTime startDate, required DateTime endDate}) async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
await _executeApiCall(isExpenseTypeReportLoading, () async {
final response = await ApiService.getExpenseTypeReportApi(
projectId: id,
startDate: startDate,
endDate: endDate,
);
expenseTypeReportData.value =
(response?.success == true) ? response!.data : null;
});
} }
Future<void> fetchProjectProgress() async { Future<void> fetchProjectProgress() async {
final String projectId = projectController.selectedProjectId.value; final id = projectController.selectedProjectId.value;
if (projectId.isEmpty) return; if (id.isEmpty) return;
try { await _executeApiCall(isProjectLoading, () async {
isProjectLoading.value = true;
final response = await ApiService.getProjectProgress( final response = await ApiService.getProjectProgress(
projectId: projectId, days: getProjectDays()); projectId: id, days: getProjectDays());
if (response?.success == true) {
if (response != null && response.success) { projectChartData.value = response!.data
projectChartData.value = .map((d) => ChartTaskData.fromProjectData(d))
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList(); .toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else { } else {
projectChartData.clear(); 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 { Future<void> fetchDashboardTasks({required String projectId}) async {
if (projectId.isEmpty) return; await _executeApiCall(isTasksLoading, () async {
try {
isTasksLoading.value = true;
final response = await ApiService.getDashboardTasks(projectId: projectId); final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response?.success == true) {
if (response != null && response.success) { totalTasks.value = response!.data?.totalTasks ?? 0;
totalTasks.value = response.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else { } else {
totalTasks.value = 0; totalTasks.value = 0;
completedTasks.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 { Future<void> fetchDashboardTeams({required String projectId}) async {
if (projectId.isEmpty) return; await _executeApiCall(isTeamsLoading, () async {
try {
isTeamsLoading.value = true;
final response = await ApiService.getDashboardTeams(projectId: projectId); final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response?.success == true) {
if (response != null && response.success) { totalEmployees.value = response!.data?.totalEmployees ?? 0;
totalEmployees.value = response.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0; inToday.value = response.data?.inToday ?? 0;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else { } else {
totalEmployees.value = 0; totalEmployees.value = 0;
inToday.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;
}
} }
} }
enum MonthlyExpenseDuration {
oneMonth,
threeMonths,
sixMonths,
twelveMonths,
all,
}

View File

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart'; import 'package:on_field_work/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart'; import 'package:on_field_work/controller/directory/notes_controller.dart';
class AddCommentController extends GetxController { class AddCommentController extends GetxController {
final String contactId; final String contactId;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
class AddContactController extends GetxController { class AddContactController extends GetxController {
final RxList<String> categories = <String>[].obs; final RxList<String> categories = <String>[].obs;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
class BucketController extends GetxController { class BucketController extends GetxController {
RxBool isCreating = false.obs; RxBool isCreating = false.obs;

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/contact_model.dart'; import 'package:on_field_work/model/directory/contact_model.dart';
import 'package:marco/model/directory/contact_bucket_list_model.dart'; import 'package:on_field_work/model/directory/contact_bucket_list_model.dart';
import 'package:marco/model/directory/directory_comment_model.dart'; import 'package:on_field_work/model/directory/directory_comment_model.dart';
class DirectoryController extends GetxController { class DirectoryController extends GetxController {
// -------------------- CONTACTS -------------------- // -------------------- CONTACTS --------------------

View File

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart'; import 'package:on_field_work/controller/directory/directory_controller.dart';
class ManageBucketController extends GetxController { class ManageBucketController extends GetxController {
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs; RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/note_list_response_model.dart'; import 'package:on_field_work/model/directory/note_list_response_model.dart';
class NotesController extends GetxController { class NotesController extends GetxController {
RxList<NoteModel> notesList = <NoteModel>[].obs; RxList<NoteModel> notesList = <NoteModel>[].obs;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/document/document_details_model.dart'; import 'package:on_field_work/model/document/document_details_model.dart';
import 'package:marco/model/document/document_version_model.dart'; import 'package:on_field_work/model/document/document_version_model.dart';
class DocumentDetailsController extends GetxController { class DocumentDetailsController extends GetxController {
/// Observables /// Observables

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/document/master_document_type_model.dart'; import 'package:on_field_work/model/document/master_document_type_model.dart';
import 'package:marco/model/document/master_document_tags.dart'; import 'package:on_field_work/model/document/master_document_tags.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
class DocumentUploadController extends GetxController { class DocumentUploadController extends GetxController {
// Observables // Observables

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/document/document_filter_model.dart'; import 'package:on_field_work/model/document/document_filter_model.dart';
import 'package:marco/model/document/documents_list_model.dart'; import 'package:on_field_work/model/document/documents_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
class DocumentController extends GetxController { class DocumentController extends GetxController {
// ==================== Observables ==================== // ==================== Observables ====================
@ -34,11 +35,10 @@ class DocumentController extends GetxController {
// Additional filters // Additional filters
final isUploadedAt = true.obs; final isUploadedAt = true.obs;
final isVerified = RxnBool(); final isVerified = RxnBool();
final startDate = Rxn<String>(); final startDate = Rxn<DateTime>();
final endDate = Rxn<String>(); final endDate = Rxn<DateTime>();
// ==================== Lifecycle ==================== // ==================== Lifecycle ====================
@override @override
void onClose() { void onClose() {
// Don't dispose searchController here - it's managed by the page // Don't dispose searchController here - it's managed by the page
@ -87,13 +87,23 @@ class DocumentController extends GetxController {
entityId: entityId, entityId: entityId,
reset: true, reset: true,
); );
// Show success snackbar
showAppSnackbar(
title: 'Success',
message: isActive ? 'Document deactivated' : 'Document activated',
type: SnackbarType.success,
);
return true; return true;
} else { } else {
errorMessage.value = 'Failed to update document state'; errorMessage.value = 'Failed to update document state';
_showError('Failed to update document state');
return false; return false;
} }
} catch (e) { } catch (e) {
errorMessage.value = 'Error updating document: $e'; errorMessage.value = 'Error updating document: $e';
_showError('Error updating document: $e');
debugPrint('❌ Error toggling document state: $e'); debugPrint('❌ Error toggling document state: $e');
return false; return false;
} finally { } finally {
@ -110,17 +120,13 @@ class DocumentController extends GetxController {
bool reset = false, bool reset = false,
}) async { }) async {
try { try {
// Reset pagination if needed
if (reset) { if (reset) {
pageNumber.value = 1; pageNumber.value = 1;
documents.clear(); documents.clear();
hasMore.value = true; hasMore.value = true;
} }
// Don't fetch if no more data
if (!hasMore.value && !reset) return; if (!hasMore.value && !reset) return;
// Prevent duplicate requests
if (isLoading.value) return; if (isLoading.value) return;
isLoading.value = true; isLoading.value = true;
@ -136,8 +142,8 @@ class DocumentController extends GetxController {
); );
if (response != null && response.success) { if (response != null && response.success) {
if (response.data.data.isNotEmpty) { if (response.data?.data.isNotEmpty ?? false) {
documents.addAll(response.data.data); documents.addAll(response.data!.data);
pageNumber.value++; pageNumber.value++;
} else { } else {
hasMore.value = false; hasMore.value = false;
@ -147,12 +153,24 @@ class DocumentController extends GetxController {
errorMessage.value = response?.message ?? 'Failed to fetch documents'; errorMessage.value = response?.message ?? 'Failed to fetch documents';
if (documents.isEmpty) { if (documents.isEmpty) {
_showError('Failed to load documents'); _showError('Failed to load documents');
} else {
showAppSnackbar(
title: 'Warning',
message: 'No more documents to load',
type: SnackbarType.warning,
);
} }
} }
} catch (e) { } catch (e) {
errorMessage.value = 'Error fetching documents: $e'; errorMessage.value = 'Error fetching documents: $e';
if (documents.isEmpty) { if (documents.isEmpty) {
_showError('Error loading documents'); _showError('Error loading documents');
} else {
showAppSnackbar(
title: 'Error',
message: 'Error fetching additional documents',
type: SnackbarType.error,
);
} }
debugPrint('❌ Error fetching documents: $e'); debugPrint('❌ Error fetching documents: $e');
} finally { } finally {
@ -185,17 +203,12 @@ class DocumentController extends GetxController {
isVerified.value != null; isVerified.value != null;
} }
/// Show error message /// Show error message via snackbar
void _showError(String message) { void _showError(String message) {
Get.snackbar( showAppSnackbar(
'Error', title: 'Error',
message, message: message,
snackPosition: SnackPosition.BOTTOM, type: SnackbarType.error,
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade900,
margin: const EdgeInsets.all(16),
borderRadius: 8,
duration: const Duration(seconds: 3),
); );
} }

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class DynamicMenuController extends GetxController { class DynamicMenuController extends GetxController {
// UI reactive states // UI reactive states

View File

@ -1,11 +1,11 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';

View File

@ -1,10 +1,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/global_project_model.dart'; import 'package:on_field_work/model/global_project_model.dart';
import 'package:marco/model/employees/assigned_projects_model.dart'; import 'package:on_field_work/model/employees/assigned_projects_model.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
class AssignProjectController extends GetxController { class AssignProjectController extends GetxController {
final String employeeId; final String employeeId;

View File

@ -1,91 +1,60 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/attendance/attendance_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/model/project_model.dart'; import 'package:on_field_work/model/employees/employee_details_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart';
import 'package:marco/controller/project_controller.dart';
class EmployeesScreenController extends GetxController { class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = []; /// Data lists
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs; RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>(); Rxn<EmployeeDetailsModel>();
/// Loading states
RxBool isLoading = false.obs;
RxBool isLoadingEmployeeDetails = false.obs; RxBool isLoadingEmployeeDetails = false.obs;
/// Selection state
RxBool isAllEmployeeSelected = false.obs;
RxSet<String> selectedEmployeeIds = <String>{}.obs;
/// Upload state tracking (if needed later)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
<EmployeeModel>[].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
isLoading.value = true; fetchAllEmployees();
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();
logSafe(
"Projects fetched: ${projects.length} projects loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
logSafe("No project data found or API call failed.",
level: LogLevel.warning);
},
);
isLoading.value = false;
update();
}
void clearEmployees() {
employees.clear();
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
} }
/// 🔹 Fetch all employees (no project filter)
Future<void> fetchAllEmployees({String? organizationId}) async { Future<void> fetchAllEmployees({String? organizationId}) async {
isLoading.value = true; isLoading.value = true;
update(['employee_screen_controller']); update(['employee_screen_controller']);
await _handleApiCall( await _handleApiCall(
() => ApiService.getAllEmployees( () => ApiService.getAllEmployees(organizationId: organizationId),
organizationId: organizationId), // pass orgId to API
onSuccess: (data) { onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
logSafe( logSafe(
"All Employees fetched: ${employees.length} employees loaded.", "All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info, level: LogLevel.info,
); );
// Reset selection states when new data arrives
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
}, },
onEmpty: () { onEmpty: () {
employees.clear(); employees.clear();
logSafe( selectedEmployeeIds.clear();
"No Employee data found or API call failed", isAllEmployeeSelected.value = false;
level: LogLevel.warning, logSafe("No Employee data found or API call failed",
); level: LogLevel.warning);
}, },
); );
@ -93,28 +62,7 @@ class EmployeesScreenController extends GetxController {
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
Future<void> fetchEmployeesByProject(String projectId, /// 🔹 Fetch details for a specific employee
{String? organizationId}) async {
if (projectId.isEmpty) return;
isLoading.value = true;
await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId,
organizationId: organizationId),
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
},
onEmpty: () => employees.clear(),
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeeDetails(String? employeeId) async { Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return; if (employeeId == null || employeeId.isEmpty) return;
@ -124,31 +72,80 @@ class EmployeesScreenController extends GetxController {
() => ApiService.getEmployeeDetails(employeeId), () => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) { onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data); selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe( logSafe("Employee details loaded for $employeeId",
"Employee details loaded for $employeeId", level: LogLevel.info);
level: LogLevel.info,
);
}, },
onEmpty: () { onEmpty: () {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe( logSafe("No employee details found for $employeeId",
"No employee details found for $employeeId", level: LogLevel.warning);
level: LogLevel.warning,
);
}, },
onError: (e) { onError: (e) {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe( logSafe("Error fetching employee details for $employeeId",
"Error fetching employee details for $employeeId", level: LogLevel.error, error: e);
level: LogLevel.error,
error: e,
);
}, },
); );
isLoadingEmployeeDetails.value = false; isLoadingEmployeeDetails.value = false;
} }
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId
Future<void> fetchReportingManagers(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
try {
// Always clear before new fetch (to avoid mixing old data)
selectedEmployeePrimaryManagers.clear();
selectedEmployeeSecondaryManagers.clear();
// Fetch from existing API helper
final data = await ApiService.getOrganizationHierarchyList(employeeId);
if (data == null || data.isEmpty) {
update(['employee_screen_controller']);
return;
}
for (final item in data) {
try {
final reportTo = item['reportTo'];
if (reportTo == null) continue;
final emp = EmployeeModel.fromJson(reportTo);
final isPrimary = item['isPrimary'] == true;
if (isPrimary) {
if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) {
selectedEmployeePrimaryManagers.add(emp);
}
} else {
if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) {
selectedEmployeeSecondaryManagers.add(emp);
}
}
} catch (_) {
// ignore malformed items
}
}
update(['employee_screen_controller']);
} catch (e) {
logSafe("Error fetching reporting managers for $employeeId",
level: LogLevel.error, error: e);
}
}
/// 🔹 Clear all employee data
void clearEmployees() {
employees.clear();
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
}
/// 🔹 Generic handler for list API responses
Future<void> _handleApiCall( Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, { Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess, required Function(List<dynamic>) onSuccess,
@ -171,6 +168,7 @@ class EmployeesScreenController extends GetxController {
} }
} }
/// 🔹 Generic handler for single-object API responses
Future<void> _handleSingleApiCall( Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, { Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess, required Function(Map<String, dynamic>) onSuccess,

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class ComingSoonController extends MyController { class ComingSoonController extends MyController {
Timer? countdownTimer; Timer? countdownTimer;

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class Error404Controller extends MyController { class Error404Controller extends MyController {
void goToDashboardScreen() { void goToDashboardScreen() {

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class Error500Controller extends MyController { class Error500Controller extends MyController {
void goToDashboardScreen() { void goToDashboardScreen() {

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,14 +11,14 @@ import 'package:intl/intl.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/model/expense/expense_type_model.dart'; import 'package:on_field_work/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/payment_types_model.dart'; import 'package:on_field_work/model/expense/payment_types_model.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
class AddExpenseController extends GetxController { class AddExpenseController extends GetxController {
// --- Text Controllers --- // --- Text Controllers ---
@ -50,10 +51,22 @@ class AddExpenseController extends GetxController {
final isEditMode = false.obs; final isEditMode = false.obs;
final isSearchingEmployees = false.obs; final isSearchingEmployees = false.obs;
// --- Paid By (Single + Multi Selection Support) ---
// single selection
final selectedPaidBy = Rxn<EmployeeModel>();
// helper setters
void setSelectedPaidBy(EmployeeModel? emp) {
selectedPaidBy.value = emp;
}
// --- Dropdown Selections & Data --- // --- Dropdown Selections & Data ---
final selectedPaymentMode = Rxn<PaymentModeModel>(); final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>(); final selectedExpenseType = Rxn<ExpenseTypeModel>();
final selectedPaidBy = Rxn<EmployeeModel>(); // final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs; final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>(); final selectedTransactionDate = Rxn<DateTime>();
@ -196,7 +209,7 @@ class AddExpenseController extends GetxController {
'Location: ${locationController.text}', 'Location: ${locationController.text}',
'Transaction Date: ${transactionDateController.text}', 'Transaction Date: ${transactionDateController.text}',
'No. of Persons: ${noOfPersonsController.text}', 'No. of Persons: ${noOfPersonsController.text}',
'Expense Type: ${selectedExpenseType.value?.name}', 'Expense Category: ${selectedExpenseType.value?.name}',
'Payment Mode: ${selectedPaymentMode.value?.name}', 'Payment Mode: ${selectedPaymentMode.value?.name}',
'Paid By: ${selectedPaidBy.value?.name}', 'Paid By: ${selectedPaidBy.value?.name}',
'Attachments: ${attachments.length}', 'Attachments: ${attachments.length}',
@ -394,47 +407,86 @@ class AddExpenseController extends GetxController {
} }
} }
Future<bool> _submitToApi(Map<String, dynamic> payload) async { Future<bool> _submitToApi(Map<String, dynamic>? payload) async {
if (isEditMode.value && editingExpenseId != null) { if (payload == null) {
return ApiService.editExpenseApi( _errorSnackbar("Payload is empty. Cannot submit.");
expenseId: editingExpenseId!, return false;
payload: payload, }
);
try {
if (isEditMode.value && editingExpenseId != null) {
// Edit existing expense
return await ApiService.editExpenseApi(
expenseId: editingExpenseId!,
payload: payload,
);
} else {
// Create new expense
return await ApiService.createExpenseApi(
projectId: payload['projectId'],
expensesTypeId: payload['expenseCategoryId'],
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'],
);
}
} catch (e) {
_errorSnackbar("Failed to submit expense: $e");
return false;
} }
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 { Future<Map<String, dynamic>?> _buildExpensePayload() async {
final now = DateTime.now(); final now = DateTime.now();
// --- Get IDs safely ---
final projectId = projectsMap[selectedProject.value];
final expenseType = selectedExpenseType.value;
final paymentMode = selectedPaymentMode.value;
final paidBy = selectedPaidBy.value;
// --- Validate essential fields ---
if (projectId == null) {
_errorSnackbar("Project not selected or invalid");
return null;
}
if (expenseType == null) {
_errorSnackbar("Expense Category not selected");
return null;
}
if (paymentMode == null) {
_errorSnackbar("Payment mode not selected");
return null;
}
if (paidBy == null) {
_errorSnackbar("Paid By not selected");
return null;
}
// --- Process existing attachments (for edit mode) ---
final existingPayload = isEditMode.value final existingPayload = isEditMode.value
? existingAttachments ? existingAttachments
.map((e) => { .map((e) => {
"documentId": e['documentId'], "documentId": e['documentId'],
"fileName": e['fileName'], "fileName": e['fileName'] ?? "",
"contentType": e['contentType'], "contentType": e['contentType'] ?? "",
"fileSize": 0, "fileSize": 0,
"description": "", "description": "",
"url": e['url'], "url": e['url'] ?? "",
"isActive": e['isActive'] ?? true, "isActive": e['isActive'] ?? true,
"base64Data": "", "base64Data": "",
}) })
.toList() .toList()
: <Map<String, dynamic>>[]; : <Map<String, dynamic>>[];
// --- Process new attachments ---
final newPayload = await Future.wait( final newPayload = await Future.wait(
attachments.map((file) async { attachments.map((file) async {
final bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
@ -449,38 +501,36 @@ class AddExpenseController extends GetxController {
}), }),
); );
final type = selectedExpenseType.value!; // --- Build final payload ---
final payload = {
return {
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId, if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectsMap[selectedProject.value]!, "projectId": projectId,
"expensesTypeId": type.id, "expenseCategoryId": expenseType.id,
"paymentModeId": selectedPaymentMode.value!.id, "paymentModeId": paymentMode.id,
"paidById": selectedPaidBy.value!.id, "paidById": paidBy.id,
"transactionDate": "transactionDate":
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(), (selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
"transactionId": transactionIdController.text, "transactionId": transactionIdController.text.trim(),
"description": descriptionController.text, "description": descriptionController.text.trim(),
"location": locationController.text, "location": locationController.text.trim(),
"supplerName": supplierController.text, "supplerName": supplierController.text.trim(),
"amount": double.parse(amountController.text.trim()), "amount": double.tryParse(amountController.text.trim()) ?? 0,
"noOfPersons": type.noOfPersonsRequired == true "noOfPersons": expenseType.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0 ? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0, : 0,
"billAttachments": [ "billAttachments": [...existingPayload, ...newPayload].isEmpty
...existingPayload,
...newPayload,
].isEmpty
? null ? null
: [...existingPayload, ...newPayload], : [...existingPayload, ...newPayload],
}; };
return payload;
} }
String validateForm() { String validateForm() {
final missing = <String>[]; final missing = <String>[];
if (selectedProject.value.isEmpty) missing.add("Project"); if (selectedProject.value.isEmpty) missing.add("Project");
if (selectedExpenseType.value == null) missing.add("Expense Type"); if (selectedExpenseType.value == null) missing.add("Expense Category");
if (selectedPaymentMode.value == null) missing.add("Payment Mode"); if (selectedPaymentMode.value == null) missing.add("Payment Mode");
if (selectedPaidBy.value == null) missing.add("Paid By"); if (selectedPaidBy.value == null) missing.add("Paid By");
if (amountController.text.trim().isEmpty) missing.add("Amount"); if (amountController.text.trim().isEmpty) missing.add("Amount");

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_detail_model.dart'; import 'package:on_field_work/model/expense/expense_detail_model.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ExpenseDetailController extends GetxController { class ExpenseDetailController extends GetxController {
@ -142,6 +142,10 @@ class ExpenseDetailController extends GetxController {
required String reimburseDate, required String reimburseDate,
required String reimburseById, required String reimburseById,
required String statusId, required String statusId,
double? baseAmount,
double? taxAmount,
double? tdsPercent,
double? netPayable,
}) async { }) async {
final success = await _apiCallWrapper( final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi( () => ApiService.updateExpenseStatusApi(
@ -151,13 +155,16 @@ class ExpenseDetailController extends GetxController {
reimburseTransactionId: reimburseTransactionId, reimburseTransactionId: reimburseTransactionId,
reimburseDate: reimburseDate, reimburseDate: reimburseDate,
reimbursedById: reimburseById, reimbursedById: reimburseById,
baseAmount: baseAmount,
taxAmount: taxAmount,
tdsPercent: tdsPercent,
netPayable: netPayable,
), ),
"submit reimbursement", "submit reimbursement",
); );
if (success == true) { if (success == true) {
// Explicitly check for true as _apiCallWrapper returns T? await fetchExpenseDetails();
await fetchExpenseDetails(); // Refresh details after successful update
return true; return true;
} else { } else {
errorMessage.value = "Failed to submit reimbursement."; errorMessage.value = "Failed to submit reimbursement.";

View File

@ -1,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_list_model.dart'; import 'package:on_field_work/model/expense/expense_list_model.dart';
import 'package:marco/model/expense/payment_types_model.dart'; import 'package:on_field_work/model/expense/payment_types_model.dart';
import 'package:marco/model/expense/expense_type_model.dart'; import 'package:on_field_work/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/expense_status_model.dart'; import 'package:on_field_work/model/expense/expense_status_model.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ExpenseController extends GetxController { class ExpenseController extends GetxController {
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
selectedCreatedByEmployees.clear(); selectedCreatedByEmployees.clear();
} }
/// Fetch master data: expense types, payment modes, and expense status /// Fetch master data: Expense Categorys, payment modes, and expense status
Future<void> fetchMasterData() async { Future<void> fetchMasterData() async {
try { try {
final expenseTypesData = await ApiService.getMasterExpenseTypes(); final expenseTypesData = await ApiService.getMasterExpenseTypes();

View File

@ -1,5 +1,5 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart'; import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
class FaqsController extends MyController { class FaqsController extends MyController {
final List<bool> dataExpansionPanel = [true, false, false, false, false, false]; final List<bool> dataExpansionPanel = [true, false, false, false, false, false];

View File

@ -1,4 +1,4 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class PricingController extends MyController { class PricingController extends MyController {
bool isMonth = false; bool isMonth = false;

View File

@ -0,0 +1,419 @@
// payment_request_controller.dart
import 'dart:io';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:on_field_work/model/finance/expense_category_model.dart';
import 'package:on_field_work/model/finance/currency_list_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
class AddPaymentRequestController extends GetxController {
// Loading States
final isLoadingPayees = false.obs;
final isLoadingCategories = false.obs;
final isLoadingCurrencies = false.obs;
final isProcessingAttachment = false.obs;
final isSubmitting = false.obs;
// Data Lists
final payees = <String>[].obs;
final categories = <ExpenseCategory>[].obs;
final currencies = <Currency>[].obs;
final globalProjects = <Map<String, dynamic>>[].obs;
// Selected Values
final selectedProject = Rx<Map<String, dynamic>?>(null);
final selectedCategory = Rx<ExpenseCategory?>(null);
final selectedPayee = Rx<EmployeeModel?>(null);
final selectedCurrency = Rx<Currency?>(null);
final isAdvancePayment = false.obs;
final selectedDueDate = Rx<DateTime?>(null);
// Text Controllers
final titleController = TextEditingController();
final dueDateController = TextEditingController();
final amountController = TextEditingController();
final descriptionController = TextEditingController();
final removedAttachments = <Map<String, dynamic>>[].obs;
// Attachments
final attachments = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].obs;
final ImagePicker _picker = ImagePicker();
@override
void onInit() {
super.onInit();
fetchAllMasterData();
fetchGlobalProjects();
}
@override
void onClose() {
titleController.dispose();
dueDateController.dispose();
amountController.dispose();
descriptionController.dispose();
super.onClose();
}
/// Fetch all master data concurrently
Future<void> fetchAllMasterData() async {
await Future.wait([
_fetchData(
payees, ApiService.getExpensePaymentRequestPayeeApi, isLoadingPayees),
_fetchData(categories, ApiService.getMasterExpenseCategoriesApi,
isLoadingCategories),
_fetchData(
currencies, ApiService.getMasterCurrenciesApi, isLoadingCurrencies),
]);
}
/// Generic fetch handler
Future<void> _fetchData<T>(
RxList<T> list, Future<dynamic> Function() apiCall, RxBool loader) async {
try {
loader.value = true;
final response = await apiCall();
if (response != null && response.data.isNotEmpty) {
list.value = response.data;
} else {
list.clear();
}
} catch (e) {
logSafe("Error fetching data: $e", level: LogLevel.error);
list.clear();
} finally {
loader.value = false;
}
}
/// Fetch projects
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
globalProjects.value = (response ?? [])
.map<Map<String, dynamic>>((e) => {
'id': e['id']?.toString() ?? '',
'name': e['name']?.toString().trim() ?? '',
})
.where((p) => p['id']!.isNotEmpty && p['name']!.isNotEmpty)
.toList();
} catch (e) {
logSafe("Error fetching projects: $e", level: LogLevel.error);
globalProjects.clear();
}
}
/// Pick due date
Future<void> pickDueDate(BuildContext context) async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedDueDate.value ?? DateTime.now(),
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 5),
);
if (pickedDate != null) {
selectedDueDate.value = pickedDate;
dueDateController.text = DateFormat('dd MMM yyyy').format(pickedDate);
}
}
/// Generic file picker for multiple sources
Future<void> pickAttachments(
{bool fromGallery = false, bool fromCamera = false}) async {
try {
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
final timestamped = await TimestampImageHelper.addTimestamp(
imageFile: File(pickedFile.path));
attachments.add(timestamped);
}
} else if (fromGallery) {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) attachments.add(File(pickedFile.path));
} else {
final result = await FilePicker.platform
.pickFiles(type: FileType.any, allowMultiple: true);
if (result != null && result.paths.isNotEmpty)
attachments.addAll(result.paths.whereType<String>().map(File.new));
}
attachments.refresh();
} catch (e) {
_errorSnackbar("Attachment error: $e");
} finally {
isProcessingAttachment.value = false;
}
}
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
File imageFile = File(pickedFile.path);
// Add timestamp to the captured image
File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: imageFile,
);
attachments.add(timestampedFile);
attachments.refresh(); // refresh UI
}
} catch (e) {
_errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false; // stop loading
}
}
/// Selection handlers
void selectProject(Map<String, dynamic> project) =>
selectedProject.value = project;
void selectCategory(ExpenseCategory category) =>
selectedCategory.value = category;
void selectPayee(EmployeeModel payee) => selectedPayee.value = payee;
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
void addAttachment(File file) => attachments.add(file);
void removeAttachment(File file) {
if (attachments.contains(file)) {
attachments.remove(file);
}
}
void removeExistingAttachment(Map<String, dynamic> existingAttachment) {
final index = existingAttachments.indexWhere(
(e) => e['id'] == existingAttachment['id']); // match by normalized id
if (index != -1) {
// Mark as inactive
existingAttachments[index]['isActive'] = false;
existingAttachments.refresh();
// Add to removedAttachments to inform API
removedAttachments.add({
"documentId": existingAttachment['id'], // ensure API receives id
"isActive": false,
});
// Show snackbar feedback
showAppSnackbar(
title: 'Removed',
message: 'Attachment has been removed.',
type: SnackbarType.success,
);
}
}
/// Build attachment payload
Future<List<Map<String, dynamic>>> buildAttachmentPayload() async {
final existingPayload = existingAttachments
.map((e) => {
"documentId": e['id'], // use the normalized id
"fileName": e['fileName'],
"contentType": e['contentType'] ?? 'application/octet-stream',
"fileSize": e['fileSize'] ?? 0,
"description": "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
})
.toList();
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": "",
};
}));
// Combine active + removed attachments
return [...existingPayload, ...newPayload, ...removedAttachments];
}
/// Submit edited payment request
Future<bool> submitEditedPaymentRequest({required String requestId}) async {
if (isSubmitting.value) return false;
try {
isSubmitting.value = true;
// Validate form
if (!_validateForm()) return false;
// Build attachment payload
final billAttachments = await buildAttachmentPayload();
final payload = {
"id": requestId,
"title": titleController.text.trim(),
"projectId": selectedProject.value?['id'] ?? '',
"expenseCategoryId": selectedCategory.value?.id ?? '',
"amount": double.tryParse(amountController.text.trim()) ?? 0,
"currencyId": selectedCurrency.value?.id ?? '',
"description": descriptionController.text.trim(),
"payee": selectedPayee.value?.id ?? "",
"dueDate": selectedDueDate.value?.toIso8601String(),
"isAdvancePayment": isAdvancePayment.value,
"billAttachments": billAttachments.map((a) {
return {
"documentId": a['documentId'],
"fileName": a['fileName'],
"base64Data": a['base64Data'] ?? "",
"contentType": a['contentType'],
"fileSize": a['fileSize'],
"description": a['description'] ?? "",
"isActive": a['isActive'] ?? true,
};
}).toList(),
};
logSafe("💡 Submitting Edited Payment Request: ${jsonEncode(payload)}");
final success = await ApiService.editExpensePaymentRequestApi(
id: payload['id'],
title: payload['title'],
projectId: payload['projectId'],
expenseCategoryId: payload['expenseCategoryId'],
amount: payload['amount'],
currencyId: payload['currencyId'],
description: payload['description'],
payee: payload['payee'],
dueDate: payload['dueDate'] ?? '',
isAdvancePayment: payload['isAdvancePayment'],
billAttachments: payload['billAttachments'],
);
logSafe("💡 Edit Payment Request API Response: $success");
if (success == true) {
logSafe("✅ Payment request edited successfully.");
return true;
} else {
return _errorSnackbar("Failed to edit payment request.");
}
} catch (e, st) {
logSafe("💥 Submit Edited Payment Request Error: $e\n$st",
level: LogLevel.error);
return _errorSnackbar("Something went wrong. Please try again later.");
} finally {
isSubmitting.value = false;
}
}
/// Submit payment request (Project API style)
Future<bool> submitPaymentRequest() async {
if (isSubmitting.value) return false;
try {
isSubmitting.value = true;
// Validate form
if (!_validateForm()) return false;
// Build attachment payload
final billAttachments = await buildAttachmentPayload();
final payload = {
"title": titleController.text.trim(),
"projectId": selectedProject.value?['id'] ?? '',
"expenseCategoryId": selectedCategory.value?.id ?? '',
"amount": double.tryParse(amountController.text.trim()) ?? 0,
"currencyId": selectedCurrency.value?.id ?? '',
"description": descriptionController.text.trim(),
"payee": selectedPayee.value?.id ?? "",
"dueDate": selectedDueDate.value?.toIso8601String(),
"isAdvancePayment": isAdvancePayment.value,
"billAttachments": billAttachments.map((a) {
return {
"fileName": a['fileName'],
"fileSize": a['fileSize'],
"contentType": a['contentType'],
};
}).toList(),
};
logSafe("💡 Submitting Payment Request: ${jsonEncode(payload)}");
final success = await ApiService.createExpensePaymentRequestApi(
title: payload['title'],
projectId: payload['projectId'],
expenseCategoryId: payload['expenseCategoryId'],
amount: payload['amount'],
currencyId: payload['currencyId'],
description: payload['description'],
payee: payload['payee'],
dueDate: selectedDueDate.value,
isAdvancePayment: payload['isAdvancePayment'],
billAttachments: billAttachments,
);
logSafe("💡 Payment Request API Response: $success");
if (success == true) {
logSafe("✅ Payment request created successfully.");
return true;
} else {
return _errorSnackbar("Failed to create payment request.");
}
} catch (e, st) {
logSafe("💥 Submit Payment Request Error: $e\n$st",
level: LogLevel.error);
return _errorSnackbar("Something went wrong. Please try again later.");
} finally {
isSubmitting.value = false;
}
}
/// Form validation
bool _validateForm() {
if (selectedProject.value == null ||
selectedProject.value!['id'].toString().isEmpty)
return _errorSnackbar("Please select a project");
if (selectedCategory.value == null)
return _errorSnackbar("Please select a category");
if (selectedPayee.value == null)
return _errorSnackbar("Please select a payee");
if (selectedCurrency.value == null)
return _errorSnackbar("Please select currency");
return true;
}
bool _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
return false;
}
/// Clear form
void clearForm() {
titleController.clear();
dueDateController.clear();
amountController.clear();
descriptionController.clear();
selectedProject.value = null;
selectedCategory.value = null;
selectedPayee.value = null;
selectedCurrency.value = null;
isAdvancePayment.value = false;
attachments.clear();
existingAttachments.clear();
removedAttachments.clear();
}
}

View File

@ -0,0 +1,149 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:on_field_work/model/finance/advance_payment_model.dart';
import 'package:on_field_work/model/finance/get_employee_model.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
class AdvancePaymentController extends GetxController {
/// Advance payments list
var payments = <AdvancePayment>[].obs;
var isLoading = false.obs;
/// Employees for dropdown search
var employees = <Employee>[].obs;
var allEmployees = <Employee>[]; // cache of last API response
var employeesLoading = false.obs;
var searchQuery = ''.obs;
var selectedEmployee = Rxn<Employee>();
/// Prevents unwanted API calls while programmatically updating search
var _suppressSearch = false.obs;
Timer? _debounceTimer;
@override
void onInit() {
super.onInit();
ever<String>(searchQuery, (q) {
if (_suppressSearch.value) return; // Skip while selecting employee
// 🔹 When user types new text, clear previous employee + payments instantly
if (selectedEmployee.value != null) {
selectedEmployee.value = null;
payments.clear();
}
// 🔹 Show fresh dropdown results for new query
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 400), () {
if (q.isNotEmpty) {
fetchEmployees(q); // repopulate dropdown
} else {
employees.clear(); // hide dropdown when search cleared
}
});
});
}
@override
void onClose() {
_debounceTimer?.cancel();
super.onClose();
}
/// Fetch employees by query
Future<void> fetchEmployees(String q) async {
if (q.isEmpty) {
employees.clear();
return;
}
if (employeesLoading.value) return;
try {
employeesLoading.value = true;
// Build query params
final queryParams = {
'allEmployee': 'true',
if (q.isNotEmpty) 'q': q, // only include search query if not empty
};
final list = await ApiService.getEmployees(queryParams: queryParams);
final parsed = Employee.listFromJson(list);
logSafe(
"✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}");
allEmployees = parsed;
_filterEmployees(q);
} catch (e, s) {
logSafe("❌ fetchEmployees error: $e\n$s", level: LogLevel.error);
employees.clear();
} finally {
employeesLoading.value = false;
}
}
/// Local filter to update list based on search text
void _filterEmployees(String query) {
final q = query.toLowerCase();
employees
..clear()
..addAll(allEmployees.where((e) {
return e.name.toLowerCase().contains(q) ||
e.email.toLowerCase().contains(q);
}));
}
/// When user selects employee
void selectEmployee(Employee emp) {
_suppressSearch.value = true;
selectedEmployee.value = emp;
employees.clear(); // hide dropdown
searchQuery.value = emp.name;
fetchAdvancePayments(emp.id);
// Re-enable search after a short delay
Future.delayed(const Duration(milliseconds: 300), () {
_suppressSearch.value = false;
});
}
/// Fetch advance payments for the selected employee
Future<void> fetchAdvancePayments(String employeeId) async {
if (employeeId.isEmpty) {
payments.clear();
return;
}
try {
isLoading.value = true;
final list = await ApiService.getAdvancePayments(employeeId);
payments.assignAll(list);
} catch (e, s) {
logSafe("❌ fetchAdvancePayments error: $e\n$s", level: LogLevel.error);
payments.clear();
} finally {
isLoading.value = false;
}
}
/// Clear employee selection
void clearSelection() {
selectedEmployee.value = null;
payments.clear();
employees.clear();
searchQuery.value = '';
}
void resetSelectionOnNewSearch() {
if (selectedEmployee.value != null) {
selectedEmployee.value = null;
payments.clear();
}
}
}

View File

@ -0,0 +1,134 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
import 'package:on_field_work/model/finance/payment_request_filter.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
class PaymentRequestController extends GetxController {
// ---------------- Observables ----------------
final RxList<PaymentRequest> paymentRequests = <PaymentRequest>[].obs;
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final RxBool isFilterApplied = false.obs;
// ---------------- Pagination ----------------
int _pageSize = 20;
int _pageNumber = 1;
bool _hasMoreData = true;
// ---------------- Filters ----------------
RxMap<String, dynamic> appliedFilter = <String, dynamic>{}.obs;
RxString searchString = ''.obs;
// ---------------- Filter Options ----------------
RxList<IdNameModel> projects = <IdNameModel>[].obs;
RxList<IdNameModel> payees = <IdNameModel>[].obs;
RxList<IdNameModel> categories = <IdNameModel>[].obs;
RxList<IdNameModel> currencies = <IdNameModel>[].obs;
RxList<IdNameModel> statuses = <IdNameModel>[].obs;
RxList<IdNameModel> createdBy = <IdNameModel>[].obs;
// ---------------- Fetch Filter Options ----------------
Future<void> fetchPaymentRequestFilterOptions() async {
try {
final response = await ApiService.getExpensePaymentRequestFilterApi();
if (response != null && response.data != null) {
projects.assignAll(response.data!.projects ?? []);
payees.assignAll(response.data!.payees ?? []);
categories.assignAll(response.data!.expenseCategory ?? []);
currencies.assignAll(response.data!.currency ?? []);
statuses.assignAll(response.data!.status ?? []);
createdBy.assignAll(response.data!.createdBy ?? []);
} else {
logSafe("Payment request filter API returned null",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception in fetchPaymentRequestFilterOptions: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
}
// ---------------- Fetch Payment Requests ----------------
Future<void> fetchPaymentRequests({int pageSize = 20}) async {
isLoading.value = true;
errorMessage.value = '';
_pageNumber = 1;
_pageSize = pageSize;
_hasMoreData = true;
paymentRequests.clear();
await _fetchPaymentRequestsFromApi();
isLoading.value = false;
}
// ---------------- Load More ----------------
Future<void> loadMorePaymentRequests() async {
if (isLoading.value || !_hasMoreData) return;
_pageNumber += 1;
isLoading.value = true;
await _fetchPaymentRequestsFromApi();
isLoading.value = false;
}
// ---------------- Internal API Call ----------------
Future<void> _fetchPaymentRequestsFromApi() async {
try {
final response = await ApiService.getExpensePaymentRequestListApi(
pageSize: _pageSize,
pageNumber: _pageNumber,
filter: appliedFilter,
searchString: searchString.value,
);
final data = response?.data;
final reqList = data?.data ?? [];
if (response != null && data != null && reqList.isNotEmpty) {
if (_pageNumber == 1) {
paymentRequests.assignAll(reqList);
} else {
paymentRequests.addAll(reqList);
}
if (reqList.length < _pageSize) {
_hasMoreData = false;
}
} else {
if (_pageNumber == 1) {
errorMessage.value = 'No payment requests found.';
}
_hasMoreData = false;
}
} catch (e, stack) {
errorMessage.value = 'Failed to fetch payment requests.';
logSafe("Exception in _fetchPaymentRequestsFromApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
_hasMoreData = false;
}
}
// ---------------- Filter Management ----------------
void setFilterApplied(bool applied) {
isFilterApplied.value = applied;
}
void applyFilter(Map<String, dynamic> filter, {String search = ''}) {
appliedFilter.assignAll(filter);
searchString.value = search;
isFilterApplied.value = filter.isNotEmpty || search.isNotEmpty;
fetchPaymentRequests();
}
void clearFilter() {
appliedFilter.clear();
searchString.value = '';
isFilterApplied.value = false;
fetchPaymentRequests();
}
}

View File

@ -0,0 +1,363 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
import 'package:on_field_work/model/expense/payment_types_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mime/mime.dart';
import 'package:on_field_work/controller/finance/payment_request_controller.dart';
class PaymentRequestDetailController extends GetxController {
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
// Employee selection
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
final TextEditingController employeeSearchController =
TextEditingController();
PaymentRequestController get paymentRequestController =>
Get.find<PaymentRequestController>();
final RxBool isSearchingEmployees = false.obs;
// Attachments
final RxList<File> attachments = <File>[].obs;
final RxList<Map<String, dynamic>> existingAttachments =
<Map<String, dynamic>>[].obs;
final isProcessingAttachment = false.obs;
// Payment mode
final selectedPaymentMode = Rxn<PaymentModeModel>();
// Text controllers for form
final TextEditingController locationController = TextEditingController();
final TextEditingController gstNumberController = TextEditingController();
// Form submission state
final RxBool isSubmitting = false.obs;
late String _requestId;
bool _isInitialized = false;
RxBool paymentSheetOpened = false.obs;
final ImagePicker _picker = ImagePicker();
/// Initialize controller
void init(String requestId) {
if (_isInitialized) return;
_isInitialized = true;
_requestId = requestId;
// Fetch payment request details + employees concurrently
Future.wait([
fetchPaymentRequestDetail(),
fetchAllEmployees(),
fetchPaymentModes(),
]);
}
/// Generic API wrapper for error handling
Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async {
isLoading.value = true;
errorMessage.value = '';
try {
final result = await apiCall();
return result;
} catch (e) {
errorMessage.value = 'Error during $operationName: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
return null;
} finally {
isLoading.value = false;
}
}
/// Fetch payment request details
Future<void> fetchPaymentRequestDetail() async {
isLoading.value = true;
try {
final response =
await ApiService.getExpensePaymentRequestDetailApi(_requestId);
if (response != null) {
paymentRequest.value = response.data;
} else {
errorMessage.value = "Failed to fetch payment request details";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
}
} catch (e) {
errorMessage.value = "Error fetching payment request details: $e";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Pick files from gallery or file picker
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) {
isProcessingAttachment.value = true;
File imageFile = File(pickedFile.path);
File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: imageFile,
);
attachments.add(timestampedFile);
attachments.refresh();
}
} catch (e) {
_errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false;
}
}
// --- Location ---
final RxBool isFetchingLocation = false.obs;
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;
}
/// 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)));
} catch (e) {
errorMessage.value = 'Failed to parse employee data: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
}
} else {
allEmployees.clear();
}
}
/// Fetch payment modes
Future<void> fetchPaymentModes() async {
isLoading.value = true;
try {
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
} else {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Failed to fetch payment modes',
type: SnackbarType.error);
}
} catch (e) {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Error fetching payment modes: $e',
type: SnackbarType.error);
} finally {
isLoading.value = false;
}
}
/// Search employees
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) {
employeeSearchResults.clear();
return;
}
isSearchingEmployees.value = true;
try {
final data =
await ApiService.searchEmployeesBasic(searchString: query.trim());
employeeSearchResults.assignAll(
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
);
} catch (e) {
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Update payment request status
Future<bool> updatePaymentRequestStatus({
required String statusId,
required String comment,
String? paidTransactionId,
String? paidById,
DateTime? paidAt,
double? baseAmount,
double? taxAmount,
String? tdsPercentage,
}) async {
isLoading.value = true;
try {
final success = await ApiService.updateExpensePaymentRequestStatusApi(
paymentRequestId: _requestId,
statusId: statusId,
comment: comment,
paidTransactionId: paidTransactionId,
paidById: paidById,
paidAt: paidAt,
baseAmount: baseAmount,
taxAmount: taxAmount,
tdsPercentage: tdsPercentage,
);
if (success) {
// Controller refreshes the data but does not show snackbars.
await fetchPaymentRequestDetail();
paymentRequestController.fetchPaymentRequests();
}
return success;
} catch (e) {
// Controller returns false on error; UI will show the snackbar.
return false;
} finally {
isLoading.value = false;
}
}
// --- Snackbar Helper ---
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
// --- Payment Mode Selection ---
void selectPaymentMode(PaymentModeModel mode) {
selectedPaymentMode.value = mode;
}
// --- Submit Expense ---
Future<bool> submitExpense(
{required String statusId, String? comment}) async {
if (selectedPaymentMode.value == null) return false;
isSubmitting.value = true;
try {
// prepare attachments
final success = await ApiService.createExpenseForPRApi(
paymentModeId: selectedPaymentMode.value!.id,
location: locationController.text,
gstNumber: gstNumberController.text,
paymentRequestId: _requestId,
billAttachments: attachments.map((file) {
final bytes = file.readAsBytesSync();
final mimeType =
lookupMimeType(file.path) ?? 'application/octet-stream';
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType": mimeType,
"description": "",
"fileSize": bytes.length,
"isActive": true,
};
}).toList(),
statusId: statusId,
comment: comment ?? '',
);
if (success) {
// Refresh the payment request details so the UI updates
await fetchPaymentRequestDetail();
}
return success;
} finally {
isSubmitting.value = false;
}
}
}

View File

@ -0,0 +1,48 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
class InfraProjectController extends GetxController {
final projects = <ProjectData>[].obs;
final isLoading = false.obs;
final searchQuery = ''.obs;
// Filtered list
List<ProjectData> get filteredProjects {
final q = searchQuery.value.trim().toLowerCase();
if (q.isEmpty) return projects;
return projects.where((p) {
return (p.name?.toLowerCase().contains(q) ?? false) ||
(p.shortName?.toLowerCase().contains(q) ?? false) ||
(p.projectAddress?.toLowerCase().contains(q) ?? false) ||
(p.contactPerson?.toLowerCase().contains(q) ?? false);
}).toList();
}
// Fetch Projects
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
try {
isLoading.value = true;
final response = await ApiService.getInfraProjectsList(
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.data != null) {
projects.assignAll(response.data!.data ?? []);
} else {
projects.clear();
}
} catch (e) {
rethrow;
} finally {
isLoading.value = false;
}
}
void updateSearch(String query) {
searchQuery.value = query;
}
}

View File

@ -0,0 +1,38 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
class InfraProjectDetailsController extends GetxController {
final String projectId;
InfraProjectDetailsController({required this.projectId});
var isLoading = true.obs;
var projectDetails = Rxn<ProjectData>();
var errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
fetchProjectDetails();
}
Future<void> fetchProjectDetails() async {
try {
isLoading.value = true;
final response = await ApiService.getInfraProjectDetails(projectId: projectId);
if (response != null && response.success == true && response.data != null) {
projectDetails.value = response.data;
isLoading.value = false;
} else {
errorMessage.value = response?.message ?? "Failed to load project details";
}
} catch (e) {
errorMessage.value = "Error fetching project details: $e";
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,3 +1,3 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class AuthLayout2Controller extends MyController {} class AuthLayout2Controller extends MyController {}

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart'; import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AuthLayoutController extends MyController { class AuthLayoutController extends MyController {

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/model/project_model.dart'; import 'package:on_field_work/model/project_model.dart';
class LayoutController extends GetxController { class LayoutController extends GetxController {
// Theme Customization // Theme Customization
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
isLoadingProjects.value = true; isLoadingProjects.value = true;
try { try {
final response = await ApiService.getProjects(); final response = await ApiService.getGlobalProjects();
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList(); final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();

View File

@ -1,5 +1,5 @@
import 'package:get/get_state_manager/get_state_manager.dart'; import 'package:get/get_state_manager/get_state_manager.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
abstract class MyController extends GetxController { abstract class MyController extends GetxController {
@override @override

View File

@ -2,11 +2,11 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/permission_service.dart'; import 'package:on_field_work/helpers/services/permission_service.dart';
import 'package:marco/model/user_permission.dart'; import 'package:on_field_work/model/user_permission.dart';
import 'package:marco/model/employees/employee_info.dart'; import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:marco/model/projects_model.dart'; import 'package:on_field_work/model/projects_model.dart';
class PermissionController extends GetxController { class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs; var permissions = <UserPermission>[].obs;
@ -15,6 +15,9 @@ class PermissionController extends GetxController {
Timer? _refreshTimer; Timer? _refreshTimer;
var isLoading = true.obs; var isLoading = true.obs;
/// NEW: reactive flag to signal permissions are loaded
var permissionsLoaded = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -52,6 +55,10 @@ class PermissionController extends GetxController {
_updateState(userData); _updateState(userData);
await _storeData(); await _storeData();
logSafe("Data loaded and state updated successfully."); logSafe("Data loaded and state updated successfully.");
// NEW: mark permissions as loaded
permissionsLoaded.value = true;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error loading data from API", logSafe("Error loading data from API",
level: LogLevel.error, error: e, stackTrace: stacktrace); level: LogLevel.error, error: e, stackTrace: stacktrace);
@ -103,7 +110,7 @@ class PermissionController extends GetxController {
} }
void _startAutoRefresh() { void _startAutoRefresh() {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async { _refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
logSafe("Auto-refresh triggered."); logSafe("Auto-refresh triggered.");
final token = await _getAuthToken(); final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
@ -117,8 +124,6 @@ class PermissionController extends GetxController {
bool hasPermission(String permissionId) { bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId); final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug);
return hasPerm; return hasPerm;
} }

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/global_project_model.dart'; import 'package:on_field_work/model/global_project_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
class ProjectController extends GetxController { class ProjectController extends GetxController {
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs; RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/service_project/service_project_details_screen_controller.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
class AddServiceProjectJobController extends GetxController {
// FORM CONTROLLERS
final titleCtrl = TextEditingController();
final descCtrl = TextEditingController();
final tagCtrl = TextEditingController();
final searchFocusNode = FocusNode();
// OBSERVABLES
final startDate = Rx<DateTime?>(DateTime.now());
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
final enteredTags = <String>[].obs;
final selectedAssignees = <EmployeeModel>[].obs;
// Branches
final branches = <Branch>[].obs;
final selectedBranch = Rxn<Branch>();
final isBranchLoading = false.obs;
// Loading
final isLoading = false.obs;
@override
void onClose() {
titleCtrl.dispose();
descCtrl.dispose();
tagCtrl.dispose();
searchFocusNode.dispose();
super.onClose();
}
// FETCH BRANCHES
Future<void> fetchBranches(String projectId) async {
isBranchLoading.value = true;
final response = await ApiService.getServiceProjectBranchesFull(
projectId: projectId,
);
if (response != null && response.success) {
branches.assignAll(response.data?.data ?? []);
}
isBranchLoading.value = false;
}
// CREATE JOB
Future<void> createJob(String projectId) async {
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
showAppSnackbar(
title: "Validation",
message: "Title and Description are required",
type: SnackbarType.warning,
);
return;
}
isLoading.value = true;
final jobId = await ApiService.createServiceProjectJobApi(
title: titleCtrl.text.trim(),
description: descCtrl.text.trim(),
projectId: projectId,
branchId: selectedBranch.value?.id,
assignees: selectedAssignees // payload mapping
.map((e) => {"employeeId": e.id, "isActive": true})
.toList(),
startDate: startDate.value!,
dueDate: dueDate.value!,
tags: enteredTags
.map((tag) => {"id": null, "name": tag, "isActive": true})
.toList(),
);
isLoading.value = false;
if (jobId != null) {
if (Get.isRegistered<ServiceProjectDetailsController>()) {
final detailsCtrl = Get.find<ServiceProjectDetailsController>();
// 🔥 1. Refresh job LIST
detailsCtrl.refreshJobsAfterAdd();
// 🔥 2. Refresh job DETAILS (FULL DATA - including tags and assignees)
await detailsCtrl.fetchJobDetail(jobId);
}
Get.back();
showAppSnackbar(
title: "Success",
message: "Job created successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to create job",
type: SnackbarType.error,
);
}
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
class ServiceProjectAllocationController extends GetxController {
final projectId = ''.obs;
// Roles
var roles = <TeamRole>[].obs;
var selectedRole = Rxn<TeamRole>();
// Employees
var roleEmployees = <Employee>[].obs;
var selectedEmployees = <Employee>[].obs;
final displayController = TextEditingController();
// Loading
var isLoading = false.obs;
@override
void onInit() {
super.onInit();
ever(selectedEmployees, (_) {
displayController.text = selectedEmployees.isEmpty
? ''
: selectedEmployees
.map((e) => '${e.firstName} ${e.lastName}')
.join(', ');
});
}
// Fetch all roles
Future<void> fetchRoles() async {
isLoading.value = true;
final result = await ApiService.getTeamRoles();
if (result != null) {
roles.assignAll(result);
}
isLoading.value = false;
}
// Fetch employees by role
Future<void> fetchEmployeesByRole(String roleId) async {
isLoading.value = true;
final allocations = await ApiService.getServiceProjectAllocationList(
projectId: projectId.value);
if (allocations != null) {
roleEmployees.assignAll(
allocations
.where((a) => a.teamRole.id == roleId)
.map((a) => a.employee)
.toList(),
);
}
isLoading.value = false;
}
void toggleEmployee(Employee emp) {
if (selectedEmployees.contains(emp)) {
selectedEmployees.remove(emp);
} else {
selectedEmployees.add(emp);
}
}
Future<bool> submitAllocation() async {
final payload = selectedEmployees
.map((e) => {
"projectId": projectId.value,
"employeeId": e.id,
"teamRoleId": selectedRole.value?.id,
"isActive": true,
})
.toList();
return await ApiService.manageServiceProjectAllocation(payload: payload);
}
}

View File

@ -0,0 +1,479 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
import 'package:on_field_work/model/service_project/job_list_model.dart';
import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart';
import 'package:geolocator/geolocator.dart';
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/model/service_project/job_status_response.dart';
import 'package:on_field_work/model/service_project/job_comments.dart';
import 'dart:convert';
import 'dart:io';
import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
class ServiceProjectDetailsController extends GetxController {
// -------------------- Observables --------------------
var projectId = ''.obs;
var projectDetail = Rxn<ProjectDetail>();
var jobList = <JobEntity>[].obs;
var jobDetail = Rxn<JobDetailsResponse>();
var showArchivedJobs = false.obs; // true = archived, false = active
// Loading states
var isLoading = false.obs;
var isJobLoading = false.obs;
var isJobDetailLoading = false.obs;
// Error messages
var errorMessage = ''.obs;
var jobErrorMessage = ''.obs;
var jobDetailErrorMessage = ''.obs;
final ImagePicker picker = ImagePicker();
var isProcessingAttachment = false.obs;
// Pagination
var pageNumber = 1;
final int pageSize = 20;
var hasMoreJobs = true.obs;
var isTagging = false.obs;
var attendanceMessage = ''.obs;
var attendanceLog = Rxn<JobAttendanceResponse>();
var teamList = <ServiceProjectAllocation>[].obs;
var isTeamLoading = false.obs;
var teamErrorMessage = ''.obs;
var filteredJobList = <JobEntity>[].obs;
// -------------------- Job Status --------------------
// With this:
var jobStatusList = <JobStatus>[].obs;
var selectedJobStatus = Rx<JobStatus?>(null);
var isJobStatusLoading = false.obs;
var jobStatusErrorMessage = ''.obs;
// -------------------- Job Comments --------------------
var jobComments = <CommentItem>[].obs;
var isCommentsLoading = false.obs;
var commentsErrorMessage = ''.obs;
// -------------------- Lifecycle --------------------
@override
void onInit() {
super.onInit();
fetchProjectJobs();
filteredJobList.value = jobList;
}
// -------------------- Project --------------------
void setProjectId(String id) {
if (projectId.value == id) return;
projectId.value = id;
// Reset pagination and list
pageNumber = 1;
hasMoreJobs.value = true;
jobList.clear();
filteredJobList.clear();
// Fetch project detail
fetchProjectDetail();
// Always fetch jobs for this project
fetchProjectJobs(refresh: true);
}
void updateJobSearch(String searchText) {
if (searchText.isEmpty) {
filteredJobList.value = jobList;
} else {
filteredJobList.value = jobList.where((job) {
final lowerSearch = searchText.toLowerCase();
return job.title.toLowerCase().contains(lowerSearch) ||
(job.description.toLowerCase().contains(lowerSearch)) ||
(job.tags?.any(
(tag) => tag.name.toLowerCase().contains(lowerSearch)) ??
false);
}).toList();
}
}
Future<void> fetchProjectTeams() async {
if (projectId.value.isEmpty) {
teamErrorMessage.value = "Invalid project ID";
return;
}
isTeamLoading.value = true;
teamErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectAllocationList(
projectId: projectId.value,
isActive: true,
);
if (result != null) {
teamList.value = result;
} else {
teamErrorMessage.value = "No teams found";
}
} catch (e) {
teamErrorMessage.value = "Error fetching teams: $e";
} finally {
isTeamLoading.value = false;
}
}
Future<void> fetchJobStatus({required String statusId}) async {
if (projectId.value.isEmpty) {
jobStatusErrorMessage.value = "Invalid project ID";
return;
}
isJobStatusLoading.value = true;
jobStatusErrorMessage.value = '';
try {
final statuses = await ApiService.getMasterJobStatus(
projectId: projectId.value,
statusId: statusId,
);
if (statuses != null && statuses.isNotEmpty) {
jobStatusList.value = statuses;
// Keep previously selected if exists, else pick first
selectedJobStatus.value = statuses.firstWhere(
(status) => status.id == selectedJobStatus.value?.id,
orElse: () => statuses.first,
);
print("Job Status List: ${jobStatusList.map((e) => e.name).toList()}");
} else {
jobStatusErrorMessage.value = "No job statuses found";
}
} catch (e) {
jobStatusErrorMessage.value = "Error fetching job status: $e";
} finally {
isJobStatusLoading.value = false;
}
}
Future<void> fetchProjectDetail() async {
if (projectId.value.isEmpty) {
errorMessage.value = "Invalid project ID";
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
final result =
await ApiService.getServiceProjectDetailApi(projectId.value);
if (result != null && result.data != null) {
projectDetail.value = result.data!;
} else {
errorMessage.value =
result?.message ?? "Failed to fetch project details";
}
} catch (e) {
errorMessage.value = "Error: $e";
} finally {
isLoading.value = false;
}
}
Future<void> fetchJobAttendanceLog(String attendanceId) async {
if (attendanceId.isEmpty) {
attendanceMessage.value = "Invalid attendance ID";
return;
}
isJobDetailLoading.value = true;
attendanceMessage.value = '';
try {
final result =
await ApiService.getJobAttendanceLog(attendanceId: attendanceId);
if (result != null) {
attendanceLog.value = result;
} else {
attendanceMessage.value = "Attendance log not found or empty";
}
} catch (e) {
attendanceMessage.value = "Error fetching attendance log: $e";
} finally {
isJobDetailLoading.value = false;
}
}
// -------------------- Job List (modified to always load) --------------------
Future<void> fetchProjectJobs({bool refresh = false}) async {
if (projectId.value.isEmpty) return;
if (refresh) pageNumber = 1;
if (!hasMoreJobs.value && !refresh) return;
isJobLoading.value = true;
jobErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectJobListApi(
projectId: projectId.value,
pageNumber: pageNumber,
pageSize: pageSize,
isActive: true,
isArchive: showArchivedJobs.value,
);
if (result != null && result.data != null) {
final newJobs = result.data?.data ?? [];
if (refresh || pageNumber == 1) {
jobList.value = newJobs;
} else {
jobList.addAll(newJobs);
}
filteredJobList.value = jobList;
hasMoreJobs.value = newJobs.length == pageSize;
if (hasMoreJobs.value) pageNumber++;
} else {
jobErrorMessage.value = result?.message ?? "Failed to fetch jobs";
}
} catch (e) {
jobErrorMessage.value = "Error fetching jobs: $e";
} finally {
isJobLoading.value = false;
}
}
Future<void> fetchMoreJobs() async => fetchProjectJobs();
// -------------------- Manual Refresh --------------------
Future<void> refresh() async {
pageNumber = 1;
hasMoreJobs.value = true;
await Future.wait([
fetchProjectDetail(),
fetchProjectJobs(),
]);
}
// -------------------- Job Detail --------------------
Future<void> fetchJobDetail(String jobId) async {
if (jobId.isEmpty) {
jobDetailErrorMessage.value = "Invalid job ID";
return;
}
isJobDetailLoading.value = true;
jobDetailErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectJobDetailApi(jobId);
if (result != null) {
jobDetail.value = result;
} else {
jobDetailErrorMessage.value = "Failed to fetch job details";
}
} catch (e) {
jobDetailErrorMessage.value = "Error fetching job details: $e";
} finally {
isJobDetailLoading.value = false;
}
}
Future<Position?> _getCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
attendanceMessage.value = "Location services are disabled.";
return null;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
attendanceMessage.value = "Location permission denied";
return null;
}
}
if (permission == LocationPermission.deniedForever) {
attendanceMessage.value =
"Location permission permanently denied. Enable it from settings.";
return null;
}
return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
} catch (e) {
attendanceMessage.value = "Failed to get location: $e";
return null;
}
}
Future<void> fetchJobComments({bool refresh = false}) async {
if (jobDetail.value?.data?.id == null) {
commentsErrorMessage.value = "Invalid job ID";
return;
}
if (refresh) pageNumber = 1;
isCommentsLoading.value = true;
commentsErrorMessage.value = '';
try {
final response = await ApiService.getJobCommentList(
jobTicketId: jobDetail.value!.data!.id!,
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.data != null) {
final newComments = response.data?.data ?? [];
if (refresh || pageNumber == 1) {
jobComments.value = newComments;
} else {
jobComments.addAll(newComments);
}
hasMoreJobs.value =
(response.data?.totalEntities ?? 0) > (pageNumber * pageSize);
if (hasMoreJobs.value) pageNumber++;
} else {
commentsErrorMessage.value =
response?.message ?? "Failed to fetch comments";
}
} catch (e) {
commentsErrorMessage.value = "Error fetching comments: $e";
} finally {
isCommentsLoading.value = false;
}
}
Future<bool> addJobComment({
required String jobId,
required String comment,
List<File>? files,
}) async {
try {
List<Map<String, dynamic>> attachments = [];
if (files != null && files.isNotEmpty) {
for (final file in files) {
final bytes = await file.readAsBytes();
final base64Data = base64Encode(bytes);
final mimeType =
lookupMimeType(file.path) ?? "application/octet-stream";
attachments.add({
"fileName": file.path.split('/').last,
"base64Data": base64Data,
"contentType": mimeType,
"fileSize": bytes.length,
"description": "",
"isActive": true,
});
}
}
final success = await ApiService.addJobComment(
jobTicketId: jobId,
comment: comment,
attachments: attachments,
);
if (success) {
await fetchJobDetail(jobId);
refresh();
}
return success;
} catch (e) {
print("Error adding comment: $e");
return false;
}
}
/// Tag In / Tag Out for a job with proper payload
Future<void> updateJobAttendance({
required String jobId,
required int action,
String comment = "Updated via app",
File? attachment,
}) async {
if (jobId.isEmpty) return;
isTagging.value = true;
attendanceMessage.value = '';
try {
final position = await _getCurrentLocation();
if (position == null) {
isTagging.value = false;
return;
}
Map<String, dynamic>? attachmentPayload;
if (attachment != null) {
final bytes = await attachment.readAsBytes();
final base64Data = base64Encode(bytes);
final mimeType =
lookupMimeType(attachment.path) ?? 'application/octet-stream';
attachmentPayload = {
"documentId": jobId,
"fileName": attachment.path.split('/').last,
"base64Data": base64Data,
"contentType": mimeType,
"fileSize": bytes.length,
"description": "Attached via app",
"isActive": true,
};
}
final payload = {
"jobTcketId": jobId,
"action": action,
"latitude": position.latitude.toString(),
"longitude": position.longitude.toString(),
"comment": comment,
"attachment": attachmentPayload,
};
final success = await ApiService.updateServiceProjectJobAttendance(
payload: payload,
);
if (success) {
attendanceMessage.value =
action == 0 ? "Tagged In successfully" : "Tagged Out successfully";
await fetchJobDetail(jobId);
} else {
attendanceMessage.value = "Failed to update attendance";
}
} catch (e) {
attendanceMessage.value = "Error updating attendance: $e";
} finally {
isTagging.value = false;
}
}
// ------------------------------------------------------------
// 🔥 AUTO REFRESH JOB LIST AFTER ADDING A JOB
// ------------------------------------------------------------
Future<void> refreshJobsAfterAdd() async {
pageNumber = 1;
hasMoreJobs.value = true;
await fetchProjectJobs();
}
}

View File

@ -0,0 +1,59 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
class ServiceProjectController extends GetxController {
final projects = <ProjectItem>[].obs;
final isLoading = false.obs;
final searchQuery = ''.obs;
/// Computed filtered project list
List<ProjectItem> get filteredProjects {
final query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return projects;
return projects.where((p) {
final nameMatch = p.name.toLowerCase().contains(query);
final shortNameMatch = p.shortName.toLowerCase().contains(query);
final addressMatch = p.address.toLowerCase().contains(query);
final contactMatch = p.contactName.toLowerCase().contains(query);
final clientMatch = p.client != null &&
(p.client!.name.toLowerCase().contains(query) ||
p.client!.contactPerson.toLowerCase().contains(query));
return nameMatch ||
shortNameMatch ||
addressMatch ||
contactMatch ||
clientMatch;
}).toList();
}
/// Fetch projects from API
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
try {
isLoading.value = true;
final result = await ApiService.getServiceProjectsListApi(
pageNumber: pageNumber,
pageSize: pageSize,
);
if (result != null && result.data != null) {
projects.assignAll(result.data!.data);
} else {
projects.clear();
}
} catch (e) {
// Optional: log or show error
rethrow;
} finally {
isLoading.value = false;
}
}
/// Update search
void updateSearch(String query) {
searchQuery.value = query;
}
}

View File

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController { class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart'; import 'package:on_field_work/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
class DailyTaskController extends GetxController { class DailyTaskController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
@ -13,6 +13,10 @@ class DailyTaskController extends GetxController {
DateTime? startDateTask; DateTime? startDateTask;
DateTime? endDateTask; DateTime? endDateTask;
// Rx fields for DateRangePickerWidget
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
List<TaskModel> dailyTasks = []; List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs; final RxSet<String> expandedDates = <String>{}.obs;
@ -33,14 +37,19 @@ class DailyTaskController extends GetxController {
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {}; Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination // Pagination
int currentPage = 1; int currentPage = 1;
int pageSize = 20; int pageSize = 20;
bool hasMore = true; bool hasMore = true;
FilterData? taskFilterData;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeDefaults(); _initializeDefaults();
_initializeRxDates();
} }
void _initializeDefaults() { void _initializeDefaults() {
@ -58,6 +67,12 @@ class DailyTaskController extends GetxController {
); );
} }
void _initializeRxDates() {
startDateTaskRx.value =
startDateTask ?? DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = endDateTask ?? DateTime.now();
}
void clearTaskFilters() { void clearTaskFilters() {
selectedBuildings.clear(); selectedBuildings.clear();
selectedFloors.clear(); selectedFloors.clear();
@ -65,9 +80,26 @@ class DailyTaskController extends GetxController {
selectedServices.clear(); selectedServices.clear();
startDateTask = null; startDateTask = null;
endDateTask = null; endDateTask = null;
// reset Rx dates as well
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = DateTime.now();
update(); update();
} }
void updateDateRange(DateTime? start, DateTime? end) {
if (start != null && end != null) {
startDateTask = start;
endDateTask = end;
startDateTaskRx.value = start;
endDateTaskRx.value = end;
update();
}
}
Future<void> fetchTaskData( Future<void> fetchTaskData(
String projectId, { String projectId, {
int pageNumber = 1, int pageNumber = 1,
@ -102,11 +134,24 @@ class DailyTaskController extends GetxController {
); );
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
if (!isLoadMore) {
groupedDailyTasks.clear();
}
for (var task in response) { for (var task in response) {
final assignmentDateKey = final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0]; task.assignmentDate.toIso8601String().split('T')[0];
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
// Initialize list if not present
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []);
// Only add task if it doesn't already exist (avoid duplicates)
if (!groupedDailyTasks[assignmentDateKey]!
.any((t) => t.id == task.id)) {
groupedDailyTasks[assignmentDateKey]!.add(task);
}
} }
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList(); dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber; currentPage = pageNumber;
} else { } else {
@ -119,16 +164,13 @@ class DailyTaskController extends GetxController {
update(); update();
} }
FilterData? taskFilterData;
Future<void> fetchTaskFilter(String projectId) async { Future<void> fetchTaskFilter(String projectId) async {
isFilterLoading.value = true; isFilterLoading.value = true;
try { try {
final filterResponse = await ApiService.getDailyTaskFilter(projectId); final filterResponse = await ApiService.getDailyTaskFilter(projectId);
if (filterResponse != null && filterResponse.success) { if (filterResponse != null && filterResponse.success) {
taskFilterData = taskFilterData = filterResponse.data;
filterResponse.data; // now taskFilterData is FilterData?
logSafe( logSafe(
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}", "Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
level: LogLevel.info, level: LogLevel.info,
@ -171,12 +213,15 @@ class DailyTaskController extends GetxController {
startDateTask = picked.start; startDateTask = picked.start;
endDateTask = picked.end; endDateTask = picked.end;
// update Rx fields as well
startDateTaskRx.value = picked.start;
endDateTaskRx.value = picked.end;
logSafe( logSafe(
"Date range selected: $startDateTask to $endDateTask", "Date range selected: $startDateTask to $endDateTask",
level: LogLevel.info, level: LogLevel.info,
); );
// Add null check before calling fetchTaskData
final projectId = controller.selectedProjectId; final projectId = controller.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) { if (projectId != null && projectId.isNotEmpty) {
await controller.fetchTaskData(projectId); await controller.fetchTaskData(projectId);
@ -190,9 +235,7 @@ class DailyTaskController extends GetxController {
required String projectId, required String projectId,
required String taskAllocationId, required String taskAllocationId,
}) async { }) async {
// re-fetch tasks
await fetchTaskData(projectId); await fetchTaskData(projectId);
update();
update(); // rebuilds UI
} }
} }

View File

@ -1,11 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart'; import 'package:on_field_work/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController { class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
@ -23,6 +23,10 @@ class DailyTaskPlanningController extends GetxController {
RxBool isFetchingProjects = true.obs; RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = true.obs; RxBool isFetchingEmployees = true.obs;
/// New: track per-building loading and loaded state for lazy infra loading
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
final Set<String> buildingsWithDetails = <String>{};
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -106,7 +110,7 @@ class DailyTaskPlanningController extends GetxController {
} }
} }
/// Fetch Infra details and then tasks per work area /// Fetch buildings list only (no deep area/workItem calls) for initial load.
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async { Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) return; if (projectId == null) return;
@ -123,25 +127,24 @@ class DailyTaskPlanningController extends GetxController {
return; return;
} }
dailyTasks = infraData.map((buildingJson) { // Filter buildings with 0 planned & completed work
final filteredBuildings = infraData.where((b) {
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
return planned > 0 || completed > 0;
}).toList();
dailyTasks = filteredBuildings.map((buildingJson) {
final building = Building( final building = Building(
id: buildingJson['id'], id: buildingJson['id'],
name: buildingJson['buildingName'], name: buildingJson['buildingName'],
description: buildingJson['description'], description: buildingJson['description'],
floors: (buildingJson['floors'] as List<dynamic>) floors: [],
.map((floorJson) => Floor( plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
id: floorJson['id'], completedWork:
floorName: floorJson['floorName'], (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
workAreas: (floorJson['workAreas'] as List<dynamic>)
.map((areaJson) => WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [],
))
.toList(),
))
.toList(),
); );
return TaskPlanningDetailsModel( return TaskPlanningDetailsModel(
id: building.id, id: building.id,
name: building.name, name: building.name,
@ -154,14 +157,75 @@ class DailyTaskPlanningController extends GetxController {
); );
}).toList(); }).toList();
await Future.wait(dailyTasks buildingLoadingStates.clear();
.expand((task) => task.buildings) buildingsWithDetails.clear();
.expand((b) => b.floors) } catch (e, stack) {
.expand((f) => f.workAreas) logSafe("Error fetching daily task data",
.map((area) async { level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isFetchingTasks.value = false;
update();
}
}
/// Fetch full infra for a single building (floors, workAreas, workItems).
/// Called lazily when user expands a building in the UI.
Future<void> fetchBuildingInfra(
String buildingId, String projectId, String? serviceId) async {
if (buildingId.isEmpty) return;
// mark loading
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
buildingLoadingStates[buildingId]!.value = true;
update();
try {
// Re-use getInfraDetails and find the building entry for the requested buildingId
final infraResponse =
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
final buildingJson = infraData.firstWhere(
(b) => b['id'].toString() == buildingId.toString(),
orElse: () => null,
);
if (buildingJson == null) {
logSafe("Building $buildingId not found in infra response",
level: LogLevel.warning);
return;
}
// Build floors & workAreas for this building
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors:
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
return Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
.map((areaJson) {
return WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [], // will populate later
);
}).toList(),
);
}).toList(),
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
);
// For each workArea, fetch its work items and populate
await Future.wait(
building.floors.expand((f) => f.workAreas).map((area) async {
try { try {
final taskResponse = final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId); serviceId: serviceId);
final taskData = taskResponse?['data'] as List<dynamic>? ?? []; final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper( area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
workItemId: taskJson['id'], workItemId: taskJson['id'],
@ -171,11 +235,14 @@ class DailyTaskPlanningController extends GetxController {
? ActivityMaster.fromJson(taskJson['activityMaster']) ? ActivityMaster.fromJson(taskJson['activityMaster'])
: null, : null,
workCategoryMaster: taskJson['workCategoryMaster'] != null workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster']) ? WorkCategoryMaster.fromJson(
taskJson['workCategoryMaster'])
: null, : null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(), plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork: (taskJson['completedWork'] as num?)?.toDouble(), completedWork:
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(), (taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned:
(taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?, description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate']) ? DateTime.tryParse(taskJson['taskDate'])
@ -187,11 +254,40 @@ class DailyTaskPlanningController extends GetxController {
level: LogLevel.error, error: e, stackTrace: stack); level: LogLevel.error, error: e, stackTrace: stack);
} }
})); }));
// Merge/replace the building into dailyTasks
bool merged = false;
for (var t in dailyTasks) {
final idx = t.buildings
.indexWhere((b) => b.id.toString() == building.id.toString());
if (idx != -1) {
t.buildings[idx] = building;
merged = true;
break;
}
}
if (!merged) {
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
dailyTasks.add(TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
));
}
// Mark as loaded
buildingsWithDetails.add(buildingId.toString());
} catch (e, stack) { } catch (e, stack) {
logSafe("Error fetching daily task data", logSafe("Error fetching infra for building $buildingId",
level: LogLevel.error, error: e, stackTrace: stack); level: LogLevel.error, error: e, stackTrace: stack);
} finally { } finally {
isFetchingTasks.value = false; buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
buildingLoadingStates[buildingId]!.value = false;
update(); update();
} }
} }
@ -211,7 +307,8 @@ class DailyTaskPlanningController extends GetxController {
); );
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
employees.assignAll(response.map((json) => EmployeeModel.fromJson(json))); employees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
if (serviceId == null && organizationId == null) { if (serviceId == null && organizationId == null) {
allEmployeesCache = List.from(employees); allEmployeesCache = List.from(employees);
@ -219,12 +316,14 @@ class DailyTaskPlanningController extends GetxController {
final currentEmployeeIds = employees.map((e) => e.id).toSet(); final currentEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates.removeWhere((key, _) => !currentEmployeeIds.contains(key)); uploadingStates
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
employees.forEach((emp) { employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs); uploadingStates.putIfAbsent(emp.id, () => false.obs);
}); });
selectedEmployees.removeWhere((e) => !currentEmployeeIds.contains(e.id)); selectedEmployees
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info); logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
} else { } else {
@ -239,13 +338,17 @@ class DailyTaskPlanningController extends GetxController {
); );
} }
} catch (e, stack) { } catch (e, stack) {
logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack); logSafe("Error fetching employees",
level: LogLevel.error, error: e, stackTrace: stack);
if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) { if (serviceId == null &&
organizationId == null &&
allEmployeesCache.isNotEmpty) {
employees.assignAll(allEmployeesCache); employees.assignAll(allEmployeesCache);
final cachedEmployeeIds = employees.map((e) => e.id).toSet(); final cachedEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates.removeWhere((key, _) => !cachedEmployeeIds.contains(key)); uploadingStates
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
employees.forEach((emp) { employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs); uploadingStates.putIfAbsent(emp.id, () => false.obs);
}); });

View File

@ -4,16 +4,16 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/work_status_model.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }

View File

@ -1,17 +1,17 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/all_organization_model.dart'; import 'package:on_field_work/model/all_organization_model.dart';
class AllOrganizationController extends GetxController { class AllOrganizationController extends GetxController {
RxList<AllOrganization> organizations = <AllOrganization>[].obs; RxList<AllOrganization> organizations = <AllOrganization>[].obs;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController { class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project /// List of organizations assigned to the selected project

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/tenant/tenant_services_model.dart'; import 'package:on_field_work/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController { class ServiceController extends GetxController {
List<Service> services = []; List<Service> services = [];

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart'; import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
class TenantSelectionController extends GetxController { class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService(); final TenantService _tenantService = TenantService();

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart'; import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart'; import 'package:on_field_work/controller/permission_controller.dart';
class TenantSwitchController extends GetxController { class TenantSwitchController extends GetxController {
final TenantService _tenantService = TenantService(); final TenantService _tenantService = TenantService();

View File

@ -1,4 +1,4 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class ButtonsController extends MyController { class ButtonsController extends MyController {
List<bool> selected = List.filled(3, false); List<bool> selected = List.filled(3, false);

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:carousel_slider/carousel_controller.dart'; import 'package:carousel_slider/carousel_controller.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CarouselsController extends MyController { class CarouselsController extends MyController {

View File

@ -1,5 +1,5 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart'; import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
class DialogsController extends MyController { class DialogsController extends MyController {
List<String> dummyTexts = List<String> dummyTexts =

View File

@ -1,3 +1,3 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
class LoadersController extends MyController {} class LoadersController extends MyController {}

View File

@ -1,5 +1,5 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart'; import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/extensions/string.dart'; import 'package:on_field_work/helpers/extensions/string.dart';
import 'package:marco/helpers/theme/admin_theme.dart'; import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/widgets/my_button.dart'; import 'package:on_field_work/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_lucide/flutter_lucide.dart';

View File

@ -1,5 +1,5 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart'; import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TabsController extends MyController { class TabsController extends MyController {

View File

@ -1,4 +1,4 @@
import 'package:marco/controller/my_controller.dart'; import 'package:on_field_work/controller/my_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ToastMessageController extends MyController { class ToastMessageController extends MyController {

View File

@ -1,5 +1,5 @@
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:marco/helpers/theme/app_notifier.dart'; import 'package:on_field_work/helpers/theme/app_notifier.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -1,5 +1,5 @@
import 'dart:ui'; import 'dart:ui';
import 'package:marco/helpers/services/localizations/translator.dart'; import 'package:on_field_work/helpers/services/localizations/translator.dart';
extension StringUtil on String { extension StringUtil on String {
Color get toColor { Color get toColor {

View File

@ -1,20 +1,54 @@
class ApiEndpoints { class ApiEndpoints {
// static const String baseUrl = "https://stageapi.marcoaiot.com/api"; static const String baseUrl = "https://stageapi.marcoaiot.com/api";
static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api";
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
// static const String baseUrl = "https://api.onfieldwork.com/api";
static const String getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories =
"/Master/expenses-categories";
static const String getExpensePaymentRequestPayee =
"/Expense/payment-request/payee";
// Dashboard Module API Endpoints // Dashboard Module API Endpoints
static const String getDashboardAttendanceOverview = static const String getDashboardAttendanceOverview =
"/dashboard/attendance-overview"; "/dashboard/attendance-overview";
static const String createExpensePaymentRequest =
"/expense/payment-request/create";
static const String getExpensePaymentRequestList =
"/Expense/get/payment-requests/list";
static const String getExpensePaymentRequestDetails =
"/Expense/get/payment-request/details";
static const String getExpensePaymentRequestFilter =
"/Expense/payment-request/filter";
static const String updateExpensePaymentRequestStatus =
"/Expense/payment-request/action";
static const String createExpenseforPR = "/expense/payment-request/action";
static const String getExpensePaymentRequestEdit =
"/expense/payment-request/edit";
static const String getDashboardProjectProgress = "/dashboard/progression"; static const String getDashboardProjectProgress = "/dashboard/progression";
static const String getDashboardTasks = "/dashboard/tasks"; static const String getDashboardTasks = "/dashboard/tasks";
static const String getDashboardTeams = "/dashboard/teams"; static const String getDashboardTeams = "/dashboard/teams";
static const String getDashboardProjects = "/dashboard/projects"; static const String getDashboardProjects = "/dashboard/projects";
static const String getDashboardMonthlyExpenses =
"/Dashboard/expense/monthly";
static const String getExpenseTypeReport = "/Dashboard/expense/type";
static const String getPendingExpenses = "/Dashboard/expense/pendings";
static const String getCollectionOverview = "/dashboard/collection-overview";
static const String getPurchaseInvoiceOverview =
"/dashboard/purchase-invoice-overview";
///// Projects Module API Endpoints
static const String createProject = "/project";
// Attendance Module API Endpoints // Attendance Module API Endpoints
static const String getProjects = "/project/list"; static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic"; static const String getGlobalProjects = "/project/list/basic";
static const String getTodaysAttendance = "/attendance/project/team"; static const String getTodaysAttendance = "/attendance/project/team";
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogs = "/attendance/project/log";
static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getAttendanceLogView = "/attendance/log/attendance";
static const String getRegularizationLogs = "/attendance/regularize"; static const String getRegularizationLogs = "/attendance/regularize";
@ -70,7 +104,7 @@ class ApiEndpoints {
static const String editExpense = "/Expense/edit"; static const String editExpense = "/Expense/edit";
static const String getMasterPaymentModes = "/master/payment-modes"; static const String getMasterPaymentModes = "/master/payment-modes";
static const String getMasterExpenseStatus = "/master/expenses-status"; static const String getMasterExpenseStatus = "/master/expenses-status";
static const String getMasterExpenseTypes = "/master/expenses-types"; static const String getMasterExpenseCategory = "/master/expenses-categories";
static const String updateExpenseStatus = "/expense/action"; static const String updateExpenseStatus = "/expense/action";
static const String deleteExpense = "/expense/delete"; static const String deleteExpense = "/expense/delete";
@ -100,4 +134,37 @@ class ApiEndpoints {
static const getAllOrganizations = "/organization/list"; static const getAllOrganizations = "/organization/list";
static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAssignedServices = "/Project/get/assigned/services";
static const String getAdvancePayments = '/Expense/get/transactions';
// Organization Hierarchy endpoints
static const String getOrganizationHierarchyList =
"/organization/hierarchy/list";
static const String manageOrganizationHierarchy =
"/organization/hierarchy/manage";
// Service Project Module API Endpoints
static const String getServiceProjectsList = "/serviceproject/list";
static const String getServiceProjectDetail = "/serviceproject/details";
static const String getServiceProjectJobList = "/serviceproject/job/list";
static const String getServiceProjectJobDetail =
"/serviceproject/job/details";
static const String editServiceProjectJob = "/serviceproject/job/edit";
static const String createServiceProjectJob = "/serviceproject/job/create";
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
static const String getTeamRoles = "/master/team-roles/list";
static const String getServiceProjectBranches = "/serviceproject/branch/list";
static const String getMasterJobStatus = "/Master/job-status/list";
static const String addJobComment = "/ServiceProject/job/add/comment";
static const String getJobCommentList = "/ServiceProject/job/comment/list";
// Infra Project Module API Endpoints
static const String getInfraProjectsList = "/project/list";
static const String getInfraProjectDetail = "/project/details";
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/helpers/services/device_info_service.dart'; import 'package:on_field_work/helpers/services/device_info_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart';
Future<void> initializeApp() async { Future<void> initializeApp() async {
try { try {

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/api_service.dart';
/// Global logger instance /// Global logger instance
Logger? _appLogger; Logger? _appLogger;

View File

@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class AuthService { class AuthService {
static const String _baseUrl = ApiEndpoints.baseUrl; static const String _baseUrl = ApiEndpoints.baseUrl;

View File

@ -1,10 +1,10 @@
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/helpers/services/local_notification_service.dart'; import 'package:on_field_work/helpers/services/local_notification_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/notification_action_handler.dart'; import 'package:on_field_work/helpers/services/notification_action_handler.dart';
/// Firebase Notification Service /// Firebase Notification Service
class FirebaseNotificationService { class FirebaseNotificationService {

View File

@ -1,5 +1,5 @@
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Language { class Language {

View File

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get_utils/src/extensions/string_extensions.dart'; import 'package:get/get_utils/src/extensions/string_extensions.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@ -1,17 +1,17 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/controller/task_planning/daily_task_controller.dart'; import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
import 'package:marco/controller/expense/expense_detail_controller.dart'; import 'package:on_field_work/controller/expense/expense_detail_controller.dart';
import 'package:marco/controller/directory/directory_controller.dart'; import 'package:on_field_work/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart'; import 'package:on_field_work/controller/directory/notes_controller.dart';
import 'package:marco/controller/document/user_document_controller.dart'; import 'package:on_field_work/controller/document/user_document_controller.dart';
import 'package:marco/controller/document/document_details_controller.dart'; import 'package:on_field_work/controller/document/document_details_controller.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:on_field_work/helpers/utils/permission_constants.dart';
/// Handles incoming FCM notification actions and updates UI/controllers. /// Handles incoming FCM notification actions and updates UI/controllers.
class NotificationActionHandler { class NotificationActionHandler {
@ -69,7 +69,6 @@ class NotificationActionHandler {
} }
break; break;
case 'Team_Modified': case 'Team_Modified':
// Call method to handle team modifications and dashboard update
_handleDashboardUpdate(data); _handleDashboardUpdate(data);
break; break;
@ -129,6 +128,11 @@ class NotificationActionHandler {
/// ---------------------- HANDLERS ---------------------- /// ---------------------- HANDLERS ----------------------
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) { static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task planning update from another project.");
return;
}
final projectId = data['ProjectId']; final projectId = data['ProjectId'];
if (projectId == null) { if (projectId == null) {
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data"); _logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
@ -159,13 +163,17 @@ class NotificationActionHandler {
} }
static void _handleExpenseUpdated(Map<String, dynamic> data) { static void _handleExpenseUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored expense update from another project.");
return;
}
final expenseId = data['ExpenseId']; final expenseId = data['ExpenseId'];
if (expenseId == null) { if (expenseId == null) {
_logger.w("⚠️ Expense update received without ExpenseId: $data"); _logger.w("⚠️ Expense update received without ExpenseId: $data");
return; return;
} }
// Update Expense List
_safeControllerUpdate<ExpenseController>( _safeControllerUpdate<ExpenseController>(
onFound: (controller) async { onFound: (controller) async {
await controller.fetchExpenses(); await controller.fetchExpenses();
@ -175,7 +183,6 @@ class NotificationActionHandler {
'✅ ExpenseController refreshed from expense notification.', '✅ ExpenseController refreshed from expense notification.',
); );
// Update Expense Detail (if open and matches this expenseId)
_safeControllerUpdate<ExpenseDetailController>( _safeControllerUpdate<ExpenseDetailController>(
onFound: (controller) async { onFound: (controller) async {
if (controller.expense.value?.id == expenseId) { if (controller.expense.value?.id == expenseId) {
@ -190,6 +197,11 @@ class NotificationActionHandler {
} }
static void _handleAttendanceUpdated(Map<String, dynamic> data) { static void _handleAttendanceUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored attendance update from another project.");
return;
}
_safeControllerUpdate<AttendanceController>( _safeControllerUpdate<AttendanceController>(
onFound: (controller) => controller.refreshDataFromNotification( onFound: (controller) => controller.refreshDataFromNotification(
projectId: data['ProjectId'], projectId: data['ProjectId'],
@ -201,6 +213,11 @@ class NotificationActionHandler {
static void _handleTaskUpdated(Map<String, dynamic> data, static void _handleTaskUpdated(Map<String, dynamic> data,
{required bool isComment}) { {required bool isComment}) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task update from another project.");
return;
}
_safeControllerUpdate<DailyTaskController>( _safeControllerUpdate<DailyTaskController>(
onFound: (controller) => controller.refreshTasksFromNotification( onFound: (controller) => controller.refreshTasksFromNotification(
projectId: data['ProjectId'], projectId: data['ProjectId'],
@ -213,11 +230,15 @@ class NotificationActionHandler {
/// ---------------------- DOCUMENT HANDLER ---------------------- /// ---------------------- DOCUMENT HANDLER ----------------------
static void _handleDocumentModified(Map<String, dynamic> data) { static void _handleDocumentModified(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored document update from another project.");
return;
}
String entityTypeId; String entityTypeId;
String entityId; String entityId;
String? documentId = data['DocumentId']; String? documentId = data['DocumentId'];
// Determine entity type and ID
if (data['Keyword'] == 'Employee_Document_Modified') { if (data['Keyword'] == 'Employee_Document_Modified') {
entityTypeId = Permissions.employeeEntity; entityTypeId = Permissions.employeeEntity;
entityId = data['EmployeeId'] ?? ''; entityId = data['EmployeeId'] ?? '';
@ -237,7 +258,6 @@ class NotificationActionHandler {
_logger.i( _logger.i(
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId"); "🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
// Refresh Document List
if (Get.isRegistered<DocumentController>()) { if (Get.isRegistered<DocumentController>()) {
_safeControllerUpdate<DocumentController>( _safeControllerUpdate<DocumentController>(
onFound: (controller) async { onFound: (controller) async {
@ -255,11 +275,9 @@ class NotificationActionHandler {
_logger.w('⚠️ DocumentController not registered, skipping list refresh.'); _logger.w('⚠️ DocumentController not registered, skipping list refresh.');
} }
// Refresh Document Details (if open)
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) { if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
_safeControllerUpdate<DocumentDetailsController>( _safeControllerUpdate<DocumentDetailsController>(
onFound: (controller) async { onFound: (controller) async {
// Refresh details regardless of current document
await controller.fetchDocumentDetails(documentId); await controller.fetchDocumentDetails(documentId);
_logger.i( _logger.i(
"✅ DocumentDetailsController refreshed for Document $documentId"); "✅ DocumentDetailsController refreshed for Document $documentId");
@ -276,13 +294,10 @@ class NotificationActionHandler {
/// ---------------------- DIRECTORY HANDLERS ---------------------- /// ---------------------- DIRECTORY HANDLERS ----------------------
static void _handleContactModified(Map<String, dynamic> data) { static void _handleContactModified(Map<String, dynamic> data) {
final contactId = data['ContactId'];
// Always refresh the contact list
_safeControllerUpdate<DirectoryController>( _safeControllerUpdate<DirectoryController>(
onFound: (controller) { onFound: (controller) {
controller.fetchContacts(); controller.fetchContacts();
// If a specific contact is provided, refresh its notes as well final contactId = data['ContactId'];
if (contactId != null) { if (contactId != null) {
controller.fetchCommentsForContact(contactId); controller.fetchCommentsForContact(contactId);
} }
@ -293,7 +308,6 @@ class NotificationActionHandler {
'✅ Directory contacts (and notes if applicable) refreshed from notification.', '✅ Directory contacts (and notes if applicable) refreshed from notification.',
); );
// Refresh notes globally as well
_safeControllerUpdate<NotesController>( _safeControllerUpdate<NotesController>(
onFound: (controller) => controller.fetchNotes(), onFound: (controller) => controller.fetchNotes(),
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.', notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
@ -302,7 +316,6 @@ class NotificationActionHandler {
} }
static void _handleContactNoteModified(Map<String, dynamic> data) { static void _handleContactNoteModified(Map<String, dynamic> data) {
// Refresh both contacts and notes when a note is modified
_handleContactModified(data); _handleContactModified(data);
} }
@ -324,6 +337,11 @@ class NotificationActionHandler {
/// ---------------------- DASHBOARD HANDLER ---------------------- /// ---------------------- DASHBOARD HANDLER ----------------------
static void _handleDashboardUpdate(Map<String, dynamic> data) { static void _handleDashboardUpdate(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored dashboard update from another project.");
return;
}
_safeControllerUpdate<DashboardController>( _safeControllerUpdate<DashboardController>(
onFound: (controller) async { onFound: (controller) async {
final type = data['type'] ?? ''; final type = data['type'] ?? '';
@ -347,11 +365,9 @@ class NotificationActionHandler {
controller.projectController.selectedProjectId.value; controller.projectController.selectedProjectId.value;
final projectIdsString = data['ProjectIds'] ?? ''; final projectIdsString = data['ProjectIds'] ?? '';
// Convert comma-separated string to List<String>
final notificationProjectIds = final notificationProjectIds =
projectIdsString.split(',').map((e) => e.trim()).toList(); projectIdsString.split(',').map((e) => e.trim()).toList();
// Refresh only if current project ID is in the list
if (notificationProjectIds.contains(currentProjectId)) { if (notificationProjectIds.contains(currentProjectId)) {
await controller.fetchDashboardTeams(projectId: currentProjectId); await controller.fetchDashboardTeams(projectId: currentProjectId);
} }
@ -375,17 +391,40 @@ class NotificationActionHandler {
/// ---------------------- UTILITY ---------------------- /// ---------------------- UTILITY ----------------------
static bool _isCurrentProject(Map<String, dynamic> data) {
try {
final dashboard = Get.find<DashboardController>();
final currentProjectId =
dashboard.projectController.selectedProjectId.value;
final notificationProjectId = data['ProjectId']?.toString();
if (notificationProjectId == null || notificationProjectId.isEmpty) {
return true; // No project info allow global refresh
}
return notificationProjectId == currentProjectId;
} catch (e) {
_logger.w("⚠️ Could not verify project context: $e");
return true;
}
}
static void _safeControllerUpdate<T>({ static void _safeControllerUpdate<T>({
required void Function(T controller) onFound, required void Function(T controller) onFound,
required String notFoundMessage, required String notFoundMessage,
required String successMessage, required String successMessage,
}) { }) {
if (!Get.isRegistered<T>()) {
_logger.w(notFoundMessage);
return;
}
try { try {
final controller = Get.find<T>(); final controller = Get.find<T>();
onFound(controller); onFound(controller);
_logger.i(successMessage); _logger.i(successMessage);
} catch (e) { } catch (e) {
_logger.w(notFoundMessage); _logger.w('⚠️ Error updating controller: $e');
} }
} }
} }

View File

@ -2,13 +2,13 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/user_permission.dart'; import 'package:on_field_work/model/user_permission.dart';
import 'package:marco/model/employees/employee_info.dart'; import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:marco/model/projects_model.dart'; import 'package:on_field_work/model/projects_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart';
class PermissionService { class PermissionService {
// In-memory cache keyed by user token // In-memory cache keyed by user token

View File

@ -2,13 +2,13 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/model/employees/employee_info.dart'; import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:marco/model/user_permission.dart'; import 'package:on_field_work/model/user_permission.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
class LocalStorage { class LocalStorage {
static const String _loggedInUserKey = "user"; static const String _loggedInUserKey = "user";

View File

@ -1,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart'; import 'package:on_field_work/model/tenant/tenant_list_model.dart';
/// Abstract interface for tenant service functionality /// Abstract interface for tenant service functionality
abstract class ITenantService { abstract class ITenantService {
@ -63,29 +63,39 @@ class TenantService implements ITenantService {
{bool hasRetried = false}) async { {bool hasRetried = false}) async {
try { try {
final headers = await _authorizedHeaders(); final headers = await _authorizedHeaders();
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
level: LogLevel.info);
final response = await http final response = await http.get(
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers); Uri.parse("$_baseUrl/auth/get/user/tenants"),
final data = jsonDecode(response.body); headers: headers,
);
logSafe( // Handle empty response BEFORE decoding
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]", if (response.body.isEmpty || response.body.trim().isEmpty) {
level: LogLevel.info); logSafe("❌ Empty tenant response — auto logout");
await LocalStorage.logout();
if (response.statusCode == 200 && data['success'] == true) { return null;
logSafe("✅ Tenants fetched successfully.");
return List<Map<String, dynamic>>.from(data['data']);
} }
Map<String, dynamic> data;
try {
data = jsonDecode(response.body);
} catch (e) {
logSafe("❌ Invalid JSON in tenant response — auto logout");
await LocalStorage.logout();
return null;
}
// SUCCESS CASE
if (response.statusCode == 200 && data['success'] == true) {
final list = data['data'];
if (list is! List) return null;
return List<Map<String, dynamic>>.from(list);
}
// TOKEN EXPIRED
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) return getTenants(hasRetried: true); if (refreshed) return getTenants(hasRetried: true);
logSafe("❌ Token refresh failed while fetching tenants.",
level: LogLevel.error);
return null; return null;
} }
@ -130,7 +140,7 @@ class TenantService implements ITenantService {
} }
// 🔹 Register FCM token after tenant selection // 🔹 Register FCM token after tenant selection
final fcmToken = LocalStorage.getFcmToken(); final fcmToken = LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) { if (fcmToken?.isNotEmpty ?? false) {
final success = await AuthService.registerDeviceToken(fcmToken!); final success = await AuthService.registerDeviceToken(fcmToken!);
logSafe( logSafe(

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
enum LeftBarThemeType { light, dark } enum LeftBarThemeType { light, dark }
enum ContentThemeType { light, dark } enum ContentThemeType { light, dark }

View File

@ -1,8 +1,8 @@
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/widgets/my.dart'; import 'package:on_field_work/helpers/widgets/my.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@ -6,13 +6,13 @@
* */ * */
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:marco/helpers/theme/admin_theme.dart'; import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/widgets/my.dart'; import 'package:on_field_work/helpers/widgets/my.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_constant.dart'; import 'package:on_field_work/helpers/widgets/my_constant.dart';
import 'package:marco/helpers/widgets/my_screen_media.dart'; import 'package:on_field_work/helpers/widgets/my_screen_media.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:on_field_work/helpers/widgets/my_text_style.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -230,7 +230,7 @@ class AppStyle {
containerRadius: AppStyle.containerRadius.medium, containerRadius: AppStyle.containerRadius.medium,
cardRadius: AppStyle.cardRadius.medium, cardRadius: AppStyle.cardRadius.medium,
buttonRadius: AppStyle.buttonRadius.medium, buttonRadius: AppStyle.buttonRadius.medium,
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/dashboard'), defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'),
)); ));
bool isMobile = true; bool isMobile = true;
try { try {

View File

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/services/json_decoder.dart'; import 'package:on_field_work/helpers/services/json_decoder.dart';
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:marco/helpers/services/localizations/translator.dart'; import 'package:on_field_work/helpers/services/localizations/translator.dart';
import 'package:marco/helpers/services/navigation_services.dart'; import 'package:on_field_work/helpers/services/navigation_services.dart';
import 'package:marco/helpers/theme/admin_theme.dart'; import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/app_notifier.dart'; import 'package:on_field_work/helpers/theme/app_notifier.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/wave_background.dart'; import 'package:on_field_work/helpers/widgets/wave_background.dart';
import 'package:marco/helpers/theme/admin_theme.dart'; import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/theme_customizer.dart';
class ThemeOption { class ThemeOption {
final String label; final String label;
@ -63,6 +63,9 @@ class ThemeController extends GetxController {
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false; showApplied.value = false;
// Navigate to dashboard after applying theme
Get.offAllNamed('/dashboard');
} }
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
class BaseBottomSheet extends StatefulWidget { class BaseBottomSheet extends StatefulWidget {
final String title; final String title;

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class ContactPickerHelper { class ContactPickerHelper {
static Future<String?> pickIndianPhoneNumber(BuildContext context) async { static Future<String?> pickIndianPhoneNumber(BuildContext context) async {

View File

@ -1,6 +1,9 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DateTimeUtils { class DateTimeUtils {
/// Default date format
static const String defaultFormat = 'dd MMM yyyy';
/// Converts a UTC datetime string to local time and formats it. /// Converts a UTC datetime string to local time and formats it.
static String convertUtcToLocal(String utcTimeString, static String convertUtcToLocal(String utcTimeString,
{String format = 'dd-MM-yyyy'}) { {String format = 'dd-MM-yyyy'}) {

View File

@ -1,7 +1,7 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/app_logger.dart';
class LauncherUtils { class LauncherUtils {
/// Launches the phone dialer with the provided phone number /// Launches the phone dialer with the provided phone number

View File

@ -1,7 +1,7 @@
import 'package:marco/helpers/theme/admin_theme.dart'; import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart';
import 'package:marco/helpers/widgets/my_dashed_divider.dart'; import 'package:on_field_work/helpers/widgets/my_dashed_divider.dart';
import 'package:marco/helpers/widgets/my_navigation_mixin.dart'; import 'package:on_field_work/helpers/widgets/my_navigation_mixin.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
mixin UIMixin { mixin UIMixin {

View File

@ -25,14 +25,16 @@ class Permissions {
// ------------------- Project Infrastructure -------------------------- // ------------------- Project Infrastructure --------------------------
/// Permission to manage project infrastructure (e.g., site details) /// Permission to manage project infrastructure (e.g., site details)
static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373"; static const String manageProjectInfra =
"cf2825ad-453b-46aa-91d9-27c124d63373";
/// Permission to view infrastructure-related details /// Permission to view infrastructure-related details
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"; static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
// ------------------- Attendance Management --------------------------- // ------------------- Attendance Management ---------------------------
/// Permission to regularize (edit/update) attendance records /// Permission to regularize (edit/update) attendance records
static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; static const String regularizeAttendance =
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
// ------------------- Task Management --------------------------------- // ------------------- Task Management ---------------------------------
/// Permission to create and manage tasks /// Permission to create and manage tasks
@ -90,7 +92,8 @@ class Permissions {
// ------------------- Application Roles ------------------------------- // ------------------- Application Roles -------------------------------
/// Application role ID for users with full expense management rights /// Application role ID for users with full expense management rights
static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7"; static const String expenseManagement =
"a4e25142-449b-4334-a6e5-22f70e4732d7";
// ------------------- Document Entities ------------------------------- // ------------------- Document Entities -------------------------------
/// Entity ID for project documents /// Entity ID for project documents
@ -118,3 +121,73 @@ class Permissions {
/// Permission to verify documents /// Permission to verify documents
static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0"; static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
} }
/// Contains constants for menu item IDs fetched from the sidebar menu API.
class MenuItems {
/// Dashboard menu
static const String dashboard = "29e03eda-03e8-4714-92fa-67ae0dc53202";
/// Daily Task Planning menu
static const String dailyTaskPlanning =
"77ac5205-f823-442e-b9e4-2420d658aa02";
/// Daily Progress Report menu
static const String dailyProgressReport =
"299e3cf5-d034-4403-b4a1-ea46d2714832";
/// Employees menu
static const String employees = "78f0206d-c6cc-44d0-832a-2031ed203018";
/// Attendance menu
static const String attendance = "2f212030-f36b-456c-8e7c-11f00f9ba42b";
/// Directory menu
static const String directory = "31bc367b-7c58-4604-95eb-da059a384103";
/// Expense & Reimbursement menu
static const String expenseReimbursement =
"0f0dc1a7-1aca-4cdb-9d7a-8a769ce40728";
/// Payment Requests menu
static const String paymentRequests = "b350a59f-2372-4f68-8dcf-f7cfc44523ca";
/// Advance Payment Statements menu
static const String advancePaymentStatements =
"e0251cc1-e6d9-417a-9c76-489cc4b6c347";
/// Finance menu
static const String finance = "5ac409dd-bbe0-4d56-bcb9-229bd3a6353c";
/// Documents menu
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
/// Service Projects
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
/// Infrastructure Projects
static const String infraProjects = "5fab4b88-c9a0-417b-aca2-130980fdb0cf";
}
/// Contains all job status IDs used across the application.
class JobStatus {
/// Level 1 - New
static const String newStatus = "32d76a02-8f44-4aa0-9b66-c3716c45a918";
/// Level 2 - Assigned
static const String assigned = "cfa1886d-055f-4ded-84c6-42a2a8a14a66";
/// Level 3 - In Progress
static const String inProgress = "5a6873a5-fed7-4745-a52f-8f61bf3bd72d";
/// Level 4 - Work Done
static const String workDone = "aab71020-2fb8-44d9-9430-c9a7e9bf33b0";
/// Level 5 - Review Done
static const String reviewDone = "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7";
/// Level 6 - Closed
static const String closed = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69";
/// Level 7 - On Hold
static const String onHold = "75a0c8b8-9c6a-41af-80bf-b35bab722eb2";
}

View File

@ -1,4 +1,5 @@
import 'package:marco/helpers/extensions/date_time_extension.dart'; import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/extensions/date_time_extension.dart';
class Utils { class Utils {
static getDateStringFromDateTime(DateTime dateTime, static getDateStringFromDateTime(DateTime dateTime,
@ -44,6 +45,10 @@ class Utils {
return "$hour:$minute${showSecond ? ":" : ""}$second$meridian"; return "$hour:$minute${showSecond ? ":" : ""}$second$meridian";
} }
static String formatDate(DateTime date) {
return DateFormat('d MMM yyyy').format(date);
}
static String getDateTimeStringFromDateTime(DateTime dateTime, static String getDateTimeStringFromDateTime(DateTime dateTime,
{bool showSecond = true, {bool showSecond = true,
bool showDate = true, bool showDate = true,
@ -76,4 +81,12 @@ class Utils {
return "${b.toStringAsFixed(2)} Bytes"; return "${b.toStringAsFixed(2)} Bytes";
} }
} }
static String formatCurrency(num amount,
{String currency = "INR", String locale = "en_US"}) {
// Use en_US for standard K, M, B formatting
final symbol = NumberFormat.simpleCurrency(name: currency).currencySymbol;
final formatter = NumberFormat.compact(locale: 'en_US');
return "$symbol${formatter.format(amount)}";
}
} }

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_container.dart'; import 'package:on_field_work/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
class Avatar extends StatelessWidget { class Avatar extends StatelessWidget {
final String firstName; final String firstName;
final String lastName; final String lastName;
final String? imageUrl;
final double size; final double size;
final Color? backgroundColor; final Color? backgroundColor;
final Color textColor; final Color textColor;
@ -13,6 +14,7 @@ class Avatar extends StatelessWidget {
super.key, super.key,
required this.firstName, required this.firstName,
required this.lastName, required this.lastName,
this.imageUrl,
this.size = 46.0, this.size = 46.0,
this.backgroundColor, this.backgroundColor,
this.textColor = Colors.white, this.textColor = Colors.white,
@ -20,9 +22,24 @@ class Avatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String initials = "${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}".toUpperCase(); if (imageUrl != null && imageUrl!.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(size / 2),
child: Image.network(
imageUrl!,
width: size,
height: size,
fit: BoxFit.cover,
),
);
}
final Color bgColor = backgroundColor ?? _getFlatColorFromName('$firstName$lastName'); String initials =
"${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}"
.toUpperCase();
final Color bgColor =
backgroundColor ?? _getFlatColorFromName('$firstName$lastName');
return MyContainer.rounded( return MyContainer.rounded(
height: size, height: size,
@ -32,36 +49,36 @@ class Avatar extends StatelessWidget {
child: Center( child: Center(
child: MyText( child: MyText(
initials, initials,
fontSize: size * 0.45, // 👈 scales with avatar size fontSize: size * 0.45,
fontWeight: 600, fontWeight: 600,
color: textColor, color: textColor,
), ),
), ),
); );
} }
}
// Use fixed flat color palette and pick based on hash
Color _getFlatColorFromName(String name) { // Use fixed flat color palette and pick based on hash
final colors = <Color>[ Color _getFlatColorFromName(String name) {
Color(0xFFE57373), // Red final colors = <Color>[
Color(0xFFF06292), // Pink Color(0xFFE57373), // Red
Color(0xFFBA68C8), // Purple Color(0xFFF06292), // Pink
Color(0xFF9575CD), // Deep Purple Color(0xFFBA68C8), // Purple
Color(0xFF7986CB), // Indigo Color(0xFF9575CD), // Deep Purple
Color(0xFF64B5F6), // Blue Color(0xFF7986CB), // Indigo
Color(0xFF4FC3F7), // Light Blue Color(0xFF64B5F6), // Blue
Color(0xFF4DD0E1), // Cyan Color(0xFF4FC3F7), // Light Blue
Color(0xFF4DB6AC), // Teal Color(0xFF4DD0E1), // Cyan
Color(0xFF81C784), // Green Color(0xFF4DB6AC), // Teal
Color(0xFFAED581), // Light Green Color(0xFF81C784), // Green
Color(0xFFDCE775), // Lime Color(0xFFAED581), // Light Green
Color(0xFFFFD54F), // Amber Color(0xFFDCE775), // Lime
Color(0xFFFFB74D), // Orange Color(0xFFFFD54F), // Amber
Color(0xFFA1887F), // Brown Color(0xFFFFB74D), // Orange
Color(0xFF90A4AE), // Blue Grey Color(0xFFA1887F), // Brown
]; Color(0xFF90A4AE), // Blue Grey
];
int index = name.hashCode.abs() % colors.length;
return colors[index]; int index = name.hashCode.abs() % colors.length;
} return colors[index];
} }

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