Compare commits

...

250 Commits

Author SHA1 Message Date
venbaittech
491965dc7f forexsign_bg_colr_chng 2025-10-31 18:36:34 +05:30
venbaittech
85fd7c0b47 address_input_restrict 2025-10-30 16:31:33 +05:30
venbaittech
220e7c4c9e final code 2025-10-29 15:17:44 +05:30
venbaittech
08ac38a64b travel_agent_remarks_access 2025-10-29 12:34:19 +05:30
venbaittech
816d8b1bf1 Passport File Download 2025-10-29 10:20:48 +05:30
venbaittech
a838aaeae0 Broken Access 2025-10-29 09:28:12 +05:30
venbaittech
aaff31e8d4 ui_input_formatters 2025-10-27 17:35:57 +05:30
venbaittech
52878d6810 FORBIDEN REFRESH HANDLING 2025-10-16 09:24:18 +05:30
venbaittech
bd7d538481 UI_PASSPORT_DOC_DOWNLOAD 2025-10-08 15:06:06 +05:30
venbaittech
030d1487a6 API_SECURITY 2025-10-08 11:37:26 +05:30
venbaittech
f0a0eb3c1d TP_Service_Changes Trip_Info_Modal 2025-09-20 11:43:02 +05:30
venbaittech
de7491cc6e API-SECURITY 2025-09-03 10:02:29 +05:30
venbaittech
6f251973a5 UI_TP_SEQ_PARL 2025-08-30 18:22:59 +05:30
venbaittech
f9a3cbd91b UI_CHNGS_TravelPolcy 2025-08-29 17:57:33 +05:30
venbaittech
020e890dae APISECURITY 2025-08-21 14:57:52 +05:30
venbaittech
5b55ab17a7 APP-SIGNATURE-MERGED 2025-08-20 17:50:40 +05:30
venbaittech
f7342a14b4 APP-SIGNATURE 2025-08-20 17:45:27 +05:30
venba-Inspriron-3558
d22e8bfcac templateForex file 2025-08-11 09:36:58 +05:30
venbaittech
d29caf66c5 USRMNG_Role 2025-08-06 17:25:16 +05:30
venbaittech
005f526798 UI_HOTEL_NAME_ALIGN 2025-08-06 11:10:39 +05:30
venbaittech
bf9a2b1277 UI_HOTEL_NAME 2025-08-05 18:32:24 +05:30
venbaittech
a7310238c0 UI_SEMATICS 2025-08-05 15:19:26 +05:30
venbaittech
07cc47d8bb Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-08-05 12:17:27 +05:30
venbaittech
b40f65514d UI_DISABLE_ROLE_PROFILE 2025-08-05 12:17:20 +05:30
venba-Inspriron-3558
2dee35c37c Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-08-04 18:11:44 +05:30
venba-Inspriron-3558
6f85000d9d hotel master category added 2025-08-04 18:07:28 +05:30
venbaittech
fa00f90f8a FIX_TRACKER 2025-08-01 18:30:41 +05:30
venbaittech
b24215272f FOREX DWNLD 2025-08-01 16:48:08 +05:30
venbaittech
a32fb0d0c3 UI_TRIPTYPE_SERVICE_HANDLING 2025-07-31 18:15:37 +05:30
venbaittech
8d6b62f0fa FIX_DOMESTIC_VISA 2025-07-31 10:56:45 +05:30
venbaittech
9c910f020c UI_SERVICES_MOBILE 2025-07-31 10:17:00 +05:30
venbaittech
4960c472be UI_CHANGE_MOBILE 2025-07-30 18:09:46 +05:30
venbaittech
6c98829a21 MERGED 2025-07-30 12:40:58 +05:30
venbaittech
1b6188c584 UI_USR_MNGMT_APPROVER 2025-07-30 12:33:02 +05:30
venba-Inspriron-3558
5a776c39d0 status board temp alignment 2025-07-30 11:53:55 +05:30
venba-Inspriron-3558
d2b3b792d9 lastest code with cf23f4f9 2025-07-29 15:24:58 +05:30
venbaittech
cf23f4f936 BUG_FIX 2025-07-22 14:46:43 +05:30
venba-Inspriron-3558
058875d571 merged code 2025-07-19 09:45:47 +05:30
venba-Inspriron-3558
ea0456d97c approval and my trips 2025-07-19 09:36:57 +05:30
venbaittech
9f6cfedf42 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-07-18 18:21:21 +05:30
venbaittech
80db60c39c BUGFIX_USER_MNGMT 2025-07-18 18:21:11 +05:30
venba-Inspriron-3558
9fac4152ab code merge 2025-07-18 15:07:51 +05:30
venba-Inspriron-3558
5bbda6f8fd colors and a4 amentmend policy 2025-07-18 15:07:42 +05:30
venbaittech
186a07d126 UI_TRIPS_CARD_CHANGES 2025-07-18 11:46:27 +05:30
venbaittech
66d62ca8f8 UI_ADDITIONAL_COLOR, UI_ALLTRIPS_CARD 2025-07-18 09:47:33 +05:30
venba-Inspriron-3558
bc8968f9c5 Merge with dashboard 2025-07-17 12:02:35 +05:30
venba-Inspriron-3558
0ec324bf03 dashboard mobile 2025-07-17 12:02:27 +05:30
venbaittech
8dea38b3a6 CHANGE_PAGINATION 2025-07-17 11:04:11 +05:30
venba-Inspriron-3558
35229fa755 july 1 to july 15 2025-07-15 15:58:38 +05:30
venba-Inspriron-3558
3be15a71ef fixes 2 2025-06-30 18:23:02 +05:30
venba-Inspriron-3558
12d11ed0d8 fixes 1 2025-06-30 09:26:38 +05:30
venba-Inspriron-3558
e15666e3b8 Merged code 2025-06-27 13:01:35 +05:30
venba-Inspriron-3558
63e51d8c5f minor issues fixes 8 2025-06-27 13:01:23 +05:30
venbaittech
ae31d1fea5 accomdoation changes 2025-06-27 10:54:16 +05:30
venba-Inspriron-3558
57f7f29054 minor issues fixes 7 2025-06-27 09:17:04 +05:30
venba-Inspriron-3558
179e1db5d7 minor issues fixes 6 2025-06-25 18:28:58 +05:30
venba-Inspriron-3558
c0ecff7697 Merge with old in branch 'main' 2025-06-25 12:40:29 +05:30
venba-Inspriron-3558
7e03cae3c2 minor issues fixes 5 2025-06-25 12:40:16 +05:30
venbaittech
efa0294820 flight trips validation 2025-06-25 12:34:44 +05:30
venbaittech
d2aef160c5 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-25 11:18:44 +05:30
venbaittech
29e30d3452 flight changes 2025-06-25 11:18:33 +05:30
venba-Inspriron-3558
91f1e86afa minor issues fixes 4 2025-06-24 18:17:30 +05:30
venbaittech
eee3182898 exceptional modal 2025-06-24 12:23:44 +05:30
venbaittech
77e0ba054c Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-23 19:26:46 +05:30
venba-Inspriron-3558
0d0b2a4799 minor issues fixes 3 2025-06-23 19:24:41 +05:30
venbaittech
c4478cce85 travelAgent-restriction 2025-06-23 19:18:50 +05:30
venbaittech
534c8213d9 merge 2025-06-23 16:46:31 +05:30
venbaittech
6c12c4ca35 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-23 16:44:36 +05:30
venba-Inspriron-3558
fae41a2f7e minor issues fixes 2 2025-06-23 16:27:27 +05:30
venbaittech
15623eb065 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-23 16:14:37 +05:30
venba-Inspriron-3558
045b256551 reportsample deleted 2025-06-23 16:13:22 +05:30
venba-Inspriron-3558
284ff8779f minor issues fixes 2025-06-23 16:11:45 +05:30
venbaittech
4926b48e4f sonumber added 2025-06-23 16:10:39 +05:30
venbaittech
1c77a03067 merged 2025-06-21 19:28:19 +05:30
venba-Inspriron-3558
f95b5d9c37 merge with old code 2025-06-21 19:18:24 +05:30
venbaittech
53fd28d65d Exceptional Classes 2025-06-21 19:17:07 +05:30
venba-Inspriron-3558
428dff9ca8 merge with old code 2025-06-21 13:52:46 +05:30
venba-Inspriron-3558
f029c8b9b6 merge with out code 2025-06-21 13:44:21 +05:30
venbaittech
e33af3cd8a travelAgentLst 2025-06-20 14:44:50 +05:30
venbaittech
29eedb3162 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-19 17:20:09 +05:30
venbaittech
7f897a7296 trip highlighter 2025-06-19 17:19:54 +05:30
venba-Inspriron-3558
6169e8b1a2 report completed 5 2025-06-19 17:17:51 +05:30
venba-Inspriron-3558
c0218ad1c9 report completed 4 2025-06-19 17:01:32 +05:30
venbaittech
691ba181c6 group highlighter 2025-06-19 11:32:30 +05:30
venbaittech
1fcd57f676 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-19 09:16:47 +05:30
venbaittech
2003fc68bc loader user creation 2025-06-19 09:16:34 +05:30
venba-Inspriron-3558
3e0a3c87d0 Merge with old code 2025-06-18 19:00:07 +05:30
venba-Inspriron-3558
305ac4f329 report completed 3 2025-06-18 18:59:48 +05:30
venbaittech
e1d1266853 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-18 18:40:55 +05:30
venbaittech
6bb37994de authentication 2025-06-18 18:39:02 +05:30
venba-Inspriron-3558
287b7b9ad2 Merge with old code 2025-06-18 16:26:19 +05:30
venba-Inspriron-3558
ce9bcb113b report completed 2 2025-06-18 16:26:09 +05:30
venba-Inspriron-3558
25239c6e62 report completed 2025-06-18 16:25:32 +05:30
venbaittech
08f80a8afa Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-17 14:58:41 +05:30
venbaittech
c2bbbb2214 tracker items 2025-06-17 14:56:05 +05:30
venba-Inspriron-3558
e2b9781cd6 reports and bugs 2025-06-17 09:35:42 +05:30
venbaittech
3e973707a6 taxi 2025-06-17 09:07:10 +05:30
venba-Inspriron-3558
03a5fbfaf1 merge with old code 2025-06-12 19:06:47 +05:30
venba-Inspriron-3558
400a7a2ec0 userlist 2025-06-12 19:04:06 +05:30
venbaittech
438b5ec4dc Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-12 18:22:13 +05:30
venbaittech
563f808dfc changes 2025-06-12 18:20:54 +05:30
venbaittech
4121213bff user mangmnt validations 2025-06-12 16:51:59 +05:30
venba-Inspriron-3558
b81f3803e0 spelling mistake 2025-06-12 13:59:19 +05:30
venbaittech
16e58dca07 forex validations 2025-06-12 13:05:15 +05:30
venba-Inspriron-3558
128b0ea9e6 merge with old code 2025-06-11 17:52:15 +05:30
venba-Inspriron-3558
65528b1df3 issues fixes 2025-06-11 17:45:03 +05:30
venbaittech
b66391fbb5 date validations 2025-06-11 17:41:07 +05:30
venbaittech
66b62c7ad4 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-11 12:17:25 +05:30
venbaittech
675d82eab5 # 2025-06-11 12:17:18 +05:30
venbaittech
2a77ebc7d1 validations 2025-06-11 12:05:57 +05:30
vadivelJ96
036d0704fd latest commit 10-06-2025 2025-06-11 09:56:12 +05:30
venbaittech
2e85273b06 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-10 19:22:54 +05:30
venbaittech
0537c45545 validations 2025-06-10 19:22:47 +05:30
venba-Inspriron-3558
ae6750bef3 minor 2025-06-10 19:21:55 +05:30
venba-Inspriron-3558
5a3c2a7fb9 merge with old 2025-06-10 19:10:26 +05:30
venba-Inspriron-3558
e8eaad1a1f validation changes 2025-06-10 18:58:21 +05:30
venbaittech
26ece5fb8a date validation merge 2025-06-10 18:50:16 +05:30
venba-Inspriron-3558
f1c153162b merge with old 2025-06-10 17:18:36 +05:30
venba-Inspriron-3558
7527838cf5 date validation 2025-06-10 17:14:28 +05:30
venbaittech
c61183c472 forex 2025-06-10 13:14:29 +05:30
venbaittech
7f448d3e2f Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-10 11:36:31 +05:30
venbaittech
e0cd77fbd5 bug fix 2025-06-10 11:36:13 +05:30
venba-Inspriron-3558
ffa40af980 label misstake 2025-06-10 11:10:56 +05:30
venba-Inspriron-3558
424d93cc9f dropdown fixes 2025-06-10 10:41:40 +05:30
venbaittech
4887c69950 ItineratRefresh 2025-06-09 18:43:20 +05:30
venbaittech
a8712dcb68 merge 2025-06-09 15:01:30 +05:30
venbaittech
20a24c7d46 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-09 12:21:43 +05:30
venba-Inspriron-3558
80f4f62fb1 Merge - branch main 2025-06-09 12:17:00 +05:30
venbaittech
de960412a0 Current Time Validations 2025-06-09 12:16:02 +05:30
venba-Inspriron-3558
5238e245f4 issues fixes 2025-06-09 12:15:55 +05:30
venbaittech
5ccb201b7a Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-09 09:06:53 +05:30
venbaittech
ec0c74a0a2 forex validation 2025-06-09 09:06:37 +05:30
b157fc40a7 merged 2025-06-07 18:17:20 +05:30
bf1cd002e1 round trip validation 2025-06-07 18:14:05 +05:30
venba-Inspriron-3558
e00b27f994 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-06-07 18:05:03 +05:30
venba-Inspriron-3558
99b8ae5b21 itnerary files 2025-06-07 18:03:57 +05:30
5cd189a8cc merge 2025-06-07 14:57:39 +05:30
b1d19aec84 user mangement client chnages 2025-06-07 14:23:12 +05:30
venba-Inspriron-3558
20b74c5f5c grouplist missing 2025-06-07 10:23:07 +05:30
venba-Inspriron-3558
b3eb4051bf symbols only 2025-06-07 10:18:09 +05:30
venbaittech
f48680c2a5 user passport changes 2025-06-06 18:05:58 +05:30
venbaittech
fe6ccab809 merge 2025-06-06 14:20:25 +05:30
venbaittech
a41402f929 user changes 2025-06-06 13:59:38 +05:30
venba-Inspriron-3558
0368c05c35 flight.dart form page 2025-06-06 13:02:40 +05:30
vadivelJ96
f663fd776f latest commit 06-06-2025 2025-06-06 12:04:50 +05:30
vadivelJ96
ef79391915 latest commit 06-06-2025 2025-06-06 11:51:45 +05:30
vadivelJ96
86da658dcd latest commit 06-06-2025 2025-06-06 11:49:17 +05:30
venbaittech
194af031d0 merge 2025-06-06 11:47:16 +05:30
venbaittech
1a568f6878 Bug Fix 2025-06-06 10:23:46 +05:30
venbaittech
335fbacede merge 2025-06-05 20:12:05 +05:30
venbaittech
cf15771518 merge 2025-06-05 12:48:19 +05:30
venbaittech
b246c79789 user mangement bugs 2025-06-05 12:40:16 +05:30
venba-Inspriron-3558
dca5c4ba6b particular file traveller 2025-06-05 11:30:46 +05:30
venbaittech
7c9968c159 plan creation bugs 2025-06-04 16:10:52 +05:30
venbaittech
8c828b1dac login 2025-06-04 11:29:51 +05:30
venbaittech
454c07b04a merge 2025-06-03 18:38:48 +05:30
venbaittech
3104812979 merge 2025-06-03 18:34:29 +05:30
venbaittech
3e3ef56694 Page Shaking 2025-06-03 18:13:45 +05:30
venba-Inspriron-3558
5d7faf309a issues fixes 2025-06-03 18:08:43 +05:30
venba-Inspriron-3558
1005ff962d minor issues fixes 2025-06-03 14:59:50 +05:30
venba-Inspriron-3558
905759ef90 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/Screens/policy/policy_list.dart
#	pubspec.lock
2025-06-02 19:24:07 +05:30
venba-Inspriron-3558
6aa5767c9f pubspec 2025-06-02 19:22:41 +05:30
venba-Inspriron-3558
d476f467ad list 2025-06-02 19:17:46 +05:30
venbaittech
b88973d43c Testing Semantics 2025-06-02 16:54:43 +05:30
venbaittech
ff74934520 Merge 2025-06-02 15:41:21 +05:30
venbaittech
5ddd028fa8 Mail Template Basic Format 2025-06-02 15:13:28 +05:30
41010df10f tolltip for trips 2025-05-31 17:49:22 +05:30
ac303e666f issues fixes 2025-05-31 17:08:06 +05:30
3b24228080 traveller 2025-05-30 15:27:40 +05:30
venbaittech
954fc5873e merge 2025-05-29 20:17:14 +05:30
venbaittech
6c3571f4e8 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-05-29 20:01:32 +05:30
venbaittech
057da00078 Travel Policy 2025-05-29 20:00:56 +05:30
e981a27b0c alignment 2025-05-29 14:28:11 +00:00
venbaittech
dbe248195d Merge 2025-05-29 10:30:27 +05:30
venbaittech
41ed141e13 Merge 2025-05-29 10:01:30 +05:30
venbaittech
33185ab3b7 Travel Policy BugFix 2025-05-29 09:50:33 +05:30
bc00123e56 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat into main 2025-05-28 13:21:52 +00:00
52fa34bd0d minor fixes 2025-05-28 13:21:42 +00:00
154f18a59b UI changes dashboard 2025-05-28 17:17:51 +05:30
dc4a1530a6 dashboard UI Changes 2025-05-28 17:14:43 +05:30
venbaittech
a1837db9fe Travel Policy 2025-05-28 15:40:58 +05:30
venbaittech
9cab4b02a3 merge 2025-05-28 12:51:36 +05:30
venbaittech
93f69128b5 travel policy 2025-05-28 12:40:58 +05:30
2891e93d8d group changes 2025-05-28 06:33:30 +00:00
venbaittech
b630ac35b7 merged 2025-05-26 18:46:25 +05:30
venbaittech
b625692dad travel policy 2025-05-26 18:21:30 +05:30
6c7c41008a hotels 2025-05-26 11:53:33 +00:00
3bf6caabe5 status dashboard 2025-05-26 09:44:56 +00:00
venbaittech
8de950586a Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-05-26 14:10:38 +05:30
venbaittech
9d1873c749 merge 2025-05-26 14:10:27 +05:30
8f4e2c8c99 status dashboard 2025-05-26 05:31:18 +00:00
venbaittech
42f3f465d5 forex calculations 2025-05-26 10:59:01 +05:30
3dab5db7c3 card 2025-05-25 16:58:32 +00:00
b34f9dc60a costcenter 2025-05-23 13:39:53 +00:00
venbaittech
c99ac679a6 merged 2025-05-23 15:04:12 +05:30
venbaittech
b1ec5e4fed Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-05-23 14:52:36 +05:30
venbaittech
7c9ec2310b org settings 2025-05-23 14:52:28 +05:30
ca934a3a88 logo alignment 2025-05-23 05:22:22 +00:00
157b329de1 validation in departments 2025-05-22 13:01:12 +00:00
3474a1dbca Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat into main 2025-05-22 12:26:36 +00:00
7e9ea0134b validation in departments 2025-05-22 12:26:23 +00:00
venbaittech
aa284d9563 Redirection 2025-05-22 17:20:10 +05:30
venbaittech
00441fe907 Microsoft Login Integration 2025-05-22 15:55:23 +05:30
venbaittech
7b515573c7 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-05-22 15:20:50 +05:30
venbaittech
5c3cb6c87a tempaltes 2025-05-22 15:20:41 +05:30
e4acf06234 token from microsoft account 2025-05-22 09:49:17 +00:00
3fb412edba merged code 2025-05-22 08:58:00 +00:00
e387bd783c apicall 2025-05-22 08:53:43 +00:00
venbaittech
2fb77ea7e8 Merge branch 'main' of bitbucket.org:venbainformationtechnology/ts-tat 2025-05-22 14:17:46 +05:30
venbaittech
a9761f0684 micorsoft redirect 2025-05-22 14:17:38 +05:30
a4e32596c2 routes 2025-05-22 05:30:19 +00:00
9746318f2c Merge api service 2025-05-22 05:12:04 +00:00
a5142b5ee4 department code 2025-05-22 05:08:44 +00:00
venbaittech
48b2d3ca25 merge 2025-05-22 09:50:01 +05:30
venbaittech
dbc0bf0c22 apiService merge 2025-05-22 09:20:25 +05:30
77b63c271d template merge 2025-05-22 09:17:27 +05:30
8cc56017ce template 2025-05-22 09:15:24 +05:30
venbaittech
3be2f695c6 Header Icons , Forex Search 2025-05-21 14:21:52 +05:30
venbaittech
d99d9433ec Template List 2025-05-21 13:09:31 +05:30
venbaittech
c560963e50 Forex CRUD- User Mnagement Delegationt 2025-05-21 11:25:31 +05:30
4559eb7262 template details 2025-05-21 10:52:41 +05:30
9caf98de7c template list 2025-05-21 10:47:54 +05:30
venbaittech
d85209139f Forex List 2025-05-20 15:04:49 +05:30
venbaittech
cafa8cbc95 Plan Creation Bug 2025-05-19 16:20:18 +05:30
venbaittech
b55b908f42 userManagement changes ,profile highlighter 2025-05-19 11:21:20 +05:30
venbaittech
53f2c4d5b5 profile changes 2025-05-16 14:37:53 +05:30
venbaittech
db7df6b7de user management - remarks list 2025-05-15 19:11:21 +05:30
venbaittech
75592f9879 user management -TravelDetails 2025-05-13 19:42:23 +05:30
venbaittech
4435eeadd3 user management 2025-05-08 18:12:07 +05:30
venbaittech
12310625ef google fonts 2025-05-07 11:35:48 +05:30
venbaittech
2b8172d5f4 plan header ui-changes 2025-05-07 10:36:05 +05:30
venbaittech
94d2a730fb Pagination, ListView 2025-05-05 18:25:42 +05:30
venbaittech
1999e63c0f loginpage changes 2025-05-05 09:05:01 +05:30
venbaittech
a76464bbbc search for all pages 2025-05-03 17:59:17 +05:30
a509583cb6 forgot password done 2025-05-03 14:40:33 +05:30
venbaittech
36975e716f ui changes 2025-04-30 19:46:53 +05:30
venbaittech
5c628282c6 itinerary changes 2025-04-30 13:29:03 +05:30
venbaittech
15992c4008 layout changes 2025-04-28 18:50:32 +05:30
venbaittech
5c31a33ff2 removed home page 2025-04-28 09:14:48 +05:30
venbaittech
5e4a093456 plan iternary ui changes 2025-04-25 13:04:25 +05:30
venbaittech
36d3b365be ui changes 2025-04-25 10:58:14 +05:30
venbaittech
d59aaf90bc ui changes group,policy 2025-04-22 16:29:31 +05:30
venbaittech
ceb6aece50 ui changes 2025-04-22 12:56:30 +05:30
venbaittech
e0ed41ad35 Policy, Approval Screens 2025-04-19 17:40:19 +05:30
venbaittech
a91166727a policy ui/Functionily 2025-04-10 16:55:45 +05:30
venbaittech
886bef2baa OrgLevel Data, UserDetails 2025-04-08 16:14:01 +05:30
venbaittech
1f28816021 policy ui changes 2025-04-05 14:16:08 +05:30
venbaittech
83d71279eb policy ui changes 2025-04-04 16:56:26 +05:30
venbaittech
4f9c353e02 ui changes 2025-04-04 16:51:25 +05:30
venbaittech
204a71179f system setting 2025-04-04 11:23:23 +05:30
211 changed files with 107423 additions and 9718 deletions

10
.run/ts-tat.run.xml Normal file
View File

@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ts-tat" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="additionalArgs" value="--web-browser-flag &quot;--disable-web-security&quot;" />
<option name="envs">
<entry key="google-chrome --disable-web-security --user-data-dir" value="~/chromeTemp" />
</option>
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@ -1,8 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="frontend"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
@ -25,11 +28,22 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 772 B

View File

@ -5,6 +5,7 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -15,4 +16,7 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<paths>
<cache-path name="cache" path="." />
</paths>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="." />
<external-path name="external" path="." />
<!-- You can add more paths as needed -->
</paths>

View File

@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

3
devtools_options.yaml Normal file
View File

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

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

View File

@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 721 B

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:frontend/services/apiService.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../../config/apiUrl.dart';
import '../../utils/auth_utils.dart';
class TripInformation extends StatefulWidget {
final String planId;
final Color layoutColorForUser;
const TripInformation({
Key? key,
required this.planId,
required this.layoutColorForUser,
}) : super(key: key);
@override
_TripInformationState createState() => _TripInformationState();
}
class _TripInformationState extends State<TripInformation> {
final ApiService apiService = ApiService();
late Future<Map<String, dynamic>> _tripInfoFuture;
Future<Map<String, dynamic>> fetchComments() async {
final String apiUrldata =
'$apiUrl/api/plans/planInfo?plan_id=${widget.planId}';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'app-signature': 'ts-traveltool-2025-signature-123456',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final jsonData = json.decode(response.body);
final Map<String, dynamic> dataMap =
jsonData['data'] as Map<String, dynamic>;
return dataMap;
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return {};
// throw Exception('Failed to load users');
} else {
throw Exception('Failed to load comments');
}
}
@override
void initState() {
super.initState();
_tripInfoFuture = fetchComments();
print('_tripInfoFuture : $_tripInfoFuture');
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
title: Text(
'Trip Information',
style: GoogleFonts.poppins(color: Colors.black),
),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500, // You can adjust this width
maxHeight:
400, // Optional: limit height to make it scrollable vertically
),
child: FutureBuilder<Map<String, dynamic>>(
future: _tripInfoFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}',
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
);
}
if (!snapshot.hasData) {
return Text(
'No data found.',
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
);
}
final data = snapshot.data!;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
keyValueColumn("Traveller", data["traveller"] ?? ""),
keyValueColumn("Group Name", data["group_name"] ?? ""),
keyValueColumn(
"Allowed Plan Type",
data["allowed_plan_type"] ?? "",
),
keyValueColumn(
"Policy Action Flow",
data["policy_action_flow"] ?? "",
),
keyValueColumn("Policy Type", data["policy_type"] ?? ""),
const SizedBox(height: 12),
Text(
"Approval Criteria:",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
...List.generate((data["approval_criteria"] as List).length, (
index,
) {
final item = data["approval_criteria"][index];
return Card(
elevation: 1,
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
keyValueRow("Action", item["action"] ?? '-'),
keyValueRow("Approver", item["approver"] ?? '-'),
keyValueRow(
"Is Action Done",
item["is_action_done"] ?? '-',
),
keyValueRow("Action On", item["action_on"] ?? '-'),
keyValueRow(
"Email Status",
item["email_status"] ?? '-',
),
],
),
),
);
}),
],
),
);
},
),
),
actions: [],
);
}
Widget keyValueColumn(String key, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$key ",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
SizedBox(height: 5),
Text(value, style: GoogleFonts.poppins(fontSize: 13)),
// Expanded(
// child: Text(value, style: GoogleFonts.poppins(fontSize: 13)),
// ),
],
),
);
}
Widget keyValueRow(String key, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
"$key ",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
SizedBox(height: 5),
Expanded(
child: Text(value, style: GoogleFonts.poppins(fontSize: 12)),
),
],
),
);
}
}

View File

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../../config/apiUrl.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
class CommentModalList extends StatefulWidget {
final String planId;
final Color layoutColorForUser;
final String role;
const CommentModalList({
Key? key,
required this.planId,
required this.layoutColorForUser,
required this.role,
}) : super(key: key);
@override
_CommentModalListState createState() => _CommentModalListState();
}
class _CommentModalListState extends State<CommentModalList> {
final ApiService apiService = ApiService();
late Future<List<Map<String, dynamic>>> _commentsFuture;
// Future<List<Map<String, dynamic>>> fetchComments1() async {
// final response = await http.get(
// Uri.parse(
// '$apiUrl/api/plans/getRemarksByPlanId?plan_id=${widget.planId}',
// ),
// );
//
// if (response.statusCode == 200) {
// final jsonData = json.decode(response.body);
// final List<dynamic> dataList = jsonData['data'];
// return dataList.cast<Map<String, dynamic>>();
// } else {
// throw Exception('Failed to load comments');
// }
// }
Future<List<Map<String, dynamic>>> fetchComments() async {
// final String apiUrldata =
// '$apiUrl/getRemarksByPlanId?plan_id=${widget.planId}&user_id=$userId';
final String apiUrldata =
'$apiUrl/api/plans/getRemarksByPlanId?plan_id=${widget.planId}';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'app-signature': 'ts-traveltool-2025-signature-123456',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final jsonData = json.decode(response.body);
final List<dynamic> dataList = jsonData['data'];
return dataList.cast<Map<String, dynamic>>();
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return [];
// throw Exception('Failed to load users');
} else {
throw Exception('Failed to load comments');
}
}
@override
void initState() {
super.initState();
_commentsFuture = fetchComments();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
title: Text('Comments', style: GoogleFonts.poppins(color: Colors.black)),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500, // You can adjust this width
maxHeight:
400, // Optional: limit height to make it scrollable vertically
),
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _commentsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text(
'No Comments For This Plan',
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Text(
'No comments found.',
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
);
}
final comments = snapshot.data!;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: [
DataColumn(
label: Text(
'Name',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.black,
),
),
),
DataColumn(
label: Text(
'Comment',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.black,
),
),
),
DataColumn(
label: Text(
'Updated On',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.black,
),
),
),
],
rows:
comments.map((comment) {
final name = comment['created_by_name'] ?? 'Unknown';
final remark = comment['remarks'] ?? '';
final rawDateStr = comment['updated_on'];
String updatedOn = '';
if (rawDateStr != null && rawDateStr.isNotEmpty) {
try {
final parsedDate = DateTime.parse(rawDateStr);
updatedOn = DateFormat(
'd MMM yyyy',
).format(parsedDate); // e.g., 15 May 2025
} catch (e) {
updatedOn = rawDateStr.split(' ').first; // fallback
}
}
return DataRow(
cells: [
DataCell(
Text(
name,
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
),
DataCell(
Text(
remark,
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
),
DataCell(
Text(
updatedOn,
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
),
],
);
}).toList(),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Close',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: widget.layoutColorForUser,
),
),
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Future<dynamic> showApprovalDialog(
BuildContext context,
Color layoutColor,
) async {
String selectedAction = ""; // "", "accept", "reject"
String remarks = "";
return showDialog<String>(
context: context,
barrierDismissible: false, // force user to tap cancel
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.all(24),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"To Approve or Reject Trip",
style: TextStyle(
fontFamily: "Inter",
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: const Icon(Icons.close, size: 15),
onPressed: () {
Navigator.pop(context, null); // Close the dialog
},
),
],
),
// Accept and Reject Buttons
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
selectedAction == "accept"
? Colors.green
: Colors.grey.shade200,
foregroundColor:
selectedAction == "accept"
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 10),
),
onPressed: () {
setState(() {
selectedAction = "accept";
});
},
child: const Text("Accept"),
),
),
const SizedBox(width: 20),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
selectedAction == "reject"
? Colors.redAccent
: Colors.grey.shade200,
foregroundColor:
selectedAction == "reject"
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 10),
),
onPressed: () {
setState(() {
selectedAction = "reject";
});
},
child: const Text(
"Reject",
style: TextStyle(fontFamily: "Inter"),
),
),
),
],
),
const SizedBox(height: 20),
// Dynamic Content Based on Selection
if (selectedAction == "accept") ...[
const Text(
"Do you want to approve?",
style: TextStyle(fontFamily: "Inter", fontSize: 14),
),
] else if (selectedAction == "reject") ...[
const Text(
"Please enter reason for rejection:",
style: TextStyle(fontFamily: "Inter", fontSize: 14),
),
const SizedBox(height: 10),
TextField(
style: TextStyle(fontFamily: "Inter", fontSize: 14),
maxLines: 3,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
onChanged: (value) => remarks = value,
decoration: InputDecoration(
hintText: "Remarks...",
hintStyle: const TextStyle(
fontSize: 10, // 👈 Set your desired font size here
color: Colors.grey,
fontFamily:
"Inter", // optional if you want consistent font
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: layoutColor, width: 1),
),
),
),
],
],
),
actionsAlignment: MainAxisAlignment.center,
actions: [
// Cancel Button
if (selectedAction.isNotEmpty)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
side: BorderSide(color: layoutColor, width: 1.5),
),
onPressed: () {
Navigator.pop(context, null); // No result
},
child: const Text(
"Cancel",
style: TextStyle(fontFamily: "Inter", fontSize: 12),
),
),
if (selectedAction.isNotEmpty)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor,
foregroundColor: Colors.white,
),
onPressed: () {
if (selectedAction == "accept") {
Navigator.pop(context, "approved"); // Return approval
} else if (selectedAction == "reject" &&
remarks.trim().isNotEmpty) {
Navigator.pop(context, remarks.trim()); // Return remarks
}
},
child: const Text(
"OK",
style: TextStyle(fontFamily: "Inter", fontSize: 12),
),
),
],
);
},
);
},
);
}
/// Show confirm dialog for approval
Future<bool?> showApproveDialog1(BuildContext context, Color layoutColor) {
return showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
backgroundColor: Colors.white,
title: const Text(
"Confirm Approval",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text("Are you sure you want to approve this plan?"),
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, true),
child: const Text("OK"),
),
],
),
);
}
/// Show confirm dialog for rejection with remarks input
Future<String?> showRejectDialog1(
BuildContext context,
Color layoutColor,
) async {
String remarks = "";
final confirmed = await showDialog<bool>(
context: context,
builder: (context) {
return StatefulBuilder(
builder:
(context, setState) => AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.all(36),
// title: const Text("Confirm Rejection"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Confirm Rejection",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text("Please enter reason for rejection."),
const SizedBox(height: 10),
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
maxLines: 3,
onChanged: (value) => remarks = value,
decoration: const InputDecoration(
hintText: "Remarks...",
hintStyle: TextStyle(
fontSize: 10, // 👈 Set your desired font size here
color: Colors.grey,
fontFamily:
"Inter", // optional if you want consistent font
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blueGrey,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey, width: 1),
),
),
),
],
),
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
if (remarks.trim().isEmpty) return;
Navigator.pop(context, true);
},
child: const Text("OK"),
),
],
),
);
},
);
return confirmed == true ? remarks : null;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:super_tooltip/super_tooltip.dart';
class ApprovalTooltip extends StatefulWidget {
final List<Map<String, dynamic>> planStatusList;
final bool hasApprovals;
final String statusText;
final bool isApproverApproved;
final bool isApproverRejected;
final Color Function(String) getStatusColor;
const ApprovalTooltip({
super.key,
required this.planStatusList,
required this.hasApprovals,
required this.statusText,
required this.isApproverApproved,
required this.isApproverRejected,
required this.getStatusColor,
});
@override
State<ApprovalTooltip> createState() => _ApprovalTooltipState();
}
class _ApprovalTooltipState extends State<ApprovalTooltip> {
late SuperTooltip tooltip;
@override
void initState() {
super.initState();
tooltip = SuperTooltip(
popupDirection: TooltipDirection.down,
content: _buildTooltipContent(),
borderColor: Colors.grey.shade300,
backgroundColor: Colors.white,
shadowColor: Colors.black38,
barrierColor: Colors.transparent,
);
}
Widget _buildTooltipContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!widget.hasApprovals)
Center(
child: Text(
"--- No Approvals ---",
style: const TextStyle(
fontFamily: "Archivo",
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
for (int i = 0; i < widget.planStatusList.length; i++) ...[
if (widget.planStatusList[i].entries.any((entry) =>
entry.key.contains('status') &&
entry.value != null &&
entry.value.toString().isNotEmpty)) ...[
_buildApprovalItem(
"Approver ${i + 1}",
widget.planStatusList[i].entries
.firstWhere(
(entry) => entry.key.contains('status'),
orElse: () => MapEntry('', ''),
)
.value
.toString(),
),
const SizedBox(height: 6),
],
],
],
);
}
Widget _buildApprovalItem(String title, String value) {
return Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
value,
style: const TextStyle(
fontSize: 12,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return MouseRegion(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: widget.getStatusColor(widget.statusText)),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 6),
child: Text(
widget.statusText,
style: TextStyle(
fontFamily: "Roboto",
fontWeight: FontWeight.w400,
fontSize: 12,
color: widget.getStatusColor(
widget.isApproverApproved
? "Approved"
: widget.isApproverRejected
? "Rejected"
: (widget.statusText),
),
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,34 +4,31 @@ import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LoginPage1 extends StatefulWidget {
const LoginPage1({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
}
class _LoginPageState extends State<LoginPage1> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: LoginWidget(),
body: LoginWidget(),
);
}
}
// StatefulWidget for Body Content
class LoginWidget extends StatefulWidget{
class LoginWidget extends StatefulWidget {
const LoginWidget({super.key});
@override
_LoginWidgetState createState() => _LoginWidgetState();
}
class _LoginWidgetState extends State<LoginWidget>{
class _LoginWidgetState extends State<LoginWidget> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@ -47,7 +44,8 @@ class _LoginWidgetState extends State<LoginWidget>{
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'},
'Accept': 'application/json'
},
body: jsonEncode({
'email': _emailController.text.trim(),
'password': _passwordController.text.trim(),
@ -61,11 +59,12 @@ class _LoginWidgetState extends State<LoginWidget>{
backgroundColor: Colors.green, // Set background to green
),
);
context.go('/home'); // Login Success
}
else {
context.go('/listPlan'); // Login Success
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login Failed: ${jsonDecode(response.body)['message']}")),
SnackBar(
content: Text(
"Login Failed: ${jsonDecode(response.body)['message']}")),
);
}
} catch (e) {
@ -76,16 +75,13 @@ class _LoginWidgetState extends State<LoginWidget>{
}
}
@override
Widget build(BuildContext context){
Widget build(BuildContext context) {
double myHeight = MediaQuery.of(context).size.height;
double myWidth = MediaQuery.of(context).size.width;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
@ -94,19 +90,17 @@ class _LoginWidgetState extends State<LoginWidget>{
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Center(
child: Text(
"Hello! , Welcome Back",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blueAccent
),
color: Colors.blueAccent),
),
),
),
SizedBox(height: myHeight/10),
SizedBox(height: myHeight / 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: const Text(
@ -114,33 +108,32 @@ class _LoginWidgetState extends State<LoginWidget>{
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blueAccent
),
color: Colors.blueAccent),
),
),
SizedBox(height:8),
SizedBox(height: 8),
TextFormField(
controller: _emailController,
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: 'Enter the email',
labelStyle: TextStyle(color: Colors.grey,),
labelStyle: TextStyle(
color: Colors.grey,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), // Rounded corners// Remove the default border
borderSide: BorderSide.none
),
borderRadius: BorderRadius.circular(
12), // Rounded corners// Remove the default border
borderSide: BorderSide.none),
),
validator: (value){
if(value == null || value.isEmpty){
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required Email';
}
return null;
},
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
@ -149,18 +142,19 @@ class _LoginWidgetState extends State<LoginWidget>{
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.blueAccent
),
color: Colors.blueAccent),
),
),
SizedBox(height:8),
SizedBox(height: 8),
TextFormField(
controller:_passwordController,
controller: _passwordController,
style: const TextStyle(fontSize: 12),
obscureText: _obscureText,
decoration: InputDecoration(
labelText: "Enter the password",
labelStyle: TextStyle(color: Colors.grey,),
labelStyle: TextStyle(
color: Colors.grey,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
suffixIcon: IconButton(
icon: Icon(
@ -176,11 +170,9 @@ class _LoginWidgetState extends State<LoginWidget>{
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none
)
),
validator: (value){
if(value == null || value.isEmpty){
borderSide: BorderSide.none)),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required Password';
}
return null;
@ -189,21 +181,18 @@ class _LoginWidgetState extends State<LoginWidget>{
const SizedBox(height: 20),
Center(
child: ElevatedButton(
onPressed: (){
onPressed: () {
_login(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent, // Change button color
foregroundColor: Colors.white, // Text color
padding: const EdgeInsets.symmetric(horizontal: 24,vertical: 12),
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
)
),
child: const Text(
"Login")
),
)),
child: const Text("Login")),
),
],
),
@ -211,6 +200,3 @@ class _LoginWidgetState extends State<LoginWidget>{
);
}
}

View File

@ -0,0 +1,492 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../../config/apiUrl.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../widgets/custom_text_forex.dart';
import 'costCenter_list.dart';
class CostCenterData extends StatefulWidget {
final Future<List<dynamic>> Function() fetchGetCostCenter;
final bool isDesktop;
final Color? layoutColor;
final int? costcenterId; // <-- Add this
final Map<String, dynamic>? costcenterData;
const CostCenterData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetCostCenter,
this.costcenterId,
this.costcenterData,
});
@override
CostCenterDataState createState() => CostCenterDataState();
}
class CostCenterDataState extends State<CostCenterData> {
final ApiService apiService = ApiService();
Map<String, dynamic>? apiData;
// final Map<String, FocusNode> focusNodes = {
// "name": FocusNode(),
// "description": FocusNode(),
// };
Map<String, FocusNode> focusNodes = {};
Map<String, bool> focusStates = {};
final Map<String, TextEditingController> controllers = {};
Map<String, String> errorMessages = {};
String? selectedName;
String? selectedDescription;
String? userId;
int? costcenterDataId;
late String isActive = "1";
bool isDisable = false;
List<String> dataHeader = ["name", "description"];
Map<String, dynamic> costcenterDetails() {
final data = {
// "cost_center_id": int.parse(costcenterId),
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"created_by": userId,
"is_active": isActive,
};
return data;
}
@override
void initState() {
super.initState();
apiData = null;
for (var field in dataHeader) {
controllers[field] = TextEditingController();
focusNodes["${field}FocusNode"] = FocusNode();
focusStates["${field}Focused"] = false;
}
for (var key in focusNodes.keys) {
_addFocusListener(focusNodes[key]!, (focus) {
setState(() {
focusStates[key.replaceFirst("FocusNode", "Focused")] = focus;
});
});
}
if (widget.costcenterId != null) {
print('Editing D ID: ${widget.costcenterId}');
updateCostCenterDetails();
}
}
void _clearError() {
setState(() {
errorMessages.clear();
});
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
for (var node in focusNodes.values) {
node.dispose();
}
super.dispose();
}
void _addFocusListener(FocusNode node, Function(bool) updateState) {
node.addListener(() {
setState(() {
updateState(node.hasFocus);
});
});
}
void updateCostCenterDetails() {
print("Inside Update Function - ${widget.costcenterData}");
final data = widget.costcenterData;
if (data == null) return;
setState(() {
controllers['name']?.text = data['name'] ?? '';
controllers['description']?.text = data['description'].toString();
isActive = data["is_active"];
final costcenterId = int.tryParse(data['cost_center_id'].toString());
costcenterDataId = costcenterId;
});
}
void toggleStatus() {
setState(() {
isActive = isActive == "1" ? "0" : "1";
});
}
bool validateData() {
errorMessages.clear();
final data = {
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
};
final requiredFields = ["name", "description"];
bool hasFocused = false;
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field]!.trim().isEmpty) {
errorMessages[field] = "Required";
// if (!hasFocused) {
// focusNodes[field]?.requestFocus();
// hasFocused = true;
// }
}
}
return errorMessages.isEmpty;
}
Future<void> handleSubmit() async {
userId = await getUserId();
setState(() {
isDisable = true;
// This triggers UI rebuild with error messages
if (validateData()) {
postCostCenterData();
} else {
isDisable = false;
}
});
final costcenterData1 = costcenterDetails();
print("submit data - $costcenterData1");
}
Future<void> postCostCenterData({int isActive = 1}) async {
// final remarksData = getData();
final costcenterData = costcenterDetails();
print("initially value of the CostCenter - $costcenterData");
final String apiUrldata;
if (costcenterDataId != null) {
print("for edit costcenter id - $costcenterDataId");
apiUrldata = '$apiUrl/api/updateCostCenter/$costcenterDataId';
costcenterData["cost_center_id"] = costcenterDataId.toString();
costcenterData["updated_by"] = userId;
(costcenterData.containsKey("created_by"))
? costcenterData.remove("created_by")
: '';
} else {
print("for add CostCenter id - null");
apiUrldata = '$apiUrl/api/createCostCenter';
print("called apiUrl - $apiUrldata");
costcenterData["created_by"] = userId;
}
print("recently CostCenter data - $costcenterData");
final token = await getToken(); // Fetch token
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final uri = Uri.parse(apiUrldata);
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
};
final body = jsonEncode(costcenterData);
final response =
costcenterDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
switch (response.statusCode) {
case 200:
print("Update - Response: ${response.body}");
_clearError();
widget.fetchGetCostCenter();
if (context.mounted) {
Navigator.of(context).pop(); // Close modal only if mounted
}
setState(() {
isDisable = false;
});
break;
case 201:
print("Save - Response: ${response.body}");
_clearError();
await widget.fetchGetCostCenter();
if (context.mounted) {
Navigator.of(context).pop(); // Close modal only if mounted
}
setState(() {
isDisable = false;
});
break;
case 403:
print("403-FORB");
await apiService.logout(context);
break;
// throw Exception('Failed to load users');
default:
print("Failed to submit costcenter. Status: ${response.statusCode}");
print("Error: ${response.body}");
setState(() {
isDisable = false;
});
}
} catch (e) {
print(" Error submitting plan: $e");
setState(() {
isDisable = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Row 1: Title + Edit + Delete buttons
Row(
children: [
Text(
(costcenterDataId != null)
? 'Edit CostCenter'
: 'Create CostCenter',
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
),
const Spacer(),
],
),
const SizedBox(height: 2),
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
const SizedBox(height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Name *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["nameFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["name"],
focusNode: focusNodes["nameFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["name"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["name"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 15),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Description *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["descriptionFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 100,
child: TextField(
controller: controllers["description"],
focusNode: focusNodes["descriptionFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
style: const TextStyle(fontSize: 12),
maxLines: null,
expands: true,
decoration: const InputDecoration(
labelText: "Description",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["description"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["description"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 15),
if (costcenterDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
isActive == "1" ? "Active" : "Inactive",
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color: isActive == "1" ? Colors.green : Colors.grey,
),
),
),
),
],
),
if (costcenterDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// SizedBox(
// child: ElevatedButton(
// onPressed: () {
// // You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: widget.layoutColor,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: Text('Cancel',
// style: GoogleFonts.poppins(
// fontSize: 13, color: Colors.white)),
// ),
// ),
// SizedBox(
// width: 10,
// ),
SizedBox(
child: ElevatedButton(
onPressed:
isDisable
? null
: () async {
setState(() {
isDisable = true;
});
await handleSubmit();
// You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// Optional: re-enable only on error
// setState(() {
// isDisable = false;
// });
},
// onPressed: () {
// handleSubmit();
// // You can get text from commentController.text
// // Navigator.of(context).pop(); // Close the modal
// },
style: ElevatedButton.styleFrom(
backgroundColor: widget.layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
),
// : SizedBox.shrink(),
],
),
);
}
}

View File

@ -0,0 +1,984 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../utils/pagination.dart';
import '../../widgets/custom_breadcrumb_navigation.dart';
import 'costCenterDetails.dart';
class CostCenterList extends StatefulWidget {
const CostCenterList({super.key});
@override
CostCenterListState createState() => CostCenterListState();
}
class CostCenterListState extends State<CostCenterList> {
final GlobalKey<CostCenterListState> costCenterListKey =
GlobalKey<CostCenterListState>();
final ApiService apiService = ApiService();
Future<List<dynamic>>? futureCostCenter;
// late Future<List<dynamic>> futureCostCenter;
late Map<String, dynamic> depSingleData;
String? selectedCostCenterId;
String? orgId;
Color? layoutColor;
Color? bodyColor;
List allCostCenter = [];
List filteredCostCenter = [];
TextEditingController searchController = TextEditingController();
int currentPage = 0;
int itemsPerPage = 10;
@override
void initState() {
super.initState();
_checkAuthAndLoadData();
// futureCostCenter = fetchGetCostCenter();
//
// futureCostCenter.then((object) {
// setState(() {
// allCostCenter = object;
// });
// });
//
// WidgetsBinding.instance.addPostFrameCallback((_) {
// loadInitialData();
// });
// futurePlans = fetchPlans();
}
void _checkAuthAndLoadData() async {
final String? token = await getToken(); // Your async function to get token
if (token == null || token.isEmpty) {
// Token doesn't exist → redirect to login
context.go(
"/",
); // or use: router.go("/") if you're using `GoRouter` directly
return;
}
if (!mounted) return;
try {
futureCostCenter = fetchGetCostCenter();
final roleUser = await getRoleUser();
if (roleUser != null &&
(roleUser == 'Org Admin' || roleUser == 'Travel Admin')) {
futureCostCenter?.then((object) {
setState(() {
allCostCenter = object;
});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
loadInitialData();
});
} else {
apiService.logout(context);
}
} catch (e) {
print("group : $e");
}
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor =
bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Future<List<dynamic>> refreshData() {
print("Calling Refresh Data");
futureCostCenter = fetchGetCostCenter();
return futureCostCenter!.then((object) {
print("Calling Refresh Data $object");
setState(() {
allCostCenter = object;
filteredCostCenter = object;
searchController.text = "";
});
return object;
});
}
Future<List<dynamic>> fetchGetCostCenter() async {
// return [];
final String apiUrlData = '$apiUrl/api/getCostCenterMaster?for=table_view';
final String? token = await getToken();
print("Fetch CostCenter");
print("2KN Here : $token");
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
},
);
print("called api : $apiUrlData");
if (response.statusCode == 200) {
final data = json.decode(response.body);
print(data['data']);
return data['data']; // Returning raw JSON list
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return [];
// throw Exception('Failed to load users');
} else {
throw Exception('Failed to load users');
}
}
void filterCostCenter(String query) {
// print("all before filtering: $query");
// final lowerQuery = query.toLowerCase();
// setState(() {
// filteredCostCenter = allCostCenter.where((object) {
// return (object['cost_center_id']?.toLowerCase().contains(lowerQuery) ??
// false) ||
// (object['description']?.toLowerCase().contains(lowerQuery) ?? false) ||
// (object['user']?.toLowerCase().contains(lowerQuery) ?? false) ||
// (object['is_active']?.toLowerCase().contains(lowerQuery) ?? false);
// }).toList();
// });
// print("filteredPlans: $filteredCostCenter");
print("all before filtering: $query");
final lowerQuery = query.toLowerCase();
setState(() {
filteredCostCenter =
allCostCenter.where((object) {
final isActiveStatus =
object['is_active'] == "1" ? "active" : "inactive";
return (object['cost_center_id']?.toLowerCase().contains(
lowerQuery,
) ??
false) ||
(object['name']?.toLowerCase().contains(lowerQuery) ?? false) ||
(object['description']?.toLowerCase().contains(lowerQuery) ??
false) ||
(isActiveStatus.contains(lowerQuery));
}).toList();
currentPage = 0;
});
print("filteredCostCenter: $filteredCostCenter");
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
backgroundColor: Color(0xFFf5f5f5),
// appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'),
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical:
MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
// const Expanded(child: Center(child: Text("User Page Content"))),
Expanded(child: buildGroupList(isDesktop)),
],
),
),
);
},
);
}
Widget buildGroupList(bool isDesktop) {
return Container(
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
),
// decoration: BoxDecoration(
// // color: Colors.amber,
// // color: bodyColor,
// color: Color(0xFFE1F5FE),
// border: Border.all(
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// width: 3.5)),
child: buildUserTable(isDesktop),
);
}
Widget buildUserTable(bool isDesktop) {
return Container(
// margin: isDesktop
// ? EdgeInsets.all(10.0)
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
// padding: const EdgeInsets.all(10),
height:
isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Divider(
// thickness: 0.2, // how "thick" the line is
// color: Colors.grey, // optional
// ),
Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
child: BreadcrumbNavigation(
isDesktop: isDesktop,
breadcrumbItems: [
BreadcrumbItem(
title: 'Org Settings',
tooltip: 'Go To Organization Settings',
onTap: (context) {
context.go("/OrganizationSettings");
},
),
BreadcrumbItem(title: 'Cost Center Details'),
],
),
),
// Text(
// 'Cost Center Details',
// style: GoogleFonts.poppins(
// fontSize: isDesktop ? 16 : 14,
// fontWeight: FontWeight.w600,
// color: Colors.black,
// ),
// ),
],
),
if (isDesktop)
SizedBox(width: MediaQuery.of(context).size.width * 0.08),
// SizedBox(width: MediaQuery.of(context).size.width * 0.16),
if (isDesktop)
Container(
width: MediaQuery.of(context).size.width * 0.2,
height: 40,
child: TextField(
controller: searchController,
onChanged: filterCostCenter,
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: TextStyle(
fontSize: 12,
color: Color(0xFF9E9DBD),
),
prefixIcon: Icon(
Icons.search,
color: Color(0xFF9E9DBD),
size: 18,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
style: GoogleFonts.poppins(fontSize: 12),
),
),
// SizedBox(width: 16),
Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF114D8B),
foregroundColor: Colors.white,
disabledBackgroundColor: Color(0xFF114D8B),
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Color(0xFF114D8B), width: 2),
),
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
),
onPressed: () async {
showDialog(
context: context,
builder:
(context) => CostCenterData(
isDesktop: isDesktop,
layoutColor: layoutColor!,
fetchGetCostCenter: refreshData,
// role:
// "Travel Agent"
),
);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
children: [
Text(
"Add Cost Center",
style: GoogleFonts.poppins(
fontSize: isDesktop ? 13 : 11,
),
),
SizedBox(width: 8), // spacing between icon and text
Icon(
Icons.add_circle_outline_rounded,
size: 15,
color: Colors.white,
),
],
),
),
],
),
if (!isDesktop) SizedBox(height: 5),
isDesktop
? SizedBox.shrink()
: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.8,
height: 35,
child: TextField(
controller: searchController,
onChanged: filterCostCenter,
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: TextStyle(
fontSize: 12,
color: Color(0xFF9E9DBD),
),
prefixIcon: Icon(
Icons.search,
color: Color(0xFF9E9DBD),
size: 18,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
style: GoogleFonts.poppins(fontSize: 12),
),
),
// SizedBox(width: 16),
],
),
const SizedBox(height: 10),
FutureBuilder<List<dynamic>>(
future: futureCostCenter,
builder: (context, snapshot) {
final adjHgt = MediaQuery.of(context).size.height;
if (futureCostCenter == null) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError ||
!snapshot.hasData ||
snapshot.data!.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// const Icon(Icons.error_outline,
// color: Colors.redAccent, size: 60),
// const SizedBox(height: 16),
// Text(
// "Oops!",
// style: GoogleFonts.poppins(
// fontSize: 20,
// fontWeight: FontWeight.bold,
// color: Colors.redAccent),
// ),
SizedBox(height: adjHgt / 4),
Text(
"No Data Found ",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const SizedBox(height: 20),
// Text(
// "Please Create CostCenter Details",
// textAlign: TextAlign.center,
// style: GoogleFonts.poppins(
// fontSize: 16,
// color: Colors.grey,
// ),
// ),
// const SizedBox(height: 20),
],
),
),
);
}
/* Here collect the list to displayed the data in table or card Used */
List<dynamic> object =
filteredCostCenter.isNotEmpty
? filteredCostCenter
: allCostCenter;
/* List is Sorting here */
object.sort((a, b) {
DateTime dateA = DateTime.parse(a['created_on']);
DateTime dateB = DateTime.parse(b['created_on']);
return dateB.compareTo(dateA); // Descending: newest first
});
/* For pagination for list ... */
List paginatedCostCenter =
object
.skip(currentPage * itemsPerPage)
.take(itemsPerPage)
.toList();
/* Table ... */
Widget table = LayoutBuilder(
builder: (context, constraints) {
double minWidth = isDesktop ? constraints.maxWidth : 1300;
return ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: DataTable(
dividerThickness: 0.5,
columnSpacing: isDesktop ? 24.0 : 16.0,
border: TableBorder(
horizontalInside: BorderSide(
width: 0.5,
color: Colors.grey.shade200,
),
),
columns: [
DataColumn(
label: Text(
'Name',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
DataColumn(
label: Text(
'Description',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
DataColumn(
label: Text(
'Status',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
DataColumn(
label: Text(
'Actions',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
rows:
paginatedCostCenter.map((tableObject) {
String costcenterId =
tableObject['cost_center_id']
.toString(); // Get user ID
bool isSelected =
selectedCostCenterId == costcenterId;
return DataRow(
cells: [
DataCell(
Text(
tableObject['name'] ?? '',
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
),
),
),
DataCell(
Text(
tableObject['description'] ?? 'N/A',
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
),
DataCell(
Text(
tableObject['is_active'] == "1"
? 'Active'
: 'Inactive',
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color:
tableObject['is_active'] == "1"
? Colors.green
: Colors.grey,
),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
),
DataCell(
// UserActionsMenu(
// user: forex,
// getUserDetails: (id) =>
// apiService.getSingleUser(id),
// ),
GestureDetector(
child: Tooltip(
message: 'Edit CostCenter Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
),
),
onTap: () async {
// final userId = getUserId(user['user_id']);
// final usersData = await getUserDetails(userId);
//
final costcenterId = int.tryParse(
tableObject['cost_center_id']
.toString(),
);
if (costcenterId != null) {
print(
"Table cell - costcenter Id -- $costcenterId",
);
final data = await apiService
.getCostCenterDetailsFind(
context,
costcenterId,
);
print("CostCenterId -- $data");
showDialog(
context: context,
builder:
(context) => CostCenterData(
isDesktop: isDesktop,
costcenterId:
costcenterId, // Pass the ID
costcenterData: data,
layoutColor: layoutColor!,
// fetchGetForex: fetchGetForex,
fetchGetCostCenter:
refreshData,
// role:
// "Travel Agent"
),
);
} else {
print("Invalid ID");
}
},
),
),
],
);
}).toList(),
),
);
},
);
/* Card ... */
Widget buildMobileCardView(List<dynamic> paginatedUser) {
return ListView.builder(
itemCount: paginatedUser.length,
itemBuilder: (context, index) {
final cardObject = paginatedUser[index];
return Card(
color: Colors.white,
margin: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status and Employee Code
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
cardObject['name'] ?? 'N/A',
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.black87,
fontWeight: FontWeight.w700,
),
),
GestureDetector(
child: Tooltip(
message: 'Edit CostCenter Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
),
),
onTap: () async {
// final userId = getUserId(user['user_id']);
// final usersData = await getUserDetails(userId);
//
final costcenterId = int.tryParse(
cardObject['cost_center_id']
.toString(),
);
if (costcenterId != null) {
print(
"costcenterId -- $costcenterId",
);
final data = await apiService
.getCostCenterDetailsFind(
context,
costcenterId,
);
print("CostCenterId -- $data");
showDialog(
context: context,
builder:
(context) => CostCenterData(
isDesktop: isDesktop,
costcenterId:
costcenterId, // Pass the ID
costcenterData: data,
layoutColor: layoutColor!,
// fetchGetCostCenter: fetchGetCostCenter,
fetchGetCostCenter:
refreshData,
// role:
// "Travel Agent"
),
);
} else {
print("Invalid ID");
}
},
),
// PopupMenuButton<int>(
// color: Colors.white,
// padding: EdgeInsets.zero,
// offset: Offset(0, 30),
// icon: Icon(
// Icons.more_vert,
// color: Color(0xFF475569),
// size: 14,
// ),
// itemBuilder: (context) => [
// CustomPopupMenuEntry(
// child: Container(
// padding: EdgeInsets.symmetric(
// horizontal: 8, vertical: 8),
// child: Row(
// mainAxisSize:
// MainAxisSize.min,
// mainAxisAlignment:
// MainAxisAlignment.center,
// children: [
// IconButton(
// icon: Icon(
// Icons
// .remove_red_eye,
// color: Color(
// 0xFF475569),
// size: 18),
// onPressed: () {
// print(
// "USerDAta - $user");
// // dynamic usersData = apiService
// // .getSingleUser(user[
// // 'user_id']
// // is String
// // ? int.parse(user[
// // 'user_id'])
// // : user[
// // 'user_id']);
// //
// // print(
// // "USerDAta - $usersData");
//
// context.go(
// "/CreateUserDetails",
// extra: {
// "selectedUser":
// user,
// "isViewMode": true
// },
// );
// }),
// IconButton(
// icon: Image.asset(
// 'assets/images/IconsImg/edit.png',
// width: 20,
// height: 15),
// onPressed: () {
// context.go(
// "/CreateUserDetails",
// extra: {
// "selectedUser":
// user,
// "isViewMode": false
// },
// );
// },
// ),
// ],
// ),
// ),
// ),
// ],
// ),
],
),
SizedBox(height: 2),
// Trip Id and Trip Name
// Name
Row(
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
cardObject['description'] ?? '',
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.black87,
),
),
],
),
SizedBox(width: 10),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
cardObject['description'] ?? '',
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.black87,
),
),
],
),
],
),
// Actions
// Actions
],
),
),
);
},
);
}
return Expanded(
child: Column(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child:
isDesktop
? (searchController.text.isNotEmpty &&
filteredCostCenter.isEmpty
? Center(
child: Text(
"No Matches Found",
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey,
),
),
)
: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: table,
))
: (searchController.text.isNotEmpty &&
filteredCostCenter.isEmpty
? Center(
child: Text(
"No Matches Found",
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey,
),
),
)
: buildMobileCardView(
paginatedCostCenter,
)),
),
// Expanded(
// child: isDesktop
// ? SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: table, // <-- your existing table
// )
// : buildMobileCardView(paginatedCostCenter),
// ),
PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
totalItems: object.length,
activeColor: layoutColor, // your theme color
onPageChanged: (page) {
setState(() {
currentPage = page;
});
},
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 0;
});
},
),
],
),
);
},
),
],
),
),
),
);
}
}

View File

@ -9,27 +9,19 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInfo) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
appBar: isDesktop ? null : const CustomAppBar(title:'Home'),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
body:
Row(
body: Row(
children: [
if(isDesktop)
CustomDrawer(isDesktop: true),
if (isDesktop) CustomDrawer(isDesktop: true),
const Expanded(child: Center(child: Text("Home Page Content"))),
],
),
);
});
});
}
}

View File

@ -0,0 +1,423 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:html' as html;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:frontend/routes/custom_router.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../utils/pagination.dart';
class StatusDashboard extends StatefulWidget {
const StatusDashboard({super.key});
@override
StatusDashboardState createState() => StatusDashboardState();
}
class StatusDashboardState extends State<StatusDashboard> {
Map<String, dynamic>? apiData;
String? organizationId;
late bool _dialogShown = false;
late StreamSubscription<html.PopStateEvent> _popStateListener;
@override
void initState() {
super.initState();
_checkAuthAndLoadData();
checkbackbutton();
// loadDashboardData();
}
void checkbackbutton() async {
// Push a dummy state so back button triggers popstate instead of navigating
html.window.history.pushState(null, 'home', html.window.location.href);
_popStateListener = html.window.onPopState.listen((event) {
if (!_dialogShown && mounted) {
_showBackConfirmationDialog();
}
// Re-push to prevent leaving
html.window.history.pushState(null, 'home', html.window.location.href);
});
}
@override
void dispose() {
_popStateListener.cancel(); // Remove the browser popstate listener
super.dispose();
}
void _showBackConfirmationDialog() {
if (!mounted) return;
_dialogShown = true;
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text("Confirm"),
content: Text("Do you want to logout?"),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Close dialog
_dialogShown = false;
},
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.pop(context); // Close dialog
_dialogShown = false;
await _logoutAndRedirect(context);
},
child: Text("Logout"),
),
],
),
);
}
Future<void> _logoutAndRedirect(BuildContext context) async {
print("logue 0");
// Example: clear session or shared preferences
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
context.go("/");
print("logue 1");
}
void _checkAuthAndLoadData() async {
final String? token = await getToken(); // Your async function to get token
if (token == null || token.isEmpty) {
// Token doesn't exist → redirect to login
context.go(
"/",
); // or use: router.go("/") if you're using `GoRouter` directly
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
checkbackbutton();
});
// If token exists, load the dashboard data
loadDashboardData();
}
Future<void> loadDashboardData() async {
try {
final data = await fetchStatusDashboard();
setState(() {
apiData = data;
});
} catch (e) {
print("Error loading dashboard: $e");
}
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Future<String?> getOrgId() async {
final prefs = await SharedPreferences.getInstance();
final String? userDataString = prefs.getString('user_data');
if (userDataString != null) {
try {
final Map<String, dynamic> userData = jsonDecode(userDataString);
return userData["org_id"]?.toString();
} catch (e) {
return null;
}
}
return null;
}
Future<Map<String, dynamic>> fetchStatusDashboard() async {
organizationId = await getOrgId();
final String apiUrlData =
'$apiUrl/api/plans/statusDashboard?org_id=$organizationId';
final String? token = await getToken();
print("Fetch StatusDashboard -- 2KN Here : $token");
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
print("called api : $apiUrlData");
if (response.statusCode == 200) {
final apiData = json.decode(response.body);
print("Fetch StatusDashboard -- Reponse Here : $apiData");
return apiData; // Returning raw JSON list
} else {
throw Exception('Failed to load users');
}
}
Color getStatusColor(String status) {
if (status == "Domestic") return Colors.white;
if (status == "International") return Colors.white;
if (status == "Partially Approved") return Colors.yellow.shade100;
if (status == "Pending Approval") return Colors.greenAccent.shade100;
if (status == "Approved") return Colors.green.shade100;
if (status == "Completed") return Colors.green.shade500;
if (status == "Rejected") return Colors.red.shade100;
if (status == "Cancelled") return Colors.red.shade200;
return Colors.grey.shade100;
}
IconData getStatusIcon(String title) {
switch (title) {
case "Domestic":
return Icons.directions_bus_filled_outlined;
case "International":
return Icons.airplanemode_active;
case "Partially Approved":
return Icons.pending;
case "Approved":
return Icons.check_circle_outline;
case "Completed":
return Icons.done_all;
case "Rejected":
return Icons.cancel_outlined;
case "Cancelled":
return Icons.cancel_schedule_send_outlined;
default:
return Icons.info_outline;
}
}
@override
Widget build(BuildContext context) {
final typeBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeBasedCount']) ?? [],
);
print("apiData - => $apiData");
print("typeBasedCount => $typeBasedCount");
final statusBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusBasedCount']) ?? [],
);
print("statusBasedCount - => $statusBasedCount");
// 👇 Local function to create the card widget
Widget buildInfoCard(String title, int count, double width) {
print('title $title');
final color = getStatusColor(title);
final icon = getStatusIcon(title);
return Card(
elevation: 3,
color: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: width,
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 32,
color:
(title == 'Domestic' || title == 'International')
? Colors.green
: Colors.black54,
), // 👈 Add Icon here
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize:
(title == 'Domestic' || title == 'International')
? 16
: 14,
),
),
const SizedBox(height: 8),
Text(
count.toString(),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
// color: Colors.blue,
),
),
],
),
),
);
}
void _handleBackButton() {
final location = GoRouterState.of(context).uri.toString();
print("location - $location");
if (location.contains('/StatusDashboard')) {
// Do nothing or show "Press again to exit" toast
print("Blocked back on dashboard");
} else {
print("dashboard ..");
}
}
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return PopScope(
canPop: false, // Allow back navigation only if not login screen
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
_handleBackButton(); // Show exit confirmation dialog
},
child: Scaffold(
backgroundColor: const Color(0xFFf5f5f5),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical: 10, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
// Only needed if child layout depends on height
child: Container(
decoration: BoxDecoration(
color:
isDesktop
? Colors.white
: const Color(0xFFFCFCFC),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Wrap(
spacing: 10,
runSpacing: 10,
children:
typeBasedCount.map((item) {
double cardWidth =
isDesktop
? (MediaQuery.of(
context,
).size.width *
0.75 -
10) /
2 // 80% width padding adjusted
: MediaQuery.of(
context,
).size.width -
24; // full width with padding
return SizedBox(
width: cardWidth,
child: buildInfoCard(
item['value'],
item['count'],
cardWidth,
),
);
}).toList(),
),
const SizedBox(height: 20),
Wrap(
spacing: 10,
runSpacing: 10,
children:
statusBasedCount.map((item) {
double cardWidth =
isDesktop
? (MediaQuery.of(
context,
).size.width *
0.90 -
50) /
6 // desktop layout: 6 cards per row
: MediaQuery.of(
context,
).size.width -
24; // mobile: full width
return SizedBox(
width: cardWidth,
child: buildInfoCard(
item['value'],
item['count'],
cardWidth,
),
);
}).toList(),
),
],
),
),
),
],
),
),
),
),
);
},
),
),
),
);
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,934 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:html' as html;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:frontend/routes/custom_router.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../utils/pagination.dart';
class StatusDashboard extends StatefulWidget {
const StatusDashboard({super.key});
@override
StatusDashboardState createState() => StatusDashboardState();
}
class StatusDashboardState extends State<StatusDashboard> {
Map<String, dynamic>? apiData;
String? organizationId;
late bool _dialogShown = false;
bool loader = false;
late StreamSubscription<html.PopStateEvent> _popStateListener;
String selectedView = 'Today';
String AccessKeyFlag = "Both";
Map<String, dynamic>? currentData;
List typeData = [];
List statusData = [];
List<int> flightData = [];
List<int> accommodationData = [];
List<int> forexData = [];
final ApiService apiService = ApiService();
final categoryIcons = {
"flight": Icons.flight,
"accommodation": Icons.hotel_outlined,
"forex": Icons.attach_money,
};
List<String> statusLabels = [
"Pending Approval",
"Partially Approved",
"Approved",
"Rejected",
"Cancelled",
];
// Extract counts for "Both" section
List<int> getStatusValues(Map<String, dynamic> serviceData) {
final both = serviceData['Both'] ?? {};
return statusLabels.map((label) => (both[label] ?? 0) as int).toList();
}
@override
void initState() {
super.initState();
_checkAuthAndLoadData();
checkbackbutton();
// loadDashboardData();
}
void checkbackbutton() async {
// Push a dummy state so back button triggers popstate instead of navigating
html.window.history.pushState(null, 'home', html.window.location.href);
_popStateListener = html.window.onPopState.listen((event) {
if (!_dialogShown && mounted) {
_showBackConfirmationDialog();
}
// Re-push to prevent leaving
html.window.history.pushState(null, 'home', html.window.location.href);
});
}
@override
void dispose() {
_popStateListener.cancel(); // Remove the browser popstate listener
super.dispose();
}
void _showBackConfirmationDialog() {
if (!mounted) return;
_dialogShown = true;
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text("Confirm"),
content: Text("Do you want to logout?"),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Close dialog
_dialogShown = false;
},
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
// Navigator.pop(context); // Close dialog
_dialogShown = false;
await _logoutAndRedirect(context);
},
child: Text("Logout"),
),
],
),
);
}
Future<void> _logoutAndRedirect(BuildContext context) async {
print("logue 0");
Navigator.pop(context); // closes dialog
_dialogShown = false;
await apiService.logout(context);
// Example: clear session or shared preferences
// final prefs = await SharedPreferences.getInstance();
// await prefs.clear();
// context.go("/");
print("logue 1");
}
void _checkAuthAndLoadData() async {
final String? token = await getToken(); // Your async function to get token
if (token == null || token.isEmpty) {
// Token doesn't exist → redirect to login
context.go(
"/",
); // or use: router.go("/") if you're using `GoRouter` directly
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
checkbackbutton();
});
// If token exists, load the dashboard data
loadDashboardData();
}
Future<void> loadDashboardData() async {
try {
final data = await fetchStatusDashboard();
setState(() {
apiData = data;
});
} catch (e) {
print("Error loading dashboard: $e");
}
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Future<String?> getOrgId() async {
final prefs = await SharedPreferences.getInstance();
final String? userDataString = prefs.getString('user_data');
if (userDataString != null) {
try {
final Map<String, dynamic> userData = jsonDecode(userDataString);
return userData["org_id"]?.toString();
} catch (e) {
return null;
}
}
return null;
}
Future<Map<String, dynamic>> fetchStatusDashboard() async {
organizationId = await getOrgId();
final String apiUrlData =
'$apiUrl/api/plans/statusDashboard?org_id=$organizationId';
final String? token = await getToken();
print("Fetch StatusDashboard -- 2KN Here : $token");
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
print("called api : $apiUrlData");
if (response.statusCode == 200) {
final apiData = json.decode(response.body);
print("Fetch StatusDashboard -- Reponse Here : $apiData");
return apiData; // Returning raw JSON list
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return {};
} else {
throw Exception('Failed to load users');
}
}
Color getStatusColor(String status) {
if (status == "Domestic") return Colors.white;
if (status == "International") return Colors.white;
if (status == "Partially Approved") return Colors.yellow.shade100;
if (status == "Pending Approval") return Colors.greenAccent.shade100;
if (status == "Approved") return Colors.green.shade100;
if (status == "Completed") return Colors.green.shade500;
if (status == "Rejected") return Colors.red.shade100;
if (status == "Cancelled") return Colors.red.shade200;
return Colors.grey.shade100;
}
IconData getStatusIcon(String title) {
switch (title) {
case "Domestic":
return Icons.directions_bus_filled_outlined;
case "International":
return Icons.airplanemode_active;
case "Partially Approved":
return Icons.pending;
case "Approved":
return Icons.check_circle_outline;
case "Completed":
return Icons.done_all;
case "Rejected":
return Icons.cancel_outlined;
case "Cancelled":
return Icons.cancel_schedule_send_outlined;
default:
return Icons.info_outline;
}
}
@override
Widget build(BuildContext context) {
final typeBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeBasedCount']) ?? [],
);
print("apiData - => $apiData");
print("typeBasedCount => $typeBasedCount");
final statusBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusBasedCount']) ?? [],
);
final statusBasedTodayCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusBasedTodayCount']) ?? [],
);
final statusBasedWeeklyCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusBasedWeeklyCount']) ?? [],
);
final typeBasedTodayCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeBasedTodayCount']) ?? [],
);
final typeBasedWeeklyCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeBasedWeeklyCount']) ?? [],
);
final typeTodayBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeTodayBasedCount']) ?? [],
);
final typeWeeklyBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['typeWeeklyBasedCount']) ?? [],
);
final statusTodayBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusTodayBasedCount']) ?? [],
);
final statusWeeklyBasedCount = List<Map<String, dynamic>>.from(
(apiData?['data']?['statusWeeklyBasedCount']) ?? [],
);
final List todayList = List<Map<String, dynamic>>.from(
apiData?['data']?['todayTripCounts'] ?? [],
);
final List weekList = List<Map<String, dynamic>>.from(
apiData?['data']?['weekTripCounts'] ?? [],
);
// Merge all maps in the list into one map
Map<String, dynamic> todayBasedCount = {};
Map<String, dynamic> weekBasedCount = {};
for (final item in todayList) {
todayBasedCount.addAll(item);
}
for (final item in weekList) {
weekBasedCount.addAll(item);
}
setState(() {
// currentData = selectedView == "Today" ? todayBasedCount : weekBasedCount;
typeData =
selectedView == "Today" ? typeBasedTodayCount : typeBasedWeeklyCount;
statusData =
selectedView == "Today"
? statusBasedTodayCount
: statusBasedWeeklyCount;
flightData =
selectedView == "Today"
? getStatusValues(todayBasedCount['flight'] ?? {})
: getStatusValues(weekBasedCount['flight'] ?? {});
accommodationData =
selectedView == "Today"
? getStatusValues(todayBasedCount['acomodation'] ?? {})
: getStatusValues(weekBasedCount['acomodation'] ?? {});
forexData =
selectedView == "Today"
? getStatusValues(todayBasedCount['forex'] ?? {})
: getStatusValues(weekBasedCount['forex'] ?? {});
});
print("statusBasedCount - => $statusBasedCount");
print("Current Data - => $currentData");
// 👇 Local function to create the card widget
Widget buildInfoCard(String title, int count, double width) {
print('title $title');
final color = getStatusColor(title);
final icon = getStatusIcon(title);
return Card(
elevation: 3,
color: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: width,
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 32,
color:
(title == 'Domestic' || title == 'International')
? Colors.green
: Colors.black54,
), // 👈 Add Icon here
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize:
(title == 'Domestic' || title == 'International')
? 16
: 14,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Text(
count.toString(),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
// color: Colors.blue,
),
),
],
),
),
);
}
void _handleBackButton() {
final location = GoRouterState.of(context).uri.toString();
print("location - $location");
if (location.contains('/StatusDashboard')) {
// Do nothing or show "Press again to exit" toast
print("Blocked back on dashboard");
} else {
print("dashboard ..");
}
}
String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
Widget toggleChip(String label) {
bool isSelected = selectedView == label;
return GestureDetector(
onTap: () {
setState(() {
selectedView = label;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF003A78) : Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
height: 36,
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 10,
color: isSelected ? Colors.white : Colors.black,
fontWeight: FontWeight.w700,
),
),
),
);
}
Widget buildTopCard({
// required IconData icon,
required Color color,
required String label,
required int count,
required Color bgColor,
}) {
return Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(1),
),
child: Column(
children: [
Tooltip(
message: label,
child: Image.asset(
label == 'Domestic'
? 'assets/images/IconsImg/Domestic_new.png'
: 'assets/images/IconsImg/International_new.png',
// width: 65,
// height: label == 'Domestic' ? 37 : 40,
),
),
// Icon(icon, color: color, size: 40),
const SizedBox(height: 10),
Text(
count.toString(),
style: GoogleFonts.poppins(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 10,
color: color,
// color: const Color(0xFFEAF3FB),
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}
Widget statusCard(String label, int count) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
decoration: BoxDecoration(
color: const Color(0xFFEAF3FB),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
count.toString(),
style: GoogleFonts.poppins(
fontSize: 18,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Text(
label,
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black87,
),
),
],
),
),
],
);
}
Widget buildCategoryCard(
String title,
IconData icon,
List<String> statusLabels,
List<int> values,
) {
final statusIcons = [
Icons.calendar_today,
Icons.assignment,
Icons.verified,
Icons.block,
Icons.cancel,
];
final statusColors = [
Color(0xFFFBFFCA),
Color(0xFFFFF1CD), // Colors.yellow.shade100, // Color(0xFFCAE77B),
Color(0xFFDAFFE8), // Colors.green.shade100, // Color(0xFF72D480),
Color(0xFFFFD6D3), // Colors.red.shade100, // Color(0xFFF88C8C),
Color(0xFFFFA8A8), // Colors.red.shade200, // Color(0xFFE94B4B),
];
final statusIconColors = [
Color(0xFFB1BC1A),
Color(0xFFCC9300),
Color(0xFF509269),
Color(0xFFD25E55),
Color(0xFFAE0303),
];
return Container(
padding: const EdgeInsets.all(16),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// ),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blueGrey,
blurRadius: 5,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
// color: Colors.blue,
color: Color(0xFF004A8E),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 20),
),
const SizedBox(width: 10),
Text(
title,
style: GoogleFonts.poppins(
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
...List.generate(values.length, (index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// Icon(statusIcons[index], size: 15, color: statusIconColors[index]),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: statusColors[index], // background color
borderRadius: BorderRadius.circular(
4,
), // square with slight rounding
),
alignment: Alignment.center,
child: Icon(
statusIcons[index],
size: 15,
color: statusIconColors[index], // icon color
),
),
const SizedBox(width: 6),
Text(
statusLabels[index],
style: GoogleFonts.poppins(color: Colors.black87),
),
],
),
Text(
values[index].toString(),
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
],
),
);
}),
],
),
);
}
// Widget buildCategoryCard(String title, IconData icon,statusLabels,statusvalue) {
//
//
// return Container(
// padding: const EdgeInsets.all(16),
// // decoration: BoxDecoration(
// // color: Colors.white,
// // borderRadius: BorderRadius.circular(12),
// // ),
//
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// Container(
// padding: const EdgeInsets.all(8),
// decoration: const BoxDecoration(
// color: Colors.blue,
// shape: BoxShape.circle,
// ),
// child: Icon(
// icon,
// color: Colors.white,
// size: 20,
// ),
// ),
// const SizedBox(width: 10),
// Text(title, style: GoogleFonts.poppins(
// color: Colors.black87,
// fontWeight: FontWeight.bold
// ),),
// ],
// ),
// const SizedBox(height: 12),
// ...List.generate(statusvalue.length, (index) {
// return Padding(
// padding: const EdgeInsets.symmetric(vertical: 10),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Row(
// children: [
// // Icon(statusIcons[index], size: 20, color: statusColors[index]),
// Container(
// padding: const EdgeInsets.all(8),
// decoration: BoxDecoration(
// color: statusColors[index], // Background color behind the icon
// borderRadius: BorderRadius.circular(10), // Rounded square
// boxShadow: [
// BoxShadow(
// color: Colors.grey, // Shadow color
// spreadRadius: 1,
// blurRadius: 5,
// offset: const Offset(0, 2), // Shadow position
// ),
// ],
// ),
// child: Icon(
// statusIcons[index],
// color: statusIconColors[index],
// size: 20,
// // color: statusColors[index],
// ),
// ),
// const SizedBox(width: 6),
// Text(statusLabels[index]),
// ],
// ),
//
// ],
// ),
// );
// }),
// ],
// ),
// );
// }
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return PopScope(
canPop: false, // Allow back navigation only if not login screen
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
_handleBackButton(); // Show exit confirmation dialog
},
child: Scaffold(
backgroundColor: const Color(0xFFf5f5f5),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical: 10, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
// child: ConstrainedBox(
// constraints: BoxConstraints(
// minHeight: constraints.maxHeight,
// ),
// ),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// First Row: Toggle Buttons Row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: 36,
decoration: BoxDecoration(
color: const Color(0xFFEAEAEA),
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
toggleChip("Today"),
toggleChip("This Week"),
],
),
),
],
),
const SizedBox(height: 20),
// Second Row: Domestic & International and Status
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// LEFT SIDE
Expanded(
flex: 2,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blueGrey,
blurRadius: 5,
offset: Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(15),
child: Row(
children: [
...typeData.map((item) {
return Expanded(
child: Column(
children: [
buildTopCard(
// icon: item['value'] == "Domestic"
// ? Icons.home
// : Icons.travel_explore,
color:
item['value'] == "Domestic"
? Color(0xFF0DB04B)
: Color(0xFF004A8E),
label: item['value'],
count: item['count'],
bgColor:
item['value'] == "Domestic"
? const Color(0xFFD6FBE4)
: const Color(0xFFD9E8FF),
),
],
),
);
}).toList(),
],
),
),
),
const SizedBox(width: 20),
// RIGHT SIDE
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blueGrey,
blurRadius: 5,
offset: Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Status",
style: GoogleFonts.poppins(
fontSize: 19,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 23),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
...statusData
.map(
(item) => statusCard(
item['value'],
item['count'],
),
)
.toList(),
],
),
],
),
),
),
],
),
const SizedBox(height: 20),
// Third Row: Flight, Accommodation, Forex
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: buildCategoryCard(
"Flight",
Icons.flight,
statusLabels,
flightData,
),
),
const SizedBox(width: 10),
Expanded(
child: buildCategoryCard(
"Accomodation",
Icons.hotel_outlined,
statusLabels,
accommodationData,
),
),
const SizedBox(width: 10),
Expanded(
child: buildCategoryCard(
"Forex",
Icons.attach_money,
statusLabels,
forexData,
),
),
],
),
],
),
);
},
),
),
),
);
},
);
}
}

View File

@ -0,0 +1,483 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../../config/apiUrl.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../widgets/custom_text_forex.dart';
import 'department_list.dart';
class DepartmentData extends StatefulWidget {
final Future<List<dynamic>> Function() fetchGetDepartment;
final bool isDesktop;
final Color? layoutColor;
final int? departmentId; // <-- Add this
final Map<String, dynamic>? departmentData;
const DepartmentData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetDepartment,
this.departmentId,
this.departmentData,
});
@override
DepartmentDataState createState() => DepartmentDataState();
}
class DepartmentDataState extends State<DepartmentData> {
final ApiService apiService = ApiService();
Map<String, dynamic>? apiData;
// final Map<String, FocusNode> focusNodes = {
// "name": FocusNode(),
// "description": FocusNode(),
// };
final Map<String, TextEditingController> controllers = {};
Map<String, String> errorMessages = {};
Map<String, FocusNode> focusNodes = {};
Map<String, bool> focusStates = {};
String? selectedName;
String? selectedDescription;
String? userId;
int? departmentDataId;
late String isActive = "1";
bool isDisable = false;
List<String> dataHeader = ["dropdown_value", "description"];
Map<String, dynamic> departmentDetails() {
final data = {
// "department_id": int.parse(departmentId),
"dropdown_value": controllers["dropdown_value"]?.text,
"description": controllers["description"]?.text,
"created_by": userId,
"is_active": isActive,
};
return data;
}
@override
void initState() {
super.initState();
apiData = null;
for (var field in dataHeader) {
controllers[field] = TextEditingController();
focusNodes["${field}FocusNode"] = FocusNode();
focusStates["${field}Focused"] = false;
}
for (var key in focusNodes.keys) {
_addFocusListener(focusNodes[key]!, (focus) {
setState(() {
focusStates[key.replaceFirst("FocusNode", "Focused")] = focus;
});
});
}
if (widget.departmentId != null) {
print('Editing D ID: ${widget.departmentId}');
updateDepartmentDetails();
}
}
void _clearError() {
setState(() {
errorMessages.clear();
});
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
for (var node in focusNodes.values) {
node.dispose();
}
super.dispose();
}
void _addFocusListener(FocusNode node, Function(bool) updateState) {
node.addListener(() {
setState(() {
updateState(node.hasFocus);
});
});
}
void updateDepartmentDetails() {
print("Inside Update Function - ${widget.departmentData}");
final data = widget.departmentData;
if (data == null) return;
setState(() {
controllers['dropdown_value']?.text = data['dropdown_value'] ?? '';
// controllers['description']?.text = data['description'].toString();
isActive = data["is_active"];
final departmentId = int.tryParse(data['id'].toString());
departmentDataId = departmentId;
});
}
void toggleStatus() {
setState(() {
isActive = isActive == "1" ? "0" : "1";
});
}
bool validateData() {
errorMessages.clear();
final data = {
"dropdown_value": controllers["dropdown_value"]?.text,
// "description": controllers["description"]?.text,
};
final requiredFields = ["dropdown_value"];
bool hasFocused = false;
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field]!.trim().isEmpty) {
errorMessages[field] = "Required";
// if (!hasFocused) {
// focusNodes[field]?.requestFocus();
// hasFocused = true;
// }
}
}
return errorMessages.isEmpty;
}
Future<void> handleSubmit() async {
userId = await getUserId();
setState(() {
isDisable = true;
// This triggers UI rebuild with error messages
if (validateData()) {
postDepartmentData();
} else {
isDisable = false;
}
});
final departmentData1 = departmentDetails();
print("submit data - $departmentData1");
}
Future<void> postDepartmentData({int isActive = 1}) async {
// final remarksData = getData();
final departmentData = departmentDetails();
print("initially value of the Department - $departmentData");
final String apiUrldata;
if (departmentDataId != null) {
print("for edit department id - $departmentDataId");
apiUrldata = '$apiUrl/api/updateDepartment/$departmentDataId';
departmentData["id"] = departmentDataId.toString();
departmentData["updated_by"] = userId;
(departmentData.containsKey("created_by"))
? departmentData.remove("created_by")
: '';
} else {
print("for add Department id - null");
apiUrldata = '$apiUrl/api/createDepartment';
print("called apiUrl - $apiUrldata");
departmentData["created_by"] = userId;
}
print("recently Department data - $departmentData");
final token = await getToken(); // Fetch token
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final uri = Uri.parse(apiUrldata);
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
};
final body = jsonEncode(departmentData);
final response =
departmentDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
print("successfully!");
print("Response: ${response.body}");
_clearError();
widget.fetchGetDepartment();
// dispose();
if (context.mounted) {
Navigator.of(context).pop(); // Close modal only if mounted
}
setState(() {
isDisable = false;
});
} else if (response.statusCode == 404) {
if (context.mounted) {
Navigator.of(context).pop();
}
final message = jsonDecode(response.body)['message'] ?? 'Unknown error';
setState(() {
isDisable = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
),
);
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return null;
// throw Exception('Failed to load users');
} else {
print("Failed to submit. Status: ${response.statusCode}");
print("Error: ${response.body}");
setState(() {
isDisable = false;
});
}
} catch (e) {
print(" Error submitting plan: $e");
setState(() {
isDisable = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Row 1: Title + Edit + Delete buttons
Row(
children: [
Text(
(departmentDataId != null)
? 'Edit Department'
: 'Create Department',
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
),
const Spacer(),
],
),
const SizedBox(height: 2),
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
const SizedBox(height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Name *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["dropdown_valueFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
controller: controllers["dropdown_value"],
focusNode: focusNodes["dropdown_valueFocusNode"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["dropdown_value"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["dropdown_value"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 15),
// Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// "Description *",
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w600,
// color: Color(0xFF575A74),
// ),
// ),
// SizedBox(height: 5),
// CustomTextFieldForexWrapper(
// isFocused: focusStates["descriptionFocused"] ?? false,
// isDesktop: widget.isDesktop,
// color: Colors.transparent,
// child: SizedBox(
// height: 100,
// child: TextField(
// controller: controllers["description"],
// focusNode: focusNodes["descriptionFocusNode"],
// style: const TextStyle(fontSize: 12),
// maxLines: null,
// expands: true,
// decoration: const InputDecoration(
// labelText: "Description",
// labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
// floatingLabelBehavior: FloatingLabelBehavior.never,
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(vertical: 16),
// ),
// ),
// ),
// ),
// if (errorMessages["description"] != null) ...[
// SizedBox(height: 5), // Space before error message
// Text(
// errorMessages["description"]!,
// style: TextStyle(color: Colors.red, fontSize: 12),
// ),
// ],
// ],
// ),
// SizedBox(height: 15),
if (departmentDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
isActive == "1" ? "Active" : "Inactive",
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color: isActive == "1" ? Colors.green : Colors.red,
),
),
),
),
],
),
if (departmentDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// SizedBox(
// child: ElevatedButton(
// onPressed: () {
// // You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: widget.layoutColor,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: Text('Cancel',
// style: GoogleFonts.poppins(
// fontSize: 13, color: Colors.white)),
// ),
// ),
// SizedBox(
// width: 10,
// ),
SizedBox(
child: ElevatedButton(
onPressed:
isDisable
? null
: () async {
setState(() {
isDisable = true;
});
await handleSubmit();
// You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// Optional: re-enable only on error
// setState(() {
// isDisable = false;
// });
},
style: ElevatedButton.styleFrom(
backgroundColor: widget.layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
),
// : SizedBox.shrink(),
],
),
);
}
}

View File

@ -0,0 +1,980 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../utils/pagination.dart';
import '../../widgets/custom_breadcrumb_navigation.dart';
import 'departmentDetails.dart';
class DepartmentList extends StatefulWidget {
const DepartmentList({super.key});
@override
DepartmentListState createState() => DepartmentListState();
}
class DepartmentListState extends State<DepartmentList> {
final GlobalKey<DepartmentListState> departmentListKey =
GlobalKey<DepartmentListState>();
final ApiService apiService = ApiService();
// late Future<List<dynamic>> futureDepartment;
Future<List<dynamic>>? futureDepartment;
late Map<String, dynamic> depSingleData;
String? selectedDepartmentId;
String? orgId;
Color? layoutColor;
Color? bodyColor;
List allDepartment = [];
List filteredDepartment = [];
TextEditingController searchController = TextEditingController();
int currentPage = 0;
int itemsPerPage = 10;
@override
void initState() {
super.initState();
_checkAuthAndLoadData();
// futureDepartment = fetchGetDepartment();
//
// futureDepartment.then((object) {
// setState(() {
// allDepartment = object;
// });
// });
//
// WidgetsBinding.instance.addPostFrameCallback((_) {
// loadInitialData();
// });
// futurePlans = fetchPlans();
}
void _checkAuthAndLoadData() async {
final String? token = await getToken(); // Your async function to get token
if (token == null || token.isEmpty) {
// Token doesn't exist → redirect to login
context.go(
"/",
); // or use: router.go("/") if you're using `GoRouter` directly
return;
}
if (!mounted) return;
try {
futureDepartment = fetchGetDepartment();
final roleUser = await getRoleUser();
if (roleUser != null &&
(roleUser == 'Org Admin' || roleUser == 'Travel Admin')) {
futureDepartment?.then((object) {
setState(() {
allDepartment = object;
});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
loadInitialData();
});
} else {
apiService.logout(context);
}
} catch (e) {
print("group : $e");
}
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor =
bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Future<List<dynamic>> refreshData() {
print("Calling Refresh Data");
futureDepartment = fetchGetDepartment();
return futureDepartment!.then((object) {
print("Calling Refresh Data $object");
setState(() {
allDepartment = object;
filteredDepartment = object;
searchController.text = "";
});
return object;
});
}
Future<List<dynamic>> fetchGetDepartment() async {
// return [];
final String apiUrlData = '$apiUrl/api/getDepartmentList?for=table_view';
final String? token = await getToken();
print("Fetch Department");
print("2KN Here : $token");
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
},
);
print("called api : $apiUrlData");
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['data']; // Returning raw JSON list
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return [];
// throw Exception('Failed to load users');
} else {
throw Exception('Failed to load users');
}
}
void filterDepartment(String query) {
// print("all before filtering: $query");
// final lowerQuery = query.toLowerCase();
// setState(() {
// filteredDepartment = allDepartment.where((object) {
// return (object['department_id']?.toLowerCase().contains(lowerQuery) ??
// false) ||
// (object['description']?.toLowerCase().contains(lowerQuery) ?? false) ||
// (object['user']?.toLowerCase().contains(lowerQuery) ?? false) ||
// (object['is_active']?.toLowerCase().contains(lowerQuery) ?? false);
// }).toList();
// });
// print("filteredPlans: $filteredDepartment");
print("all before filtering: $query");
final lowerQuery = query.toLowerCase();
setState(() {
filteredDepartment =
allDepartment.where((object) {
final isActiveStatus =
object['is_active'] == "1" ? "active" : "inactive";
return (object['id']?.toLowerCase().contains(lowerQuery) ??
false) ||
(object['dropdown_value']?.toLowerCase().contains(lowerQuery) ??
false) ||
// (object['description']?.toLowerCase().contains(lowerQuery) ??
// false) ||
(isActiveStatus.contains(lowerQuery));
}).toList();
currentPage = 0;
});
print("filteredDepartment: $filteredDepartment");
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
backgroundColor: Color(0xFFf5f5f5),
// appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'),
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical:
MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
// const Expanded(child: Center(child: Text("User Page Content"))),
Expanded(child: buildGroupList(isDesktop)),
],
),
),
);
},
);
}
Widget buildGroupList(bool isDesktop) {
return Container(
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
),
// decoration: BoxDecoration(
// // color: Colors.amber,
// // color: bodyColor,
// color: Color(0xFFE1F5FE),
// border: Border.all(
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// width: 3.5)),
child: buildUserTable(isDesktop),
);
}
Widget buildUserTable(bool isDesktop) {
return Container(
// margin: isDesktop
// ? EdgeInsets.all(10.0)
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
// padding: const EdgeInsets.all(10),
height:
isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Divider(
// thickness: 0.2, // how "thick" the line is
// color: Colors.grey, // optional
// ),
Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
child: BreadcrumbNavigation(
isDesktop: isDesktop,
breadcrumbItems: [
BreadcrumbItem(
title: 'Org Settings',
tooltip: 'Go To Organization Settings',
onTap: (context) {
context.go("/OrganizationSettings");
},
),
BreadcrumbItem(title: 'Department Details'),
],
),
),
// Text(
// 'Department Details',
// style: GoogleFonts.poppins(
// fontSize: isDesktop ? 16 : 14,
// fontWeight: FontWeight.w600,
// color: Colors.black,
// ),
// ),
],
),
if (isDesktop)
SizedBox(width: MediaQuery.of(context).size.width * 0.08),
// SizedBox(width: MediaQuery.of(context).size.width * 0.16),
if (isDesktop)
Container(
width: MediaQuery.of(context).size.width * 0.2,
height: 40,
child: TextField(
controller: searchController,
onChanged: filterDepartment,
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: TextStyle(
fontSize: 12,
color: Color(0xFF9E9DBD),
),
prefixIcon: Icon(
Icons.search,
color: Color(0xFF9E9DBD),
size: 18,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
style: GoogleFonts.poppins(fontSize: 12),
),
),
// SizedBox(width: 16),
Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF114D8B),
foregroundColor: Colors.white,
disabledBackgroundColor: Color(0xFF114D8B),
disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Color(0xFF114D8B), width: 2),
),
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
),
onPressed: () async {
showDialog(
context: context,
builder:
(context) => DepartmentData(
isDesktop: isDesktop,
layoutColor: layoutColor!,
fetchGetDepartment: refreshData,
// role:
// "Travel Agent"
),
);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
children: [
Text(
"Add Department",
style: GoogleFonts.poppins(
fontSize: isDesktop ? 13 : 11,
),
),
SizedBox(width: 8), // spacing between icon and text
Icon(
Icons.add_circle_outline_rounded,
size: 15,
color: Colors.white,
),
],
),
),
],
),
if (!isDesktop) SizedBox(height: 5),
isDesktop
? SizedBox.shrink()
: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.8,
height: 35,
child: TextField(
controller: searchController,
onChanged: filterDepartment,
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: TextStyle(
fontSize: 12,
color: Color(0xFF9E9DBD),
),
prefixIcon: Icon(
Icons.search,
color: Color(0xFF9E9DBD),
size: 18,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade200,
width: 0.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
style: GoogleFonts.poppins(fontSize: 12),
),
),
// SizedBox(width: 16),
],
),
const SizedBox(height: 10),
FutureBuilder<List<dynamic>>(
future: futureDepartment,
builder: (context, snapshot) {
final adjHgt = MediaQuery.of(context).size.height;
if (futureDepartment == null) {
CircularProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError ||
!snapshot.hasData ||
snapshot.data!.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// const Icon(Icons.error_outline,
// color: Colors.redAccent, size: 60),
// const SizedBox(height: 16),
// Text(
// "Oops!",
// style: GoogleFonts.poppins(
// fontSize: 20,
// fontWeight: FontWeight.bold,
// color: Colors.redAccent),
// ),
SizedBox(height: adjHgt / 4),
Text(
"No Data Found ",
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const SizedBox(height: 20),
// Text(
// "Please Create Department Details",
// textAlign: TextAlign.center,
// style: GoogleFonts.poppins(
// fontSize: 16,
// color: Colors.grey,
// ),
// ),
// const SizedBox(height: 20),
],
),
),
);
}
/* Here collect the list to displayed the data in table or card Used */
List<dynamic> object =
filteredDepartment.isNotEmpty
? filteredDepartment
: allDepartment;
/* List is Sorting here */
object.sort((a, b) {
DateTime dateA = DateTime.parse(a['created_on']);
DateTime dateB = DateTime.parse(b['created_on']);
return dateB.compareTo(dateA); // Descending: newest first
});
/* For pagination for list ... */
List paginatedDepartment =
object
.skip(currentPage * itemsPerPage)
.take(itemsPerPage)
.toList();
/* Table ... */
Widget table = LayoutBuilder(
builder: (context, constraints) {
double minWidth = isDesktop ? constraints.maxWidth : 1300;
return ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: DataTable(
dividerThickness: 0.5,
columnSpacing: isDesktop ? 24.0 : 16.0,
border: TableBorder(
horizontalInside: BorderSide(
width: 0.5,
color: Colors.grey.shade200,
),
),
columns: [
DataColumn(
label: Text(
'Name',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
// DataColumn(
// label: Text(
// 'Description',
// style: GoogleFonts.poppins(
// fontSize: 13,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
DataColumn(
label: Text(
'Status',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
DataColumn(
label: Text(
'Actions',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
rows:
paginatedDepartment.map((tableObject) {
String departmentId =
tableObject['id'].toString(); // Get user ID
bool isSelected =
selectedDepartmentId == departmentId;
return DataRow(
cells: [
DataCell(
Text(
tableObject['dropdown_value'] ?? '',
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
),
),
),
// DataCell(
// Text(
// tableObject['description'] ?? 'N/A',
// style: TextStyle(
// fontSize: 13,
// fontFamily: "Inter",
// ),
// softWrap: true,
// overflow: TextOverflow.ellipsis,
// ),
// ),
DataCell(
Text(
tableObject['is_active'] == "1"
? 'Active'
: 'Inactive',
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color:
tableObject['is_active'] == "1"
? Colors.green
: Colors.grey,
),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
),
DataCell(
// UserActionsMenu(
// user: forex,
// getUserDetails: (id) =>
// apiService.getSingleUser(id),
// ),
GestureDetector(
child: Tooltip(
message: 'Edit Department Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
),
),
onTap: () async {
// final userId = getUserId(user['user_id']);
// final usersData = await getUserDetails(userId);
//
final departmentId = int.tryParse(
tableObject['id'].toString(),
);
if (departmentId != null) {
print(
"Table cell - department Id -- $departmentId",
);
final data = await apiService
.getDepartmentDetailsFind(
context,
departmentId,
);
print("DepartmentId -- $data");
showDialog(
context: context,
builder:
(context) => DepartmentData(
isDesktop: isDesktop,
departmentId:
departmentId, // Pass the ID
departmentData: data,
layoutColor: layoutColor!,
// fetchGetForex: fetchGetForex,
fetchGetDepartment:
refreshData,
// role:
// "Travel Agent"
),
);
} else {
print("Invalid ID");
}
},
),
),
],
);
}).toList(),
),
);
},
);
/* Card ... */
Widget buildMobileCardView(List<dynamic> paginatedUser) {
return ListView.builder(
itemCount: paginatedUser.length,
itemBuilder: (context, index) {
final cardObject = paginatedUser[index];
return Card(
color: Colors.white,
margin: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status and Employee Code
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
cardObject['dropdown_value'] ?? 'N/A',
style: GoogleFonts.poppins(
fontSize: 10,
color: Colors.black87,
fontWeight: FontWeight.w700,
),
),
GestureDetector(
child: Tooltip(
message: 'Edit Department Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
),
),
onTap: () async {
// final userId = getUserId(user['user_id']);
// final usersData = await getUserDetails(userId);
//
final departmentId = int.tryParse(
cardObject['id'].toString(),
);
if (departmentId != null) {
print(
"departmentId -- $departmentId",
);
final data = await apiService
.getDepartmentDetailsFind(
context,
departmentId,
);
print("DepartmentId -- $data");
showDialog(
context: context,
builder:
(context) => DepartmentData(
isDesktop: isDesktop,
departmentId:
departmentId, // Pass the ID
departmentData: data,
layoutColor: layoutColor!,
// fetchGetDepartment: fetchGetDepartment,
fetchGetDepartment:
refreshData,
// role:
// "Travel Agent"
),
);
} else {
print("Invalid ID");
}
},
),
// PopupMenuButton<int>(
// color: Colors.white,
// padding: EdgeInsets.zero,
// offset: Offset(0, 30),
// icon: Icon(
// Icons.more_vert,
// color: Color(0xFF475569),
// size: 14,
// ),
// itemBuilder: (context) => [
// CustomPopupMenuEntry(
// child: Container(
// padding: EdgeInsets.symmetric(
// horizontal: 8, vertical: 8),
// child: Row(
// mainAxisSize:
// MainAxisSize.min,
// mainAxisAlignment:
// MainAxisAlignment.center,
// children: [
// IconButton(
// icon: Icon(
// Icons
// .remove_red_eye,
// color: Color(
// 0xFF475569),
// size: 18),
// onPressed: () {
// print(
// "USerDAta - $user");
// // dynamic usersData = apiService
// // .getSingleUser(user[
// // 'user_id']
// // is String
// // ? int.parse(user[
// // 'user_id'])
// // : user[
// // 'user_id']);
// //
// // print(
// // "USerDAta - $usersData");
//
// context.go(
// "/CreateUserDetails",
// extra: {
// "selectedUser":
// user,
// "isViewMode": true
// },
// );
// }),
// IconButton(
// icon: Image.asset(
// 'assets/images/IconsImg/edit.png',
// width: 20,
// height: 15),
// onPressed: () {
// context.go(
// "/CreateUserDetails",
// extra: {
// "selectedUser":
// user,
// "isViewMode": false
// },
// );
// },
// ),
// ],
// ),
// ),
// ),
// ],
// ),
],
),
SizedBox(height: 2),
// Trip Id and Trip Name
// Name
// Row(
// children: [
// Column(
// crossAxisAlignment:
// CrossAxisAlignment.start,
// children: [
// Text(
// cardObject['description'] ?? '',
// style: GoogleFonts.poppins(
// fontSize: 10,
// color: Colors.black87,
// ),
// ),
// ],
// ),
// SizedBox(width: 10),
// Column(
// crossAxisAlignment:
// CrossAxisAlignment.start,
// children: [
// Text(
// cardObject['description'] ?? '',
// style: GoogleFonts.poppins(
// fontSize: 10,
// color: Colors.black87,
// ),
// ),
// ],
// ),
// ],
// ),
// Actions
// Actions
],
),
),
);
},
);
}
return Expanded(
child: Column(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child:
isDesktop
? (searchController.text.isNotEmpty &&
filteredDepartment.isEmpty
? Center(
child: Text(
"No Matches Found",
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey,
),
),
)
: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: table,
))
: (searchController.text.isNotEmpty &&
filteredDepartment.isEmpty
? Center(
child: Text(
"No Matches Found",
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.grey,
),
),
)
: buildMobileCardView(
paginatedDepartment,
)),
),
// Expanded(
// child: isDesktop
// ? SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: table, // <-- your existing table
// )
// : buildMobileCardView(paginatedDepartment),
// ),
PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
totalItems: object.length,
activeColor: layoutColor, // your theme color
onPageChanged: (page) {
setState(() {
currentPage = page;
});
},
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 0;
});
},
),
],
),
);
},
),
],
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,782 @@
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:frontend/Screens/organization/mailSettings.dart';
import 'package:frontend/Screens/organization/themeColor.dart';
import 'package:frontend/utils/auth_utils.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:responsive_builder/responsive_builder.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../widgets/custom_text_field.dart';
import '../../widgets/custom_user_form.dart';
import 'groupList.dart';
class Group extends StatefulWidget {
final Map<String, dynamic>? group;
const Group({Key? key, required this.group}) : super(key: key);
static Group fromState(GoRouterState state) {
return Group(group: state.extra as Map<String, dynamic>?);
}
@override
_groupState createState() => _groupState();
}
class _groupState extends State<Group> {
final ApiService apiService = ApiService();
Color? layoutColor;
Color? bodyColor;
String? orgId;
String? userId;
String? selectedGroupId;
String? selectedDomestic;
String? selectedInternational;
List<dynamic>? apiAllPolicy;
List<dynamic>? apiForDomestic;
List<dynamic>? apiForInternational;
Map<String, String> errorMessages = {};
final Map<String, TextEditingController> controllers = {};
Map<String, FocusNode> focusNodes = {};
Map<String, bool> focusStates = {};
List<String> dataHeader = ["name", "description"];
Map<String, dynamic> get groupData {
final data = {
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"domestic_policy_id": selectedDomestic,
"international_policy_id": selectedInternational,
"org_id": orgId,
"created_by": userId,
};
// Only add group_id if it's an edit operation
if (widget.group != null && widget.group!.containsKey('group_id')) {
data["group_id"] = selectedGroupId;
}
return data;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadAllServices();
for (var field in dataHeader) {
controllers[field] = TextEditingController();
focusNodes["${field}FocusNode"] = FocusNode();
focusStates["${field}Focused"] = false;
}
print("Focus Nodes KeysII: ${focusNodes.keys.toList()}");
print("Focus States KeysII: ${focusStates.keys.toList()}");
print("Text Controllers KeysII: ${controllers.keys.toList()}");
updateData();
loadInitialData();
});
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor =
bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
Future<void> updateData() async {
// Ensure apiselectedUser is not null before printing
if (widget.group != null) {
print("API Selected User Has Data - ${widget.group}");
setState(() {
// Wrap in setState to update the UI
selectedGroupId = widget.group?["group_id"] ?? "";
controllers["name"]?.text = widget.group?["name"] ?? "";
controllers["description"]?.text = widget.group?["description"] ?? "";
if (widget.group?["domestic_policy_id"] != null) {
selectedDomestic = widget.group!["domestic_policy_id"].toString();
}
if (widget.group?["international_policy_id"] != null) {
selectedInternational =
widget.group!["international_policy_id"].toString();
}
});
} else {
print("API Selected User Has Data - No data available yet");
}
}
Future<void> loadAllServices() async {
try {
final result = await apiService.fetchAllPolicy(context);
orgId = await getOrgId();
userId = await getUserId();
final roleUser = await getRoleUser();
if (roleUser != null &&
(roleUser == 'Org Admin' || roleUser == 'Travel Admin')) {
setState(() {
apiAllPolicy = result;
apiForDomestic =
result.where((policy) => policy["domestic"] == "1").toList();
apiForInternational =
result.where((policy) => policy["international"] == "1").toList();
});
print("Fetched services: $apiAllPolicy");
print("Domestic policies: $apiForDomestic");
print("International policies: $apiForInternational");
} else {
apiService.logout(context);
}
} catch (e) {
print('Error fetching role list: $e');
}
}
bool isValidData(Map<String, dynamic> data) {
errorMessages.clear(); // Reset errors
// Required fields that must not be empty
List<String> requiredFields = ["name", "description"];
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field].toString().trim().isEmpty) {
errorMessages[field] = "Required";
}
}
// At least one of the two policies must be selected
if ((selectedDomestic == null || selectedDomestic!.isEmpty) &&
(selectedInternational == null || selectedInternational!.isEmpty)) {
errorMessages["domestic_policy_id"] = "Select at least one policy";
errorMessages["international_policy_id"] = "Select at least one policy";
}
return errorMessages.isEmpty;
return errorMessages.isEmpty; // Valid if there are no errors
}
void _clearError(String field) {
if (mounted && errorMessages.containsKey(field)) {
setState(() {
errorMessages.remove(field);
});
}
}
void handleSubmit() {
print("HandleSubmiy - $groupData");
setState(() {
if (!isValidData(groupData)) {
print("USERDETAILS : $groupData");
print("Validation Failed: Required fields are missing.");
return; // Stop execution if validation fails
} else {
print("USERDETAILS : $groupData");
postGroupData(groupData);
}
});
}
Future<void> postGroupData(Map<String, dynamic> groupData) async {
final token = await getToken(); // Fetch token
final bool isEdit = widget.group != null && widget.group!.isNotEmpty;
print("isEdit: $isEdit");
print("postGroupData- ${widget.group?['group_id']}");
String apiUrlData;
if (isEdit) {
if (selectedGroupId == null) {
print("selectedGroupId is null. Cannot proceed.");
return;
}
final int? groupId = int.tryParse(selectedGroupId!);
if (groupId == null) {
print("groupId is invalid. selectedGroupId = $selectedGroupId");
return;
}
apiUrlData = '$apiUrl/api/groups/update/$groupId';
} else {
apiUrlData = '$apiUrl/api/groups/create';
}
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final response =
await (isEdit
? http.put(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
},
body: jsonEncode(groupData),
)
: http.post(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
},
body: jsonEncode(groupData),
));
if (response.statusCode == 200 || response.statusCode == 201) {
print("GRPDATA");
print("Group submitted successfully!");
print("Response: ${response.body}");
await apiService.handleTokenRefresh(context, userId!);
context.go('/group');
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return null;
// throw Exception('Failed to load users');
} else {
print("Failed to submit group. Status: ${response.statusCode}");
print("Error: ${response.body}");
}
} catch (e) {
print("Error submitting group: $e");
}
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
// backgroundColor: Colors.white,
backgroundColor: Color(0xFFf5f5f5),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical:
MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(8),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(child: buildData(isDesktop, context)),
],
),
),
);
},
);
}
Widget buildData(bool isDesktop, context) {
return Container(
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
),
child: Column(
children: [
Expanded(
child: Container(
// color: bodyColor,
// color: Color(0xFFE1F5FE),
child: buildOrganizationLayout(isDesktop),
),
),
Container(
color: Colors.white,
padding: const EdgeInsets.all(8.0),
child:
isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _buildSubmit(isDesktop),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildSubmit(isDesktop),
),
),
],
),
);
}
Widget buildOrganizationLayout(isDesktop) {
return Container(
// margin: isDesktop
// ? EdgeInsets.all(10.0)
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
height:
isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
// decoration: BoxDecoration(
// border: isDesktop
// ? Border.all(
// width: 2,
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// )
// : null,
// color: Colors.white,
//
// // color: Color(0xFFF7F7FB),
//
// // color: Colors.amber,
// ),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// color: Colors.white,
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Create Group", style: GoogleFonts.poppins(fontSize: 18)),
// Container(
// color: Color(0xFFE9EBF6),
// child: IconButton(
// icon: Icon(
// Icons.close,
// ),
// onPressed: () {
// context.go('/group');
// },
// ),
// )
],
),
),
// SizedBox(
// height: 5,
// ),
Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(20),
color: Colors.white,
height: MediaQuery.of(context).size.height * 0.6,
child: Column(
children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "Mail Settings",
// style: TextStyle(color: Colors.blueAccent),
// ),
// Icon(
// Icons.keyboard_arrow_down_outlined,
// color: Colors.blueAccent,
// size: 30,
// ),
// ],
// ),
isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildFirstRow(isDesktop),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildFirstRow(isDesktop),
),
SizedBox(height: 10),
isDesktop
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildSecondRow(isDesktop),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildSecondRow(isDesktop),
),
// SizedBox(height: 15),
],
),
),
],
),
);
}
List<Widget> _buildFirstRow(bool isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Group Name",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false,
isDesktop: isDesktop,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.85,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["name"],
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
style: TextStyle(fontSize: 12),
onChanged: (value) {
_clearError("name");
},
decoration: InputDecoration(
labelText: "group name",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["name"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["name"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Description",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false,
isDesktop: isDesktop,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.85,
child: SizedBox(
height: 40,
child: TextField(
controller: controllers["description"],
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
onChanged: (value) {
_clearError("description");
},
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: "description",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["description"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["description"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
];
}
List<Widget> _buildSecondRow(bool isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select Policy For Domestic",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false,
isDesktop: isDesktop,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.85,
child: SizedBox(
height: 40,
child:
apiForDomestic == null
? Center(
child: Transform.scale(
scale: 0.5,
child: CircularProgressIndicator(),
),
)
: DropdownSearch<String>(
selectedItem:
selectedDomestic == null
? null
: apiForDomestic!
.firstWhere(
(policy) =>
policy['policy_id'] ==
selectedDomestic,
)['name']
.toString(),
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search Policy...",
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
),
),
),
),
items:
apiForDomestic!
.map((policy) => policy['name'].toString())
.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
setState(() {
selectedDomestic =
apiForDomestic!.firstWhere(
(policy) => policy['name'] == newValue,
)['policy_id'];
});
},
),
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select Policy For International",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: false,
isDesktop: isDesktop,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: MediaQuery.of(context).size.width * 0.85,
child: SizedBox(
height: 40,
child:
apiForInternational == null
? Center(
child: Transform.scale(
scale: 0.5,
child: CircularProgressIndicator(),
),
)
: DropdownSearch<String>(
selectedItem:
selectedInternational == null
? null
: apiForInternational!
.firstWhere(
(policy) =>
policy['policy_id'] ==
selectedInternational,
)['name']
.toString(),
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search Policy...",
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
),
),
),
),
items:
apiForInternational!
.map((policy) => policy['name'].toString())
.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
setState(() {
selectedInternational =
apiForInternational!.firstWhere(
(policy) => policy['name'] == newValue,
)['policy_id'];
});
},
),
),
),
],
),
];
}
List<Widget> _buildSubmit(isDesktop) {
return [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: layoutColor ?? Colors.green, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
context.go('/group');
},
child: Text("Cancel"),
),
SizedBox(width: 20),
MouseRegion(
// cursor: widget.isViewMode
// ? SystemMouseCursors.forbidden
// : SystemMouseCursors.click,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: layoutColor, // Keep original color
foregroundColor: Colors.white, // Keep original color
// disabledBackgroundColor:
// Colors.blueAccent, // Ensure color remains when disabled
// disabledForegroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
// side: BorderSide(color: Colors.blueAccent, width: 2),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: handleSubmit, // Disable when in view mode
child: Text("Submit"),
),
),
];
}
}

View File

@ -0,0 +1,917 @@
import 'dart:convert';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../../config/apiUrl.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../widgets/custom_text_forex.dart';
import 'groupList.dart';
class GroupData extends StatefulWidget {
final Future<void> Function() fetchGetGroup;
final bool isDesktop;
final Color? layoutColor;
final int? groupId; // <-- Add this
final Map<String, dynamic>? groupData;
const GroupData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetGroup,
this.groupId,
this.groupData,
});
@override
GroupDataState createState() => GroupDataState();
}
class GroupDataState extends State<GroupData> {
final ApiService apiService = ApiService();
Map<String, String> DomesticMap = {};
Map<String, String> InternationalMap = {};
late List<dynamic>? apiForDomestic;
late List<dynamic>? apiForInternational;
Map<String, dynamic>? apiData;
final Map<String, TextEditingController> controllers = {};
Map<String, FocusNode> focusNodes = {};
Map<String, bool> focusStates = {};
Map<String, String> errorMessages = {};
List<dynamic> domesticList = [];
List<dynamic> internationalList = [];
String? selectedName;
String? selectedDescription;
String? selectedInternationalPolicyID;
String? selectedInternationalPolicyName;
String? selectedDomesticPolicyID;
String? selectedDomesticPolicyName;
String? userId;
int? groupDataId;
late String isActive = "1";
bool isDisable = false;
List<String> dataHeader = [
"name",
"description",
"domestic_policy_id",
"domestic_policy_name",
"international_policy_id",
"international_policy_name",
];
Map<String, dynamic> group_Detials() {
final data = {
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"domestic_policy_id": selectedDomesticPolicyID,
"international_policy_id": selectedInternationalPolicyID,
"domestic_policy_name": selectedDomesticPolicyName,
"international_policy_name": selectedInternationalPolicyName,
"is_active": isActive,
};
return data;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadAllServices();
});
apiForDomestic = null;
apiForInternational = null;
apiData = null;
// for (var field in dataHeader) {
// controllers[field] = TextEditingController();
// }
for (var field in dataHeader) {
controllers[field] = TextEditingController();
focusNodes["${field}FocusNode"] = FocusNode();
focusStates["${field}Focused"] = false;
}
print("Focus Nodes KeysII: ${focusNodes.keys.toList()}");
print("Focus States KeysII: ${focusStates.keys.toList()}");
print("Text Controllers KeysII: ${controllers.keys.toList()}");
for (var key in focusNodes.keys) {
_addFocusListener(focusNodes[key]!, (focus) {
setState(() {
focusStates[key.replaceFirst("FocusNode", "Focused")] = focus;
});
});
}
if (widget.groupId != null) {
print('Editing group ID: ${widget.groupId}');
updateGroupDetails();
}
_clearError();
}
void _clearError() {
setState(() {
errorMessages.clear();
});
}
@override
void dispose() {
for (var node in focusNodes.values) {
node.dispose();
}
for (var controller in controllers.values) {
controller.dispose();
}
super.dispose();
}
void _addFocusListener(FocusNode node, Function(bool) updateState) {
node.addListener(() {
setState(() {
updateState(node.hasFocus);
});
});
}
void updateGroupDetails() {
print("Update - ${widget.groupData}");
final data = widget.groupData;
if (data == null) return;
setState(() {
selectedDomesticPolicyID = data['domestic_policy_id']; // For dropdown
selectedDomesticPolicyName = "";
selectedInternationalPolicyID = data['international_policy_id'];
selectedInternationalPolicyName = ""; // Optional if used elsewhere
controllers['name']?.text = data['name'] ?? '';
controllers['description']?.text = data['description'] ?? '';
isActive = data["is_active"];
final groupId = int.tryParse(data['group_id'].toString());
groupDataId = groupId;
});
}
Future<void> loadAllServices() async {
try {
final result = await apiService.fetchAllPolicy(context);
userId = await getUserId();
final roleUser = await getRoleUser();
if (roleUser != null &&
(roleUser == 'Org Admin' || roleUser == 'Travel Admin')) {
setState(() {
apiForDomestic =
result.where((policy) => policy["domestic"] == "1").toList();
apiForInternational =
result.where((policy) => policy["international"] == "1").toList();
});
print("Domestic policies: $apiForDomestic");
print("International policies: $apiForInternational");
} else {
apiService.logout(context);
}
} catch (e) {
print('Error fetching role list: $e');
}
}
void toggleStatus() {
setState(() {
isActive = isActive == "1" ? "0" : "1";
});
}
bool validateData() {
errorMessages.clear();
final data = {
"name": controllers["name"]?.text,
"description": controllers["description"]?.text,
"domestic_policy_id": selectedDomesticPolicyID,
"international_policy_id": selectedInternationalPolicyID,
"domestic_policy_name": selectedDomesticPolicyName,
"international_policy_name": selectedInternationalPolicyName,
};
final requiredFields = ["name", "description"];
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field].toString().trim().isEmpty) {
errorMessages[field] = "Required";
}
}
return errorMessages.isEmpty;
}
Future<void> handleSubmit() async {
userId = await getUserId();
setState(() {
isDisable = true;
// This triggers UI rebuild with error messages
if (validateData()) {
postGroupData();
} else {
isDisable = false;
}
});
final groupData1 = group_Detials();
print("GroupDAta - $groupData1");
}
Future<void> postGroupData({int isActive = 1}) async {
final orgId = await getOrgId();
final groupData = group_Detials();
final String apiUrlData;
// static here
groupData["org_id"] = orgId;
// dynamic here
if (groupDataId != null) {
apiUrlData = '$apiUrl/api/groups/update/$groupDataId';
groupData["group_id"] = groupDataId.toString();
groupData["updated_by"] = userId;
} else {
apiUrlData = '$apiUrl/api/groups/create';
groupData["created_by"] = userId;
}
final token = await getToken(); // Fetch token
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final uri = Uri.parse(apiUrlData);
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
};
final body = jsonEncode(groupData);
final response =
groupDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
print("Group Details Created successfully!");
print("Response: ${response.body}");
// _clearError();
_clearError();
await apiService.handleTokenRefresh(context, userId!);
await widget.fetchGetGroup();
if (context.mounted) {
Navigator.of(context).pop(); // Close modal only if mounted
}
setState(() {
isDisable = false;
});
// dispose();
} else if (response.statusCode == 404) {
if (context.mounted) {
Navigator.of(context).pop();
}
setState(() {
isDisable = false;
});
final message = jsonDecode(response.body)['message'] ?? 'Unknown error';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
),
);
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return null;
// throw Exception('Failed to load users');
} else {
print("Failed to submit plan. Status: ${response.statusCode}");
print("Error: ${response.body}");
final message = jsonDecode(response.body);
final errorMessage =
message['messages']?['error'] ?? 'Unknown error occurred';
if (errorMessage.contains("Duplicate entry")) {
_clearError();
await widget.fetchGetGroup();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Group Already Exists!"),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
),
);
}
setState(() {
isDisable = false;
});
}
} catch (e) {
print(" Error submitting plan: $e");
setState(() {
isDisable = false;
});
}
}
@override
Widget build(BuildContext context) {
late Map<String, String> DomesticMap;
late List<String> DomesticID;
late Map<String, String> InternationalMap;
late List<String> InternationalID;
domesticList = apiForDomestic ?? [];
internationalList = apiForInternational ?? [];
// print("domesticListFromAPI--$domesticList");
// Map id to names
DomesticMap = {
for (var object in domesticList)
object['policy_id'] as String: object['name'] as String,
};
// print("domestic -- map--$DomesticMap");
// Extract only id for processing
DomesticID = DomesticMap.keys.toList();
// print("domestic -- id--$DomesticID");
// print("domestic -- selected --$selectedDomesticPolicyID");
InternationalMap = {
for (var item in internationalList)
item['policy_id'] as String: item['name'] as String,
};
// Extract only id for processing
InternationalID = InternationalMap.keys.toList();
selectedInternationalPolicyID ??= null;
return AlertDialog(
backgroundColor: Colors.white,
// contentPadding: const EdgeInsets.fromLTRB(44, 40, 44, 40),
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Row 1: Title + Edit + Delete buttons
Row(
children: [
Text(
(groupDataId != null) ? 'Edit Group' : 'Create Group',
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
),
const Spacer(),
],
),
const SizedBox(height: 2),
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
const SizedBox(height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Name *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
// isFocused: false,
isFocused: focusStates["nameFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
focusNode: focusNodes["nameFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
controller: controllers["name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["name"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["name"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// "Select Policy For Domestic",
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w600,
// color: Color(0xFF575A74),
// ),
// ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Select Policy For Domestic",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
IconButton(
icon: Icon(
Icons.remove_circle_sharp,
size: 12,
color: Colors.redAccent,
),
tooltip: "Reset",
onPressed: () {
setState(() {
selectedDomesticPolicyID = null;
selectedDomesticPolicyName = null;
});
},
),
],
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["domestic_policy_nameFocused"] ?? false,
padding: const EdgeInsets.symmetric(horizontal: 0),
isDesktop: widget.isDesktop,
child: SizedBox(
height: 40,
child: Focus(
focusNode: focusNodes["domestic_policy_nameFocusNode"],
onFocusChange: (hasFocus) {
setState(() {
focusStates["domestic_policy_nameFocused"] = hasFocus;
});
},
child: GestureDetector(
//
onTap: () {
// Request focus when user taps
focusNodes["domestic_policy_nameFocusNode"]
?.requestFocus();
},
child: DropdownSearch<String>(
selectedItem: DomesticMap[selectedDomesticPolicyID],
popupProps: PopupProps.menu(
showSearchBox: true, // Enables search functionality
menuProps: const MenuProps(
backgroundColor: Colors.white,
),
constraints: BoxConstraints(maxHeight: 250),
itemBuilder:
(context, object, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 6.0,
),
child: Text(
object,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: GoogleFonts.poppins(fontSize: 11),
contentPadding: EdgeInsets.symmetric(
horizontal: 4,
),
),
),
),
items: DomesticMap.values.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
// border: InputBorder.none,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color:
(focusStates["domestic_policy_nameFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
// width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
(focusStates["domestic_policy_nameFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
// : const Color(0xFFD6D5E6),
// width: 0.5,
// const Color(0xFFD6D5E6),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: widget.layoutColor!,
width: 1,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 8.0,
),
// contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select ",
style: GoogleFonts.poppins(fontSize: 11),
),
),
onChanged: (String? newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedDomesticPolicyID =
DomesticMap.entries
.firstWhere(
(entry) => entry.value == newValue,
)
.key;
selectedDomesticPolicyName = newValue;
});
},
),
),
),
),
),
],
),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Select Policy For International",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
IconButton(
icon: Icon(
Icons.remove_circle_sharp,
size: 12,
color: Colors.redAccent,
),
tooltip: "Reset",
onPressed: () {
setState(() {
selectedInternationalPolicyID = null;
selectedInternationalPolicyName = null;
});
},
),
],
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused:
focusStates["international_policy_nameFocused"] ?? false,
padding: const EdgeInsets.symmetric(horizontal: 0),
isDesktop: widget.isDesktop,
child: SizedBox(
height: 40,
child: Focus(
focusNode: focusNodes["international_policy_nameFocusNode"],
onFocusChange: (hasFocus) {
setState(() {
focusStates["international_policy_nameFocused"] =
hasFocus;
});
},
child: GestureDetector(
onTap: () {
// Request focus when user taps
focusNodes["international_policy_nameFocusNode"]
?.requestFocus();
},
child: DropdownSearch<String>(
selectedItem:
InternationalMap[selectedInternationalPolicyID],
popupProps: PopupProps.menu(
showSearchBox: true, // Enables search functionality
menuProps: const MenuProps(
backgroundColor: Colors.white,
),
constraints: BoxConstraints(maxHeight: 250),
itemBuilder:
(context, item, isSelected) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 6.0,
),
child: Text(
item,
style: GoogleFonts.poppins(fontSize: 11.5),
),
),
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: GoogleFonts.poppins(fontSize: 11),
contentPadding: EdgeInsets.symmetric(
horizontal: 4,
),
),
),
),
items: InternationalMap.values.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
// border: InputBorder.none,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color:
(focusStates["international_policy_nameFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
(focusStates["international_policy_nameFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
// : const Color(0xFFD6D5E6),
width: 0.5,
// const Color(0xFFD6D5E6),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: widget.layoutColor!,
width: 1,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 8.0,
),
// contentPadding: EdgeInsets.symmetric(horizontal: 1),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: GoogleFonts.poppins(fontSize: 11),
),
),
onChanged: (String? newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedInternationalPolicyID =
InternationalMap.entries
.firstWhere(
(entry) => entry.value == newValue,
)
.key;
selectedInternationalPolicyName = newValue;
});
},
),
),
),
),
),
],
),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Description *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["descriptionFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 100,
child: TextField(
controller: controllers["description"],
focusNode: focusNodes["descriptionFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
style: const TextStyle(fontSize: 12),
maxLines: null,
expands: true,
decoration: const InputDecoration(
labelText: "Description",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["description"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["description"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 15),
if (groupDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
isActive == "1" ? "Active" : "Inactive",
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color: isActive == "1" ? Colors.green : Colors.red,
),
),
),
),
],
),
if (groupDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// SizedBox(
// child: ElevatedButton(
// onPressed: () {
// // You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: widget.layoutColor,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: Text('Cancel',
// style: GoogleFonts.poppins(
// fontSize: 13, color: Colors.white)),
// ),
// ),
// SizedBox(
// width: 10,
// ),
SizedBox(
child: ElevatedButton(
// onPressed: () {
// handleSubmit();
// // You can get text from commentController.text
// // Navigator.of(context).pop(); // Close the modal
// },
onPressed:
isDisable
? null
: () async {
setState(() {
isDisable = true;
});
await handleSubmit();
// You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// Optional: re-enable only on error
// setState(() {
// isDisable = false;
// });
},
style: ElevatedButton.styleFrom(
backgroundColor: widget.layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
),
// : SizedBox.shrink(),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,472 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:frontend/Screens/group/group.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:responsive_builder/responsive_builder.dart';
import '../../config/apiUrl.dart';
import '../../routes/custom_appBar.dart';
import '../../routes/custom_drawer.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import 'groupDetails.dart';
class GroupListBackUp extends StatefulWidget {
@override
_GroupListBackUpState createState() => _GroupListBackUpState();
}
class _GroupListBackUpState extends State<GroupListBackUp> {
final ApiService apiService = ApiService();
List<dynamic>? apiAllGroups;
Color? layoutColor;
Color? bodyColor;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadAllGroups();
loadInitialData();
});
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
bodyColor =
bodyStringColor != null
? Color(int.parse(bodyStringColor))
: Colors.white;
});
}
Future<void> loadAllGroups() async {
try {
final result = await apiService.fetchAllGroup(context);
setState(() {
apiAllGroups = result;
});
print("Fetched services: $apiAllGroups");
} catch (e) {
print('Error fetching role list: $e');
}
}
void handleActiveStatus(
Map<String, dynamic> groupData,
String groupId,
String currentStatus,
) async {
print("Toggling user status - $groupId (Current: $currentStatus)");
final String apiUrlData =
'$apiUrl/api/groups/update/$groupId'; // API for updating user
final String? token = await getToken();
if (token == null) {
print("Error: Token not found");
return;
}
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
String newStatus = (currentStatus == "1") ? "0" : "1";
print("STatus 1 - $newStatus");
try {
final response = await http.put(
Uri.parse(apiUrlData),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
},
body: jsonEncode({
"is_active": newStatus, // Set new status dynamically
}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
print("User status updated successfully to $newStatus!");
loadAllGroups(); // Refresh users list after update
} else {
print("Failed to update user status. Status: ${response.statusCode}");
print("Error: ${response.body}");
}
} catch (e) {
print("Error updating user status: $e");
}
}
void deleteGroup(Map<String, dynamic> groupdata, groupId, status) {
print("GroupId : $groupId");
print("Groupstatus: $status");
print("GroupsData: $groupdata");
// handleActiveStatus(groupdata, groupId, status);
print("Calling handleActiveStatus with: id=$groupId, status=$status");
handleActiveStatus(groupdata, groupId.toString(), status.toString());
}
Future<void> refreshData() async {
loadAllGroups();
}
// Future<void> deleteGroupFromApi(int groupId) async {
// try {
// await apiService.deleteGroup(groupId); // your delete API call
// deleteGroup(groupId); // remove from UI list
// } catch (e) {
// print('Error deleting group: $e');
// }
// }'
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Scaffold(
// backgroundColor: Colors.white,
backgroundColor: Color(0xFFf5f5f5),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical:
MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(8),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(child: buildGroupListLayout(isDesktop)),
],
),
),
);
},
);
}
Widget buildGroupListLayout(bool isDesktop) {
return Container(
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
),
// decoration: BoxDecoration(
// // color: Colors.amber,
// // color: bodyColor,
// color: Color(0xFFE1F5FE),
// border: Border.all(
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// width: 3.5)),
child: buildGroupData(isDesktop),
);
}
// Widget buildGroupListView(bool isDesktop) {
// return Container(
// child: Text("DAta"),
// );
// }
Widget buildGroupData(isDesktop) {
return Container(
// margin: isDesktop
// ? EdgeInsets.all(10.0)
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
// padding: const EdgeInsets.all(10),
height:
isDesktop
? MediaQuery.of(context).size.height * 0.98
: MediaQuery.of(context).size.height,
// decoration: BoxDecoration(
// border: isDesktop
// ? Border.all(
// width: 2,
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
// )
// : null,
// color: Colors.white,
// // color: Color(0xFFF7F7FB),
//
// // color: Colors.amber,
// ),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'Group List',
style: GoogleFonts.poppins(
fontSize: isDesktop ? 16 : 14,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () {},
),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Color(0xFF114D8B),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
// side: BorderSide(color: , width: 1),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () async {
// List<dynamic> users = await futureUsers;
// context.go('/CreateGroup');
showDialog(
context: context,
builder:
(context) => GroupData(
isDesktop: isDesktop,
groupId: null,
layoutColor: layoutColor!,
fetchGetGroup: refreshData,
),
);
},
child: Row(
children: [
Text('New Group', style: GoogleFonts.poppins(fontSize: 12)),
SizedBox(width: 5),
Icon(Icons.add_circle_outline_rounded, color: Colors.white),
],
),
),
],
),
SizedBox(height: 5),
Row(
children: [
Expanded(
child: Container(
height: MediaQuery.of(context).size.height * 0.75,
padding: const EdgeInsets.all(10),
// margin: const EdgeInsets.only(bottom: 10),
color: Colors.white,
// color: Colors.red.shade100,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(children: [buildGroupListView(isDesktop)]),
),
),
),
],
),
],
),
);
}
Widget buildGroupListView(bool isDesktop) {
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
return Center(child: Text("No groups found."));
}
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: apiAllGroups!.length,
itemBuilder: (context, index) {
final group = apiAllGroups![index];
return Card(
// color: bodyColor,
// color: Color(0xFFF5F5F5),
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 6, horizontal: 10),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
"Group Name",
style: GoogleFonts.poppins(
fontSize: 11.5,
fontWeight: FontWeight.w400,
),
),
),
Expanded(
child: Text(
"Domestic Policy",
style: GoogleFonts.poppins(
fontSize: 11.5,
fontWeight: FontWeight.w400,
),
),
),
Expanded(
child: Text(
"International Policy",
style: GoogleFonts.poppins(
fontSize: 11.5,
fontWeight: FontWeight.w400,
),
),
),
Expanded(
child: Text(
"Description",
style: GoogleFonts.poppins(
fontSize: 11.5,
fontWeight: FontWeight.w400,
),
),
),
],
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${group['name']}",
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
"${group['domestic_policy_name'] ?? 'N/A'}",
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
"${group['international_policy_name']}",
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
"${group['description'] ?? 'N/A'}",
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () async {
if (group['group_id'] != null) {
final newGroupID = int.tryParse(
group['group_id'].toString(),
);
if (newGroupID != null) {
final data = await apiService.getGroupDetailsFind(
context,
newGroupID,
); // Always an int
showDialog(
context: context,
builder:
(context) => GroupData(
isDesktop: isDesktop,
groupId: newGroupID,
// Pass the ID
groupData: data,
layoutColor: layoutColor!,
fetchGetGroup: refreshData,
),
);
}
} else {
print("something went wrong check properly");
}
},
// onTap: () {
// context.go("/CreateGroup", extra: group);
// },
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
),
),
SizedBox(width: 5),
GestureDetector(
onTap: () {
final idStr = group['group_id'];
final id = int.tryParse(idStr.toString());
if (id == null) {
print("group_id is null");
return;
}
final status = group['is_active'];
// print("GroupId : ${group['group_id']} ");
deleteGroup(group, id, status);
},
child: Image.asset(
'assets/images/IconsImg/delete.png',
width: 20,
height: 15,
),
),
],
),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,801 @@
import 'dart:convert';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../../config/apiUrl.dart';
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
import '../../widgets/custom_text_forex.dart';
import 'hotels_list.dart';
class HotelsData extends StatefulWidget {
final Future<List<dynamic>> Function() fetchGetHotels;
final bool isDesktop;
final Color? layoutColor;
final int? hotelsId; // <-- Add this
final Map<String, dynamic>? hotelsData;
const HotelsData({
super.key,
required this.isDesktop,
this.layoutColor,
required this.fetchGetHotels,
this.hotelsId,
this.hotelsData,
});
@override
HotelsDataState createState() => HotelsDataState();
}
class HotelsDataState extends State<HotelsData> {
final ApiService apiService = ApiService();
Map<String, String> countryMap = {};
late List<dynamic>? apiCountryData;
late List<dynamic>? apiAirlineCountryData;
Map<String, dynamic>? apiData;
final Map<String, TextEditingController> controllers = {};
Map<String, String> errorMessages = {};
Map<String, FocusNode> focusNodes = {};
Map<String, bool> focusStates = {};
List<dynamic> countryList = [];
String? selectedCountry;
String? selectedCountryName;
String? selectedCity;
String? selectedDuration;
String? selectedPerdiemAmount;
String? userId;
int? hotelsDataId;
late String isActive = "1";
bool isDisable = false;
List<String> dataHeader = [
"hotel_name",
"hotel_chain",
"category",
"country_code",
"city",
];
Map<String, dynamic> hotels_Details() {
final data = {
"hotel_name": controllers["hotel_name"]?.text,
"hotel_chain": controllers["hotel_chain"]?.text,
"category": controllers["category"]?.text,
"country_code": selectedCountry,
"country_name": selectedCountryName,
"city": controllers["city"]?.text,
"created_by": userId,
"is_active": isActive,
};
return data;
}
@override
void initState() {
super.initState();
apiCountryData = null;
apiData = null;
for (var field in dataHeader) {
controllers[field] = TextEditingController();
focusNodes["${field}FocusNode"] = FocusNode();
focusStates["${field}Focused"] = false;
}
for (var key in focusNodes.keys) {
_addFocusListener(focusNodes[key]!, (focus) {
setState(() {
focusStates[key.replaceFirst("FocusNode", "Focused")] = focus;
});
});
}
fetchCountries();
if (widget.hotelsId != null) {
print('Editing Hotles ID: ${widget.hotelsId}');
updateHotelsDetails();
}
_clearError();
}
void _clearError() {
setState(() {
errorMessages.clear();
});
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
for (var node in focusNodes.values) {
node.dispose();
}
super.dispose();
}
void _addFocusListener(FocusNode node, Function(bool) updateState) {
node.addListener(() {
setState(() {
updateState(node.hasFocus);
});
});
}
void updateHotelsDetails() {
print("Update - ${widget.hotelsData}");
final data = widget.hotelsData;
if (data == null) return;
setState(() {
selectedCountry = data['country_code']; // For dropdown
selectedCountryName =
data['country_name']; // For dropdown label or display
controllers['city']?.text = data['city'] ?? '';
controllers['hotel_chain']?.text = data['hotel_chain'] ?? '';
controllers['category']?.text = data['category'] ?? '';
controllers['hotel_name']?.text = data['hotel_name'] ?? '';
isActive = data["is_active"];
final hotelsId = int.tryParse(data['hotel_id'].toString());
hotelsDataId = hotelsId;
});
}
Future<void> fetchCountries() async {
try {
List<dynamic> countries = await apiService.fetchCountryList(context);
setState(() {
apiCountryData = countries;
});
} catch (e) {
print('Error fetching country list: $e');
}
}
void toggleStatus() {
setState(() {
isActive = isActive == "1" ? "0" : "1";
});
}
bool validateData() {
errorMessages.clear();
final data = {
"hotel_name": controllers["hotel_name"]?.text,
"hotel_chain": controllers["hotel_chain"]?.text,
"category": controllers["category"]?.text,
"country_code": selectedCountry,
"country": selectedCountryName,
"city": controllers["city"]?.text,
};
final requiredFields = [
"hotel_name",
"hotel_chain",
"country_code",
"city",
];
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field].toString().trim().isEmpty) {
errorMessages[field] = "Required";
}
}
return errorMessages.isEmpty;
}
Future<void> handleSubmit() async {
userId = await getUserId();
setState(() {
isDisable = true;
// This triggers UI rebuild with error messages
if (validateData()) {
postHotelsData();
} else {
isDisable = false;
}
});
final hotelsData1 = hotels_Details();
print("submit data - $hotelsData1");
}
Future<void> postHotelsData({int isActive = 1}) async {
final hotelsData = hotels_Details();
final String apiUrldata;
if (hotelsDataId != null) {
print("for edit hotel id - $hotelsDataId");
apiUrldata = '$apiUrl/api/updateHotels/$hotelsDataId';
hotelsData["hotel_id"] = hotelsDataId.toString();
hotelsData["updated_by"] = userId;
(hotelsData.containsKey("created_by"))
? hotelsData.remove("created_by")
: '';
(hotelsData.containsKey("country_name"))
? hotelsData.remove("country_name")
: '';
} else {
print("for add Hotel id - null");
apiUrldata = '$apiUrl/api/createHotels';
print("called apiUrl - $apiUrldata");
hotelsData["created_by"] = userId;
(hotelsData.containsKey("country_name"))
? hotelsData.remove("country_name")
: '';
}
final token = await getToken(); // Fetch token
if (token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final uri = Uri.parse(apiUrldata);
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'app-signature': 'ts-traveltool-2025-signature-123456',
};
final body = jsonEncode(hotelsData);
final response =
hotelsDataId != null
? await http.put(uri, headers: headers, body: body)
: await http.post(uri, headers: headers, body: body);
if (response.statusCode == 200 || response.statusCode == 201) {
print("Hotels Details Created successfully!");
print("Response: ${response.body}");
_clearError();
await widget.fetchGetHotels();
if (context.mounted) {
Navigator.of(context).pop(); // Close modal only if mounted
}
setState(() {
isDisable = false;
});
// Do NOT re-enable here if success
} else if (response.statusCode == 403) {
print("403-FORB");
await apiService.logout(context);
return null;
// throw Exception('Failed to load users');
} else if (response.statusCode == 404) {
if (context.mounted) {
Navigator.of(context).pop();
}
final message = jsonDecode(response.body)['message'] ?? 'Unknown error';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
),
);
setState(() {
isDisable = false;
});
} else {
print("Failed to submit plan. Status: ${response.statusCode}");
print("Error: ${response.body}");
setState(() {
isDisable = false;
});
}
} catch (e) {
print("Error submitting plan: $e");
setState(() {
isDisable = false;
});
}
}
@override
Widget build(BuildContext context) {
late Map<String, String> countryMap; // Mapping country_code -> country_name
late List<String> countryCodes; // List of country codes
// countryList = [];
countryList = apiCountryData ?? [];
// Map country codes to country names
// countryMap = {
// for (var item in countryList)
// item['country_code'] as String: item['country_name'] as String,
// };
countryMap = {
for (var country in countryList)
(country['country_code'] ?? ''):
'${country['country_name'] ?? ''} (${country['country_code'] ?? ''})',
};
// Extract only country codes for processing
countryCodes = countryMap.keys.toList();
selectedCountry ??= null;
return AlertDialog(
backgroundColor: Colors.white,
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Row 1: Title + Edit + Delete buttons
Row(
children: [
Text(
(hotelsDataId != null) ? 'Edit Hotels' : 'Create Hotels',
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
),
const Spacer(),
],
),
const SizedBox(height: 2),
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Hotel Name *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
// isFocused: false,
isFocused: focusStates["hotel_nameFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
focusNode: focusNodes["hotel_nameFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
controller: controllers["hotel_name"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Name",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["hotel_name"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["hotel_name"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
const SizedBox(height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Hotel Chain *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["hotel_chainFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
focusNode: focusNodes["hotel_chainFocusNode"],
controller: controllers["hotel_chain"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Hotel Chain",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["hotel_chain"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["hotel_chain"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
const SizedBox(height: 5),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Category",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
// isFocused: false,
isFocused: focusStates["categoryFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
focusNode: focusNodes["categoryFocusNode"],
controller: controllers["category"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-z A-Z0-9 ]'),
),
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Category",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Country *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: false,
padding: const EdgeInsets.symmetric(horizontal: 0),
isDesktop: widget.isDesktop,
child: SizedBox(
height: 40,
width: double.infinity,
child: Focus(
focusNode: focusNodes["country_codeFocusNode"],
onFocusChange: (hasFocus) {
setState(() {
focusStates["country_codeFocused"] = hasFocus;
});
},
child: GestureDetector(
onTap: () {
// Request focus when user taps
focusNodes["country_codeFocusNode"]?.requestFocus();
},
child: DropdownSearch<String>(
selectedItem: countryMap[selectedCountry],
popupProps: PopupProps.menu(
showSearchBox: true, // Enables search functionality
menuProps: const MenuProps(
backgroundColor: Colors.white,
),
constraints: BoxConstraints(maxHeight: 200),
itemBuilder: (context, item, isSelected) {
print("contryItem - $item");
final match = RegExp(
r'^(.*)\s\((.*)\)$',
).firstMatch(item);
final countryName = match?.group(1) ?? '';
final countryCode = match?.group(2) ?? '';
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 0.02,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
countryName,
style: GoogleFonts.poppins(
fontSize: 11.5,
),
),
Text(
countryCode,
style: GoogleFonts.poppins(
fontSize: 11.5,
color: Colors.grey,
),
),
],
),
),
);
},
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search ...",
hintStyle: GoogleFonts.poppins(fontSize: 11),
contentPadding: EdgeInsets.symmetric(
horizontal: 1,
vertical: 1,
),
),
),
),
items: countryMap.values.toList(),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
// border: InputBorder.none,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color:
(focusStates["country_codeFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
// width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
(focusStates["country_codeFocused"] ??
false)
? widget.layoutColor!
: Colors.white,
// : const Color(0xFFD6D5E6),
// width: 0.5,
// const Color(0xFFD6D5E6),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: widget.layoutColor!,
width: 1,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 8.0,
),
),
),
dropdownBuilder:
(context, selectedItem) => Align(
// Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select ",
style: GoogleFonts.poppins(fontSize: 11),
),
),
onChanged: (String? newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedCountry =
countryMap.entries
.firstWhere(
(entry) => entry.value == newValue,
)
.key;
final match = RegExp(
r'^(.*)\s\((.*)\)$',
).firstMatch(newValue!);
final countryName =
match?.group(1) ??
newValue; // --> "Ascension Islands"
// final countryCode = match?.group(2) ?? "";
selectedCountryName = countryName;
});
},
),
),
),
),
),
if (errorMessages["country_code"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["country_code"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
const SizedBox(height: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"City *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldForexWrapper(
isFocused: focusStates["cityFocused"] ?? false,
isDesktop: widget.isDesktop,
color: Colors.transparent,
child: SizedBox(
height: 40,
child: TextField(
focusNode: focusNodes["cityFocusNode"],
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9 _-]'),
),
],
controller: controllers["city"],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "City",
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["city"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["city"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
SizedBox(height: 10),
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
if (hotelsDataId != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Change Status ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
Tooltip(
message:
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
child: GestureDetector(
onTap: toggleStatus,
child: Text(
isActive == "1" ? "Active" : "Inactive",
style: TextStyle(
fontSize: 13,
fontFamily: "Inter",
color: isActive == "1" ? Colors.green : Colors.red,
),
),
),
),
],
),
if (hotelsDataId != null) SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// SizedBox(
// child: ElevatedButton(
// onPressed: () {
// // You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: widget.layoutColor,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// ),
// child: Text('Cancel',
// style: GoogleFonts.poppins(
// fontSize: 13, color: Colors.white)),
// ),
// ),
// SizedBox(
// width: 10,
// ),
SizedBox(
child: ElevatedButton(
onPressed:
isDisable
? null
: () async {
setState(() {
isDisable = true;
});
await handleSubmit();
// You can get text from commentController.text
// Navigator.of(context).pop(); // Close the modal
// Optional: re-enable only on error
// setState(() {
// isDisable = false;
// });
},
style: ElevatedButton.styleFrom(
backgroundColor: widget.layoutColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
),
),
],
),
// : SizedBox.shrink(),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:responsive_builder/responsive_builder.dart';
@ -6,15 +8,19 @@ import '../../widgets/custom_text_field.dart';
import '../../widgets/custom_text_itnerary_sub.dart';
class BusScreen extends StatefulWidget {
final Map<String, dynamic>? apiData;
final Function(bool) onClose;
final Function(Map<String, dynamic>)onSaveBus;
final Map<String, dynamic>? selectedItem;
final String? loginUser;
final Function(String, bool) onClose;
final Function(Map<String, dynamic>) onSaveBus;
final Map<String, dynamic>? selectedItem;
final String? loginUser;
BusScreen({
required this.onClose, this.apiData, required this.onSaveBus, required this.selectedItem, required this.loginUser});
required this.onClose,
this.apiData,
required this.onSaveBus,
required this.selectedItem,
required this.loginUser,
});
@override
_BusScreenState createState() => _BusScreenState();
@ -52,27 +58,26 @@ class _BusScreenState extends State<BusScreen> {
bool _timeFocus = false;
bool _commentsFocus = false;
Map<String, dynamic> get busData{
Map<String,dynamic> data = {
"from": _fromController.text,
"to": _toController.text,
"date": _dateController.text,
"time": _timeController.text,
"comments": _buscommentsController.text,
"created_by": widget.loginUser,
"updated_by": widget.loginUser,
Map<String, dynamic> get busData {
Map<String, dynamic> data = {
"from": _fromController.text,
"to": _toController.text,
"date": _dateController.text,
"time": _timeController.text,
"comments": _buscommentsController.text,
"created_by": widget.loginUser,
"updated_by": widget.loginUser,
};
if (widget.selectedItem != null) {
if (widget.selectedItem?["indx"] != null && widget.selectedItem?["indx"] != 0) {
data["indx"] = widget.selectedItem!["indx"];
} else if (widget.selectedItem?["bus_id"] != null && widget.selectedItem?["bus_id"] != 0) {
data["bus_id"] = widget.selectedItem!["bus_id"];
}
}
if (widget.selectedItem != null) {
if (widget.selectedItem?["indx"] != null &&
widget.selectedItem?["indx"] != 0) {
data["indx"] = widget.selectedItem!["indx"];
} else if (widget.selectedItem?["bus_id"] != null &&
widget.selectedItem?["bus_id"] != 0) {
data["bus_id"] = widget.selectedItem!["bus_id"];
}
}
return data;
}
@ -91,41 +96,39 @@ class _BusScreenState extends State<BusScreen> {
});
});
_hotelNameFocusNode.addListener(() {
setState(() {
_isHotelNameFocused = _hotelNameFocusNode.hasFocus;
});
_hotelNameFocusNode.addListener(() {
setState(() {
_isHotelNameFocused = _hotelNameFocusNode.hasFocus;
});
_fromFocusNode.addListener(() {
setState(() {
_fromFocus = _fromFocusNode.hasFocus;
});
});
_fromFocusNode.addListener(() {
setState(() {
_fromFocus = _fromFocusNode.hasFocus;
});
});
_toFocusNode.addListener(() {
setState(() {
_toFocus = _toFocusNode.hasFocus;
});
_toFocusNode.addListener(() {
setState(() {
_toFocus = _toFocusNode.hasFocus;
});
_dateFocusNode.addListener(() {
setState(() {
_dateFocus = _fromFocusNode.hasFocus;
});
});
_dateFocusNode.addListener(() {
setState(() {
_dateFocus = _dateFocusNode.hasFocus;
});
});
_timeFocusNode.addListener(() {
setState(() {
_timeFocus = _timeFocusNode.hasFocus;
});
_timeFocusNode.addListener(() {
setState(() {
_timeFocus = _timeFocusNode.hasFocus;
});
});
_commentsFocusNode.addListener(() {
setState(() {
_commentsFocus = _commentsFocusNode.hasFocus;
});
_commentsFocusNode.addListener(() {
setState(() {
_commentsFocus = _commentsFocusNode.hasFocus;
});
});
_buscommentsController = initController("comments");
_fromController = initController("from");
@ -137,12 +140,14 @@ class _BusScreenState extends State<BusScreen> {
_toController.addListener(() => _clearError("to"));
_dateController.addListener(() => _clearError("date"));
_timeController.addListener(() => _clearError("time"));
}
@override
void dispose() {
_tripTypeFocusNode.dispose();
_dateFocusNode.dispose();
_timeFocusNode.dispose();
_tripTypeController.dispose();
_hotelNameController.dispose();
_fromController.dispose();
@ -153,7 +158,6 @@ class _BusScreenState extends State<BusScreen> {
super.dispose();
}
void _clearError(String field) {
if (mounted && errorMessages.containsKey(field)) {
setState(() {
@ -161,11 +165,12 @@ class _BusScreenState extends State<BusScreen> {
});
}
}
bool isValidData(Map<String, dynamic> data) {
errorMessages.clear(); // Reset errors
// Required fields that must not be empty
List<String> requiredFields = ["from", "to","date","time"];
List<String> requiredFields = ["from", "to", "date", "time"];
// Check validation for each field
for (String field in requiredFields) {
@ -177,74 +182,75 @@ class _BusScreenState extends State<BusScreen> {
return errorMessages.isEmpty; // Valid if there are no errors
}
void handleSave() {
print("Handle Save accomadationData $busData");
void handleSave(){
print( "Handle Save accomadationData $busData");
Map<String,dynamic> data = busData;
Map<String, dynamic> data = busData;
if (!isValidData(data)) {
print("Validation Failed: Required fields are missing.");
setState(() {});
return; // Stop execution if validation fails
}else {
} else {
widget.onSaveBus(busData);
}
widget.onClose(false);// Close screen after saving
widget.onClose("Bus", false);
// widget.onClose(false); // Close screen after saving
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Container(
color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () {
widget.onClose(false);
},
child: Icon(
Icons.close,
size: 18,
color: Color(0xFF575A74),
return Container(
// color: Colors.white,
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Align(
// alignment: Alignment.centerRight,
// child: InkWell(
// onTap: () {
// widget.onClose(false);
// },
// child: Icon(
// Icons.close,
// size: 18,
// color: Color(0xFF575A74),
// ),
// ),
// ),
// Text("Bus Booking List",
// style: TextStyle(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: Color(0xFF575A74))),
// SizedBox(
// height: 6,
// ),
Padding(
padding: const EdgeInsets.only(top: 30.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
),
),
),
Text("Bus Booking List",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold,color: Color(0xFF575A74))),
SizedBox(
height: 6,
),
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
),
)
],
],
),
),
),
),
);
});
);
},
);
}
List<Widget> _buildAccomadtionForm (bool isDesktop) {
List<Widget> _buildAccomadtionForm(bool isDesktop) {
List<Widget> buildResponsiveRow(List<Widget> children) {
return [
isDesktop ? Row(children: children) : Column(children: children),
@ -254,11 +260,10 @@ class _BusScreenState extends State<BusScreen> {
List<List<Widget>> rowBuilders = [
// _builClassType(isDesktop),
_buildSecondRow(isDesktop)
_buildSecondRow(isDesktop),
];
return [
// ...buildResponsiveRow(_buildFirstRow(isDesktop)),
// Iterate over rowBuilders and wrap each in a responsive container
@ -267,73 +272,60 @@ class _BusScreenState extends State<BusScreen> {
...buildResponsiveRow(_buildThirdRow(isDesktop)),
// Actions row remains a Row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _handleAction(isDesktop),
),
];
}
List<Widget> _buildFirstRow(isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Trip Type",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
isDesktop ? Row(children: _buildTripType(isDesktop)
) :
Column(
children: _buildTripType(isDesktop)
)
isDesktop
? Row(children: _buildTripType(isDesktop))
: Column(children: _buildTripType(isDesktop)),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
];
}
List<Widget> _buildTripType(bool isDesktop){
List<Widget> _buildTripType(bool isDesktop) {
List<dynamic> purposeList = widget.apiData?['flight_trip_type'] ?? [];
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item)=>DropdownMenuItem<String>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
)).toList();
List<DropdownMenuItem<String>> dropdownItems =
purposeList
.map(
(item) => DropdownMenuItem<String>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
),
)
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
value: null,
child: Text("No options available", style: TextStyle(color: Colors.grey)),
child: Text(
"No options available",
style: TextStyle(color: Colors.grey),
),
),
);
}
// Default selected value
String? selectedPurpose = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
String? selectedPurpose =
dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
return [
CustomTextFieldWrapper(
@ -341,37 +333,37 @@ class _BusScreenState extends State<BusScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: DropdownButtonFormField<String>(
focusNode: _tripTypeFocusNode, // Assign the correct focus node
value: selectedPurpose,
style: TextStyle(fontSize: 12),
// style: TextStyle(fontSize: 12),
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10), // Proper padding
horizontal: 10,
), // Proper padding
),
onChanged: purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
print("Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
} : null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
);
}
: null,
items: dropdownItems,
),
),
),
];
}
List<Widget> _buildSecondRow(bool isDesktop) {
DateTime? _selectedCheckOutDate;
TimeOfDay? _selectedCheckOutTime;
@ -381,19 +373,20 @@ class _BusScreenState extends State<BusScreen> {
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedCheckOutDate != null && _selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
initialDate:
_selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
firstDate: today,
lastDate: DateTime(2100),
initialEntryMode: DatePickerEntryMode.calendarOnly,
);
if (pickedDate != null && pickedDate != _selectedCheckOutDate) {
setState(() {
_selectedCheckOutDate = pickedDate;
_dateController.text = DateFormat('yyyy-MM-dd').format(pickedDate);
_dateController.text = DateFormat('dd-MM-yyyy').format(pickedDate);
});
}
}
@ -405,30 +398,64 @@ class _BusScreenState extends State<BusScreen> {
);
if (pickedTime != null && pickedTime != _selectedCheckOutTime) {
setState(() {
_selectedCheckOutTime = pickedTime;
// Formatting time to HH:mm (24-hour format)
final now = DateTime.now();
final formattedTime = DateFormat('HH:mm').format(
DateTime(now.year, now.month, now.day, pickedTime.hour,
pickedTime.minute),
);
_timeController.text = formattedTime;
});
// Parse the selected date
final dateText = _dateController.text ?? "";
final selectedDate = DateFormat(
'dd-MM-yyyy',
).parse(dateText); // or 'yyyy-MM-dd' depending on your format
final now = DateTime.now();
final selectedDateTime = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
pickedTime.hour,
pickedTime.minute,
);
final isToday =
selectedDate.year == now.year &&
selectedDate.month == now.month &&
selectedDate.day == now.day;
bool isPastTime = selectedDateTime.isBefore(now);
if (isToday && isPastTime) {
setState(() {
errorMessages["time"] = "You can't select a past time.";
});
return;
} else {
setState(() {
_selectedCheckOutTime = pickedTime;
// Formatting time to HH:mm (24-hour format)
final now = DateTime.now();
final formattedTime = DateFormat('HH:mm').format(
DateTime(
now.year,
now.month,
now.day,
pickedTime.hour,
pickedTime.minute,
),
);
_timeController.text = formattedTime;
});
}
}
}
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"From",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"From *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -436,45 +463,40 @@ class _BusScreenState extends State<BusScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
child: TextField(
focusNode: _fromFocusNode,
controller: _fromController,
style: const TextStyle(fontSize: 12),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
decoration: const InputDecoration(
labelText: "From",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["from"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Required",
style: TextStyle(color: Colors.red, fontSize: 12),
),
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
],
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"To",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"To *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -482,10 +504,12 @@ class _BusScreenState extends State<BusScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
focusNode: _toFocusNode,
controller: _toController,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "To",
@ -499,28 +523,21 @@ class _BusScreenState extends State<BusScreen> {
),
if (errorMessages["to"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Required",
style: TextStyle(color: Colors.red, fontSize: 12),
),
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
],
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Date",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"Date *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -528,13 +545,18 @@ class _BusScreenState extends State<BusScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: GestureDetector(
onTap: () => _selectCheckOutDate(context),
// onTap: () => _selectCheckOutDate(context),
onTap: () {
_dateFocusNode.requestFocus();
_selectCheckOutDate(context);
},
child: AbsorbPointer(
child: TextField(
focusNode: _dateFocusNode,
controller: _dateController,
readOnly: true,
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
labelText: "Select Date",
@ -542,39 +564,34 @@ class _BusScreenState extends State<BusScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
suffixIcon: Icon(Icons.calendar_today,
size: 16, color: Colors.grey),
suffixIcon: Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey,
),
),
),
),
),
),
),
if (errorMessages["date"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Required",
style: TextStyle(color: Colors.red, fontSize: 12),
),
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
],
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Time",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"Time *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -583,38 +600,44 @@ class _BusScreenState extends State<BusScreen> {
child: SizedBox(
height: 40,
child: GestureDetector(
onTap: () => _selectCheckOutTime(context),
onTap: () {
// _selectedCheckOutTime = null; // Reset time variable
_timeController.text = ""; // Clear text field
_timeFocusNode.requestFocus();
_selectCheckOutTime(context);
},
child: AbsorbPointer(
child: TextField(
focusNode: _timeFocusNode,
controller: _timeController,
style: const TextStyle(fontSize: 12),
readOnly: true,
style: const TextStyle(fontSize: 12, color: Colors.black),
decoration: const InputDecoration(
labelText: "Select Time",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
suffixIcon:
Icon(Icons.access_time, size: 16, color: Colors.grey),
suffixIcon: Icon(
Icons.access_time,
size: 16,
color: Colors.grey,
),
),
),
),
),
),
),
if (errorMessages["time"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Required",
errorMessages["time"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
];
}
@ -625,35 +648,47 @@ class _BusScreenState extends State<BusScreen> {
children: [
Text(
"Comments",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
CustomTextFieldItnerarySubWrapper(
isFocused: _commentsFocus, // Dropdown doesn't use focus
isDesktop: isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.4
: MediaQuery.of(context).size.width * 0.66,
child: TextField(
focusNode: _commentsFocusNode,
controller: _buscommentsController,
maxLines: 6,
keyboardType: TextInputType.multiline,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: "Description",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 4),
width: isDesktop ? MediaQuery.of(context).size.width * 0.35 : null,
child: SizedBox(
height: 40,
child: TextField(
focusNode: _commentsFocusNode,
controller: _buscommentsController,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: "Comments",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
)
),
if (isDesktop) Spacer(),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _handleAction(isDesktop),
),
],
),
];
}
@ -662,39 +697,35 @@ class _BusScreenState extends State<BusScreen> {
// Close Button
ElevatedButton(
onPressed: () {
widget.onClose(false);// Close the dialog or screen
widget.onClose("Bus", false);
// widget.onClose(false); // Close the dialog or screen
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400], // Light grey color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
"Close",
style: TextStyle(color: Colors.white, fontSize: 14),
style: GoogleFonts.poppins(color: Colors.white, fontSize: 12),
),
),
SizedBox(width: 10), // Space between buttons
// Save Changes Button
ElevatedButton(
onPressed: () {
handleSave();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // Primary color for save
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: Color(0xFF114D8B), // Primary color for save
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
"Save Changes",
style: TextStyle(color: Colors.white, fontSize: 14),
style: GoogleFonts.poppins(color: Colors.white, fontSize: 12),
),
),
];
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,31 @@
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:responsive_builder/responsive_builder.dart';
import '../../utils/auth_utils.dart';
import '../../widgets/custom_text_field.dart';
import '../../widgets/custom_text_itnerary_sub.dart';
class MiscellaneousScreen extends StatefulWidget {
final Map<String, dynamic>? apiData;
final Function(bool) onClose;
final Function(String, bool) onClose;
final Function(Map<String, dynamic>) onSaveMiscellaneous;
final Map<String, dynamic>? selectedItem;
final int? selectedIndex;
final String? loginUser;
MiscellaneousScreen({
required this.onClose, required this.apiData, required this.onSaveMiscellaneous,
this.selectedItem, this.selectedIndex,required this.loginUser});
required this.onClose,
required this.apiData,
required this.onSaveMiscellaneous,
this.selectedItem,
this.selectedIndex,
required this.loginUser,
});
@override
_MiscellaneousScreenState createState() => _MiscellaneousScreenState();
@ -37,10 +46,10 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
late TextEditingController _commentsController = TextEditingController();
bool _isHotelNameFocused = false;
bool _hotelNameFocused = false;
bool _commentsFocus = false;
String? selectedSpecialType;
Color layoutColor = Colors.grey;
Map<String, String> errorMessages = {};
Map<String, dynamic> get miscellaneousData {
@ -49,14 +58,14 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
"comments": _commentsController.text,
"created_by": widget.loginUser,
"updated_by": widget.loginUser,
};
if (widget.selectedItem != null) {
if (widget.selectedItem?["indx"] != null && widget.selectedItem?["indx"] != 0) {
if (widget.selectedItem?["indx"] != null &&
widget.selectedItem?["indx"] != 0) {
data["indx"] = widget.selectedItem!["indx"];
} else if (widget.selectedItem?["miscellaneous_id"] != null && widget.selectedItem?["miscellaneous_id"] != 0) {
} else if (widget.selectedItem?["miscellaneous_id"] != null &&
widget.selectedItem?["miscellaneous_id"] != 0) {
data["miscellaneous_id"] = widget.selectedItem!["miscellaneous_id"];
}
}
@ -64,19 +73,18 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
return data;
}
@override
void initState() {
super.initState();
if (widget.selectedItem == null) {
_commentsController.clear();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
loadInitialData();
});
_hotelNameFocusNode.addListener(() {
setState(() {
_isHotelNameFocused = _hotelNameFocusNode.hasFocus;
_hotelNameFocused = _hotelNameFocusNode.hasFocus;
});
});
@ -86,27 +94,43 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
});
});
_commentsController =
TextEditingController(text: widget.selectedItem?["comments"] ?? "");
_commentsController = TextEditingController(
text: widget.selectedItem?["comments"] ?? "",
);
// Set the selected value if available
if (widget.selectedItem != null && widget.selectedItem!["special_request"] != null) {
if (widget.selectedItem != null &&
widget.selectedItem!["special_request"] != null) {
selectedSpecialType = widget.selectedItem!["special_request"].toString();
}
}
@override
void dispose() {
_tripTypeFocusNode.dispose();
_hotelNameFocusNode.dispose();
_tripTypeController.dispose();
_commentsController.dispose();
super.dispose();
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
// setState(() {
// layoutColor =
// layoutString != null
// ? Color(int.parse(layoutString))
// : Colors.redAccent;
// });
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
});
}
bool isValidData(Map<String, dynamic> data) {
errorMessages.clear(); // Reset errors
@ -114,90 +138,68 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
// Required fields that must not be empty
List<String> requiredFields = ["special_request", "comments"];
// Check validation for each field
for (String field in requiredFields) {
if (data[field] == null || data[field].toString().trim().isEmpty) {
errorMessages[field] = "This field is required";
errorMessages[field] = "Required";
}
}
return errorMessages.isEmpty; // Valid if there are no errors
}
void handleSave() {
print("Handle Save miscellaneousData $miscellaneousData");
void handleSave(){
print( "Handle Save miscellaneousData $miscellaneousData");
Map<String,dynamic> data = miscellaneousData;
Map<String, dynamic> data = miscellaneousData;
if (!isValidData(data)) {
print("Validation Failed: Required fields are missing.");
setState(() {});
return; // Stop execution if validation fails
}else {
} else {
widget.onSaveMiscellaneous(miscellaneousData); // Send object to parent
}
widget.onClose(false);// Close screen after saving
widget.onClose("Miscellaneous", false); // Close screen after saving
// widget.onClose(false); // Close screen after saving
// Clear only if this is a new entry
// if (widget.selectedItem == null) {
// _commentsController.clear();
// }
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Container(
color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () {
_commentsController.clear();
widget.onClose(false);
},
child: Icon(
Icons.close,
size: 18,
color: Color(0xFF575A74),
return Container(
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 30.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
),
),
),
Text("Miscellaneous Booking List",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold,color: Color(0xFF575A74))),
SizedBox(
height: 6,
),
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
),
)
],
],
),
),
),
),
);
});
);
},
);
}
List<Widget> _buildAccomadtionForm (bool isDesktop) {
List<Widget> _buildAccomadtionForm(bool isDesktop) {
List<Widget> buildResponsiveRow(List<Widget> children) {
return [
isDesktop ? Row(children: children) : Column(children: children),
@ -205,170 +207,279 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
];
}
return [
...buildResponsiveRow(_buildFirstRow(isDesktop)),
...buildResponsiveRow(_buildThirdRow(isDesktop)),
// Actions row remains a Row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _handleAction(isDesktop),
),
];
}
List<Widget> _buildFirstRow(isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Special Type",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"Special Type *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
isDesktop ? Row(children: _buildTripType(isDesktop)
) :
Column(
children: _buildTripType(isDesktop)
)
isDesktop
? Row(children: _buildTripType(isDesktop))
: Column(children: _buildTripType(isDesktop)),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
];
}
List<Widget> _buildTripType(bool isDesktop){
List<dynamic> purposeList = widget.apiData?['miscellaneous_special_request'] ?? [];
List<Widget> _buildTripType(bool isDesktop) {
List<dynamic> purposeList =
widget.apiData?['miscellaneous_special_request'] ?? [];
// selectedInsuranceType = purposeList.isNotEmpty ? purposeList.first['dropdown_value'] : null;
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item)=>DropdownMenuItem<String>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
)).toList();
List<DropdownMenuItem<String>> dropdownItems =
purposeList
.map(
(item) => DropdownMenuItem<String>(
value: item['dropdown_key'],
child: Text(item['dropdown_value']),
),
)
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
value: null,
child: Text("No options available", style: TextStyle(color: Colors.grey)),
child: Text(
"No options available",
style: TextStyle(color: Colors.grey),
),
),
);
}
// Default selected value
selectedSpecialType ??= dropdownItems.isNotEmpty ? dropdownItems.first.value : "No options";
selectedSpecialType ??=
dropdownItems.isNotEmpty ? dropdownItems.first.value : "No options";
return [
CustomTextFieldWrapper(
isFocused: _isHotelNameFocused,
// CustomTextFieldWrapper(
// isFocused: _isHotelNameFocused,
// isDesktop: isDesktop,
// width:
// isDesktop
// ? MediaQuery.of(context).size.width * 0.34
// : MediaQuery.of(context).size.width * 0.66,
// child: SizedBox(
// height: 40,
// child: DropdownButtonFormField<String>(
// focusNode: _tripTypeFocusNode, // Assign the correct focus node
// value: selectedSpecialType,
// style: TextStyle(fontSize: 12),
// decoration: InputDecoration(
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(
// horizontal: 10,
// ), // Proper padding
// ),
// onChanged:
// purposeList.isNotEmpty
// ? (newValue) {
// setState(() {
// selectedSpecialType = newValue;
// });
//
// print(selectedSpecialType);
// }
// : null,
// items: dropdownItems,
// ),
// ),
// ),
CustomTextFieldItnerarySubWrapper(
isFocused: _hotelNameFocused,
isDesktop: isDesktop,
padding: const EdgeInsets.symmetric(horizontal: 0),
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: null, //MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: DropdownButtonFormField<String>(
focusNode: _tripTypeFocusNode, // Assign the correct focus node
value: selectedSpecialType,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10), // Proper padding
),
onChanged: purposeList.isNotEmpty
? (newValue) {
width: double.infinity,
child: Focus(
focusNode: _hotelNameFocusNode,
onFocusChange: (hasFocus) {
setState(() {
selectedSpecialType = newValue;
_hotelNameFocused = hasFocus;
});
print(selectedSpecialType);
}
: null,
items: dropdownItems,
},
child: GestureDetector(
//
onTap: () {
// Request focus when user taps
_hotelNameFocusNode?.requestFocus();
},
child: DropdownSearch<Map<String, dynamic>>(
// focusNode: _taxiReqFocusNode,
items: purposeList.cast<Map<String, dynamic>>(),
selectedItem: purposeList.firstWhere(
(item) => item['dropdown_key'] == selectedSpecialType,
orElse: () => {},
),
itemAsString: (item) => item['dropdown_value'] ?? '',
popupProps: PopupProps.menu(
showSearchBox: false,
fit: FlexFit.loose,
menuProps: const MenuProps(backgroundColor: Colors.white),
itemBuilder: (context, item, isSelected) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: Text(
item['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
),
);
},
),
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
// border: InputBorder.none,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(
color:
(_hotelNameFocused ?? false)
? layoutColor!
: Colors.white,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
(_hotelNameFocused ?? false)
? layoutColor
: Colors.white,
// : const Color(0xFFD6D5E6),
width: 0.5,
// const Color(0xFFD6D5E6),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: layoutColor, width: 0.5),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
),
dropdownBuilder: (context, selectedItem) {
if (selectedItem == null || selectedItem.isEmpty) {
return Text(
"Select ",
style: GoogleFonts.poppins(
color: Colors.grey,
fontSize: 13,
),
);
}
return Text(
selectedItem['dropdown_value'] ?? '',
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.black,
),
);
},
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedSpecialType = newValue as String?;
});
print("Updating form data: -> ${newValue ?? ""}");
}
: null,
),
),
),
),
),
if (errorMessages["special_request"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Select Type",
style: TextStyle(color: Colors.red, fontSize: 12),
),
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
],
];
}
List<Widget> _buildThirdRow(bool isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Comments",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
"Comments *",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
// CustomTextFieldWrapper
CustomTextFieldItnerarySubWrapper(
isFocused: _commentsFocus, // Dropdown doesn't use focus
isDesktop: isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.4
: MediaQuery.of(context).size.width * 0.66,
child: TextField(
focusNode: _commentsFocusNode,
controller: _commentsController,
maxLines: 6,
keyboardType: TextInputType.multiline,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: "Description",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 4),
width:
isDesktop
? MediaQuery.of(context).size.width * 0.34
: null, //MediaQuery.of(context).size.width * 0.66,
child: SizedBox(
height: 40,
child: TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
],
focusNode: _commentsFocusNode,
controller: _commentsController,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
labelText: "Comments",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["comments"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
"Required",
style: TextStyle(color: Colors.red, fontSize: 12),
),
Text("Required", style: TextStyle(color: Colors.red, fontSize: 12)),
],
],
)
),
if (isDesktop) Spacer(),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: _handleAction(isDesktop),
),
];
}
@ -377,41 +488,36 @@ class _MiscellaneousScreenState extends State<MiscellaneousScreen> {
// Close Button
ElevatedButton(
onPressed: () {
_commentsController.clear();
widget.onClose(false);// Close the dialog or screen
widget.onClose("Miscellaneous", false);
// widget.onClose(false); // Close the dialog or screen
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400], // Light grey color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
"Close",
style: TextStyle(color: Colors.white, fontSize: 14),
style: GoogleFonts.poppins(color: Colors.white, fontSize: 12),
),
),
SizedBox(width: 10), // Space between buttons
// Save Changes Button
ElevatedButton(
onPressed: () {
handleSave();
handleSave();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // Primary color for save
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: Color(0xFF114D8B), // Primary color for save
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
"Save Changes",
style: TextStyle(color: Colors.white, fontSize: 14),
style: GoogleFonts.poppins(color: Colors.white, fontSize: 12),
),
),
];
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:responsive_builder/responsive_builder.dart';
@ -11,8 +12,12 @@ class ForexScreen extends StatefulWidget {
final Function(String tab, String key, String value) updateFormData;
final Function(bool) onClose;
ForexScreen({required this.formData, required this.updateFormData,
required this.onClose, this.apiData});
ForexScreen({
required this.formData,
required this.updateFormData,
required this.onClose,
this.apiData,
});
@override
_ForexScreenState createState() => _ForexScreenState();
@ -95,57 +100,55 @@ class _ForexScreenState extends State<ForexScreen> {
});
});
_tripTypeController =
TextEditingController(text: widget.formData["tripType"] ?? "");
_hotelNameController =
TextEditingController(text: widget.formData["_hotelName"] ?? "");
_fromController =
TextEditingController(text: widget.formData["_from"] ?? "");
_toController =
TextEditingController(text: widget.formData["_Check_In_Time"] ?? "");
_dateController =
TextEditingController(text: widget.formData["_Check_Out"] ?? "");
_endDateController =
TextEditingController(text: widget.formData["_Check_In"] ?? "");
_timeController =
TextEditingController(text: widget.formData["_Check_Out_Time"] ?? "");
_commentsController =
TextEditingController(text: widget.formData["_comments"] ?? "");
_tripTypeController = TextEditingController(
text: widget.formData["tripType"] ?? "",
);
_hotelNameController = TextEditingController(
text: widget.formData["_hotelName"] ?? "",
);
_fromController = TextEditingController(
text: widget.formData["_from"] ?? "",
);
_toController = TextEditingController(
text: widget.formData["_Check_In_Time"] ?? "",
);
_dateController = TextEditingController(
text: widget.formData["_Check_Out"] ?? "",
);
_endDateController = TextEditingController(
text: widget.formData["_Check_In"] ?? "",
);
_timeController = TextEditingController(
text: widget.formData["_Check_Out_Time"] ?? "",
);
_commentsController = TextEditingController(
text: widget.formData["_comments"] ?? "",
);
// Save data when user types
_tripTypeController.addListener(() {
widget.updateFormData(
"Bus", "_tripType", _tripTypeController.text);
widget.updateFormData("Bus", "_tripType", _tripTypeController.text);
}); // Save data when user types
_hotelNameController.addListener(() {
widget.updateFormData(
"Bus", "_hotelName", _hotelNameController.text);
widget.updateFormData("Bus", "_hotelName", _hotelNameController.text);
});
_fromController.addListener(() {
widget.updateFormData(
"Bus", "_from", _fromController.text);
widget.updateFormData("Bus", "_from", _fromController.text);
});
_toController.addListener(() {
widget.updateFormData(
"Bus", "_Check_In_Time", _toController.text);
widget.updateFormData("Bus", "_Check_In_Time", _toController.text);
});
_dateController.addListener(() {
widget.updateFormData(
"Bus", "_Check_Out", _dateController.text);
widget.updateFormData("Bus", "_Check_Out", _dateController.text);
});
_endDateController.addListener(() {
widget.updateFormData(
"Bus", "_Check_In", _endDateController.text);
widget.updateFormData("Bus", "_Check_In", _endDateController.text);
});
_timeController.addListener(() {
widget.updateFormData(
"Bus", "_Check_Out_Time", _timeController.text);
widget.updateFormData("Bus", "_Check_Out_Time", _timeController.text);
});
_commentsController.addListener(() {
widget.updateFormData(
"Bus", "_comments", _commentsController.text);
widget.updateFormData("Bus", "_comments", _commentsController.text);
});
}
@ -163,56 +166,60 @@ class _ForexScreenState extends State<ForexScreen> {
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveBuilder(builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return ResponsiveBuilder(
builder: (context, sizingInfo) {
bool isMobile = sizingInfo.isMobile;
bool isDesktop =
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
return Container(
color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () {
widget.onClose(false);
},
child: Icon(
Icons.close,
size: 18,
return Container(
color: Color(0xFFF4F4FB),
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () {
widget.onClose(false);
},
child: Icon(
Icons.close,
size: 18,
color: Color(0xFF575A74),
),
),
),
Text(
"Forex List",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF575A74),
),
),
),
Text("Forex List",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold,color: Color(0xFF575A74))),
SizedBox(
height: 6,
),
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
SizedBox(height: 6),
Padding(
padding: const EdgeInsets.all(28.0),
child: Center(
child: Column(children: _buildAccomadtionForm(isDesktop)),
),
),
)
],
],
),
),
),
),
);
});
);
},
);
}
List<Widget> _buildAccomadtionForm (bool isDesktop) {
List<Widget> _buildAccomadtionForm(bool isDesktop) {
List<Widget> buildResponsiveRow(List<Widget> children) {
return [
isDesktop ? Row(children: children) : Column(children: children),
@ -222,14 +229,12 @@ class _ForexScreenState extends State<ForexScreen> {
List<List<Widget>> rowBuilders = [
_builClassType(isDesktop),
_buildSecondRow(isDesktop)
_buildSecondRow(isDesktop),
];
return [
...buildResponsiveRow(_buildFirstRow(isDesktop)),
// Iterate over rowBuilders and wrap each in a responsive container
...rowBuilders.expand((row) => buildResponsiveRow(row)),
@ -247,23 +252,21 @@ class _ForexScreenState extends State<ForexScreen> {
];
}
List<Widget> _buildFirstRow(isDesktop) {
DateTime? _selectedCheckOutDate;
DateTime? _selectedEndDate;
Future<void> _selectCheckOutDate(BuildContext context) async {
DateTime now = DateTime.now();
DateTime today = DateTime(now.year, now.month, now.day);
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedCheckOutDate != null && _selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
initialDate:
_selectedCheckOutDate != null &&
_selectedCheckOutDate!.isAfter(today)
? _selectedCheckOutDate!
: today,
firstDate: today,
lastDate: DateTime(2100),
);
@ -274,18 +277,20 @@ class _ForexScreenState extends State<ForexScreen> {
_dateController.text = DateFormat('yyyy-MM-dd').format(pickedDate);
});
}
};
}
;
Future<void> _selectForexEndDate(BuildContext context) async {
DateTime now = DateTime.now();
DateTime today = DateTime(now.year, now.month, now.day);
DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: _selectedEndDate != null && _selectedEndDate!.isAfter(today)
? _selectedEndDate!
: today,
initialDate:
_selectedEndDate != null && _selectedEndDate!.isAfter(today)
? _selectedEndDate!
: today,
firstDate: today,
lastDate: DateTime(2100),
);
@ -298,7 +303,6 @@ class _ForexScreenState extends State<ForexScreen> {
}
}
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -306,9 +310,10 @@ class _ForexScreenState extends State<ForexScreen> {
Text(
"Forex Start Date",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
@ -330,32 +335,30 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
suffixIcon: Icon(Icons.calendar_today,
size: 16, color: Colors.grey),
suffixIcon: Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey,
),
),
),
),
),
),
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Forex End Date",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
@ -377,46 +380,50 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
suffixIcon: Icon(Icons.calendar_today,
size: 16, color: Colors.grey),
suffixIcon: Icon(
Icons.calendar_today,
size: 16,
color: Colors.grey,
),
),
),
),
),
),
),
],
),
];
}
List<Widget> _buildTripType(bool isDesktop){
List<Widget> _buildTripType(bool isDesktop) {
List<dynamic> purposeList = widget.apiData?['flight_trip_type'] ?? [];
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item)=>DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
)).toList();
List<DropdownMenuItem<String>> dropdownItems =
purposeList
.map(
(item) => DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
),
)
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
value: null,
child: Text("No options available", style: TextStyle(color: Colors.grey)),
child: Text(
"No options available",
style: TextStyle(color: Colors.grey),
),
),
);
}
// Default selected value
String? selectedPurpose = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
String? selectedPurpose =
dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
return [
CustomTextFieldWrapper(
@ -432,51 +439,62 @@ class _ForexScreenState extends State<ForexScreen> {
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10), // Proper padding
horizontal: 10,
), // Proper padding
),
onChanged: purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
print("Updating form data: Flight -> trip_type -> ${newValue ?? ""}");
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
print(
"Updating form data: Flight -> trip_type -> ${newValue ?? ""}",
);
widget.updateFormData("Flight", "trip_type", newValue ?? "");
}
: null,
widget.updateFormData(
"Flight",
"trip_type",
newValue ?? "",
);
}
: null,
items: dropdownItems,
),
),
),
];
}
List<Widget> _builClassType(bool isDesktop){
List<Widget> _builClassType(bool isDesktop) {
List<dynamic> purposeList = widget.apiData?['flight_class'] ?? [];
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item)=>DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
)).toList();
List<DropdownMenuItem<String>> dropdownItems =
purposeList
.map(
(item) => DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
),
)
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
value: null,
child: Text("No options available", style: TextStyle(color: Colors.grey)),
child: Text(
"No options available",
style: TextStyle(color: Colors.grey),
),
),
);
}
// Default selected value
String? selectedPurpose = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
String? selectedPurpose =
dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
return [
Column(
@ -485,9 +503,10 @@ class _ForexScreenState extends State<ForexScreen> {
Text(
"Class *",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
@ -504,15 +523,17 @@ class _ForexScreenState extends State<ForexScreen> {
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10), // Proper padding
horizontal: 10,
), // Proper padding
),
onChanged: purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
}
: null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
}
: null,
items: dropdownItems,
),
@ -535,27 +556,30 @@ class _ForexScreenState extends State<ForexScreen> {
];
}
List<Widget> _buildSecondRow(bool isDesktop) {
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Transport",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
isFocused: _fromFocus,
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
child: TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_-]')),
],
focusNode: _fromFocusNode,
controller: _fromController,
style: const TextStyle(fontSize: 12),
@ -565,28 +589,23 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Accomodation",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -611,21 +630,17 @@ class _ForexScreenState extends State<ForexScreen> {
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Telephone",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -633,7 +648,7 @@ class _ForexScreenState extends State<ForexScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
child: TextField(
focusNode: _fromFocusNode,
controller: _fromController,
style: const TextStyle(fontSize: 12),
@ -643,34 +658,29 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Other Expenses",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
isFocused: _toFocus,
isDesktop: isDesktop,
color:Colors.transparent,
color: Colors.transparent,
child: SizedBox(
height: 40,
@ -680,7 +690,11 @@ class _ForexScreenState extends State<ForexScreen> {
"Rs ",
// focusNode: _toFocusNode,
// controller: _toController,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600,color: Color(0xFF575A74)),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
// decoration: const InputDecoration(
// labelText: "To",
// labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
@ -694,43 +708,48 @@ class _ForexScreenState extends State<ForexScreen> {
),
],
),
];
}
List<Widget> _buildCardDetailsRow(bool isDesktop) {
List<dynamic> purposeList = widget.apiData?['flight_class'] ?? [];
List<DropdownMenuItem<String>> dropdownItems = purposeList
.map((item)=>DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
)).toList();
List<DropdownMenuItem<String>> dropdownItems =
purposeList
.map(
(item) => DropdownMenuItem<String>(
value: item['dropdown_value'],
child: Text(item['dropdown_value']),
),
)
.toList();
if (dropdownItems.isEmpty) {
dropdownItems.add(
DropdownMenuItem<String>(
value: null,
child: Text("No options available", style: TextStyle(color: Colors.grey)),
child: Text(
"No options available",
style: TextStyle(color: Colors.grey),
),
),
);
}
// Default selected value
String? selectedPurpose = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
String? selectedPurpose =
dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
return [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Card Number*",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -738,7 +757,7 @@ class _ForexScreenState extends State<ForexScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
child: TextField(
focusNode: _fromFocusNode,
controller: _fromController,
style: const TextStyle(fontSize: 12),
@ -748,7 +767,6 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
@ -756,21 +774,17 @@ class _ForexScreenState extends State<ForexScreen> {
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Currency*",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -787,36 +801,34 @@ class _ForexScreenState extends State<ForexScreen> {
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10), // Proper padding
horizontal: 10,
), // Proper padding
),
onChanged: purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
}
: null,
onChanged:
purposeList.isNotEmpty
? (newValue) {
setState(() {
selectedPurpose = newValue;
});
}
: null,
items: dropdownItems,
),
),
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Card*",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -841,21 +853,17 @@ class _ForexScreenState extends State<ForexScreen> {
),
],
),
if (isDesktop)
Spacer()
else
SizedBox(
height: 8,
),
if (isDesktop) Spacer() else SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Cash*",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldItnerarySubWrapper(
@ -863,7 +871,7 @@ class _ForexScreenState extends State<ForexScreen> {
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
child: TextField(
focusNode: _fromFocusNode,
controller: _fromController,
style: const TextStyle(fontSize: 12),
@ -873,16 +881,12 @@ class _ForexScreenState extends State<ForexScreen> {
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
];
}
@ -894,17 +898,19 @@ class _ForexScreenState extends State<ForexScreen> {
Text(
"Delivery Location",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74)),
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
SizedBox(height: 5),
CustomTextFieldWrapper(
isFocused: _commentsFocus, // Dropdown doesn't use focus
isDesktop: isDesktop,
width: isDesktop
? MediaQuery.of(context).size.width * 0.4
: MediaQuery.of(context).size.width * 0.66,
width:
isDesktop
? MediaQuery.of(context).size.width * 0.4
: MediaQuery.of(context).size.width * 0.66,
child: TextField(
focusNode: _commentsFocusNode,
controller: _commentsController,
@ -921,7 +927,7 @@ class _ForexScreenState extends State<ForexScreen> {
),
),
],
)
),
];
}
@ -944,10 +950,14 @@ class _ForexScreenState extends State<ForexScreen> {
),
Text(
"Check If You Don't Have a forex Account",
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF575A74)),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF575A74),
),
),
],
)
),
];
}
@ -960,9 +970,7 @@ class _ForexScreenState extends State<ForexScreen> {
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400], // Light grey color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
@ -971,7 +979,6 @@ class _ForexScreenState extends State<ForexScreen> {
),
),
SizedBox(width: 10), // Space between buttons
// Save Changes Button
ElevatedButton(
onPressed: () {
@ -979,9 +986,7 @@ class _ForexScreenState extends State<ForexScreen> {
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // Primary color for save
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: Text(
@ -991,4 +996,4 @@ class _ForexScreenState extends State<ForexScreen> {
),
];
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +1,713 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
class AccomodationListWidget extends StatelessWidget {
import '../../utils/auth_utils.dart';
final List<Map<String,dynamic>> accommodationList;
class AccomodationListWidget extends StatefulWidget {
final List<Map<String, dynamic>> accommodationList;
final Function(bool, Map<String, dynamic>, String) onOpen;
final Function(Map<String, dynamic>) onDeleteAccommodation;
final Function(Map<String, dynamic>) onDeleteAccommodation;
final Function(String, bool) onAddNew;
final bool isViewMode;
const AccomodationListWidget({super.key, required this.accommodationList, required this.onOpen, required this.onDeleteAccommodation});
const AccomodationListWidget({
super.key,
required this.accommodationList,
required this.onOpen,
required this.onDeleteAccommodation,
required this.onAddNew,
required this.isViewMode,
});
@override
State<AccomodationListWidget> createState() => _AccomodationListWidgetState();
}
class _AccomodationListWidgetState extends State<AccomodationListWidget> {
Color? layoutColor;
Color? secondColor;
Color? thridColor;
bool orgHasService = false;
@override
void initState() {
super.initState();
loadInitialData();
getOrgServices();
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? secondString = await getSecondaryColor();
String? thridString = await getTernaryColor();
// String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
secondColor =
layoutString != null
? Color(int.parse(secondString!))
: Colors.orange;
thridColor =
layoutString != null
? Color(int.parse(thridString!))
: Colors.orangeAccent;
// bodyColor =
// bodyStringColor != null
// ? Color(int.parse(bodyStringColor))
// : Colors.white;
});
}
void getOrgServices() async {
final services = await getOrgServicesName();
print("getOrgServices storage: $services");
// just names
final names = services.map((s) => s['name']).toList();
print("Names only: $names");
final hasFlight = names.contains("Accomodation");
setState(() {
orgHasService = hasFlight;
});
print("orgHasService: $orgHasService");
}
@override
Widget build(BuildContext context) {
return
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Accomodation Booking List",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: MediaQuery.of(context).size.width ,
child: DataTable(
border: TableBorder(
bottom: BorderSide(color: Colors.black12),
horizontalInside: BorderSide(color: Colors.black12), // Only horizontal lines
),
columns: const [
DataColumn(label: Text('#')),
DataColumn(label: Text('Destination City')),
DataColumn(label: Text('Hotel Name')),
DataColumn(label: Text('CheckIn Date')),
DataColumn(label: Text('CheckOut Date')),
DataColumn(label: Text('Actions')),
],
rows: _buildDataRows(),
),
final isDesktop = MediaQuery.of(context).size.width > 1024;
return Padding(
padding:
isDesktop ? const EdgeInsets.all(16.0) : const EdgeInsets.all(5.0),
child: Container(
margin: const EdgeInsets.only(top: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header with title and button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if ((!isDesktop) && widget.accommodationList.isNotEmpty)
Text(
" ",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (widget.accommodationList.isNotEmpty && orgHasService)
MouseRegion(
cursor:
widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap:
widget.isViewMode
? null
: () {
print("New data");
widget.onAddNew("Accomodation", true);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
children: [
// Text(
// "Add New",
// style: TextStyle(fontSize: 13),
// ),
// SizedBox(width: 8), // spacing between icon and text
Icon(
Icons.add_circle_sharp,
size: 30,
color: Color(0xFF114D8B),
),
// Container(
// decoration: BoxDecoration(
// shape: BoxShape.circle,
// border: Border.all(
// color: Color(0xFF114D8B), // Outline color
// width: 2, // Outline thickness
// ),
// ),
// height: 30,
// width: 30,
// child: Center(
// child: Icon(
// Icons.add,
// size: 20,
// color: Colors.black,
// ),
// ),
// ),
],
),
),
// onPressed: isViewMode
// ? null
// : () {
// print("New data");
//
// },
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Icon(
// Icons.add_circle_sharp,
// size: 30,
// color: Color(0xFF114D8B),
// )
// ],
// ),
),
// MouseRegion(
// cursor: isViewMode
// ? SystemMouseCursors.forbidden
// : SystemMouseCursors.click,
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF114D8B),
// foregroundColor: Colors.white,
// disabledBackgroundColor: Color(0xFF114D8B),
// disabledForegroundColor: Colors.white,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// side: BorderSide(color: Color(0xFF114D8B), width: 2),
// ),
// padding:
// EdgeInsets.symmetric(horizontal: 20, vertical: 12),
// ),
// onPressed: isViewMode
// ? null
// : () {
// print("New data");
// onAddNew("Accomodation", true);
// },
// child: Row(
// mainAxisSize:
// MainAxisSize.min, // Ensures content fits nicely
// children: [
// // spacing between icon and text
// Icon(
// Icons.add_circle_sharp,
// size: 30,
// color: Color(0xFF114D8B),
// )
// ],
// ),
// ),
// ),
],
),
),
],
const SizedBox(height: 16),
// Scroll behavior based on device
_buildData(context, isDesktop),
],
),
),
);
}
List<DataRow> _buildDataRows() {
List<Map<String, dynamic>> filteredList = accommodationList
.where((item) => item["is_active"] == "1")
.toList();
Widget _buildData(BuildContext context, bool isDesktop) {
List<Map<String, dynamic>> filteredList =
widget.accommodationList
.where((item) => item["is_active"] == "1")
.toList();
print("filteredList- $filteredList");
return filteredList.asMap() .entries.map((entry) {
// String formatDate(String dateString) {
// try {
// DateTime date = DateTime.parse(dateString);
// return DateFormat('d MMM yy').format(date); // Example: 4 Apr 25
// } catch (e) {
// return "Invalid Date";
// }
// }
final Map<String, dynamic> item = entry.value;
String formatDate(String dateString) {
try {
DateTime date = DateFormat("dd-MM-yyyy").parse(dateString);
return DateFormat('dd MMM yy').format(date); // Example: 23 May 25
} catch (e) {
return "Invalid Date";
}
}
return DataRow(cells: [
String formatTime(String timeString) {
try {
final parts = timeString.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
DataCell(Text(item["indx"]?.toString() ?? "N/A")), // Index column
DataCell(Text(item["destination_city"]!)),
DataCell(Text(item["hotel_name"]!)),
DataCell(Text(item["checkin_date"]!)),
DataCell(Text(item["checkout_date"]!)),
DataCell(Row(
children: [
IconButton(
icon: Icon(Icons.remove_red_eye, color: Colors.blue),
onPressed: () {
// View action
},
final now = DateTime.now();
DateTime dateTime;
if (hour == 24 && minute == 0) {
// 24:00 is treated as 00:00 on the next day
dateTime = DateTime(
now.year,
now.month,
now.day,
).add(const Duration(days: 1));
} else {
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
throw FormatException("Invalid hour or minute");
}
dateTime = DateTime(now.year, now.month, now.day, hour, minute);
}
return DateFormat('HH:mm').format(dateTime); // 24-hour format
} catch (e) {
return "Invalid Time";
}
}
// String formatTime(String timeString) {
// try {
// final parts = timeString.split(':');
// final now = DateTime.now();
// final dateTime = DateTime(
// now.year,
// now.month,
// now.day,
// int.parse(parts[0]),
// int.parse(parts[1]),
// );
// return DateFormat.jm().format(dateTime); // e.g., 12:16 PM or 12 AM
// } catch (e) {
// return "Invalid Time";
// }
// }
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final item = filteredList[index];
return Container(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
// color: Colors.orange.shade50,
color: isDesktop ? Colors.white : thridColor,
// color: Colors.white,
boxShadow:
isDesktop
? [
BoxShadow(
color: Colors.black12,
blurRadius: 3,
offset: Offset(0, 1),
),
]
: [],
border: Border(
top: BorderSide(
color: Colors.white70, // Change color to match your theme
width: 2,
),
),
IconButton(
icon: Icon(Icons.edit, color: Colors.green),
onPressed: () {
onOpen(true, item, "Accomodation");
},
),
child: Padding(
padding: isDesktop ? EdgeInsets.all(12) : EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
// mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
flex: 1,
child: Text(
item["hotel_name"]!,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight:
isDesktop ? FontWeight.w800 : FontWeight.w600,
color: !isDesktop ? Colors.black : Color(0xFF575A74),
),
),
),
Spacer(),
if (orgHasService) ...[
GestureDetector(
onTap: () => widget.onOpen(true, item, "Accomodation"),
child: Tooltip(
message: 'Edit Accomodation Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
color:
isDesktop ? Color(0xFF114D8B) : Colors.black87,
),
),
),
SizedBox(width: 10),
GestureDetector(
onTap: () => widget.onDeleteAccommodation(item),
child: Tooltip(
message: 'Delete Accommodation Details',
child: Image.asset(
'assets/images/IconsImg/delete.png',
width: 20,
height: 15,
color: Colors.red,
),
),
),
],
],
),
// Divider(color: Colors.blueGrey.shade50),
SizedBox(height: 4),
isDesktop
? Row(
children: [
Expanded(
flex: 2,
child: Text(
" DestinationCity",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
" Check (in) ",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
" Check (out) ",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
" Comments",
style: GoogleFonts.poppins(fontSize: 11),
),
),
],
)
: SizedBox.shrink(),
SizedBox(height: 8),
isDesktop
? Row(
children: [
Expanded(
flex: 2,
child: Text(
item["destination_city"],
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${formatDate(item["checkin_date"]!)} ${formatTime(item["checkin_time"]!)}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: Text(
"${formatDate(item["checkout_date"]!)} ${formatTime(item["checkout_time"]!)}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 10),
Expanded(
flex: 2,
child: _buildComments(
"Comments:",
item["comments"] ?? "N/A",
),
),
],
)
: Container(
color: Colors.white,
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow(
"Destination City",
item["destination_city"],
),
SizedBox(height: 5),
_buildRow(
"Check (in)",
"${formatDate(item["checkin_date"]!)} ${formatTime(item["checkin_time"]!)}",
),
SizedBox(height: 5),
_buildRow(
"Check (out)",
"${formatDate(item["checkout_date"]!)} ${formatTime(item["checkout_time"]!)}",
),
SizedBox(height: 5),
_buildRow("Comments:", item["comments"] ?? "N/A"),
],
),
),
// Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Wrap(
// spacing: 20, // horizontal space between items
// runSpacing: 10, // vertical space between rows
// children: [
// _buildInfoColumn("City", item["destination_city"]),
// _buildInfoColumn("Check (In)", "${formatDate(item["checkin_date"]!)} ${formatTime(item["checkin_time"]!)}"),
// _buildInfoColumn("Check (Out)", "${formatDate(item["checkout_date"]!)} ${formatTime(item["checkout_time"]!)}"),
// // City - long name handled with flexible space
// // Expanded(
// // flex: 3,
// // child: _buildInfoColumn("City", item["destination_city"]),
// // ),
//
// // SizedBox(width: 8),
//
// // Check In and Check Out stacked right side
// // Expanded(
// // flex: 4,
// // child: Column(
// // crossAxisAlignment: CrossAxisAlignment.end,
// // children: [
// // _buildInfoColumn(
// // "Check In",
// // "${formatDate(item["checkin_date"]!)} ${formatTime(item["checkin_time"]!)}",
// // ),
// // SizedBox(height: 6),
// // _buildInfoColumn(
// // "Check Out",
// // "${formatDate(item["checkout_date"]!)} ${formatTime(item["checkout_time"]!)}",
// // ),
// // ],
// // ),
// // ),
// ],
// ),
// const SizedBox(height: 10),
// const Text(
// "Comment",
// style: TextStyle(fontWeight: FontWeight.w500),
// ),
// const SizedBox(height: 5),
// if (item["comments"] != null && item["comments"].toString().trim().isNotEmpty)
// Container(
// width: double.infinity,
// padding: const EdgeInsets.all(8),
// decoration: BoxDecoration(
// border: Border.all(color: Color(0xFF7098DD), width: 1),
// borderRadius: BorderRadius.circular(6),
// ),
// child: Text(
// item["comments"],
// style: const TextStyle(fontSize: 12),
// ),
// )
// else
// const Text( "N/A", style: TextStyle(fontSize: 12), ),
// ],
// )
],
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
onDeleteAccommodation(item);
},
),
);
},
);
}
Widget _buildComments(String title, String value) {
// Define character limits based on title
Map<String, int> limits = {
// "Special Request:": 30,
"Comments:": 20,
// Add more keys and limits if needed
};
int limit = limits[title] ?? 50; // Default limit if title not found
bool exceedsLimit = value.length > limit;
String wrapText(String text, int maxLineLength) {
final pattern = RegExp('.{1,$maxLineLength}(\\s+|\$)');
return pattern.allMatches(text).map((m) => m.group(0)!).join('\n');
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child:
exceedsLimit
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black,
), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
// Helper widget
Widget _buildInfoColumn(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 4),
Text(
value,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black87),
),
],
);
}
Widget _buildRow(String title, String value) {
// Define character limits based on title
Map<String, int> limits = {
// "Special Request:": 30,
"Comments:": 10,
// Add more keys and limits if needed
};
int limit = limits[title] ?? 50; // Default limit if title not found
bool exceedsLimit = value.length > limit;
String wrapText(String text, int maxLineLength) {
final pattern = RegExp('.{1,$maxLineLength}(\\s+|\$)');
return pattern.allMatches(text).map((m) => m.group(0)!).join('\n');
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
// color: Colors.yellowAccent.shade100,
width: 100,
child: Text(
title,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
],
)),
]);
}).toList();
),
),
SizedBox(width: 5),
// Expanded(child: Text(value, style: TextStyle(fontSize: 12))),
Expanded(
child:
exceedsLimit
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black,
), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
)
: Text(value, style: TextStyle(fontSize: 12)),
),
],
);
}
Widget _buildDateTimeRow(String title, String date, String time) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
title,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 5),
Expanded(
child: Text("$date, $time", style: GoogleFonts.poppins(fontSize: 12)),
),
],
);
}
}

View File

@ -1,111 +1,667 @@
import 'dart:convert';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../config/apiUrl.dart';
import '../../data/models/plan.dart';
import '../../utils/auth_utils.dart';
class BusListWidget extends StatelessWidget{
class BusListWidget extends StatefulWidget {
final List<Map<String, dynamic>> busList;
final Function(bool, Map<String, dynamic>, String) onOpen;
final Function(Map<String,dynamic>) onDeleteBus;
const BusListWidget({super.key, required this.busList, required this.onOpen, required this.onDeleteBus});
final Function(Map<String, dynamic>) onDeleteBus;
final Function(String, bool) onAddNew;
final bool isViewMode;
const BusListWidget({
super.key,
required this.busList,
required this.onOpen,
required this.onDeleteBus,
required this.onAddNew,
required this.isViewMode,
});
@override
State<BusListWidget> createState() => _BusListWidgetState();
}
class _BusListWidgetState extends State<BusListWidget> {
Color? layoutColor;
Color? secondColor;
Color? thridColor;
bool orgHasService = false;
@override
void initState() {
super.initState();
loadInitialData();
getOrgServices();
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? secondString = await getSecondaryColor();
String? thridString = await getTernaryColor();
// String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
secondColor =
layoutString != null
? Color(int.parse(secondString!))
: Colors.orange;
thridColor =
layoutString != null
? Color(int.parse(thridString!))
: Colors.orangeAccent;
// bodyColor =
// bodyStringColor != null
// ? Color(int.parse(bodyStringColor))
// : Colors.white;
});
}
void getOrgServices() async {
final services = await getOrgServicesName();
print("getOrgServices storage: $services");
// just names
final names = services.map((s) => s['name']).toList();
print("Names only: $names");
final hasFlight = names.contains("Bus");
setState(() {
orgHasService = hasFlight;
});
print("orgHasService: $orgHasService");
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.of(context).size.width > 1024;
return Padding(
padding: const EdgeInsets.all(16.0),
padding:
isDesktop ? const EdgeInsets.all(16.0) : const EdgeInsets.all(5.0),
child: Container(
margin: const EdgeInsets.only(top: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header with title and button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if ((!isDesktop) && widget.busList.isNotEmpty)
Text(
" ",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (widget.busList.isNotEmpty && orgHasService)
MouseRegion(
cursor:
widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap:
widget.isViewMode
? null
: () {
print("New data");
widget.onAddNew("Bus", true);
},
child: Row(
mainAxisSize:
MainAxisSize.min, // Ensures content fits nicely
children: [
// Text(
// "Add New",
// style: TextStyle(fontSize: 13),
// ),
// SizedBox(width: 8), // spacing between icon and text
Icon(
Icons.add_circle_sharp,
size: 30,
color: Color(0xFF114D8B),
),
// Container(
// decoration: BoxDecoration(
// shape: BoxShape.circle,
// border: Border.all(
// color: Color(0xFF114D8B), // Outline color
// width: 2, // Outline thickness
// ),
// ),
// height: 30,
// width: 30,
// child: Center(
// child: Icon(
// Icons.add,
// size: 20,
// color: Colors.black,
// ),
// ),
// ),
],
),
),
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF114D8B),
// foregroundColor: Colors.white,
// disabledBackgroundColor: Color(0xFF114D8B),
// disabledForegroundColor: Colors.white,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// side: BorderSide(color: Color(0xFF114D8B), width: 2),
// ),
// padding:
// EdgeInsets.symmetric(horizontal: 20, vertical: 12),
// ),
// onPressed: isViewMode
// ? null
// : () {
// print("New data");
// onAddNew("Bus", true);
// },
// child: Row(
// mainAxisSize:
// MainAxisSize.min, // Ensures content fits nicely
// children: [
// Icon(
// Icons.add_circle_sharp,
// size: 30,
// color: Color(0xFF114D8B),
// )
// ],
// ),
// ),
),
],
),
const SizedBox(height: 16),
// Scroll behavior based on device
_buildData(context, isDesktop),
],
),
),
);
}
Widget _buildData(BuildContext context, bool isDesktop) {
List<Map<String, dynamic>> filteredList =
widget.busList.where((item) => item["is_active"] == "1").toList();
print("filteredList- $filteredList");
// String formatDate(String dateString) {
// try {
// DateTime date = DateTime.parse(dateString);
// return DateFormat('d MMM yy').format(date); // Example: 4 Apr 25
// } catch (e) {
// return "Invalid Date";
// }
// }
//
// String formatTime(String timeString) {
// try {
// final parts = timeString.split(':');
// final now = DateTime.now();
// final dateTime = DateTime(
// now.year,
// now.month,
// now.day,
// int.parse(parts[0]),
// int.parse(parts[1]),
// );
// return DateFormat.jm().format(dateTime); // e.g., 12:16 PM or 12 AM
// } catch (e) {
// return "Invalid Time";
// }
// }
String formatDate(String dateString) {
try {
DateTime date = DateFormat("dd-MM-yyyy").parse(dateString);
return DateFormat('dd MMM yy').format(date); // Example: 23 May 25
} catch (e) {
return "Invalid Date";
}
}
String formatTime(String timeString) {
try {
final parts = timeString.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
final now = DateTime.now();
DateTime dateTime;
if (hour == 24 && minute == 0) {
// 24:00 is treated as 00:00 on the next day
dateTime = DateTime(
now.year,
now.month,
now.day,
).add(const Duration(days: 1));
} else {
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
throw FormatException("Invalid hour or minute");
}
dateTime = DateTime(now.year, now.month, now.day, hour, minute);
}
return DateFormat('HH:mm').format(dateTime); // 24-hour format
} catch (e) {
return "Invalid Time";
}
}
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final item = filteredList[index];
return Container(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
// color: Colors.orange.shade50,
color: isDesktop ? Colors.white : thridColor,
boxShadow:
isDesktop
? [
BoxShadow(
color: Colors.black12,
blurRadius: 3,
offset: Offset(0, 1),
),
]
: [],
border: Border(
top: BorderSide(
color: Colors.white70, // Change color to match your theme
width: 2,
),
),
),
child: Padding(
padding: isDesktop ? EdgeInsets.all(12) : EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"${item["from"]} - ${item["to"]}",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w800,
color:
!isDesktop ? Color(0xFF114D8B) : Color(0xFF575A74),
),
),
Spacer(),
if (widget.busList.isNotEmpty && orgHasService) ...[
GestureDetector(
onTap: () => widget.onOpen(true, item, "Bus"),
child: Tooltip(
message: 'Edit Bus Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
// color: Color(0xFF114D8B),
color:
!isDesktop ? Colors.black : Color(0xFF575A74),
),
),
),
SizedBox(width: 10),
GestureDetector(
onTap: () => widget.onDeleteBus(item),
child: Tooltip(
message: 'Delete Bus Details',
child: Image.asset(
'assets/images/IconsImg/delete.png',
width: 20,
height: 15,
color: Colors.red,
),
),
),
],
],
),
// Divider(color: Colors.blueGrey.shade50),
SizedBox(height: 4),
isDesktop
? Row(
children: [
Expanded(
flex: 3,
child: Text(
"Planned Trips",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 3,
child: Text(
" Date",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 3,
child: Text(
" Comments",
style: GoogleFonts.poppins(fontSize: 11),
),
),
],
)
: SizedBox.shrink(),
SizedBox(height: 8),
isDesktop
? Row(
children: [
Expanded(
flex: 3,
child: Text(
"${item["from"]} - ${item["to"]} ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 10),
Expanded(
flex: 3,
child: Text(
"${formatDate(item["date"]!)} ${formatTime(item["time"]!)}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 10),
Expanded(
flex: 3,
child: _buildComments(
"Comments:",
item["comments"] ?? "N/A",
),
),
],
)
: Container(
color: Colors.white,
padding: const EdgeInsets.all(4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow(
"PlannedTrips",
'${item["from"]!} - ${item["to"]!}',
),
SizedBox(height: 5),
// _buildRow("To", item["to"]!),
// SizedBox(width: 10),
_buildDateTimeRow(
"Date",
formatDate(item["date"]!),
formatTime(item["time"]!),
),
SizedBox(height: 5),
_buildRow("Comments", item["comments"] ?? "N/A"),
],
),
),
// Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Top Row (Country, Currency, Total)
// Wrap(
// spacing: 20, // horizontal space between items
// runSpacing: 10, // vertical space between rows
// // Row(
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// _infoBlock("From", item["from"] ?? "N/A"),
// _infoBlock("To", item["to"]! ?? "N/A"),
// _infoBlock(
// "Date",
// (item["date"] != null && item["time"] != null)
// ? "${formatDate(item["date"]!)} ${formatTime(item["time"]!)}"
// : "N/A",
// ),
// ],
// ),
// const SizedBox(height: 16),
//
// // Comment label
// Text(
// "Comment",
// style: GoogleFonts.poppins(
// fontSize: 14,
// fontWeight: FontWeight.w600,
// ),
// ),
// const SizedBox(height: 8),
// if (item["comments"] != null &&
// item["comments"].toString().trim().isNotEmpty)
// Container(
// width: double.infinity,
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// border: Border.all(
// color: const Color(0xFF7098DD),
// width: 1,
// ),
// borderRadius: BorderRadius.circular(6),
// ),
// child: Text(
// item["comments"] ?? "N/A",
// style: GoogleFonts.poppins(
// fontSize: 12,
// color: Colors.black87,
// ),
// ),
// )
// else
// const Text("N/A", style: TextStyle(fontSize: 12)),
// ],
// ),
],
),
),
);
},
);
}
Widget _infoBlock(String label, String value) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Bus Booking List",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Container(
// color: Colors.blueGrey,
width: double.infinity,
child: Center(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Expanded(
child: SizedBox(
width: MediaQuery.of(context).size.width ,
child: DataTable(
border: TableBorder(
bottom: BorderSide(color: Colors.black12),
horizontalInside: BorderSide(color: Colors.black12), // Only horizontal lines
),
columns: const [
// DataColumn(label: Text('#')),
DataColumn(label: Text('From')),
DataColumn(label: Text('To')),
DataColumn(label: Text('Date')),
DataColumn(label: Text('Time')),
DataColumn(label: Text('Actions')),
],
rows: _buildDataRows(),
),
),
),
),
label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
value,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black87),
),
],
),
),
);
}
List<DataRow> _buildDataRows() {
Widget _buildComments(String title, String value) {
// Define character limits based on title
Map<String, int> limits = {
// "Special Request:": 30,
"Comments:": 30,
// Add more keys and limits if needed
};
List<Map<String, dynamic>> filteredList = busList
.where((item) => item["is_active"] == "1")
.toList();
print("filteredList- $filteredList");
int limit = limits[title] ?? 50; // Default limit if title not found
bool exceedsLimit = value.length > limit;
String wrapText(String text, int maxLineLength) {
final pattern = RegExp('.{1,$maxLineLength}(\\s+|\$)');
return pattern.allMatches(text).map((m) => m.group(0)!).join('\n');
}
return filteredList.asMap().entries.map((entry) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child:
exceedsLimit
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black,
), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
)
: Text(
value,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
final Map<String, dynamic> item = entry.value;
Widget _buildRow(String title, String value) {
// Define character limits based on title
Map<String, int> limits = {
// "Special Request:": 30,
"Comments:": 10,
// Add more keys and limits if needed
};
return DataRow(cells: [
// DataCell(Text(item["indx"]?.toString() ?? "N/A")), // Index column
DataCell(Text(item["from"]!)),
DataCell(Text(item["to"]!)),
DataCell(Text(item["date"]!)),
DataCell(Text(item["time"]!)),
DataCell(Row(
children: [
IconButton(
icon: Icon(Icons.remove_red_eye, color: Colors.blue),
onPressed: () {
// View action
},
int limit = limits[title] ?? 50; // Default limit if title not found
bool exceedsLimit = value.length > limit;
String wrapText(String text, int maxLineLength) {
final pattern = RegExp('.{1,$maxLineLength}(\\s+|\$)');
return pattern.allMatches(text).map((m) => m.group(0)!).join('\n');
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
title,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
IconButton(
icon: Icon(Icons.edit, color: Colors.green),
onPressed: () {
onOpen(true, item, "Bus");
},
),
),
SizedBox(width: 8),
Expanded(
child:
exceedsLimit
? Tooltip(
message: wrapText(value, 50),
decoration: BoxDecoration(
color: Colors.grey.shade200, // Black background
borderRadius: BorderRadius.circular(6),
),
textStyle: TextStyle(
color: Colors.black,
), // Tooltip text color
padding: EdgeInsets.all(8),
preferBelow: false,
child: Text(
value.substring(0, limit) + "...",
style: TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
)
: Text(value, style: GoogleFonts.poppins(fontSize: 12)),
),
],
);
}
Widget _buildDateTimeRow(String title, String date, String time) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
title,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
onDeleteBus(item);
},
),
],
)),
]);
}).toList();
),
),
SizedBox(width: 8),
Expanded(
child: Text("$date, $time", style: GoogleFonts.poppins(fontSize: 12)),
),
],
);
}
}

View File

@ -1,94 +1,914 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
class FlightListWidget extends StatelessWidget {
final List<Map<String,dynamic>> flightList;
final Function( bool,Map<String,dynamic>, String) onOpen;
final Function(Map<String,dynamic>) onDeleteFlight;
import '../../services/apiService.dart';
import '../../utils/auth_utils.dart';
const FlightListWidget({super.key, required this.flightList, required this.onOpen, required this.onDeleteFlight});
class FlightListWidget extends StatefulWidget {
final bool hasAction;
final String? tripType;
final List<Map<String, dynamic>> flightList;
final Function(bool, Map<String, dynamic>, String) onOpen;
final Function(Map<String, dynamic>) onDeleteFlight;
final Map<String, dynamic>? apiData;
final Function(String, bool) onAddNew;
final bool isViewMode;
const FlightListWidget({
Key? key,
required this.flightList,
required this.onOpen,
required this.onDeleteFlight,
required this.onAddNew,
required this.apiData,
required this.isViewMode,
required this.hasAction,
this.tripType,
}) : super(key: key);
@override
_FlightListWidgetState createState() => _FlightListWidgetState();
}
class _FlightListWidgetState extends State<FlightListWidget> {
ApiService apiService = ApiService();
Color? layoutColor;
Color? secondColor;
Color? thridColor;
bool orgHasService = false;
// late Map<String, String> countryMap;
Map<String, String> countryMap = {};
@override
void initState() {
super.initState();
loadCountryList(); // Call your method here
loadInitialData();
getOrgServices();
}
void loadInitialData() async {
String? layoutString = await getLayoutColor();
String? secondString = await getSecondaryColor();
String? thridString = await getTernaryColor();
// String? bodyStringColor = await getBodyColor();
setState(() {
layoutColor =
layoutString != null
? Color(int.parse(layoutString))
: Colors.redAccent;
secondColor =
layoutString != null
? Color(int.parse(secondString!))
: Colors.orange;
thridColor =
layoutString != null
? Color(int.parse(thridString!))
: Colors.orangeAccent;
// bodyColor =
// bodyStringColor != null
// ? Color(int.parse(bodyStringColor))
// : Colors.white;
});
}
void getOrgServices() async {
final services = await getOrgServicesName();
print("getOrgServices storage: $services");
// just names
final names = services.map((s) => s['name']).toList();
print("Names only: $names");
final hasFlight = names.contains("Flight");
setState(() {
orgHasService = hasFlight;
});
print("orgHasService: $orgHasService");
}
Future<void> loadCountryList() async {
final result = await apiService.fetchFlightsCountryList(
context,
widget.tripType,
);
print("ResultCountry : $result");
// Create a map: Country_Code -> "City, Airport"
Map<String, String> tempCountryMap = {};
for (var country in result) {
// String city = country['City'] ?? '';
// String airport = country['Airport'] ?? '';
// String displayName = '${country['City']} - ${country['Airport']}';
// // String displayName = '${country['City']} | ${country['Airport']}';
final code = country['Code'] ?? '';
final city = country['City'] ?? '';
final airport = country['Airport'] ?? '';
final displayName = '$city ($code)\n$airport';
tempCountryMap[country['Code']] = displayName;
}
setState(() {
countryMap = tempCountryMap; // Update the map
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Flight Booking List",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
final isDesktop = MediaQuery.of(context).size.width > 1024;
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
// width: 1000,
width: MediaQuery.of(context).size.width ,
child: DataTable(
border: TableBorder(
bottom: BorderSide(color: Colors.black12),
horizontalInside: BorderSide(color: Colors.black12), // Only horizontal lines
),
columns: const [
// DataColumn(label: Text('#')),
DataColumn(label: Text('Trip Type')),
DataColumn(label: Text('From')),
DataColumn(label: Text('To')),
DataColumn(label: Text('Actions')),
],
rows: _buildDataRows(),
),
ApiService apiService = ApiService();
late Map<String, String> countryMap;
late List<String> countryCodes;
void checkClass() {
if (widget.hasAction) {
if (widget.tripType?.isNotEmpty == true) {
print("Teppp - $widget.tripType");
widget.onAddNew("Flight", true);
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Select Trip Type'),
content: const Text(
'Please select a trip type before adding a flight.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
);
},
);
}
} else {
print("Teppp No - $widget.tripType");
widget.onAddNew("Flight", true);
}
}
// return Padding(
// padding: const EdgeInsets.all(16.0),
// child: Container(
// margin: const EdgeInsets.only(top: 16.0),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// // Header with title and button
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// "Flight",
// style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
// ),
// widget.flightList.isNotEmpty
// ? MouseRegion(
// cursor:
// widget.isViewMode
// ? SystemMouseCursors.forbidden
// : SystemMouseCursors.click,
// child: GestureDetector(
// onTap:
// widget.isViewMode
// ? null
// : () {
// widget.onAddNew("Flight", true);
// // checkClass();
// print("New data");
// },
// child: Row(
// mainAxisSize:
// MainAxisSize.min, // Ensures content fits nicely
// children: [
// // Text(
// // "Add New",
// // style: TextStyle(fontSize: 13),
// // ),
// // SizedBox(width: 8), // spacing between icon and text
// Icon(
// Icons.add_circle_sharp,
// size: 30,
// color: Color(0xFF114D8B),
// ),
// ],
// ),
// ),
// )
// : SizedBox.shrink(),
// ],
// ),
// const SizedBox(height: 16),
//
// // Scroll behavior based on device
// _buildData(context, isDesktop),
// ],
// ),
// ),
// );
return Padding(
padding:
isDesktop ? const EdgeInsets.all(16.0) : const EdgeInsets.all(5.0),
child: Container(
margin: const EdgeInsets.only(top: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Row: Title (mobile only) + Add Button (conditional)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Title (optional on desktop or mobile)
if ((!isDesktop) && widget.flightList.isNotEmpty)
Text(
" ",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
// Right side: either "Add" icon or nothing
if (widget.flightList.isNotEmpty && orgHasService)
MouseRegion(
cursor:
widget.isViewMode
? SystemMouseCursors.forbidden
: SystemMouseCursors.click,
child: GestureDetector(
onTap:
widget.isViewMode
? null
: () {
widget.onAddNew("Flight", true);
print("New data");
},
child: Icon(
Icons.add_circle_sharp,
size: 30,
color: const Color(0xFF114D8B),
),
),
)
else if (!isDesktop)
SizedBox.shrink(),
// Centered "No Data Found" message for mobile and empty list
// Expanded(
// child: Center(
// child: Text(
// "No Data Found",
// textAlign: TextAlign.center,
// style: GoogleFonts.poppins(
// fontSize: 18,
// fontWeight: FontWeight.w500,
// color: Colors.grey,
// ),
// ),
// ),
// ),
],
),
isDesktop ? SizedBox(height: 16) : SizedBox(height: 5),
// Actual flight data UI
_buildData(context, isDesktop),
],
),
),
);
}
List<DataRow> _buildDataRows() {
List<Map<String, dynamic>> filteredList = flightList
.where((item) => item["is_active"] == "1")
.toList();
Widget _buildData(BuildContext context, bool isDesktop) {
List<Map<String, dynamic>> filteredList =
widget.flightList.where((item) => item["is_active"] == "1").toList();
print("filteredList- $filteredList");
List<dynamic> visatypeList = widget.apiData?['flight_class'] ?? [];
return filteredList.asMap().entries.map((entry) {
Map<String,dynamic> item = entry.value;
print("Trip Type: ${item["trip_type"]}");
String getRequestForClass(String? specialRequestKey) {
if (specialRequestKey == null) return "N/A";
return visatypeList
.firstWhere(
(element) =>
element["dropdown_key"].toString() == specialRequestKey,
orElse: () => {"dropdown_value": "N/A"},
)["dropdown_value"]
.toString();
}
return DataRow(cells: [
// DataCell(Text(item["indx"]?.toString() ?? "N/A")),
DataCell(Text(item["trip_type"]?.toString() ?? "N/A")),
DataCell(Text(item["trips"].isNotEmpty ? item["trips"][0]["from_place"]?.toString() ?? "N/A" : "N/A")),
DataCell(Text(item["trips"].isNotEmpty ? item["trips"][0]["to_place"]?.toString() ?? "N/A" : "N/A")),
// String formatDate(String dateString) {
// try {
// DateTime date = DateTime.parse(dateString);
// return DateFormat('d MMM yy').format(date); // Example: 4 Apr 25
// } catch (e) {
// return "Invalid Date";
// }
// }
//
// String formatTime(String timeString) {
// try {
// final parts = timeString.split(':');
// final now = DateTime.now();
// final dateTime = DateTime(
// now.year,
// now.month,
// now.day,
// int.parse(parts[0]),
// int.parse(parts[1]),
// );
// return DateFormat.jm().format(dateTime); // e.g., 12:16 PM or 12 AM
// } catch (e) {
// return "Invalid Time";
// }
// }
DataCell(Row(
children: [
IconButton(
icon: Icon(Icons.remove_red_eye, color: Colors.blue),
onPressed: () {
// View action
},
String formatDate(String dateString) {
try {
DateTime date = DateFormat("dd-MM-yyyy").parse(dateString);
return DateFormat('dd MMM yy').format(date); // Example: 23 May 25
} catch (e) {
return "Invalid Date";
}
}
String formatTime(String timeString) {
try {
final parts = timeString.split(':');
int hour = int.parse(parts[0]);
int minute = int.parse(parts[1]);
final now = DateTime.now();
DateTime dateTime;
if (hour == 24 && minute == 0) {
// 24:00 is treated as 00:00 on the next day
dateTime = DateTime(
now.year,
now.month,
now.day,
).add(const Duration(days: 1));
} else {
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
throw FormatException("Invalid hour or minute");
}
dateTime = DateTime(now.year, now.month, now.day, hour, minute);
}
return DateFormat('HH:mm').format(dateTime); // 24-hour format
} catch (e) {
return "Invalid Time";
}
}
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final item = filteredList[index];
String? tripTypeName = '';
switch (item["trip_type"]?.toString()) {
case "Roundtrip":
tripTypeName = "Round Trip";
break;
case "Multitrip":
tripTypeName = "Multi-Trip";
break;
case "Oneway":
tripTypeName = "One-Way";
break;
default:
tripTypeName = item["trip_type"]?.toString();
break;
}
return Container(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
// color: isDesktop ? Colors.white : Color(0xFFFFF0F5),
color: isDesktop ? Colors.white : thridColor,
// color: Colors.orange.shade50,
// color: Colors.white,
boxShadow:
isDesktop
? [
BoxShadow(
color: Colors.black12,
blurRadius: 2,
offset: Offset(0, 2), // down
),
// Top shadow
BoxShadow(
color: Colors.black12,
blurRadius: 0.5,
offset: Offset(0, -1), // up
),
]
: [],
border: Border(
top: BorderSide(
color: Colors.white70, // Change color to match your theme
width: 2,
),
),
IconButton(
icon: Icon(Icons.edit, color: Colors.green),
onPressed: () {
onOpen(true, item, "Flight");
},
),
child: Padding(
padding: isDesktop ? EdgeInsets.all(12) : EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Row(
children: [
Text(
tripTypeName ?? "N/A",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight:
isDesktop ? FontWeight.w800 : FontWeight.w600,
color: !isDesktop ? Colors.black : Color(0xFF575A74),
// Color(0xFF114D8B)
),
),
Spacer(),
if (orgHasService) ...[
GestureDetector(
onTap: () => widget.onOpen(true, item, "Flight"),
child: Tooltip(
message: 'Edit Flight Details',
child: Image.asset(
'assets/images/IconsImg/edit.png',
width: 20,
height: 15,
color:
isDesktop
? Color(0xFF114D8B)
: Colors.black87,
),
),
),
SizedBox(width: 10),
GestureDetector(
onTap: () => widget.onDeleteFlight(item),
child: Tooltip(
message: 'Delete Flight Details',
child: Image.asset(
'assets/images/IconsImg/delete.png',
width: 20,
height: 15,
color: Colors.red,
),
),
),
],
],
),
),
// Divider(color: Colors.blueGrey.shade50),
SizedBox(height: 4),
if (isDesktop)
Row(
children: [
Expanded(
flex: 2,
child: Text(
"Class",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 5,
child: Text(
"Sector",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
"Date",
style: GoogleFonts.poppins(fontSize: 11),
),
),
Expanded(
flex: 2,
child: Text(
"Time",
style: GoogleFonts.poppins(fontSize: 11),
),
),
],
),
SizedBox(height: 4),
// Trip Rows
if (isDesktop)
if (item["trips"] != null && item["trips"].isNotEmpty)
...item["trips"].map<Widget>((trip) {
print("werewrewrTrip - $trip");
String fromPlaceCountry =
countryMap[trip["from_place"]?.toString()] ??
"Unknown Country";
final parts = fromPlaceCountry.split('\n');
final cityAndCode = parts[0];
final fromairport = parts.length > 1 ? parts[1] : '';
// Extract city and code from "City (CODE)"
final cityMatch = RegExp(
r'^(.*)\s+\(([^)]+)\)$',
).firstMatch(cityAndCode);
final fromcity = cityMatch?.group(1) ?? '';
final fromcode = cityMatch?.group(2) ?? '';
String toPlaceCountry =
countryMap[trip["to_place"]?.toString()] ??
"Unknown Country";
final toparts = toPlaceCountry.split('\n');
final tocityAndCode = toparts[0];
final toairport = toparts.length > 1 ? toparts[1] : '';
// Extract city and code from "City (CODE)"
final tocityMatch = RegExp(
r'^(.*)\s+\(([^)]+)\)$',
).firstMatch(tocityAndCode);
final tocity = tocityMatch?.group(1) ?? '';
final tocode = tocityMatch?.group(2) ?? '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
getRequestForClass(trip["class"].toString()) ??
"N/A",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
Expanded(
flex: 4,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"$fromcity ($fromcode)",
// "${trip["from_place"]?.toString()} - ${trip["to_place"]?.toString()}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
Text(
"$fromairport",
// "${trip["from_place"]?.toString()} - ${trip["to_place"]?.toString()}",
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w400,
),
),
],
),
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Image.asset(
'assets/images/IconsImg/Frame.png',
width: 20,
height: 20,
),
),
),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"$tocity ($tocode)",
// "${trip["from_place"]?.toString()} - ${trip["to_place"]?.toString()}",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
Text(
"$toairport",
// "${trip["from_place"]?.toString()} - ${trip["to_place"]?.toString()}",
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
),
Spacer(),
Expanded(
flex: 2,
child: Text(
formatDate(trip["date"] ?? ""),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
Expanded(
flex: 2,
child: Text(
formatTime(trip["time"] ?? ""),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
),
);
}).toList(),
if (!isDesktop &&
item["trips"] != null &&
item["trips"].isNotEmpty)
...item["trips"].map<Widget>((trip) {
String fromPlaceCountry =
countryMap[trip["from_place"]?.toString()] ??
"Unknown Country";
final parts = fromPlaceCountry.split('\n');
final cityAndCode = parts[0];
final fromairport = parts.length > 1 ? parts[1] : '';
// Extract city and code from "City (CODE)"
final cityMatch = RegExp(
r'^(.*)\s+\(([^)]+)\)$',
).firstMatch(cityAndCode);
final fromcity = cityMatch?.group(1) ?? '';
final fromcode = cityMatch?.group(2) ?? '';
String toPlaceCountry =
countryMap[trip["to_place"]?.toString()] ??
"Unknown Country";
final toparts = toPlaceCountry.split('\n');
final tocityAndCode = toparts[0];
final toairport = toparts.length > 1 ? toparts[1] : '';
// Extract city and code from "City (CODE)"
final tocityMatch = RegExp(
r'^(.*)\s+\(([^)]+)\)$',
).firstMatch(tocityAndCode);
final tocity = tocityMatch?.group(1) ?? '';
final tocode = tocityMatch?.group(2) ?? '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(6),
color: Colors.grey.shade50,
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildKeyValueRow(
"Class",
getRequestForClass(trip["class"].toString()) ??
"N/A",
),
_buildKeyValueRow(
"From",
"$fromcity ($fromcode), $fromairport ",
),
_buildKeyValueRow(
"To",
"$tocity ($tocode), $toairport",
),
_buildKeyValueRow(
"Date",
formatDate(trip["date"] ?? ""),
),
_buildKeyValueRow(
"Time",
formatTime(trip["time"] ?? ""),
),
],
),
),
);
// return Padding(
// padding: const EdgeInsets.symmetric(vertical: 6.0),
// child: Container(
// // decoration: BoxDecoration(
// // border: Border.all(color: Colors.grey.shade300),
// // borderRadius: BorderRadius.circular(6),
// // color: Colors.grey.shade50,
// // ),
// // padding: const EdgeInsets.all(12),
// child: Row(
// children: [
// /// LEFT SIDE: Class and Airport Info
// Expanded(
// flex: 2,
// child: Row(
// children: [
// Text(
// '${getRequestForClass(trip["class"].toString()) ?? "N/A"} (',
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// Text(
// // fromairport,
// fromcode,
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// const Padding(
// padding: EdgeInsets.symmetric(
// horizontal: 6,
// ),
// child: Image(
// image: AssetImage(
// 'assets/images/IconsImg/Frame.png',
// ),
// width: 18,
// height: 18,
// ),
// ),
// Text(
// '$tocode)',
// // '$toairport)',
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// ],
// ),
// ),
//
// /// RIGHT SIDE: Date and Time
// Expanded(
// flex: 1,
// child: Text(
// '${formatDate(trip["date"] ?? "")} ${formatTime(trip["time"] ?? "")}',
// textAlign: TextAlign.right,
// style: const TextStyle(
// fontSize: 13,
// color: Colors.black87,
// ),
// ),
// ),
// ],
// ),
// ),
// );
// return Padding(
// padding: const EdgeInsets.symmetric(vertical: 6.0),
// child: Container(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Wrap(
// crossAxisAlignment: WrapCrossAlignment.center,
// spacing: 4,
// runSpacing: 4,
// children: [
// Text(
// '${getRequestForClass(trip["class"].toString()) ?? "N/A"} (',
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// Text(
// fromcode,
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// const Padding(
// padding: EdgeInsets.symmetric(horizontal: 4),
// child: Image(
// image: AssetImage('assets/images/IconsImg/Frame.png'),
// width: 18,
// height: 18,
// ),
// ),
// Text(
// '$tocode)',
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 14,
// color: Colors.black87,
// ),
// ),
// ],
// ),
// const SizedBox(height: 6),
// Align(
// alignment: Alignment.centerRight,
// child: Text(
// '${formatDate(trip["date"] ?? "")} ${formatTime(trip["time"] ?? "")}',
// textAlign: TextAlign.right,
// style: const TextStyle(
// fontSize: 13,
// color: Colors.black87,
// ),
// ),
// ),
// ],
// ),
// ),
// );
}).toList(),
],
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
onDeleteFlight(item);
},
),
);
},
);
}
Widget _buildKeyValueRow(String key, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 70, // adjust width as needed
child: Text(
"$key",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
)),
]);
}).toList();
),
Expanded(
child: Text(value, style: GoogleFonts.poppins(fontSize: 12)),
),
],
),
);
}
}

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