Compare commits

...

202 Commits

Author SHA1 Message Date
12b632f087 added new api for list of org 2025-10-11 18:06:51 +05:30
6ee4fb6d04 Updated TeamEmployeeList to fetch employees using useOrganizationEmployees during search. 2025-10-11 17:48:25 +05:30
67bb685d4b added new api for orgaization dropdown 2025-10-11 16:55:22 +05:30
8fd4e7f3f1 added space between first and last name 2025-10-11 16:30:27 +05:30
aca2decb00 clear fully cache after remove session 2025-10-11 16:26:04 +05:30
f7f4b68997 changed reimburse data during transaction to current date 2025-10-11 15:09:04 +05:30
f839613066 Merge pull request 'Fix: Ensure orgData can be cleared when opening Organization Modal' (#474) from HotChanges_11_10_25 into main
Reviewed-on: #474
Merged
2025-10-11 08:42:26 +00:00
b58bd33774 added requested b and requested at column inside rgularization 2025-10-11 14:08:27 +05:30
136bc94c5b removed dbugger and add new classes 2025-10-11 13:12:23 +05:30
281a956ac8 fixed directory header layout 2025-10-11 12:43:37 +05:30
31882c3d12 REMOVED UNNEEDED CARD CLASS 2025-10-11 11:13:38 +05:30
a64635cd37 Fix: Ensure orgData can be cleared when opening Organization Modal 2025-10-11 10:29:40 +05:30
9b8c8c34ab Merge pull request 'HotChanges_10_10_25' (#472) from HotChanges_10_10_25 into main
Reviewed-on: #472
Merged
2025-10-10 14:50:04 +00:00
704ba79289 prevent to select future date 2025-10-10 20:17:06 +05:30
3440467107 wapped header content into separated div 2025-10-10 18:25:19 +05:30
f4edcfd2f3 changed prod url 2025-10-10 18:18:20 +05:30
62e5c6899a profile icon should display always at end 2025-10-10 18:17:24 +05:30
00a23b3de9 changed class for table responsive 2025-10-10 18:11:43 +05:30
79161a8ede prevent to pick future date 2025-10-10 18:09:35 +05:30
860779d096 fixed service list show in Dialy Task 2025-10-10 18:08:21 +05:30
a2067e150d fixed service list show 2025-10-10 17:51:12 +05:30
edce5ef614 added newhook that return only organization employee for employee list (Active or Inactive) 2025-10-10 16:48:22 +05:30
dd944b3414 datepicker should not take future date 2025-10-10 16:46:34 +05:30
13d3572cf6 fixed datepicke ui 2025-10-10 16:45:43 +05:30
0dd7c19457 added navigation hook to replace path 2025-10-10 15:37:59 +05:30
3cc4f0b416 mismatch service api fixed 2025-10-10 14:15:30 +05:30
f8095ac9bf upgraded progress 2025-10-10 10:55:51 +05:30
6280abf95e added marging between fltes input box 2025-10-09 17:32:39 +05:30
1200937097 Merge pull request 'HotChanges_09_10_25 : Added Daily Task Filter' (#467) from HotChanges_09_10_25 into main
Reviewed-on: #467
Merged
2025-10-09 12:00:21 +00:00
5d1ccb9572 changed buttons size 2025-10-09 17:25:03 +05:30
91ffc5a0e0 added filter for DailyTask Report 2025-10-09 17:23:04 +05:30
1c4804fed2 added for serviceI dropdown list 2025-10-09 16:48:15 +05:30
0e5e716df2 removed debuggger 2025-10-09 16:20:43 +05:30
aecaee7116 addded project when project goin to update state 2025-10-09 15:38:59 +05:30
97dca1a10b removed unused code 2025-10-07 14:53:48 +05:30
c35eacca5a addded filter added inside api 2025-10-07 14:02:25 +05:30
5e27ed36fa Merge pull request 'HotChanges_06-10-25 : Image Gallery and Attendanc CheckIn check-Out' (#459) from HotChanges_06-10-25 into main
Reviewed-on: #459
Merged
2025-10-07 07:12:49 +00:00
8bcfcc5718 fixed attendance - check In -out and persisted date range from Redux store for attendance logs 2025-10-07 07:12:49 +00:00
ad1bef4f7b added gallery and gallery filter 2025-10-07 07:12:49 +00:00
9886fac03e employee attendance display sorted by date, 2025-10-06 17:20:36 +05:30
a28a7fb444 checked-Out Time should be greather than or equal to checked In time 2025-10-06 16:07:31 +05:30
470421b730 added two date and service-name column inside ProjectAssignedOrgslist and fixed inactive fn in project team 2025-10-06 10:58:07 +05:30
7505b790a7 added trim fn to avoid extra spacing 2025-10-06 10:06:31 +05:30
8fd13247c7 Merge pull request 'HotChanges_04_10_25' (#450) from HotChanges_04_10_25 into main
Reviewed-on: #450
Merged
2025-10-05 04:28:55 +00:00
0fec257354 make removeSession flexible to clear local, session, or both 2025-10-05 00:21:19 +05:30
638c033705 fixed employee name show whenever update expense - for paid by 2025-10-05 00:14:39 +05:30
7872e21477 changed date utils and added search employee inside manage expense 2025-10-04 19:50:26 +05:30
6928bbd309 Changes in ProjectRepository api for getProjectInfraByProject 2025-10-04 17:50:54 +05:30
3693af3d00 added missing projects at ManageContact (Edit&Crate) 2025-10-04 16:36:11 +05:30
d5df200ede Merge pull request 'Organization_Management : Organization Hierarchy' (#443) from Organization_Management into main
Reviewed-on: #443
Merged
2025-09-30 09:07:30 +00:00
9c6450496e added closed fun after create project and changed label name 2025-09-30 14:22:01 +05:30
cfd3986479 fixed small changed like heading, font change 2025-09-30 11:59:16 +05:30
764b145ad9 empty select activity, planne work fields after successfully created task 2025-09-29 18:31:56 +05:30
20c7cf7f37 added right path for organization info show 2025-09-29 18:00:48 +05:30
6d74940c0c added optional chain at view org details 2025-09-29 17:55:56 +05:30
02dcd8611f user could not create subtask completed equal to approve task and fixd error imported subTask component 2025-09-29 17:29:11 +05:30
02600308e8 rmoved unused and console 2025-09-29 16:53:24 +05:30
d1c72291a3 update date utility for localtoUtc fn 2025-09-29 15:20:54 +05:30
fdbd81c5e7 removed console 2025-09-29 15:13:36 +05:30
28d5ef653d added optional chain for manage project to prevent run time error 2025-09-29 15:10:49 +05:30
550b142d74 fixed run time date error 2025-09-29 15:03:52 +05:30
68335f0695 added temp. placeholder for image gallary 2025-09-29 14:38:23 +05:30
4ea20981fc Merge pull request 'Changes in Directory edit' (#407) from Kartik_Bug#1180 into Organization_Management
Reviewed-on: #407
Merged
2025-09-29 09:00:27 +00:00
965e1e4808 Correction in Edit Bucket in Directory. 2025-09-29 09:00:27 +00:00
1c376fe91f Changes in Directory edit 2025-09-29 09:00:27 +00:00
61835cb189 added clear filter 2025-09-29 12:55:31 +05:30
375c482b61 renamed Inactive to Includes Inactive 2025-09-29 12:04:33 +05:30
72424eee53 added updation for fllter 2025-09-29 11:52:11 +05:30
22514b1fa0 Merge pull request 'ProjectUpdationForOrganizaion : added Two fields inside Project create and update form - Promoter and PMC and added Remember me' (#442) from ProjectUpdationForOrganizaion into Organization_Management
Reviewed-on: #442
Merged
2025-09-28 18:44:30 +00:00
61b209a082 addded remember me functionality 2025-09-29 00:13:52 +05:30
198e31290c updated project files 2025-09-28 22:15:07 +05:30
482f8a9bcb added organization api in repo. and removed unuse files 2025-09-27 23:54:44 +05:30
2489095b0b Merge pull request 'Date picker in Document Filter shows pre-filled date instead of blank' (#422) from Kartik_Bug#1250 into Organization_Management
Reviewed-on: #422
Merged
2025-09-27 09:41:15 +00:00
eb8d269662 Date picker in Document Filter shows pre-filled date instead of blank 2025-09-27 09:41:15 +00:00
bbd8ed12f6 Merge pull request 'Date field not cleared in Tenant filter after clicking Clear button' (#432) from Kartik_Bug#1306 into Organization_Management
Reviewed-on: #432
Merged
2025-09-27 09:40:05 +00:00
1e7b4ba21e Date field not cleared in Tenant filter after clicking Clear button 2025-09-27 09:40:05 +00:00
4de3987a37 Merge pull request 'Add extra spacing in Infra Work details.' (#434) from Kartik_Bug#1359 into Organization_Management
Reviewed-on: #434
Merged
2025-09-27 09:38:34 +00:00
7455d8a221 Add extra spacing in Infra Work details. 2025-09-27 09:38:34 +00:00
3f4b7d08d4 Merge pull request 'Add an info (ℹ️) icon to the Daily Progress Report’s Total Pending Task' (#435) from Kartik_Task#1361 into Organization_Management
Reviewed-on: #435
Merged
2025-09-27 09:37:36 +00:00
7ac3268514 Add an info (ℹ️) icon to the Daily Progress Report’s Total Pending Task 2025-09-27 09:37:36 +00:00
f8740472de Merge pull request 'Scrollbar Behavior in "Choose Organization" (Projects → Organization Tab)' (#436) from Kartik_Task#1374 into Organization_Management
Reviewed-on: #436
Merged
2025-09-27 09:36:27 +00:00
be72ca9a58 Scrollbar Behavior in "Choose Organization" (Projects → Organization Tab) 2025-09-27 09:36:27 +00:00
c71c00c0f7 Merge pull request 'Selected Building and Floor Should Persist While Creating Work Area and Activity (Project Infrastructure)' (#438) from Kartik_Bug#1372 into Organization_Management
Reviewed-on: #438
Merged
2025-09-27 09:35:27 +00:00
112e0ff798 Selected Building and Floor Should Persist While Creating Work Area and Activity (Project Infrastructure) 2025-09-27 09:35:27 +00:00
96eb030457 Merge pull request 'Tenant Creation – Error banner for plan selection/currency should not be shown' (#439) from Kartik_Bug#1370 into Organization_Management
Reviewed-on: #439
Merged
2025-09-27 09:34:48 +00:00
a873ace109 Tenant Creation – Error banner for plan selection/currency should not be shown 2025-09-27 09:34:48 +00:00
fb164bd2f2 Merge pull request 'Switch Tenant & Goto workspace option label should be “Switch Workspace' (#440) from Kartik_Bug#1366 into Organization_Management
Reviewed-on: #440
Merged
2025-09-27 09:34:07 +00:00
0d6708619f Switch Tenant & Goto workspace option label should be “Switch Workspace 2025-09-27 09:34:07 +00:00
edba191a2e Merge pull request 'Tenant Creation – Logo removed when navigating back from second page' (#441) from Kartik_Bug#1369 into Organization_Management
Reviewed-on: #441
Merged
2025-09-27 09:33:46 +00:00
ddfe09b570 Tenant Creation – Logo removed when navigating back from second page 2025-09-27 09:33:46 +00:00
acb899dd2e Merge pull request 'Change the label in Assign Organization.' (#433) from Kartik_Bug#1353 into Organization_Management
Reviewed-on: #433
Merged
2025-09-27 09:31:52 +00:00
ae66cb3705 Change the label in Assign Organization. 2025-09-27 09:31:52 +00:00
d52fa00de0 Merge pull request 'Team Assign ToProject : With other Organization employees' (#437) from TeamAssignToProject into Organization_Management
Reviewed-on: #437
Merged
2025-09-27 09:29:59 +00:00
ca8a41bb63 added style classes for footer 2025-09-27 09:29:59 +00:00
265c74f079 added filter and sorted employee list - Team 2025-09-27 09:29:59 +00:00
2ef1fcfd1d successfullly assigned employe to project 2025-09-27 09:29:59 +00:00
3233043cf2 initial setup for assign emp to project 2025-09-27 09:29:59 +00:00
3fddb686d3 change get employee at assign task to employee 2025-09-27 14:58:39 +05:30
4fd6e5cc1a Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-27 10:33:40 +05:30
1fb8eb9ef1 Create Task popup will open at all the time while submitting. 2025-09-26 16:21:37 +05:30
1a3890e837 Removing Create and Activity from MasterModal.jsx file. 2025-09-26 15:34:08 +05:30
2dbf08e330 Removing create and Edit activity component. 2025-09-26 15:02:03 +05:30
b044b88c49 Adding border in Group and Activities popup and Removing mandatory field in Tenant. 2025-09-26 14:56:19 +05:30
db815ba038 Rmoving Refresh icon and Label in Tenant. 2025-09-26 11:51:55 +05:30
53fa013c39 split projectList component 2025-09-26 11:47:03 +05:30
22a1ad45e7 Change the name of Switch Tenant to Workspace 2025-09-26 11:37:31 +05:30
91dcd7c132 The images on the Forgot page and Request Demo page should be centered after the update. 2025-09-26 10:53:31 +05:30
7d18edfa9b added tenant selection path within otp sign in way 2025-09-26 10:01:09 +05:30
fb08e48edd added iniial state of daly progress filte panel 2025-09-25 19:30:55 +05:30
69c225ac72 Adding Services dropdown in Organization Creation dropdown. 2025-09-25 16:09:36 +05:30
182280e91d Merge pull request 'Correction of Delete Employee Popup Texts' (#424) from Kartik_Bug#1261 into Organization_Management
Reviewed-on: #424
Mereged
2025-09-25 09:21:54 +00:00
38bd8d36c0 Correction of Delete Employee Popup Texts 2025-09-25 09:21:54 +00:00
a4bccc9bf6 Merge pull request 'Verify mandatory indicator (*) for Reference field while creating a Tenant' (#427) from Kartik_Bug#1295 into Organization_Management
Reviewed-on: #427
Merged
2025-09-25 09:20:48 +00:00
0e6dd93260 Verify mandatory indicator (*) for Reference field while creating a Tenant 2025-09-25 09:20:48 +00:00
245219ad71 Merge pull request 'Inconsistent search bar height in Tenant module' (#429) from Kartik_Bug#1300 into Organization_Management
Reviewed-on: #429
Merged
2025-09-25 09:19:07 +00:00
978a497f28 Inconsistent search bar height in Tenant module 2025-09-25 09:19:07 +00:00
451d0a785f Merge pull request 'Change all create button UI.' (#430) from Kartik_Task#1345 into Organization_Management
Reviewed-on: #430
Merged
2025-09-25 09:17:56 +00:00
84f9cb2e29 Change all create button UI. 2025-09-25 09:17:56 +00:00
9d9ca28bad Merge pull request 'Incorrect button sequence in Tenant Edit form' (#428) from Kartik_Bug#1301 into Organization_Management
Reviewed-on: #428
Merged
2025-09-25 09:17:16 +00:00
1d218056ac Incorrect button sequence in Tenant Edit form 2025-09-25 09:17:16 +00:00
58837cef0c Merge pull request 'Adding Activity-Group in Create Task popup.' (#431) from Kartik_Task_InfraMask#1266 into Organization_Management
Reviewed-on: #431
merged
2025-09-25 09:00:46 +00:00
1cd3bf6c7f updated assigned services poject whenver assigned new service 2025-09-25 09:00:46 +00:00
d2b10495bd Added Services Column in Edit activity modal 2025-09-25 09:00:46 +00:00
ae9c4833b3 Added Activity Group, and implemented sorting for Service, Activity Group, and Activities. 2025-09-25 09:00:46 +00:00
e69efe61cb Adding Activity-Group in Create Task popup. 2025-09-25 09:00:46 +00:00
92b1531b75 daily task planning filtering according to service 2025-09-25 14:11:04 +05:30
57edd92dce fixed blocking of dailyprogress by using pagination 2025-09-25 12:36:55 +05:30
49eaf857ad In Choose Orgainzation popup Find Organization and Search box show in one line. 2025-09-25 11:46:12 +05:30
ccdfc193c6 Change the position of setting and Organization in Project nav. 2025-09-25 09:52:13 +05:30
817b31379e Switch Tenant button move to top in User Profile. 2025-09-24 17:33:55 +05:30
b36120f73d Merge pull request 'In ExpensePanel add new Toggle button.' (#421) from Kartik_Task#1303 into Organization_Management
Reviewed-on: #421
2025-09-24 11:14:01 +00:00
e2ae2e5fbd change labels as per guidance 2025-09-24 16:43:36 +05:30
521e6690cb uncommit important stuff 2025-09-24 15:09:34 +05:30
1f4a7e5e9c forgot uncomment important stuff- tenant 2025-09-24 15:08:42 +05:30
1d3fcff859 Merge pull request 'Activities redirection issue – Redirects to “Marco Secure Solution Pvt Ltd” project by default' (#423) from Kartik_Bug#1292 into Organization_Management
Reviewed-on: #423
Merged
2025-09-24 07:20:45 +00:00
1286184e1f Activities redirection issue – Redirects to “Marco Secure Solution Pvt Ltd” project by default 2025-09-24 07:20:45 +00:00
4e315aafcf added small ui changed like table b-padding, button size and fixd delete activity and group 2025-09-24 12:49:00 +05:30
52e12426af Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-23 17:49:01 +05:30
d975664023 integrated service group wise activity operation delete,edit and create 2025-09-23 17:48:56 +05:30
d87dae4799 In ExpensePanel add new Toggle button. 2025-09-23 16:35:48 +05:30
4683eff749 In the ProjectList view, when we select 'View Details', the project is automatically set to 'marcosecure' instead of the selected project. 2025-09-23 11:06:16 +05:30
772a3e2829 Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-22 18:44:13 +05:30
ca7b0cda13 initially setup of service management 2025-09-22 18:44:05 +05:30
a380a7ab29 Changes in Choose Organization1 popup increase the space. 2025-09-22 16:25:02 +05:30
c609387924 Changes in Organization popup 2025-09-22 16:00:24 +05:30
69cc3b9383 Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-22 13:13:22 +05:30
6ebbc853bc optimized logout fun and intergrated inside Tenant selection page 2025-09-22 13:13:13 +05:30
71dd35adc2 Changes in Services Dropdown. 2025-09-22 12:40:00 +05:30
533b40d1bf Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-22 10:31:06 +05:30
42a80bbd68 removed unused code 2025-09-22 10:30:57 +05:30
397020ccb4 Merge pull request 'Creating a Services in Master Compoenents.' (#408) from Master_Services into Organization_Management
Reviewed-on: #408
2025-09-22 05:00:06 +00:00
71932ea6dc Merge pull request 'Integrate the API for Work Area and fetch data filtered by serviceId.' (#419) from Kartik_Task_Infra#1265 into Organization_Management
Reviewed-on: #419
2025-09-22 04:07:58 +00:00
693cabf63d Merge pull request 'Calling api for Attendance component for Organization.' (#420) from Kartik_Task_Att#1236 into Organization_Management
Reviewed-on: #420
2025-09-22 04:07:09 +00:00
4afe43d116 handle one tenant have , directly move to dashboard once logged 2025-09-22 00:09:34 +05:30
b9b3788dda configured tenant level login 2025-09-21 18:59:34 +05:30
5e1ccc9b05 Calling api for Attendance component for Organization. 2025-09-21 18:17:47 +05:30
aee510f527 revert pramod changed - (tenant login mistake 2025-09-21 16:58:29 +05:30
7e6020e3db Merge branch 'organization_management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-21 16:42:26 +05:30
cf78d17cf5 Merge pull request 'Add an Organization column in the Attendance grid across all tabs.' (#414) from Kartik_Task_OrgCol_att#1238 into Organization_Management
Reviewed-on: #414
2025-09-21 11:11:21 +00:00
72dbdb0fe0 Changes in Attendance 2025-09-21 16:40:23 +05:30
afcd1934f9 Integrate the API for Work Area and fetch data filtered by serviceId. 2025-09-21 15:10:31 +05:30
Pramod
3e5afe0bc6 revertlogin initial steup in organization_management brnach 2025-09-21 10:13:12 +05:30
Pramod
070fa93fca initially setup login 2025-09-21 10:06:38 +05:30
53a9cbc30b Merge pull request 'Changes in the Teams Services dropdown will reflect the Services data.' (#418) from Kartik_Task_TeamGrid#1259 into Organization_Management
Reviewed-on: #418
2025-09-20 15:01:16 +00:00
70acf57266 Remove message from Services dropdwon. 2025-09-20 20:05:25 +05:30
b2d7349fc9 Adding search funcionality in Teams for Organization and Services. 2025-09-20 19:37:47 +05:30
a0f7e5c57b Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Task_TeamGrid#1259 2025-09-20 19:32:41 +05:30
fa7dc2860c Changes in the Teams Services dropdown will reflect the Services data. 2025-09-20 19:30:47 +05:30
a1a935b0d5 Merge pull request 'changed Uii for org. or project assigned' (#417) from organization_level_login into Organization_Management
Reviewed-on: #417
2025-09-20 13:27:30 +00:00
Pramod
e610cc08c1 changed Uii for org. or project assigned 2025-09-20 18:47:09 +05:30
9711144236 Adding a new Grid in Teams Grid. 2025-09-20 15:40:17 +05:30
1a88f5fec5 chenages msg 2025-09-20 14:02:57 +05:30
ce73cfad21 Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-20 13:59:24 +05:30
7dafd4a45f fetched assigned organozation to project 2025-09-20 13:59:20 +05:30
2e3e3aa6ca Merge pull request 'Adding Filter Icon in Attendance tab and add functionality in all Attendance component.' (#416) from Kartik_Task_AttFilter#1235 into Organization_Management
Reviewed-on: #416
2025-09-20 06:45:11 +00:00
158c934a9f Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Management 2025-09-20 12:14:40 +05:30
f6d864d42e assign to project and Tenant flow is integrated with api 2025-09-20 12:14:34 +05:30
90b96864be Changes in Attendance filter 2025-09-20 12:13:49 +05:30
5a048f7066 Merge pull request 'Adding Dropdown and API call in Daily Progress Report.' (#413) from Kartik_Task_DailyProgress#1232 into Organization_Management
Reviewed-on: #413
2025-09-20 06:22:43 +00:00
25de45b31b Merge pull request 'Adding Dropdown in Daily Task Planning and call api.' (#412) from Kartik_Task_DailyTask#1231 into Organization_Management
Reviewed-on: #412
2025-09-20 06:09:58 +00:00
58b5da1793 Changes in Daily Task planning 2025-09-20 11:39:14 +05:30
0746e5c349 Merge pull request 'Adding Dropdown in Create Task Popup.' (#411) from Kartik_Task#1230 into Organization_Management
Reviewed-on: #411
2025-09-20 06:03:55 +00:00
83143dff0a Merge pull request 'Adding Services Dropdown in Infrastructure.' (#410) from Kartik_Task#1229 into Organization_Management
Reviewed-on: #410
2025-09-20 06:00:05 +00:00
994f22e8c0 Changes in Infrastructure service dropdown. 2025-09-20 11:29:29 +05:30
84a5be52f8 Merge pull request 'Adding Dropdown and Organization Column in Teams' (#409) from Kartik_Task#1227 into Organization_Management
Reviewed-on: #409
2025-09-20 05:52:30 +00:00
b39df5f665 Changes in Teams dropdown. 2025-09-20 11:21:56 +05:30
9bdcc74486 Adding Filter Icon in Attendance tab and add functionality in all Attendance component. 2025-09-20 10:28:18 +05:30
005fdb3490 added assigned org to project 2025-09-19 23:46:06 +05:30
9223f7a176 Adding Card in Daily Progress Report. 2025-09-19 20:03:36 +05:30
00d6774e06 Change the position of Services in Create Task popup in Infrastructure. 2025-09-19 19:26:46 +05:30
1ef82ad0b2 Only 1 project or no project is assigned then dropdown will be hide. 2025-09-19 19:20:01 +05:30
1da587d010 Adding condition if single or no project assigned then dropdown is not shown 2025-09-19 19:13:02 +05:30
164b82e1c7 Add an Organization column in the Attendance grid across all tabs. 2025-09-19 17:00:02 +05:30
9b37288901 Adding Dropdown in Daily Task Planning and call api. 2025-09-19 16:32:34 +05:30
54e609883d Adding Dropdown in Create Task Popup. 2025-09-19 16:13:09 +05:30
7d17422681 Calling Api for Services dropdown. 2025-09-19 16:02:36 +05:30
d67121c150 Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Task#1227 2025-09-19 15:55:25 +05:30
e154bac64a Calling api for services dropdwon. 2025-09-19 15:54:29 +05:30
ea350db98b Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Task#1229 2025-09-19 15:27:16 +05:30
ea219b7176 Adding Services Dropdown in Infrastructure. 2025-09-19 15:03:25 +05:30
af5519fd60 Adding Dropdown and Organization Column in Teams 2025-09-19 14:53:40 +05:30
08194dd8ef Creating a Services in Master Compoenents. 2025-09-19 14:18:56 +05:30
171 changed files with 9738 additions and 6849 deletions

119
package-lock.json generated
View File

@ -809,9 +809,9 @@
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -1552,13 +1552,13 @@
"peer": true
},
"node_modules/@types/node": {
"version": "22.13.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz",
"integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==",
"version": "24.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
"undici-types": "~7.12.0"
}
},
"node_modules/@types/prop-types": {
@ -1835,9 +1835,10 @@
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -1845,6 +1846,19 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@ -2625,9 +2639,9 @@
"integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw=="
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -2741,9 +2755,9 @@
}
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"license": "MIT",
"peer": true
},
@ -3138,9 +3152,9 @@
"dev": true
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
@ -5163,9 +5177,9 @@
}
},
"node_modules/schema-utils": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -5567,24 +5581,28 @@
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@ -5777,9 +5795,9 @@
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
"license": "MIT",
"peer": true
},
@ -5907,9 +5925,9 @@
}
},
"node_modules/watchpack": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"license": "MIT",
"peer": true,
"dependencies": {
@ -5927,21 +5945,23 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.98.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
"version": "5.101.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.1",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@ -5951,11 +5971,11 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.0",
"schema-utils": "^4.3.2",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@ -5974,15 +5994,22 @@
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT",
"peer": true
},
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",

View File

@ -9,4 +9,274 @@
}
.table_header_border {
border-bottom:2px solid var(--bs-table-border-color) ;
}
}
.text-xxs { font-size: 0.55rem; } /* 8px */
.text-xs { font-size: 0.75rem; } /* 12px */
.text-sm { font-size: 0.875rem; } /* 14px */
.text-base { font-size: 1rem; } /* 16px */
.text-lg { font-size: 1.125rem; } /* 18px */
.text-xl { font-size: 1.25rem; } /* 20px */
.text-2xl { font-size: 1.5rem; } /* 24px */
.text-3xl { font-size: 1.875rem; } /* 30px */
.text-4xl { font-size: 2.25rem; } /* 36px */
.text-5xl { font-size: 3rem; } /* 48px */
.text-6xl { font-size: 3.75rem; } /* 60px */
.text-7xl { font-size: 4.5rem; } /* 72px */
.text-8xl { font-size: 6rem; } /* 96px */
.text-9xl { font-size: 8rem; } /* 128px */
/* */
.w-0 { width: 0px; }
.w-px { width: 1px; }
.w-1 { width: 0.25rem; } /* 4px */
.w-2 { width: 0.5rem; } /* 8px */
.w-3 { width: 0.75rem; } /* 12px */
.w-4 { width: 1rem; } /* 16px */
.w-5 { width: 1.25rem; } /* 20px */
.w-6 { width: 1.5rem; } /* 24px */
.w-8 { width: 2rem; } /* 32px */
.w-10 { width: 2.5rem; } /* 40px */
.w-12 { width: 3rem; } /* 48px */
.w-16 { width: 4rem; } /* 64px */
.w-20 { width: 5rem; } /* 80px */
.w-24 { width: 6rem; } /* 96px */
.w-32 { width: 8rem; } /* 128px */
.w-40 { width: 10rem; } /* 160px */
.w-48 { width: 12rem; } /* 192px */
.w-56 { width: 14rem; } /* 224px */
.w-64 { width: 16rem; } /* 256px */
.w-auto { width: auto; }
.w-full { width: 100%; }
.w-screen{ width: 100vw; }
.w-min { width: min-content; }
.w-max { width: max-content; }
.h-0 { height: 0px; }
.h-px { height: 1px; }
.h-1 { height: 0.25rem; } /* 4px */
.h-2 { height: 0.5rem; } /* 8px */
.h-3 { height: 0.75rem; } /* 12px */
.h-4 { height: 1rem; } /* 16px */
.h-5 { height: 1.25rem; } /* 20px */
.h-6 { height: 1.5rem; } /* 24px */
.h-8 { height: 2rem; } /* 32px */
.h-10 { height: 2.5rem; } /* 40px */
.h-12 { height: 3rem; } /* 48px */
.h-16 { height: 4rem; } /* 64px */
.h-20 { height: 5rem; } /* 80px */
.h-24 { height: 6rem; } /* 96px */
.h-32 { height: 8rem; } /* 128px */
.h-40 { height: 10rem; } /* 160px */
.h-48 { height: 12rem; } /* 192px */
.h-56 { height: 14rem; } /* 224px */
.h-64 { height: 16rem; } /* 256px */
.h-auto { height: auto; }
.h-full { height: 100%; }
.h-screen{ height: 100vh; }
.h-min { height: min-content; }
.h-max { height: max-content; }
/* ==========================
Base Font Sizes (mobile first)
========================== */
.text-xxs { font-size: 0.55rem; } /* 8px */
.text-xs { font-size: 0.75rem; } /* 12px */
.text-sm { font-size: 0.875rem; } /* 14px */
.text-base{ font-size: 1rem; } /* 16px */
.text-lg { font-size: 1.125rem; } /* 18px */
.text-xl { font-size: 1.25rem; } /* 20px */
.text-2xl { font-size: 1.5rem; } /* 24px */
.text-3xl { font-size: 1.875rem; } /* 30px */
.text-4xl { font-size: 2.25rem; } /* 36px */
.text-5xl { font-size: 3rem; } /* 48px */
.text-6xl { font-size: 3.75rem; } /* 60px */
.text-7xl { font-size: 4.5rem; } /* 72px */
.text-8xl { font-size: 6rem; } /* 96px */
.text-9xl { font-size: 8rem; } /* 128px */
/* ==========================
Base Heights
========================== */
.h-0 { height: 0; }
.h-px { height: 1px; }
.h-1 { height: 0.25rem; } /* 4px */
.h-2 { height: 0.5rem; } /* 8px */
.h-3 { height: 0.75rem; } /* 12px */
.h-4 { height: 1rem; } /* 16px */
.h-5 { height: 1.25rem; } /* 20px */
.h-6 { height: 1.5rem; } /* 24px */
.h-8 { height: 2rem; } /* 32px */
.h-10 { height: 2.5rem; } /* 40px */
.h-12 { height: 3rem; } /* 48px */
.h-16 { height: 4rem; } /* 64px */
.h-20 { height: 5rem; } /* 80px */
.h-24 { height: 6rem; } /* 96px */
.h-32 { height: 8rem; } /* 128px */
.h-40 { height: 10rem; } /* 160px */
.h-48 { height: 12rem; } /* 192px */
.h-56 { height: 14rem; } /* 224px */
.h-64 { height: 16rem; } /* 256px */
.h-full { height: 100%; }
.h-screen{ height: 100vh; }
/* ==========================
Base Widths
========================== */
.w-0 { width: 0; }
.w-px { width: 1px; }
.w-1 { width: 0.25rem; }
.w-2 { width: 0.5rem; }
.w-3 { width: 0.75rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-8 { width: 2rem; }
.w-10 { width: 2.5rem; }
.w-12 { width: 3rem; }
.w-16 { width: 4rem; }
.w-20 { width: 5rem; }
.w-24 { width: 6rem; }
.w-32 { width: 8rem; }
.w-40 { width: 10rem; }
.w-48 { width: 12rem; }
.w-56 { width: 14rem; }
.w-64 { width: 16rem; }
.w-full { width: 100%; }
.w-screen{ width: 100vw; }
/* ==========================
Responsive Variants
========================== */
@media (min-width: 576px) { /* sm */
/* Font */
.text-xxs-sm { font-size: 0.55rem; }
.text-xs-sm { font-size: 0.75rem; }
.text-sm-sm { font-size: 0.875rem; }
.text-base-sm{ font-size: 1rem; }
.text-lg-sm { font-size: 1.125rem; }
.text-xl-sm { font-size: 1.25rem; }
.text-2xl-sm{ font-size: 1.5rem; }
/* Height */
.h-1-sm{ height: 0.25rem; }
.h-2-sm{ height: 0.5rem; }
.h-3-sm{ height: 0.75rem; }
.h-4-sm{ height: 1rem; }
.h-5-sm{ height: 1.25rem; }
.h-6-sm{ height: 1.5rem; }
.h-8-sm{ height: 2rem; }
.h-10-sm{ height: 2.5rem; }
/* Width */
.w-1-sm{ width: 0.25rem; }
.w-2-sm{ width: 0.5rem; }
.w-3-sm{ width: 0.75rem; }
.w-4-sm{ width: 1rem; }
.w-5-sm{ width: 1.25rem; }
.w-6-sm{ width: 1.5rem; }
.w-8-sm{ width: 2rem; }
.w-10-sm{ width: 2.5rem; }
}
@media (min-width: 768px) { /* md */
/* Font */
.text-xxs-md { font-size: 0.55rem; }
.text-xs-md { font-size: 0.75rem; }
.text-sm-md { font-size: 0.875rem; }
.text-base-md{ font-size: 1rem; }
.text-lg-md { font-size: 1.125rem; }
.text-xl-md { font-size: 1.25rem; }
.text-2xl-md{ font-size: 1.5rem; }
/* Height */
.h-1-md{ height: 0.25rem; }
.h-2-md{ height: 0.5rem; }
.h-3-md{ height: 0.75rem; }
.h-4-md{ height: 1rem; }
.h-5-md{ height: 1.25rem; }
.h-6-md{ height: 1.5rem; }
.h-8-md{ height: 2rem; }
.h-10-md{ height: 2.5rem; }
/* Width */
.w-1-md{ width: 0.25rem; }
.w-2-md{ width: 0.5rem; }
.w-3-md{ width: 0.75rem; }
.w-4-md{ width: 1rem; }
.w-5-md{ width: 1.25rem; }
.w-6-md{ width: 1.5rem; }
.w-8-md{ width: 2rem; }
.w-10-md{ width: 2.5rem; }
}
@media (min-width: 992px) { /* lg */
/* Font */
.text-xxs-lg { font-size: 0.55rem; }
.text-xs-lg { font-size: 0.75rem; }
.text-sm-lg { font-size: 0.875rem; }
.text-base-lg{ font-size: 1rem; }
.text-lg-lg { font-size: 1.125rem; }
.text-xl-lg { font-size: 1.25rem; }
.text-2xl-lg{ font-size: 1.5rem; }
/* Height */
.h-1-lg{ height: 0.25rem; }
.h-2-lg{ height: 0.5rem; }
.h-3-lg{ height: 0.75rem; }
.h-4-lg{ height: 1rem; }
.h-5-lg{ height: 1.25rem; }
.h-6-lg{ height: 1.5rem; }
.h-8-lg{ height: 2rem; }
.h-10-lg{ height: 2.5rem; }
/* Width */
.w-1-lg{ width: 0.25rem; }
.w-2-lg{ width: 0.5rem; }
.w-3-lg{ width: 0.75rem; }
.w-4-lg{ width: 1rem; }
.w-5-lg{ width: 1.25rem; }
.w-6-lg{ width: 1.5rem; }
.w-8-lg{ width: 2rem; }
.w-10-lg{ width: 2.5rem; }
}
@media (min-width: 1200px) { /* xl */
/* Font */
.text-xxs-xl { font-size: 0.55rem; }
.text-xs-xl { font-size: 0.75rem; }
.text-sm-xl { font-size: 0.875rem; }
.text-base-xl{ font-size: 1rem; }
.text-lg-xl { font-size: 1.125rem; }
.text-xl-xl { font-size: 1.25rem; }
.text-2xl-xl{ font-size: 1.5rem; }
/* Height */
.h-1-xl{ height: 0.25rem; }
.h-2-xl{ height: 0.5rem; }
.h-3-xl{ height: 0.75rem; }
.h-4-xl{ height: 1rem; }
.h-5-xl{ height: 1.25rem; }
.h-6-xl{ height: 1.5rem; }
.h-8-xl{ height: 2rem; }
.h-10-xl{ height: 2.5rem; }
/* Width */
.w-1-xl{ width: 0.25rem; }
.w-2-xl{ width: 0.5rem; }
.w-3-xl{ width: 0.75rem; }
.w-4-xl{ width: 1rem; }
.w-5-xl{ width: 1.25rem; }
.w-6-xl{ width: 1.5rem; }
.w-8-xl{ width: 2rem; }
.w-10-xl{ width: 2.5rem; }
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="120" fill="#EFF1F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"/>
</svg>

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -18613,6 +18613,10 @@ li:not(:first-child) .dropdown-item,
min-height: 70vh !important;
}
.modal-min-h{
min-height: 60vh !important;
}
.flex-fill {
flex: 1 1 auto !important;
}

View File

@ -1,12 +1,22 @@
import React from 'react'
import ManageOrganization from './components/Organization/ManageOrganization'
import { useOrganizationModal } from './hooks/useOrganization';
import React, { useEffect } from "react";
import { useOrganizationModal } from "./hooks/useOrganization";
import OrganizationModal from "./components/Organization/OrganizationModal";
import { useAuthModal, useModal } from "./hooks/useAuth";
import SwitchTenant from "./pages/authentication/SwitchTenant";
import ChangePasswordPage from "./pages/authentication/ChangePassword";
const ModalProvider = () => {
const { isOpen } = useOrganizationModal();
const { isOpen, onClose } = useOrganizationModal();
const { isOpen: isAuthOpen } = useAuthModal();
const {isOpen:isChangePass} = useModal("ChangePassword")
return <>{isOpen && <ManageOrganization />}</>;
return (
<>
{isOpen && <OrganizationModal />}
{isAuthOpen && <SwitchTenant />}
{isChangePass && <ChangePasswordPage /> }
</>
);
};
export default ModalProvider
export default ModalProvider;

View File

@ -12,22 +12,19 @@ import { useQueryClient } from "@tanstack/react-query";
import eventBus from "../../services/eventBus";
import { useSelectedProject } from "../../slices/apiDataManager";
const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, }) => {
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [todayDate, setTodayDate] = useState(new Date());
const [ShowPending, setShowPending] = useState(false);
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedProject();
const {
attendance,
loading: attLoading,
recall: attrecall,
isFetching
} = useAttendance(selectedProject);
} = useAttendance(selectedProject, organizationId);
const filteredAttendance = ShowPending
? attendance?.filter(
(att) => att?.checkInTime !== null && att?.checkOutTime === null
@ -62,12 +59,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
const role = item.jobRoleName?.toLowerCase() || "";
return (
fullName.includes(lowercasedSearchTerm) ||
role.includes(lowercasedSearchTerm) // also search by role
role.includes(lowercasedSearchTerm) // also search by role
);
});
}, [group1, group2, searchTerm]);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
finalFilteredData,
ITEMS_PER_PAGE
@ -116,7 +112,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<>
<div
className="table-responsive text-nowrap h-100"
style={{ minHeight: "200px" }} // 🔹 Ensures fixed height
style={{ minHeight: "200px" }} // Ensures fixed height
>
<div className="d-flex text-start align-items-center py-2">
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
@ -142,6 +138,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
<tr className="border-top-1">
<th colSpan={2}>Name</th>
<th>Role</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>
Check-In
@ -190,6 +187,8 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</td>
<td>{item.jobRoleName}</td>
<td>{item.organizationName || "--"}</td>
<td>
{item.checkInTime
? convertShortTime(item.checkInTime)
@ -213,7 +212,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
))}
{!attendance && (
<tr>
<td colSpan={6} className="text-center text-secondary" style={{ height: "200px" }}>
<td
colSpan={7}
className="text-center text-secondary"
style={{ height: "200px" }}
>
No employees assigned to the project!
</td>
</tr>
@ -221,6 +224,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
</tbody>
</table>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">

View File

@ -5,7 +5,11 @@ import { convertShortTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux";
import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import {
clearCacheKey,
getCachedData,
useSelectedProject,
} from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus";
import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance";
@ -33,195 +37,142 @@ const usePagination = (data, itemsPerPage) => {
};
};
const AttendanceLog = ({ handleModalData, searchTerm }) => {
// const selectedProject = useSelector(
// (store) => store.localVariables.projectId
// );
const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false)
const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const selectedProject = useSelectedProject();
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [processedData, setProcessedData] = useState([]);
const today = new Date();
today.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const isSameDay = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() === today.getTime();
};
const isSameDay = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() === today.getTime();
};
const isBeforeToday = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() < today.getTime();
};
const isBeforeToday = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() < today.getTime();
};
const sortByName = (a, b) => {
const nameA = (a.firstName + a.lastName).toLowerCase();
const nameB = (b.firstName + b.lastName).toLowerCase();
return nameA.localeCompare(nameB);
};
const sortByName = (a, b) => {
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
return nameA?.localeCompare(nameB);
};
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs(
selectedProject,
dateRange.startDate,
dateRange.endDate,
organizationId
);
const {
data = [],
isLoading,
error,
refetch,
isFetching,
} = useAttendancesLogs(
selectedProject,
dateRange.startDate,
dateRange.endDate
);
const filtering = (data) => {
const filteredData = showPending
? data.filter((item) => item.checkOutTime === null)
: data;
const processedData = useMemo(() => {
const filteredData = showPending
? data.filter((item) => item.checkOutTime === null)
: data;
const group1 = filteredData
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
.sort(sortByName);
const group2 = filteredData
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
.sort(sortByName);
const group3 = filteredData
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
.sort(sortByName);
const group4 = filteredData.filter(
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
);
const group5 = filteredData
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
.sort(sortByName);
const group6 = filteredData
.filter((d) => d.activity === 5)
.sort(sortByName);
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName);
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName);
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName);
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime));
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName);
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName);
const sortedList = [
...group1,
...group2,
...group3,
...group4,
...group5,
...group6,
];
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6];
// Group by date
const groupedByDate = sortedList.reduce((acc, item) => {
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
if (date) {
acc[date] = acc[date] || [];
acc[date].push(item);
}
return acc;
}, {});
const sortedDates = Object.keys(groupedByDate).sort(
(a, b) => new Date(b) - new Date(a)
);
const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
setProcessedData(finalData);
};
useEffect(() => {
filtering(data);
}, [data, showPending]);
// New useEffect to handle search filtering
const filteredSearchData = useMemo(() => {
if (!searchTerm) {
return processedData;
const groupedByDate = sortedList.reduce((acc, item) => {
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
if (date) {
acc[date] = acc[date] || [];
acc[date].push(item);
}
const lowercasedSearchTerm = searchTerm.toLowerCase();
return processedData.filter((item) => {
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
return fullName.includes(lowercasedSearchTerm);
});
}, [processedData, searchTerm]);
return acc;
}, {});
const {
currentPage,
totalPages,
currentItems: paginatedAttendances,
paginate,
resetPage,
} = usePagination(filteredSearchData, 20);
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
return sortedDates.flatMap((date) => groupedByDate[date]);
}, [data, showPending]);
useEffect(() => {
resetPage();
}, [filteredSearchData, resetPage]);
const filteredSearchData = useMemo(() => {
if (!searchTerm) return processedData;
const handler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10);
if (
selectedProject === msg.projectId &&
startDate <= checkIn &&
checkIn <= endDate
) {
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
return;
}
const updatedAttendance = oldData.map((record) =>
record.id === msg.response.id
? { ...record, ...msg.response }
: record
);
filtering(updatedAttendance);
return updatedAttendance;
});
resetPage();
}
},
[selectedProject, dateRange, filtering, resetPage]
const lowercased = searchTerm.toLowerCase();
return processedData.filter((item) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased)
);
}, [processedData, searchTerm]);
useEffect(() => {
eventBus.on("attendance_log", handler);
return () => eventBus.off("attendance_log", handler);
}, [handler]);
const {
currentPage,
totalPages,
currentItems: paginatedAttendances,
paginate,
resetPage,
} = usePagination(filteredSearchData, 20);
const employeeHandler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) {
// dispatch(
// fetchAttendanceData({
// ,
// fromDate: startDate,
// toDate: endDate,
// })
// );
useEffect(() => {
resetPage();
}, [filteredSearchData]);
const handler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10);
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) {
queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
return;
}
return oldData.map((record) =>
record.id === msg.response.id ? { ...record, ...msg.response } : record
);
});
resetPage();
}
},
[selectedProject, dateRange, resetPage]
);
useEffect(() => {
eventBus.on("attendance_log", handler);
return () => eventBus.off("attendance_log", handler);
}, [handler]);
const employeeHandler = useCallback(
(msg) => {
const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) {
refetch();
}
},
[data, refetch]
);
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
refetch()
}
},
[selectedProject, dateRange, data, refetch]
);
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
return (
<>
<div
className="dataTables_length text-start py-2 d-flex justify-content-between"
className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length"
>
<div className="d-flex align-items-center my-0 ">
@ -229,7 +180,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
onRangeChange={setDateRange}
defaultStartDate={yesterday}
/>
<div className="form-check form-switch text-start m-0 ms-5">
<div className="form-check form-switch text-start ms-1 ms-md-2 align-items-center mb-0">
<input
type="checkbox"
className="form-check-input"
@ -242,18 +193,16 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
<label className="form-check-label ms-0">Show Pending</label>
</div>
</div>
<div className="col-md-2 m-0 text-end">
<i
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
}`}
title="Refresh"
onClick={() => refetch()}
/>
</div>
</div>
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
<div
className="table-responsive text-nowrap"
style={{ minHeight: "200px" }}
>
{isLoading ? (
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<p className="text-secondary">Loading...</p>
</div>
) : filteredSearchData?.length > 0 ? (
@ -264,6 +213,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
Name
</th>
<th className="border-top-1">Date</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>{" "}
Check-In
@ -282,9 +232,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
const previousAttendance = arr[index - 1];
const previousDate = previousAttendance
? moment(
previousAttendance.checkInTime ||
previousAttendance.checkOutTime
).format("YYYY-MM-DD")
previousAttendance.checkInTime ||
previousAttendance.checkOutTime
).format("YYYY-MM-DD")
: null;
if (!previousDate || currentDate !== previousDate) {
@ -293,7 +243,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
key={`header-${currentDate}`}
className="table-row-header"
>
<td colSpan={6} className="text-start">
<td colSpan={8} className="text-start">
<strong>
{moment(currentDate).format("DD-MM-YYYY")}
</strong>
@ -323,6 +273,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
attendance.checkInTime || attendance.checkOutTime
).format("DD-MMM-YYYY")}
</td>
<td>{attendance.organizationName || "--"}</td>
<td>{convertShortTime(attendance.checkInTime)}</td>
<td>
{attendance.checkOutTime
@ -344,7 +295,12 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
</tbody>
</table>
) : (
<div className="my-4"><span className="text-secondary">No Record Available !</span></div>
<div className="my-12">
<span className="text-secondary">
No data available for the selected date range. Please Select
another date.
</span>
</div>
)}
</div>
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
@ -370,8 +326,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
(pageNumber) => (
<li
key={pageNumber}
className={`page-item ${currentPage === pageNumber ? "active" : ""
}`}
className={`page-item ${
currentPage === pageNumber ? "active" : ""
}`}
>
<button
className="page-link"
@ -383,8 +340,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
)
)}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link"

View File

@ -33,7 +33,7 @@ const createSchema = (modeldata) => {
const checkOut = new Date(checkIn);
checkOut.setHours(hour, minute, 0, 0);
return checkOut > checkIn;
return checkOut >= checkIn;
}
return true;
}, {
@ -96,12 +96,12 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
};
return (
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 d-flex justify-content-center">
<label className="fs-5 text-dark text-center">
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 d-flex justify-content-center mb-4">
<label className="fs-5 tex-semibold text-center">
{modeldata?.checkInTime && !modeldata?.checkOutTime
? "Check-out :"
: "Check-in :"}
? "Check-Out "
: "Check-In "}
</label>
</div>
@ -120,7 +120,7 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
modeldata?.checkInTime && !modeldata?.checkOutTime
? formatDate(modeldata?.checkInTime?.split("T")[0]) || ""
: formatDate(today)
}
}
disabled
/>
</div>

View File

@ -1,4 +1,3 @@
import React, { useState, useEffect } from "react";
import "../../components/Project/ProjectInfra.css";
import BuildingModel from "../Project/Infrastructure/BuildingModel";
@ -8,25 +7,33 @@ import WorkAreaModel from "../Project/Infrastructure/WorkAreaModel";
import TaskModel from "../Project/Infrastructure/TaskModel";
import ProjectRepository from "../../repositories/ProjectRepository";
import Breadcrumb from "../../components/common/Breadcrumb";
import {useProjectDetails, useProjectInfra, useProjects} from "../../hooks/useProjects";
import {useHasUserPermission} from "../../hooks/useHasUserPermission";
import {APPROVE_TASK, ASSIGN_REPORT_TASK, MANAGE_PROJECT_INFRA} from "../../utils/constants";
import {useDispatch, useSelector} from "react-redux";
import {useProfile} from "../../hooks/useProfile";
import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
import {
useCurrentService,
useProjectDetails,
useProjectInfra,
useProjects,
} from "../../hooks/useProjects";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
APPROVE_TASK,
ASSIGN_REPORT_TASK,
MANAGE_PROJECT_INFRA,
} from "../../utils/constants";
import { useDispatch, useSelector } from "react-redux";
import { useProfile } from "../../hooks/useProfile";
import { refreshData, setProjectId } from "../../slices/localVariablesSlice";
import InfraTable from "../Project/Infrastructure/InfraTable";
import { useSelectedProject } from "../../slices/apiDataManager";
import Loader from "../common/Loader";
const InfraPlanning = () => {
const { profile: LoggedUser, refetch: fetchData } = useProfile();
const dispatch = useDispatch();
const selectedProject = useSelectedProject();
const selectedService = useCurrentService();
const { projectInfra, isLoading, isError, error, isFetched } = useProjectInfra(selectedProject);
const { projectInfra, isLoading, isError, error, isFetched } =
useProjectInfra(selectedProject, selectedService || "" );
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const canApproveTask = useHasUserPermission(APPROVE_TASK);
@ -55,7 +62,7 @@ const InfraPlanning = () => {
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return (
<div className="card text-center">
<div className="text-center">
<p className="my-3">No Result Found</p>
</div>
);
@ -63,11 +70,9 @@ const InfraPlanning = () => {
return (
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card">
<div className="card-body" style={{ padding: "0.5rem" }}>
<div className="row">
<InfraTable buildings={projectInfra} projectId={selectedProject} />
</div>
<div className="card-body" style={{ padding: "0.5rem" }}>
<div className="row">
<InfraTable buildings={projectInfra} projectId={selectedProject} />
</div>
</div>
</div>
@ -75,4 +80,3 @@ const InfraPlanning = () => {
};
export default InfraPlanning;

View File

@ -1,25 +1,43 @@
import React, { useCallback, useEffect, useState, useMemo } from "react";
import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils";
import RegularizationActions from "./RegularizationActions";
import { useSelector } from "react-redux";
import { useRegularizationRequests } from "../../hooks/useAttendance";
import moment from "moment";
import usePagination from "../../hooks/usePagination";
import eventBus from "../../services/eventBus";
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
import {
cacheData,
clearCacheKey,
useSelectedProject,
} from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query";
import Pagination from "../../components/common/Pagination";
const Regularization = ({ handleRequest, searchTerm }) => {
const Regularization = ({
handleRequest,
searchTerm,
projectId,
organizationId,
IncludeInActive,
}) => {
const queryClient = useQueryClient();
// var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]);
const { regularizes, loading, error, refetch } =
useRegularizationRequests(selectedProject);
const { regularizes, loading, error, refetch } = useRegularizationRequests(
selectedProject,
organizationId,
IncludeInActive
);
useEffect(() => {
if(!regularizes) return
if(regularizes?.length) {
setregularizedList(regularizes);
}
}, [regularizes]);
const sortByName = (a, b) => {
@ -54,18 +72,15 @@ const Regularization = ({ handleRequest, searchTerm }) => {
}
const lowercasedSearchTerm = searchTerm.toLowerCase();
return sortedList.filter((item) => {
const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
const fullName = `${item?.firstName} ${item?.lastName}`.toLowerCase();
return fullName.includes(lowercasedSearchTerm);
});
}, [regularizesList, searchTerm]);
const { currentPage, totalPages, currentItems, paginate } =
usePagination(filteredSearchData, 20);
// Reset pagination when the search term or data changes
useEffect(() => {
}, [filteredSearchData]);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
filteredSearchData,
20
);
useEffect(() => {
eventBus.on("regularization", handler);
@ -87,9 +102,15 @@ const Regularization = ({ handleRequest, searchTerm }) => {
}, [employeeHandler]);
return (
<div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
<div
className="table-responsive text-nowrap pb-4"
style={{ minHeight: "200px" }}
>
{loading ? (
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<p className="text-secondary">Loading...</p>
</div>
) : currentItems?.length > 0 ? (
@ -98,24 +119,28 @@ const Regularization = ({ handleRequest, searchTerm }) => {
<tr>
<th colSpan={2}>Name</th>
<th>Date</th>
<th>Organization</th>
<th>
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
</th>
<th>
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
</th>
<th colSpan={2}>
Requested By
</th>
<th >
Requested At
</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{currentItems?.map((att, index) => (
<tr key={index}>
<td colSpan={2}>
<td colSpan={2}>
<div className="d-flex justify-content-start align-items-center">
<Avatar
firstName={att.firstName}
lastName={att.lastName}
></Avatar>
<Avatar firstName={att.firstName} lastName={att.lastName} />
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal">
@ -126,9 +151,28 @@ const Regularization = ({ handleRequest, searchTerm }) => {
</div>
</td>
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
<td>{att.organizationName || "--"}</td>
<td>{convertShortTime(att.checkInTime)}</td>
<td>
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
</td>
<td colSpan={2}>
{att.requestedBy ? ( <div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal">
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName}
</span>
</a>
</div>
</div>):(<small>--</small>)}
</td>
<td>
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt,true) : "--"}
</td>
<td className="text-center ">
<RegularizationActions
@ -136,7 +180,6 @@ const Regularization = ({ handleRequest, searchTerm }) => {
handleRequest={handleRequest}
refresh={refetch}
/>
{/* </div> */}
</td>
</tr>
))}
@ -154,7 +197,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
</span>
</div>
)}
{!loading && totalPages > 1 && (
{/* {!loading && totalPages > 1 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1 mt-3">
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
@ -192,6 +235,14 @@ const Regularization = ({ handleRequest, searchTerm }) => {
</li>
</ul>
</nav>
)} */}
{totalPages > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={paginate}
/>
)}
</div>
);

View File

@ -77,7 +77,7 @@ export const ReportTask = ({ report, closeModal }) => {
return (
<div className="container m-0">
<div className="text-center">
<p className="fs-6 fw-semibold">Report Task</p>
<p className="fs-5 fw-semibold">Report Task</p>
</div>
<div className="mb-1 row text-start">
<label htmlFor="html5-text-input" className="col-md-4 col-form-label">
@ -101,16 +101,14 @@ export const ReportTask = ({ report, closeModal }) => {
<label htmlFor="html5-email-input" className="col-md-4 col-form-label">
Wrok Area :
</label>
<div className="col-md-8 text-start text-wrap">
<label className=" col-form-label">
{" "}
{report?.workItem?.workArea?.floor?.building?.name}{" "}
<i className="bx bx-chevron-right"></i>{" "}
{report?.workItem?.workArea?.floor?.floorName}{" "}
<i className="bx bx-chevron-right"> </i>
{report?.workItem?.workArea?.areaName}
</label>
</div>
<div className="col-md-8 text-start">
<div className="text-wrap">
{report?.workItem?.workArea?.floor?.building?.name} <i className="bx bx-chevron-right"></i>
{report?.workItem?.workArea?.floor?.floorName} <i className="bx bx-chevron-right"></i>
{report?.workItem?.workArea?.areaName}
</div>
</div>
</div>
<div className="mb-1 row text-start">
<label htmlFor="html5-email-input" className="col-md-4 col-form-label">

View File

@ -110,6 +110,8 @@ const ReportTaskComments = ({
approvedTask: defaultCompletedTask || 0,
});
}, [defaultCompletedTask]);
const completed_Task = watch("approvedTask")
return (
<div className="p-2 p-sm-1">
<div className="modal-body p-sm-4 p-0">
@ -339,13 +341,13 @@ const ReportTaskComments = ({
<div
className={` ${
actionAllow && !commentsData.approvedBy
? " d-flex justify-content-between"
? " d-flex justify-content-between align-items-center"
: "text-end"
} mt-2`}
>
<div
className={`form-check ${
!(actionAllow && !commentsData.approvedBy) && "d-none"
!(actionAllow && !commentsData.approvedBy && defaultCompletedTask > completed_Task ) && "d-none"
} `}
>
<input
@ -383,7 +385,7 @@ const ReportTaskComments = ({
: "Comment"}
</button>
</span>
</div>
</div>
</form>
<ul

View File

@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { string, z } from "zod";
import {
useActivitiesMaster,
useServices,
useWorkCategoriesMaster,
} from "../../hooks/masterHook/useMaster";
import showToast from "../../services/toastService";
@ -25,6 +26,8 @@ const SubTask = ({ activity, onClose }) => {
const { activities, loading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster();
const { Task, loading: TaskLoading } = useTaskById(activity?.id);
const {data,isError,isLoading,error} = useServices();
const {
register,
handleSubmit,
@ -97,8 +100,8 @@ const SubTask = ({ activity, onClose }) => {
};
return (
<div className="container-xxl my-1">
<p className="fw-semibold">Create Sub Task</p>
<form className="row g-2" onSubmit={handleSubmit(onSubmitForm)}>
<p className="fw-semibold fs-5">Create Sub Task</p>
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmitForm)}>
<div className="col-6">
<label className="form-label">Building</label>
<input
@ -128,27 +131,15 @@ const SubTask = ({ activity, onClose }) => {
disabled
/>
</div>
<div className="col-12">
<label className="form-label">Work Category</label>
<select
className="form-select form-select-sm"
{...register("workCategoryId")}
onChange={handleCategoryChange}
>
<option value="">
{categoryLoading ? "Loading..." : "-- Select Category --"}
</option>
{categoryData.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
{errors.workCategoryId && (
<div className="danger-text">{errors.workCategoryId.message}</div>
)}
</div>
<div className="col-12">
<label className="form-label">Service</label>
<input
type="text"
className="form-control form-control-sm"
value={activity?.workItem?.activityMaster?.activityGroup?.service?.name || ""}
disabled
/>
</div>
<div className="col-12">
<label className="form-label">Select Activity</label>
<select
@ -172,6 +163,27 @@ const SubTask = ({ activity, onClose }) => {
)}
</div>
<div className="col-12">
<label className="form-label">Work Category</label>
<select
className="form-select form-select-sm"
{...register("workCategoryId")}
onChange={handleCategoryChange}
>
<option value="">
{categoryLoading ? "Loading..." : "-- Select Category --"}
</option>
{categoryData.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
{errors.workCategoryId && (
<div className="danger-text">{errors.workCategoryId.message}</div>
)}
</div>
<div className="col-4">
<label className="form-label">Planned Work</label>
<input
@ -219,7 +231,15 @@ const SubTask = ({ activity, onClose }) => {
)}
</div>
<div className="col-12 text-center">
<div className="d-flex flex-row gap-3 justify-content-end py-2">
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={() => onClose()}
disabled={isPending}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary me-2"
@ -227,14 +247,7 @@ const SubTask = ({ activity, onClose }) => {
>
{isPending ? "Please wait..." : "Submit"}
</button>
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={() => onClose()}
disabled={isPending}
>
Cancel
</button>
</div>
</form>
</div>

View File

@ -1,42 +0,0 @@
import React, { createContext, useState, useContext } from "react";
import ChangePasswordPage from "../../pages/authentication/ChangePassword";
const ChangePasswordContext = createContext();
export const ChangePasswordProvider = ({ children }) => {
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const openChangePassword = () => setIsChangePasswordOpen(true);
const closeChangePassword = () => setIsChangePasswordOpen(false);
return (
<ChangePasswordContext.Provider
value={{ isChangePasswordOpen, openChangePassword, closeChangePassword }}
>
{children}
{isChangePasswordOpen && (
<>
{/* This is the main Bootstrap modal container */}
{/* It provides the fixed positioning and high z-index */}
<div
className="modal fade show" // 'fade' for animation, 'show' to make it visible
style={{ display: 'block' }} // Explicitly set display: block for immediate visibility
tabIndex="-1" // Makes the modal focusable
role="dialog" // ARIA role for accessibility
aria-labelledby="changePasswordModalLabel" // Link to a heading for accessibility
aria-modal="true" // Indicate it's a modal dialog
>
{/* The ChangePasswordPage component itself contains the modal-dialog and modal-content */}
<ChangePasswordPage onClose={closeChangePassword} />
</div>
{/* The modal backdrop */}
<div className="modal-backdrop fade show"></div>
</>
)}
</ChangePasswordContext.Provider>
);
};
export const useChangePassword = () => useContext(ChangePasswordContext);

View File

@ -0,0 +1,107 @@
import React, { useState } from "react";
import { useCurrentService } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
TaskReportDefaultValue,
TaskReportFilterSchema,
} from "./TaskRportScheam";
import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
import { localToUtc } from "../../utils/appUtils";
import { useTaskFilter } from "../../hooks/useTasks";
const TaskReportFilterPanel = ({ handleFilter }) => {
const [resetKey, setResetKey] = useState(0);
const selectedProject = useSelectedProject();
const selectedService = useCurrentService();
const { data } = useTaskFilter(selectedProject);
const methods = useForm({
resolver: zodResolver(TaskReportFilterSchema),
defaultValues: TaskReportDefaultValue,
});
const {
register,
reset,
handleSubmit,
formState: { errors },
} = methods;
const onSubmit = (formData) => {
const filterPayload = {
...formData,
dateFrom: localToUtc(formData.dateFrom),
dateTo: localToUtc(formData.dateTo),
};
handleFilter(filterPayload);
};
const onClear = () => {
setResetKey((prev) => prev + 1);
handleFilter(TaskReportDefaultValue);
reset(TaskReportDefaultValue);
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="mb-3 w-100">
<label className="fw-semibold">Choose Date Range:</label>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="dateFrom"
endField="dateTo"
resetSignal={resetKey}
defaultRange={false}
maxDate={new Date()}
/>
</div>
<div className="row mb-2">
<SelectMultiple
name="buildingIds"
label="Building"
options={data?.buildings}
labelKey="name"
valueKey="id"
/>
</div>
<div className="row mb-2">
<SelectMultiple
name="floorIds"
label="Floor"
options={data?.floors}
labelKey="name"
valueKey="id"
/>
</div>
<div className="row mb-2">
<SelectMultiple
name="activityIds"
label="Activities"
options={data?.activities}
labelKey="name"
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
);
};
export default TaskReportFilterPanel;

View File

@ -0,0 +1,301 @@
import React, { useState, useEffect, useMemo } from "react";
import { useTaskList } from "../../hooks/useTasks";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
import DailyProgrssReport, {
useDailyProgrssContext,
} from "../../pages/DailyProgressReport/DailyProgrssReport";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import {
APPROVE_TASK,
ASSIGN_REPORT_TASK,
ITEMS_PER_PAGE,
} from "../../utils/constants";
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../common/Pagination";
import { TaskReportListSkeleton } from "./TaskRepprtListSkeleton";
import HoverPopup from "../common/HoverPopup";
const TaskReportList = () => {
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState({
selectedBuilding: "",
selectedFloors: [],
selectedActivities: [],
});
const dispatch = useDispatch();
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { service, openModal, closeModal,filter } = useDailyProgrssContext();
const selectedProject = useSelectedProject();
const { projectNames } = useProjectName();
const { data, isLoading, isError, error } = useTaskList(
selectedProject,
ITEMS_PER_PAGE,
currentPage,
service,filter
);
const ProgrssReportColumn = [
{
key: "activity",
label: "Activity",
getValue: (task) => task.workItem.activityMaster?.activityName || "N/A",
align: "text-start",
},
{
key: "assigned",
label: "Total Assigned",
getValue: (task) => task.plannedTask ?? "N/A",
align: "text-start",
},
{
key: "completed",
label: "Completed",
getValue: (task) => task.completedTask ?? "N/A",
align: "text-start",
},
{
key: "assignAt",
label: "Assign Date",
getValue: (task) =>
task.assignmentDate ? formatUTCToLocalTime(task.assignmentDate) : "N/A",
align: "text-start",
},
{
key: "team",
label: "Team",
getValue: (task) =>
task.teamMembers?.map((m) => `${m.firstName} ${m.lastName}`).join(", ") ||
"N/A",
align: "text-start",
},
];
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
useEffect(() => {
if (!selectedProject && projectNames.length > 0) {
dispatch(setProjectId(projectNames[0].id));
}
}, [selectedProject, projectNames, dispatch]);
useEffect(() => {
setFilters({
selectedBuilding: "",
selectedFloors: [],
selectedActivities: [],
});
}, [selectedProject]);
// Filter and Group wise data
const filteredTasks = useMemo(() => {
if (!data?.data) return [];
return data?.data.filter((task) => {
const { selectedBuilding, selectedFloors, selectedActivities } = filters;
if (
selectedBuilding &&
task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding
)
return false;
if (
selectedFloors.length > 0 &&
!selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)
)
return false;
if (
selectedActivities.length > 0 &&
!selectedActivities.includes(
task?.workItem?.activityMaster?.activityName
)
)
return false;
return true;
});
}, [data?.data, filters, currentPage]);
const groupedTasks = useMemo(() => {
const groups = {};
filteredTasks.forEach((task) => {
const date = task.assignmentDate.split("T")[0];
if (!groups[date]) groups[date] = [];
groups[date].push(task);
});
return Object.keys(groups)
.sort((a, b) => new Date(b) - new Date(a))
.map((date) => ({ date, tasks: groups[date] }));
}, [filteredTasks, paginate, currentPage, selectedProject]);
const renderTeamMembers = (task, refIndex) => (
<div
key={refIndex}
tabIndex="0"
className="d-flex align-items-center avatar-group justify-content-center"
data-bs-toggle="popover"
data-bs-trigger="focus"
data-bs-placement="left"
data-bs-html="true"
data-bs-content={`
<div class="border border-secondary rounded custom-popover p-2 px-3">
${task.teamMembers
.map(
(m) => `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="avatar avatar-xs">
<span class="avatar-initial rounded-circle bg-label-primary">
${m?.firstName?.charAt(0) || ""}${m?.lastName?.charAt(0) || ""
}
</span>
</div>
<span>${m.firstName} ${m.lastName}</span>
</div>`
)
.join("")}
</div>
`}
>
{task.teamMembers.slice(0, 3).map((m) => (
<div
key={m.id}
className="avatar avatar-xs"
title={`${m.firstName} ${m.lastName}`}
>
<span className="avatar-initial rounded-circle bg-label-primary">
{m?.firstName.slice(0, 1)}
</span>
</div>
))}
{task.teamMembers.length > 3 && (
<div
className="avatar avatar-xs"
title={`${task.teamMembers.length - 3} more`}
>
<span className="avatar-initial rounded-circle bg-label-secondary">
+{task.teamMembers.length - 3}
</span>
</div>
)}
</div>
);
if (isLoading) return <TaskReportListSkeleton />;
if (isError) return <div>Loading....</div>;
return (
<div className="mt-2 table-responsive text-nowrap">
<table className="table">
<thead>
<tr>
<th className="text-start">Activity</th>
<th>
<span>
Total Pending{" "}
<HoverPopup
title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>
<span>
Reported/Planned{" "}
<HoverPopup
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{groupedTasks.length === 0 && (
<tr>
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
No reports available
</td>
</tr>
)}
{groupedTasks.map(({ date, tasks }) => (
<React.Fragment key={date}>
<tr className="table-row-header text-start">
<td colSpan={6}>
<strong>{formatUTCToLocalTime(date)}</strong>
</td>
</tr>
{tasks.map((task, idx) => (
<tr key={task.id || idx}>
<td className="flex-wrap text-start">
<div>
{task.workItem.activityMaster?.activityName || "No Activity Name"}
</div>
<div className="text-sm py-2">
{task.workItem.workArea?.floor?.building?.name} {" "}
{task.workItem.workArea?.floor?.floorName} {" "}
{task.workItem.workArea?.areaName}
</div>
</td>
<td>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
);
};
export default TaskReportList;

View File

@ -0,0 +1,62 @@
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
export const TaskReportListSkeleton = () => {
const skeletonRows = 8; // Number of placeholder rows
return (
<div>
<table className="table">
<thead>
<tr>
<th>Activity</th>
<th>Assigned</th>
<th>Completed</th>
<th>Assign On</th>
<th>Team</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{[...Array(skeletonRows)].map((_, idx) => (
<tr key={idx}>
<td>
<SkeletonLine height={16} width="70%" />
<SkeletonLine height={12} width="50%" />
</td>
<td>
<SkeletonLine height={16} width="60%" />
</td>
<td>
<SkeletonLine height={16} width="60%" />
</td>
<td>
<SkeletonLine height={16} width="80%" />
</td>
<td className="text-center">
<div className="d-flex justify-content-center gap-1">
{[...Array(3)].map((_, i) => (
<SkeletonLine key={i} height={24} width={24} className="rounded-circle" />
))}
</div>
</td>
<td>
<div className="d-flex justify-content-end gap-2">
<SkeletonLine height={24} width="60px" />
<SkeletonLine height={24} width="60px" />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@ -0,0 +1,17 @@
import { z } from "zod";
export const TaskReportFilterSchema = z.object({
buildingIds: z.array(z.string()).optional(),
floorIds: z.array(z.string()).optional(),
activityIds: z.array(z.string()).optional(),
dateFrom: z.string().optional(),
dateTo: z.string().optional(),
});
export const TaskReportDefaultValue = {
buildingIds:[],
floorIds:[],
activityIds:[],
dateFrom:null,
dateTo:null
}

View File

@ -1,194 +1,194 @@
import React, { useState, useEffect } from "react";
import LineChart from "../Charts/LineChart";
import { useProjects } from "../../hooks/useProjects";
import { useDashboard_ActivityData } from "../../hooks/useDashboard_Data";
import ApexChart from "../Charts/Circlechart";
// import React, { useState, useEffect } from "react";
// import LineChart from "../Charts/LineChart";
// import { useProjects } from "../../hooks/useProjects";
// import { useDashboard_ActivityData } from "../../hooks/useDashboard_Data";
// import ApexChart from "../Charts/Circlechart";
const LOCAL_STORAGE_PROJECT_KEY = "selectedActivityProjectId";
// const LOCAL_STORAGE_PROJECT_KEY = "selectedActivityProjectId";
const Activity = () => {
const { projects } = useProjects();
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
const [selectedDate, setSelectedDate] = useState(today);
const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
const initialProjectId = storedProjectId || "all";
const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
const [displayedProjectName, setDisplayedProjectName] = useState("Select Project");
const [activeTab, setActiveTab] = useState("all");
// const Activity = () => {
// const { projects } = useProjects();
// const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
// const [selectedDate, setSelectedDate] = useState(today);
// const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
// const initialProjectId = storedProjectId || "all";
// const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
// const [displayedProjectName, setDisplayedProjectName] = useState("Select Project");
// const [activeTab, setActiveTab] = useState("all");
const { dashboard_Activitydata: ActivityData, isLoading, error: isError } =
useDashboard_ActivityData(selectedDate, selectedProjectId);
// const { dashboard_Activitydata: ActivityData, isLoading, error: isError } =
// useDashboard_ActivityData(selectedDate, selectedProjectId);
useEffect(() => {
if (selectedProjectId === "all") {
setDisplayedProjectName("All Projects");
} else if (projects) {
const foundProject = projects.find((p) => p.id === selectedProjectId);
setDisplayedProjectName(foundProject ? foundProject.name : "Select Project");
} else {
setDisplayedProjectName("Select Project");
}
// useEffect(() => {
// if (selectedProjectId === "all") {
// setDisplayedProjectName("All Projects");
// } else if (projects) {
// const foundProject = projects.find((p) => p.id === selectedProjectId);
// setDisplayedProjectName(foundProject ? foundProject.name : "Select Project");
// } else {
// setDisplayedProjectName("Select Project");
// }
localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
}, [selectedProjectId, projects]);
// localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
// }, [selectedProjectId, projects]);
const handleProjectSelect = (projectId) => {
setSelectedProjectId(projectId);
};
// const handleProjectSelect = (projectId) => {
// setSelectedProjectId(projectId);
// };
const handleDateChange = (e) => {
setSelectedDate(e.target.value);
};
// const handleDateChange = (e) => {
// setSelectedDate(e.target.value);
// };
return (
<div className="card h-100">
<div className="card-header">
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0">
<div className="card-title mb-0 text-start">
<h5 className="mb-1">Activity</h5>
<p className="card-subtitle">Activity Progress Chart</p>
</div>
// return (
// <div className="card h-100">
// <div className="card-header">
// <div className="d-flex flex-wrap justify-content-between align-items-center mb-0">
// <div className="card-title mb-0 text-start">
// <h5 className="mb-1">Activity</h5>
// <p className="card-subtitle text-primary">Activity Progress Chart</p>
// </div>
<div className="btn-group">
<button
className="btn btn-outline-primary btn-sm dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{displayedProjectName}
</button>
<ul className="dropdown-menu">
<li>
<button className="dropdown-item" onClick={() => handleProjectSelect("all")}>
All Projects
</button>
</li>
{projects?.map((project) => (
<li key={project.id}>
<button
className="dropdown-item"
onClick={() => handleProjectSelect(project.id)}
>
{project.name}
</button>
</li>
))}
</ul>
</div>
</div>
</div>
// <div className="btn-group">
// <button
// className="btn btn-outline-primary btn-sm dropdown-toggle"
// type="button"
// data-bs-toggle="dropdown"
// aria-expanded="false"
// >
// {displayedProjectName}
// </button>
// <ul className="dropdown-menu">
// <li>
// <button className="dropdown-item" onClick={() => handleProjectSelect("all")}>
// All Projects
// </button>
// </li>
// {projects?.map((project) => (
// <li key={project.id}>
// <button
// className="dropdown-item"
// onClick={() => handleProjectSelect(project.id)}
// >
// {project.name}
// </button>
// </li>
// ))}
// </ul>
// </div>
// </div>
// </div>
{/* ✅ Date Picker Aligned Left with Padding */}
<div className="d-flex justify-content-start ps-3 mb-3">
<div style={{ width: "150px" }}>
<input
type="date"
className="form-control"
value={selectedDate}
onChange={handleDateChange}
/>
</div>
</div>
// {/* Date Picker Aligned Left with Padding */}
// <div className="d-flex justify-content-start ps-3 mb-3">
// <div style={{ width: "150px" }}>
// <input
// type="date"
// className="form-control"
// value={selectedDate}
// onChange={handleDateChange}
// />
// </div>
// </div>
{/* Tabs */}
<ul className="nav nav-tabs " role="tablist">
<li className="nav-item">
<button
type="button"
className={`nav-link ${activeTab === "all" ? "active" : ""}`}
onClick={() => setActiveTab("all")}
data-bs-toggle="tab"
>
Summary
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${activeTab === "logs" ? "active" : ""}`}
onClick={() => setActiveTab("logs")}
data-bs-toggle="tab"
>
Details
</button>
</li>
</ul>
// {/* Tabs */}
// <ul className="nav nav-tabs " role="tablist">
// <li className="nav-item">
// <button
// type="button"
// className={`nav-link ${activeTab === "all" ? "active" : ""}`}
// onClick={() => setActiveTab("all")}
// data-bs-toggle="tab"
// >
// Summary
// </button>
// </li>
// <li className="nav-item">
// <button
// type="button"
// className={`nav-link ${activeTab === "logs" ? "active" : ""}`}
// onClick={() => setActiveTab("logs")}
// data-bs-toggle="tab"
// >
// Details
// </button>
// </li>
// </ul>
<div className="card-body">
{activeTab === "all" && (
<div className="row justify-content-between">
<div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
{isLoading ? (
<p>Loading activity data...</p>
) : isError ? (
<p>No data available.</p>
) : (
ActivityData && (
<>
<h5 className="fw-bold mb-0 text-start w-80">
<i className="bx bx-task text-info"></i> Allocated Task
</h5>
<h4 className="mb-0 fw-bold">
{ActivityData.totalCompletedWork?.toLocaleString()}/
{ActivityData.totalPlannedWork?.toLocaleString()}
</h4>
<small className="text-muted">Completed / Assigned</small>
<div style={{ maxWidth: "180px" }}>
<ApexChart />
</div>
</>
)
)}
</div>
// <div className="card-body">
// {activeTab === "all" && (
// <div className="row justify-content-between">
// <div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
// {isLoading ? (
// <p>Loading activity data...</p>
// ) : isError ? (
// <p>No data available.</p>
// ) : (
// ActivityData && (
// <>
// <h5 className="fw-bold mb-0 text-start w-80">
// <i className="bx bx-task text-info"></i> Allocated Task
// </h5>
// <h4 className="mb-0 fw-bold">
// {ActivityData.totalCompletedWork?.toLocaleString()}/
// {ActivityData.totalPlannedWork?.toLocaleString()}
// </h4>
// <small className="text-muted">Completed / Assigned</small>
// <div style={{ maxWidth: "180px" }}>
// <ApexChart />
// </div>
// </>
// )
// )}
// </div>
<div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
{!isLoading && !isError && ActivityData && (
<>
<h5 className="fw-bold mb-0 text-start w-110">
<i className="bx bx-task text-info"></i> Activities
</h5>
<h4 className="mb-0 fw-bold">
{ActivityData.totalCompletedWork?.toLocaleString()}/
{ActivityData.totalPlannedWork?.toLocaleString()}
</h4>
<small className="text-muted ">Pending / Assigned</small>
<div style={{ maxWidth: "180px" }}>
<ApexChart />
</div>
</>
)}
</div>
</div>
)}
// <div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
// {!isLoading && !isError && ActivityData && (
// <>
// <h5 className="fw-bold mb-0 text-start w-110">
// <i className="bx bx-task text-info"></i> Activities
// </h5>
// <h4 className="mb-0 fw-bold">
// {ActivityData.totalCompletedWork?.toLocaleString()}/
// {ActivityData.totalPlannedWork?.toLocaleString()}
// </h4>
// <small className="text-muted ">Pending / Assigned</small>
// <div style={{ maxWidth: "180px" }}>
// <ApexChart />
// </div>
// </>
// )}
// </div>
// </div>
// )}
{activeTab === "logs" && (
<div className="table-responsive">
<table className="table table-bordered table-hover">
<thead>
<tr>
<th>Activity / Location</th>
<th>Assigned / Completed</th>
</tr>
</thead>
<tbody>
{[{
activity: "Code Review / Remote",
assignedToday: 3,
completed: 2
}].map((log, index) => (
<tr key={index}>
<td>{log.activity}</td>
<td>{log.assignedToday} / {log.completed}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
};
// {activeTab === "logs" && (
// <div className="table-responsive">
// <table className="table table-bordered table-hover">
// <thead>
// <tr>
// <th>Activity / Location</th>
// <th>Assigned / Completed</th>
// </tr>
// </thead>
// <tbody>
// {[{
// activity: "Code Review / Remote",
// assignedToday: 3,
// completed: 2
// }].map((log, index) => (
// <tr key={index}>
// <td>{log.activity}</td>
// <td>{log.assignedToday} / {log.completed}</td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// )}
// </div>
// </div>
// );
// };
export default Activity;
// export default Activity;

View File

@ -1,21 +1,16 @@
import React, { useState, useEffect } from "react";
import LineChart from "../Charts/LineChart";
import React, { useState, useMemo } from "react";
import ApexChart from "../Charts/Circle";
import { useProjects } from "../../hooks/useProjects";
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
import ApexChart from "../Charts/Circle";
const LOCAL_STORAGE_PROJECT_KEY = "selectedAttendanceProjectId";
import { useSelectedProject } from "../../hooks/useSelectedProject"; // your custom hook
const Attendance = () => {
const { projects } = useProjects();
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const [selectedDate, setSelectedDate] = useState(today);
const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
const initialProjectId = storedProjectId || "all";
const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
const [displayedProjectName, setDisplayedProjectName] =
useState("Select Project");
const [activeTab, setActiveTab] = useState("Summary");
// central project selection hook
const selectedProjectId = useSelectedProject()
const {
dashboard_Attendancedata: AttendanceData,
@ -23,38 +18,24 @@ const Attendance = () => {
error: isError,
} = useDashboard_AttendanceData(selectedDate, selectedProjectId);
useEffect(() => {
if (selectedProjectId === "all") {
setDisplayedProjectName("All Projects");
} else if (projects) {
const foundProject = projects.find((p) => p.id === selectedProjectId);
setDisplayedProjectName(
foundProject ? foundProject.name : "Select Project"
);
} else {
setDisplayedProjectName("Select Project");
}
localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
// project name derived once
const displayedProjectName = useMemo(() => {
if (selectedProjectId === "all") return "All Projects";
const found = projects?.find((p) => p.id === selectedProjectId);
return found?.name || "Select Project";
}, [selectedProjectId, projects]);
const handleProjectSelect = (projectId) => {
setSelectedProjectId(projectId);
};
const handleDateChange = (e) => {
setSelectedDate(e.target.value);
};
return (
<div className="card h-100">
<div className="card-header mb-1 pb-0 ">
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 pb-0 ">
{/* Header */}
<div className="card-header mb-1 pb-0">
<div className="d-flex flex-wrap justify-content-between align-items-center">
<div className="card-title mb-0 text-start">
<h5 className="mb-1">Attendance</h5>
<p className="card-subtitle">Daily Attendance Data</p>
</div>
{/* Project Dropdown */}
<div className="btn-group">
<button
className="btn btn-outline-primary btn-sm dropdown-toggle"
@ -68,7 +49,7 @@ const Attendance = () => {
<li>
<button
className="dropdown-item"
onClick={() => handleProjectSelect("all")}
onClick={() => setSelectedProjectId("all")}
>
All Projects
</button>
@ -77,7 +58,7 @@ const Attendance = () => {
<li key={project.id}>
<button
className="dropdown-item"
onClick={() => handleProjectSelect(project.id)}
onClick={() => setSelectedProjectId(project.id)}
>
{project.name}
</button>
@ -88,52 +69,43 @@ const Attendance = () => {
</div>
</div>
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 mt-0 me-5 ms-5">
{/* Tabs */}
<div>
<ul className="nav nav-tabs " role="tablist">
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "Summary" ? "active" : ""
}`}
onClick={() => setActiveTab("Summary")}
data-bs-toggle="tab"
>
Summary
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${
activeTab === "Details" ? "active" : ""
}`}
onClick={() => setActiveTab("Details")}
data-bs-toggle="tab"
>
Details
</button>
</li>
</ul>
</div>
{/* ✅ Date Picker Aligned Left with Padding */}
<div className="ps-6 mb-3 mt-0">
<div style={{ width: "120px" }}>
<input
type="date"
className="form-control p-1"
// style={{ fontSize: "1rem" }}
value={selectedDate}
onChange={handleDateChange}
/>
</div>
{/* Tabs + Date Picker */}
<div className="d-flex flex-wrap justify-content-between align-items-center me-5 ms-5">
<ul className="nav nav-tabs">
<li className="nav-item">
<button
type="button"
className={`nav-link ${AttendanceData?.activeTab === "Summary" ? "active" : ""}`}
onClick={() => (AttendanceData.activeTab = "Summary")}
>
Summary
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${AttendanceData?.activeTab === "Details" ? "active" : ""}`}
onClick={() => (AttendanceData.activeTab = "Details")}
>
Details
</button>
</li>
</ul>
<div className="ps-6 mb-3">
<input
type="date"
className="form-control p-1"
style={{ width: "120px" }}
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
/>
</div>
</div>
{/* Body */}
<div className="card-body">
{activeTab === "Summary" && (
{/* Summary */}
{AttendanceData?.activeTab === "Summary" && (
<div className="row justify-content-center">
<div className="col-12 col-md-6 d-flex flex-column align-items-center text-center mb-4">
{isLoading ? (
@ -143,7 +115,7 @@ const Attendance = () => {
) : (
AttendanceData && (
<>
<h5 className="fw-bold mb-0 text-center w-100">
<h5 className="fw-bold mb-0">
<i className="bx bx-task text-info"></i> Attendance
</h5>
<h4 className="mb-0 fw-bold">
@ -164,11 +136,9 @@ const Attendance = () => {
</div>
)}
{activeTab === "Details" && (
<div
className="table-responsive"
style={{ maxHeight: "300px", overflowY: "auto" }}
>
{/* Details */}
{AttendanceData?.activeTab === "Details" && (
<div className="table-responsive" style={{ maxHeight: "300px" }}>
<table className="table table-hover mb-0 text-start">
<thead>
<tr>
@ -178,32 +148,17 @@ const Attendance = () => {
</tr>
</thead>
<tbody>
{AttendanceData?.attendanceTable &&
AttendanceData.attendanceTable.length > 0 ? (
AttendanceData.attendanceTable.map((record, index) => (
<tr key={index}>
<td>
{record.firstName} {record.lastName}
</td>
<td>
{new Date(record.inTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</td>
<td>
{new Date(record.outTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</td>
{AttendanceData?.attendanceTable?.length ? (
AttendanceData.attendanceTable.map((r, i) => (
<tr key={i}>
<td>{r.firstName} {r.lastName}</td>
<td>{r.inTime ? new Date(r.inTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
<td>{r.outTime ? new Date(r.outTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="text-center">
No attendance data available
</td>
<td colSpan="3" className="text-center">No attendance data available</td>
</tr>
)}
</tbody>

View File

@ -132,7 +132,7 @@ const AttendanceOverview = () => {
onClick={() => setView("table")}
title="Table View"
>
<i class="bx bx-list-ul fs-5"></i>
<i className="bx bx-list-ul fs-5"></i>
</button>
</div>
</div>

View File

@ -1,33 +1,28 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus";
import GlobalRepository from "../../repositories/GlobalRepository";
const Projects = () => {
const { projectsCardData } = useDashboardProjectsCardData();
const [projectData, setProjectsData] = useState(projectsCardData);
const {
data: projectsCardData,
isLoading,
isError,
error,
refetch,
} = useDashboardProjectsCardData();
useEffect(() => {
setProjectsData(projectsCardData);
}, [projectsCardData]);
// When "project" event happens, just refetch
const handler = () => {
refetch();
};
const handler = useCallback(
async (msg) => {
try {
const response =
await GlobalRepository.getDashboardProjectsCardData();
setProjectsData(response.data);
} catch (err) {
console.error(err);
}
},
[GlobalRepository]
);
useEffect(() => {
eventBus.on("project", handler);
return () => eventBus.off("project", handler);
}, [handler]);
}, [refetch]);
const totalProjects = projectsCardData?.totalProjects ?? 0;
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -37,20 +32,29 @@ const Projects = () => {
Projects
</h5>
</div>
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">
{projectData.totalProjects?.toLocaleString()}
</h4>
<small className="text-muted">Total</small>
{isLoading ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
<div>
<h4 className="mb-0 fw-bold">
{projectData.ongoingProjects?.toLocaleString()}
</h4>
<small className="text-muted">Ongoing</small>
) : isError ? (
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
</div>
) : (
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">{totalProjects.toLocaleString()}</h4>
<small className="text-muted">Total</small>
</div>
<div>
<h4 className="mb-0 fw-bold">{ongoingProjects.toLocaleString()}</h4>
<small className="text-muted">Ongoing</small>
</div>
</div>
)}
</div>
);
};

View File

@ -1,10 +1,16 @@
import React from "react";
import { useSelector } from "react-redux";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
const TasksCard = () => {
const projectId = useSelector((store) => store.localVariables?.projectId);
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
const projectId = useSelectedProject();
const {
data: tasksCardData,
isLoading,
isError,
error,
} = useDashboardTasksCardData(projectId);
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
@ -14,28 +20,30 @@ const TasksCard = () => {
</h5>
</div>
{loading ? (
// Loader will be displayed when loading is true
{isLoading ? (
// Loader while fetching
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : error ? (
// Error message if there's an error
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
) : isError ? (
// Show error
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
) : (
// Actual data when loaded successfully
// Show data
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">
{tasksCardData?.totalTasks?.toLocaleString()}
{tasksCardData?.totalTasks?.toLocaleString() ?? 0}
</h4>
<small className="text-muted">Total</small>
</div>
<div>
<h4 className="mb-0 fw-bold">
{tasksCardData?.completedTasks?.toLocaleString()}
{tasksCardData?.completedTasks?.toLocaleString() ?? 0}
</h4>
<small className="text-muted">Completed</small>
</div>
@ -45,4 +53,4 @@ const TasksCard = () => {
);
};
export default TasksCard;
export default TasksCard;

View File

@ -1,33 +1,45 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect } from "react";
import { useSelector } from "react-redux";
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
import eventBus from "../../services/eventBus";
import { useQueryClient } from "@tanstack/react-query";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => {
const projectId = useSelector((store) => store.localVariables?.projectId);
const { teamsCardData, loading, error } = useDashboardTeamsCardData(projectId);
const queryClient = useQueryClient();
const projectId = useSelectedProject()
const [totalEmployees, setTotalEmployee] = useState(0);
const [inToday, setInToday] = useState(0);
// Update state when API data arrives
useEffect(() => {
setTotalEmployee(teamsCardData?.totalEmployees || 0);
setInToday(teamsCardData?.inToday || 0);
}, [teamsCardData]);
const {
data: teamsCardData,
isLoading,
isError,
error,
} = useDashboardTeamsCardData(projectId);
// Handle real-time updates via eventBus
const handler = useCallback((msg) => {
if (msg.activity === 1) {
setInToday((prev) => prev + 1);
}
}, []);
const handler = useCallback(
(msg) => {
if (msg.activity === 1) {
queryClient.setQueryData(["dashboardTeams", projectId], (old) => {
if (!old) return old;
return {
...old,
inToday: (old.inToday || 0) + 1,
};
});
}
},
[queryClient, projectId]
);
useEffect(() => {
eventBus.on("attendance", handler);
return () => eventBus.off("attendance", handler);
}, [handler]);
const inToday = teamsCardData?.inToday ?? 0;
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
return (
<div className="card p-3 h-100 text-center d-flex justify-content-between">
<div className="d-flex justify-content-start align-items-center mb-3">
@ -36,18 +48,17 @@ const Teams = () => {
</h5>
</div>
{loading ? (
// Blue spinner loader
{isLoading ? (
<div className="d-flex justify-content-center align-items-center flex-grow-1">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : error ? (
// Error message if data fetching fails
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
) : isError ? (
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
{error?.message || "Error loading data"}
</div>
) : (
// Display data once loaded
<div className="d-flex justify-content-around align-items-start mt-n2">
<div>
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4>
@ -63,4 +74,4 @@ const Teams = () => {
);
};
export default Teams;
export default Teams;

View File

@ -179,7 +179,7 @@ const ListViewContact = ({ data, Pagination }) => {
<tr style={{ height: "200px" }}>
<td
colSpan={contactList.length + 1}
className="text-center align-middle"
className="text-center border-0 align-middle"
>
No contacts found
</td>

View File

@ -23,7 +23,6 @@ const ManageBucket1 = () => {
const handleClose = () => {
setAction(null);
setSelectedBucket(null);
setDeleteId(null);
};
const { mutate: createBucket, isPending: creating } = useCreateBucket(() => {
handleClose();
@ -49,23 +48,26 @@ const ManageBucket1 = () => {
<p className="fs-5 fw-semibold m-0">Manage Buckets</p>
</div>
{action == "create" ? (
{action ? (
<>
<BucketForm
selectedBucket={selectedBucket}
mode={action} // pass create | edit
onSubmit={handleSubmit}
onCancel={() => {
setAction(null);
setSelectedBucket(null);
}}
isPending={creating || updating}
/>
{action === "edit" && selectedBucket && (
<AssignedBucket
selectedBucket={selectedBucket}
handleClose={handleClose}
/>
{action && (
<div>
<BucketForm
selectedBucket={selectedBucket}
mode={action} // pass create | edit
onSubmit={handleSubmit}
onCancel={() => {
setAction(null);
setSelectedBucket(null);
}}
isPending={creating || updating}
/>
{action === "edit" && (
<AssignedBucket
selectedBucket={selectedBucket}
handleClose={handleClose}
/>)}
</div>
)}
</>
) : (
@ -91,11 +93,19 @@ const ManageBucket1 = () => {
buckets={data}
loading={isLoading}
searchTerm={searchTerm}
onDelete={(id) => setDeleteBucket({isOpen:true,bucketId:id})}
onDelete={(id) => setDeleteBucket({ isOpen: true, bucketId: id })}
onEdit={(b) => {
setAction("edit")
setSelectedBucket(b)
}}
/>
</>
)}
</div>
);
};
export default ManageBucket1;

View File

@ -23,7 +23,7 @@ import Label from "../common/Label";
const ManageContact = ({ contactId, closeModal }) => {
// fetch master data
const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects();
const { data:projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { organizationList } = useOrganization();

View File

@ -96,7 +96,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={true}
defaultRange={false}
resetSignal={resetKey}
maxDate={new Date()}
/>

View File

@ -118,7 +118,7 @@ const Documents = ({ Document_Entity, Entity }) => {
return (
<DocumentContext.Provider value={contextValues}>
<div className="mt-5">
<div className="card d-flex p-2">
<div className="card page-min-h d-flex p-2">
<div className="row align-items-center">
{/* Search */}
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
@ -149,30 +149,11 @@ const Documents = ({ Document_Entity, Entity }) => {
</label>
</div>
{/* Actions */}
<div className="col-6 col-md-6 col-lg-8 text-end">
{/* <span
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
disabled={isRefetching}
onClick={() => {
setSearchText("");
setFilter(DocumentFilterDefaultValues);
refetchFn && refetchFn();
}}
>
Refresh
<i
className={`bx bx-refresh ms-1 ${
isRefetching ? "bx-spin" : ""
}`}
></i>
</span> */}
{(isSelf || canUploadDocument) && (
<button
className="btn btn-sm btn-primary me-3"
type="button"
title="Add New Document"
className="p-1 bg-primary rounded-circle cursor-pointer"
onClick={() =>
setManageDoc({
document: null,
@ -180,7 +161,10 @@ const Documents = ({ Document_Entity, Entity }) => {
})
}
>
<i className="bx bx-plus fs-4 text-white"></i>
<i className="bx bx-plus-circle me-2"></i>
<span className="d-none d-md-inline-block">
Add New Document
</span>
</button>
)}
</div>

View File

@ -1,172 +0,0 @@
import React from "react";
const DemoTable = () => {
return (
<div className="content-wrapper">
<div className="container-fluid">
<div className="card">
<div className="card-datatable table-responsive">
<table className="datatables-basic table border-top">
<thead>
<tr>
<th></th>
<th></th>
<th>id</th>
<th>Name</th>
<th>Email</th>
<th>Date</th>
<th>Salary</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
</table>
</div>
</div>
<div className="offcanvas offcanvas-end" id="add-new-record">
<div className="offcanvas-header border-bottom">
<h5 className="offcanvas-title" id="exampleModalLabel">
New Record
</h5>
<button
type="button"
className="btn-close text-reset"
data-bs-dismiss="offcanvas"
aria-label="Close"
></button>
</div>
<div className="offcanvas-body flex-grow-1">
<form
className="add-new-record pt-0 row g-2"
id="form-add-new-record"
onsubmit="return false"
>
<div className="col-sm-12">
<label className="form-label" for="basicFullname">
Full Name
</label>
<div className="input-group input-group-merge">
<span id="basicFullname2" className="input-group-text">
<i className="bx bx-user"></i>
</span>
<input
type="text"
id="basicFullname"
className="form-control dt-full-name"
name="basicFullname"
placeholder="John Doe"
aria-label="John Doe"
aria-describedby="basicFullname2"
/>
</div>
</div>
<div className="col-sm-12">
<label className="form-label" for="basicPost">
Post
</label>
<div className="input-group input-group-merge">
<span id="basicPost2" className="input-group-text">
<i className="bx bxs-briefcase"></i>
</span>
<input
type="text"
id="basicPost"
name="basicPost"
className="form-control dt-post"
placeholder="Web Developer"
aria-label="Web Developer"
aria-describedby="basicPost2"
/>
</div>
</div>
<div className="col-sm-12">
<label className="form-label" for="basicEmail">
Email
</label>
<div className="input-group input-group-merge">
<span className="input-group-text">
<i className="bx bx-envelope"></i>
</span>
<input
type="text"
id="basicEmail"
name="basicEmail"
className="form-control dt-email"
placeholder="john.doe@example.com"
aria-label="john.doe@example.com"
/>
</div>
<div className="form-text">
You can use letters, numbers & periods
</div>
</div>
<div className="col-sm-12">
<label className="form-label" for="basicDate">
Joining Date
</label>
<div className="input-group input-group-merge">
<span id="basicDate2" className="input-group-text">
<i className="bx bx-calendar"></i>
</span>
<input
type="text"
className="form-control dt-date"
id="basicDate"
name="basicDate"
aria-describedby="basicDate2"
placeholder="MM/DD/YYYY"
aria-label="MM/DD/YYYY"
/>
</div>
</div>
<div className="col-sm-12">
<label className="form-label" for="basicSalary">
Salary
</label>
<div className="input-group input-group-merge">
<span id="basicSalary2" className="input-group-text">
<i className="bx bx-dollar"></i>
</span>
<input
type="number"
id="basicSalary"
name="basicSalary"
className="form-control dt-salary"
placeholder="12000"
aria-label="12000"
aria-describedby="basicSalary2"
/>
</div>
</div>
<div className="col-sm-12">
<button
type="submit"
className="btn btn-primary data-submit me-sm-4 me-1"
>
Submit
</button>
<button
type="reset"
className="btn btn-outline-secondary"
data-bs-dismiss="offcanvas"
>
Cancel
</button>
</div>
</form>
</div>
</div>
<hr className="my-12" />
<hr className="my-12" />
<hr className="my-12" />
</div>
<div className="content-backdrop fade"></div>
</div>
);
};
export default DemoTable;

View File

@ -14,28 +14,24 @@ import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { localToUtc } from "../../utils/appUtils";
import { useParams } from "react-router-dom";
const EmpAttendance = ({ employee }) => {
const EmpAttendance = () => {
const { employeeId } = useParams();
const [attendances, setAttendnaces] = useState([]);
const [selectedDate, setSelectedDate] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [attendanceId, setAttendanecId] = useState();
const methods = useForm({
resolver: zodResolver(
z.object({
startDate: z.string(),
endDate: z.string(),
})
),
defaultValues: {
startDate: "",
endDate: "",
startDate: moment().subtract(6, "days").format("DD-MM-YYYY"),
endDate: moment().format("DD-MM-YYYY"),
},
});
const { control, register, handleSubmit, reset, watch } = methods;
const startDate = watch("startDate");
const endDate = watch("endDate");
const { watch } = methods;
const [startDate, endDate] = watch(["startDate", "endDate"]);
const {
data = [],
isLoading: loading,
@ -44,76 +40,21 @@ const EmpAttendance = ({ employee }) => {
error,
refetch,
} = useAttendanceByEmployee(
employee,
localToUtc(startDate),
localToUtc(endDate)
employeeId,
startDate ? localToUtc(startDate) : null,
endDate ? localToUtc(endDate) : null
);
const dispatch = useDispatch();
const today = new Date();
today.setHours(0, 0, 0, 0);
const isSameDay = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() === today.getTime();
};
const isBeforeToday = (dateStr) => {
if (!dateStr) return false;
const d = new Date(dateStr);
d.setHours(0, 0, 0, 0);
return d.getTime() < today.getTime();
};
const sortByName = (a, b) => {
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
return nameA?.localeCompare(nameB);
};
const group1 = data
.filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
.sort(sortByName);
const group2 = data
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
.sort(sortByName);
const group3 = data
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
.sort(sortByName);
const group4 = data
.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime))
.sort(sortByName);
const group5 = data.filter((d) => d.activity === 5).sort(sortByName);
const uniqueMap = new Map();
[...group1, ...group2, ...group3, ...group4, ...group5].forEach((rec) => {
const date = moment(rec.checkInTime || rec.checkOutTime).format(
"YYYY-MM-DD"
);
const key = `${rec.employeeId}-${date}`;
const existing = uniqueMap.get(key);
if (
!existing ||
new Date(rec.checkInTime || rec.checkOutTime) >
new Date(existing.checkInTime || existing.checkOutTime)
) {
uniqueMap.set(key, rec);
}
});
const sortedFinalList = [...uniqueMap.values()].sort(
const sorted = [...data].sort(
(a, b) =>
new Date(b.checkInTime || b.checkOutTime) -
new Date(a.checkInTime || a.checkOutTime)
new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime()
);
const currentDate = new Date().toLocaleDateString("en-CA");
console.log(sorted);
const { currentPage, totalPages, currentItems, paginate } = usePagination(
sortedFinalList,
sorted,
ITEMS_PER_PAGE
);
@ -123,7 +64,6 @@ const EmpAttendance = ({ employee }) => {
};
const closeModal = () => setIsModalOpen(false);
const onSubmit = (formData) => {};
return (
<>
{isModalOpen && (
@ -136,20 +76,10 @@ const EmpAttendance = ({ employee }) => {
className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length"
>
<div className="col-3 my-0 ">
<div className="col-4 col-md-3 my-0 ">
<>
<FormProvider {...methods}>
<form
onSubmit={handleSubmit(onSubmit)}
className="p-2 text-start"
>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
defaultRange={true}
/>
</form>
<DateRangePicker1 />
</FormProvider>
</>
</div>
@ -234,7 +164,7 @@ const EmpAttendance = ({ employee }) => {
</table>
)}
</div>
{!loading && sortedFinalList.length > 20 && (
{!loading && data.length > 20 && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li

View File

@ -1,12 +1,13 @@
import React, { useState, useEffect } from "react";
import { useChangePassword } from "../../components/Context/ChangePasswordContext";
import GlobalModel from "../common/GlobalModel";
import ManageEmployee from "./ManageEmployee";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { useModal } from "../../hooks/useAuth";
const EmpBanner = ({ profile, loggedInUser }) => {
const { openChangePassword } = useChangePassword();
const {onOpen} = useModal("ChangePassword")
const [showModal, setShowModal] = useState(false);
return (
@ -86,7 +87,7 @@ const EmpBanner = ({ profile, loggedInUser }) => {
</li>
</ul>
<ul className="list-inline mb-0 d-flex align-items-center flex-wrap justify-content-sm-start justify-content-center mt-4">
{profile?.isActive && ( // show only if active
{profile?.isActive && (
<li className="list-inline-item">
<button
className="btn btn-sm btn-primary btn-block"
@ -101,7 +102,7 @@ const EmpBanner = ({ profile, loggedInUser }) => {
{profile?.id === loggedInUser?.employeeInfo?.id && (
<button
className="btn btn-sm btn-outline-primary btn-block"
onClick={() => openChangePassword()}
onClick={onOpen}
>
Change Password
</button>

View File

@ -1,7 +0,0 @@
import React from "react";
const EmployeeList = () => {
return <div>EmployeeList</div>;
};
export default EmployeeList;

View File

@ -0,0 +1,124 @@
import { z } from "zod"
const mobileNumberRegex = /^[0-9]\d{9}$/;
export const employeeSchema =
z.object({
firstName: z.string().min(1, { message: "First Name is required" }),
middleName: z.string().optional(),
lastName: z.string().min(1, { message: "Last Name is required" }),
email: z
.string()
.max(80, "Email cannot exceed 80 characters")
.optional()
.refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
message: "Invalid email format",
})
.refine(
(val) => {
if (!val) return true;
const [local, domain] = val.split("@");
return (
val.length <= 320 && local?.length <= 64 && domain?.length <= 255
);
},
{
message: "Email local or domain part is too long",
}
),
currentAddress: z
.string()
.min(1, { message: "Current Address is required" })
.max(500, { message: "Address cannot exceed 500 characters" }),
birthDate: z
.string()
.min(1, { message: "Birth Date is required" })
.refine(
(date, ctx) => {
return new Date(date) <= new Date();
},
{
message: "Birth date cannot be in the future",
}
),
joiningDate: z
.string()
.min(1, { message: "Joining Date is required" })
.refine(
(date, ctx) => {
return new Date(date) <= new Date();
},
{
message: "Joining date cannot be in the future",
}
),
emergencyPhoneNumber: z
.string()
.min(1, { message: "Phone Number is required" })
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
emergencyContactPerson: z
.string()
.min(1, { message: "Emergency Contact Person is required" })
.regex(/^[A-Za-z\s]+$/, {
message: "Emergency Contact Person must contain only letters",
}),
aadharNumber: z
.string()
.optional()
.refine((val) => !val || /^\d{12}$/.test(val), {
message: "Aadhar card must be exactly 12 digits long",
}),
gender: z
.string()
.min(1, { message: "Gender is required" })
.refine((val) => val !== "Select Gender", {
message: "Please select a gender",
}),
panNumber: z
.string()
.optional()
.refine((val) => !val || /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(val), {
message: "Invalid PAN number",
}),
permanentAddress: z
.string()
.min(1, { message: "Permanent Address is required" })
.max(500, { message: "Address cannot exceed 500 characters" }),
phoneNumber: z
.string()
.min(1, { message: "Phone Number is required" })
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
jobRoleId: z.string().min(1, { message: "Role is required" }),
organizationId:z.string().min(1,{message:"Organization is required"}),
hasApplicationAccess:z.boolean().default(false),
}).refine((data) => {
if (data.hasApplicationAccess) {
return data.email && data.email.trim() !== "";
}
return true;
}, {
message: "Email is required when employee has access",
path: ["email"],
});
export const defatEmployeeObj = {
firstName: "",
middleName: "",
lastName: "",
email: "",
currentAddress: "",
birthDate: "",
joiningDate: "",
emergencyPhoneNumber: "",
emergencyContactPerson: "",
aadharNumber: "",
gender: "",
panNumber: "",
permanentAddress: "",
phoneNumber: "",
jobRoleId: null,
organizationId:"",
hasApplicationAccess:false
}

View File

@ -1,36 +1,37 @@
import React, { useEffect, useState } from "react";
import showToast from "../../services/toastService";
import EmployeeRepository from "../../repositories/EmployeeRepository";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import useMaster from "../../hooks/masterHook/useMaster";
import { useDispatch } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import { Link, useNavigate, useParams } from "react-router-dom";
import { formatDate } from "../../utils/dateUtils";
import { useEmployeeProfile, useUpdateEmployee } from "../../hooks/useEmployees";
import {
cacheData,
clearCacheKey,
getCachedData,
} from "../../slices/apiDataManager";
import { clearApiCacheKey } from "../../slices/apiCacheSlice";
import { useMutation } from "@tanstack/react-query";
useEmployeeProfile,
useUpdateEmployee,
} from "../../hooks/useEmployees";
import Label from "../common/Label";
import DatePicker from "../common/DatePicker";
import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema";
import { useOrganizationsList } from "../../hooks/useOrganization";
import { ITEMS_PER_PAGE } from "../../utils/constants";
const mobileNumberRegex = /^[0-9]\d{9}$/;
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
const ManageEmployee = ({ employeeId, onClosed }) => {
const dispatch = useDispatch();
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
const {
data: organzationList,
isLoading,
isError,
error: EempError,
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
const {
employee,
error,
loading: empLoading,
refetch
refetch,
} = useEmployeeProfile(employeeId);
useEffect(() => {
@ -38,6 +39,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
}, [employeeId]);
const [disabledEmail, setDisabledEmail] = useState(false);
const { data: job_role, loading } = useMaster();
const [isloading, setLoading] = useState(false);
const navigation = useNavigate();
@ -45,98 +47,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
const [currentAddressLength, setCurrentAddressLength] = useState(0);
const [permanentAddressLength, setPermanentAddressLength] = useState(0);
const userSchema = z.object({
...(employeeId ? { id: z.string().optional() } : {}),
firstName: z.string().min(1, { message: "First Name is required" }),
middleName: z.string().optional(),
lastName: z.string().min(1, { message: "Last Name is required" }),
email: z
.string()
.max(80, "Email cannot exceed 80 characters")
.optional()
.refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
message: "Invalid email format",
})
.refine(
(val) => {
if (!val) return true;
const [local, domain] = val.split("@");
return (
val.length <= 320 && local?.length <= 64 && domain?.length <= 255
);
},
{
message: "Email local or domain part is too long",
}
),
currentAddress: z
.string()
.min(1, { message: "Current Address is required" })
.max(500, { message: "Address cannot exceed 500 characters" }),
birthDate: z
.string()
.min(1, { message: "Birth Date is required" })
.refine(
(date, ctx) => {
return new Date(date) <= new Date();
},
{
message: "Birth date cannot be in the future",
}
),
joiningDate: z
.string()
.min(1, { message: "Joining Date is required" })
.refine(
(date, ctx) => {
return new Date(date) <= new Date();
},
{
message: "Joining date cannot be in the future",
}
),
emergencyPhoneNumber: z
.string()
.min(1, { message: "Phone Number is required" })
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
emergencyContactPerson: z
.string()
.min(1, { message: "Emergency Contact Person is required" })
.regex(/^[A-Za-z\s]+$/, {
message: "Emergency Contact Person must contain only letters",
}),
aadharNumber: z
.string()
.optional()
.refine((val) => !val || /^\d{12}$/.test(val), {
message: "Aadhar card must be exactly 12 digits long",
}),
gender: z
.string()
.min(1, { message: "Gender is required" })
.refine((val) => val !== "Select Gender", {
message: "Please select a gender",
}),
panNumber: z
.string()
.optional()
.refine((val) => !val || /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(val), {
message: "Invalid PAN number",
}),
permanentAddress: z
.string()
.min(1, { message: "Permanent Address is required" })
.max(500, { message: "Address cannot exceed 500 characters" }),
phoneNumber: z
.string()
.min(1, { message: "Phone Number is required" })
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
jobRoleId: z.string().min(1, { message: "Role is required" }),
});
useEffect(() => {
refetch()
}, [])
refetch();
}, []);
const {
register,
@ -147,25 +60,8 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
reset,
getValues,
} = useForm({
resolver: zodResolver(userSchema),
defaultValues: {
id: currentEmployee?.id || null,
firstName: currentEmployee?.firstName || "",
middleName: currentEmployee?.middleName || "",
lastName: currentEmployee?.lastName || "",
email: currentEmployee?.email || "",
currentAddress: currentEmployee?.currentAddress || "",
birthDate: formatDate(currentEmployee?.birthDate) || "",
joiningDate: formatDate(currentEmployee?.joiningDate) || "",
emergencyPhoneNumber: currentEmployee?.emergencyPhoneNumber || "",
emergencyContactPerson: currentEmployee?.emergencyContactPerson || "",
aadharNumber: currentEmployee?.aadharNumber || "",
gender: currentEmployee?.gender || "",
panNumber: currentEmployee?.panNumber || "",
permanentAddress: currentEmployee?.permanentAddress || "",
phoneNumber: currentEmployee?.phoneNumber || "",
jobRoleId: currentEmployee?.jobRoleId.toString() || null,
},
resolver: zodResolver(employeeSchema),
defaultValues: defatEmployeeObj,
mode: "onChange",
});
@ -176,7 +72,13 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
data.email = null;
}
updateEmployee({ ...data, IsAllEmployee }, {
const payload = { ...data };
if (employeeId) {
payload.id = employeeId;
}
updateEmployee(payload, {
onSuccess: () => {
reset();
onClosed();
@ -184,7 +86,6 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
});
};
useEffect(() => {
if (!loading && !error && employee) {
setCurrentEmployee(employee);
@ -195,37 +96,47 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
reset(
currentEmployee
? {
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
}
id: currentEmployee.id || null,
firstName: currentEmployee.firstName || "",
middleName: currentEmployee.middleName || "",
lastName: currentEmployee.lastName || "",
email: currentEmployee.email || "",
currentAddress: currentEmployee.currentAddress || "",
birthDate: formatDate(currentEmployee.birthDate) || "",
joiningDate: formatDate(currentEmployee.joiningDate) || "",
emergencyPhoneNumber: currentEmployee.emergencyPhoneNumber || "",
emergencyContactPerson:
currentEmployee.emergencyContactPerson || "",
aadharNumber: currentEmployee.aadharNumber || "",
gender: currentEmployee.gender || "",
panNumber: currentEmployee.panNumber || "",
permanentAddress: currentEmployee.permanentAddress || "",
phoneNumber: currentEmployee.phoneNumber || "",
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
organizationId: currentEmployee.organizationId || "",
hasApplicationAccess: currentEmployee.hasApplicationAccess || false,
}
: {}
);
setCurrentAddressLength(currentEmployee?.currentAddress?.length || 0);
setPermanentAddressLength(currentEmployee?.permanentAddress?.length || 0);
}, [currentEmployee, reset]);
const hasAccessAplication = watch("hasApplicationAccess");
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
<div className="text-center"><p className="fs-5 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p> </div>
<div className="text-center">
<p className="fs-5 fw-semibold">
{" "}
{employee ? "Update Employee" : "Create Employee"}
</p>{" "}
</div>
<div className="row mb-3">
<div className="col-sm-4">
<Label className="form-text text-start" required>First Name</Label>
<Label className="form-text text-start" required>
First Name
</Label>
<input
type="text"
name="firstName"
@ -244,7 +155,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
}}
/>
{errors.firstName && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.firstName.message}
</div>
)}
@ -267,14 +181,18 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
}}
/>
{errors.middleName && (
<div className="danger-text text-start " style={{ fontSize: "12px" }}>
<div
className="danger-text text-start "
style={{ fontSize: "12px" }}
>
{errors.middleName.message}
</div>
)}
</div>
<div className="col-sm-4">
<Label className="form-text text-start" required>Last Name</Label>
<Label className="form-text text-start" required>
Last Name
</Label>
<input
type="text"
{...register("lastName", {
@ -291,16 +209,24 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
}}
/>
{errors.lastName && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.lastName.message}
</div>
)}
</div>
</div>
<div className="row mb-3">
<div className="col-sm-6">
<div className="form-text text-start">Email</div>
<Label
htmlFor="email"
className="text-start form-text"
required={hasAccessAplication}
>
Email
</Label>
<input
type="email"
id="email"
@ -321,7 +247,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
)}
</div>
<div className="col-sm-6">
<Label className="form-text text-start" required>Phone Number</Label>
<Label className="form-text text-start" required>
Phone Number
</Label>
<input
type="text"
keyboardType="numeric"
@ -345,7 +273,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
<div className="row mb-3"></div>
<div className="row mb-3">
<div className="col-sm-4">
<Label className="form-text text-start" required>Gender</Label>
<Label className="form-text text-start" required>
Gender
</Label>
<div className="input-group">
<select
@ -387,7 +317,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
</div>
{errors.birthDate && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.birthDate.message}
</div>
)}
@ -408,7 +341,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
</div>
{errors.joiningDate && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.joiningDate.message}
</div>
)}
@ -416,7 +352,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
</div>
<div className="row mb-3">
<div className="col-sm-6">
<Label className="form-text text-start" required>Current Address</Label>
<Label className="form-text text-start" required>
Current Address
</Label>
<textarea
id="currentAddress"
@ -428,15 +366,11 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
maxLength={500}
onChange={(e) => {
setCurrentAddressLength(e.target.value.length);
// let react-hook-form still handle it
register("currentAddress").onChange(e);
}}
></textarea>
<div className="text-end muted">
<small>
{" "}
{500 - currentAddressLength} characters left
</small>
<small> {500 - currentAddressLength} characters left</small>
</div>
{errors.currentAddress && (
<div
@ -466,9 +400,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
}}
></textarea>
<div className="text-end muted">
<small>
{500 - permanentAddressLength} characters left
</small>
<small>{500 - permanentAddressLength} characters left</small>
</div>
{errors.permanentAddress && (
<div
@ -480,6 +412,55 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
)}
</div>
</div>
{/* -------------- */}
<div className="row mb-3">
<div className="col-sm-6">
<Label className="form-text text-start" required>
Organization
</Label>
<div className="input-group">
<select
className="form-select form-select-sm"
{...register("organizationId")}
id="organizationId"
aria-label=""
>
<option disabled value="">
Select Organization
</option>
{organzationList?.data
.sort((a, b) => a?.name?.localeCompare(b?.name))
.map((item) => (
<option value={item?.id} key={item?.id}>
{item?.name}
</option>
))}
</select>
</div>
{errors.organizationId && (
<div
className="danger-text text-start justify-content-center"
style={{ fontSize: "12px" }}
>
{errors.organizationId.message}
</div>
)}
</div>
<div className="col-sm-6 d-flex align-items-center mt-2">
<label className="form-check-label d-flex align-items-center">
<input
type="checkbox"
className="form-check-input me-2"
{...register("hasApplicationAccess")}
/>
Has Application Access ?
</label>
</div>
</div>
{/* --------------- */}
<div className="row mb-3">
{" "}
<div className="divider">
@ -488,7 +469,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
</div>
<div className="row mb-3">
<div className="col-sm-4">
<Label className="form-text text-start" required>Official Designation</Label>
<Label className="form-text text-start" required>
Official Designation
</Label>
<div className="input-group">
<select
className="form-select form-select-sm"
@ -601,15 +584,6 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
)}
</div>
</div>
{employeeId && (
<div className="row mb-3 d-none">
<div className="col-sm-12">
<input type="text" name="id" {...register("id")} />
</div>
</div>
)}
<div className="row text-end">
<div className="col-sm-12">
<button
@ -626,18 +600,11 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending
? "Please Wait..."
: employeeId
? "Update"
: "Create"}
{isPending ? "Please Wait..." : employeeId ? "Update" : "Create"}
</button>
</div>
</div>
</form>
</>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState,useMemo } from "react";
import React, { useEffect, useState, useMemo } from "react";
import { FormProvider, useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
@ -16,8 +16,11 @@ import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
import { useLocation } from "react-router-dom";
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
const { data, isLoading,isError,error,isFetching , isFetched} = useExpenseFilter();
const selectedProjectId = useSelector(
(store) => store.localVariables.projectId
);
const { data, isLoading, isError, error, isFetching, isFetched } =
useExpenseFilter();
const groupByList = useMemo(() => {
return [
@ -27,11 +30,10 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
{ id: "project", name: "Project" },
{ id: "paymentMode", name: "Payment Mode" },
{ id: "expensesType", name: "Expense Type" },
{ id: "createdAt", name: "Submitted Date" }
{ id: "createdAt", name: "Submitted Date" },
].sort((a, b) => a.name.localeCompare(b.name));
}, []);
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
const [resetKey, setResetKey] = useState(0);
@ -40,7 +42,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
defaultValues: defaultFilter,
});
const { control, register, handleSubmit, reset, watch } = methods;
const { control, handleSubmit, reset, setValue, watch } = methods;
const isTransactionDate = watch("isTransactionDate");
const closePanel = () => {
@ -78,34 +80,44 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
}, [location]);
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
if(isError && isFetched) return <div>Something went wrong Here- {error.message} </div>
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;
return (
<>
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="mb-3 w-100">
<div className="d-flex align-items-center mb-2">
<label className="form-label me-2">Choose Date:</label>
<div className="form-check form-switch m-0">
<input
className="form-check-input"
type="checkbox"
id="switchOption1"
{...register("isTransactionDate")}
/>
<label className="form-label me-2">Filter By:</label>
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", true)}
>
Transaction Date
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", false)}
>
Submitted Date
</button>
</div>
<label className="form-label mb-0 ms-2">
{isTransactionDate ? "Submitted": "Transaction" }
</label>
</div>
<label className="fw-semibold">Choose Date Range:</label>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
maxDate={new Date()}
/>
</div>
@ -169,7 +181,9 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
</div>
</div>
<div className="mb-2 text-start ">
<label htmlFor="groupBySelect" className="form-label">Group By :</label>
<label htmlFor="groupBySelect" className="form-label">
Group By :
</label>
<select
id="groupBySelect"
className="form-select form-select-sm"
@ -187,20 +201,19 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs">
<button type="submit" className="btn btn-primary btn-sm">
Apply
</button>
</div>
</form>
</FormProvider>
</>
);
};
export default ExpenseFilterPanel;
export default ExpenseFilterPanel;

View File

@ -28,6 +28,7 @@ import moment from "moment";
import DatePicker from "../common/DatePicker";
import ErrorPage from "../../pages/ErrorPage";
import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const {
@ -57,7 +58,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
});
const selectedproject = watch("projectId");
const {
projectNames,
loading: projectLoading,
@ -142,8 +143,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
};
useEffect(() => {
if (expenseToEdit && data ) {
if (expenseToEdit && data) {
reset({
projectId: data.project.id || "",
expensesTypeId: data.expensesType.id || "",
@ -156,7 +156,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
supplerName: data.supplerName || "",
amount: data.amount || "",
noOfPersons: data.noOfPersons || "",
gstNumber:data.gstNumber || "",
gstNumber: data.gstNumber || "",
billAttachments: data.documents
? data.documents.map((doc) => ({
fileName: doc.fileName,
@ -183,8 +183,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const onSubmit = (fromdata) => {
let payload = {
...fromdata,
transactionDate: localToUtc(fromdata.transactionDate)
transactionDate: localToUtc(fromdata.transactionDate),
};
if (expenseToEdit) {
const editPayload = { ...payload, id: data.id };
@ -206,7 +205,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
if (StatusLoadding || projectLoading || ExpenseLoading || isLoading)
return <ExpenseSkeleton />;
return (
<div className="container p-3">
<h5 className="m-0">
@ -215,7 +213,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>Select Project</Label>
<Label className="form-label" required>
Select Project
</Label>
<select
className="form-select form-select-sm"
{...register("projectId")}
@ -296,11 +296,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)}
</div>
<div className="col-md-6">
<div className="col-12 col-md-6 text-start">
<Label htmlFor="paidById" className="form-label" required>
Paid By
</Label>
<select
{/* <select
className="form-select form-select-sm"
id="paymentModeId"
{...register("paidById")}
@ -321,7 +321,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</select>
{errors.paidById && (
<small className="danger-text">{errors.paidById.message}</small>
)}
)} */}
<EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
forAll={expenseToEdit ? true : false}
/>
</div>
</div>
@ -330,7 +337,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<Label htmlFor="transactionDate" className="form-label" required>
Transaction Date
</Label>
<DatePicker name="transactionDate" control={control} maxDate={new Date()}/>
<DatePicker
name="transactionDate"
control={control}
maxDate={new Date()}
/>
{errors.transactionDate && (
<small className="danger-text">
@ -409,9 +420,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</small>
)}
</div>
<div className="col-md-6">
<div className="col-md-6">
<label htmlFor="statusId" className="form-label ">
GST Number
GST Number
</label>
<input
type="text"
@ -421,9 +432,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{...register("gstNumber")}
/>
{errors.gstNumber && (
<small className="danger-text">
{errors.gstNumber.message}
</small>
<small className="danger-text">{errors.gstNumber.message}</small>
)}
</div>
@ -448,7 +457,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="row my-2 text-start">
<div className="col-md-12">
<Label htmlFor="description" className="form-label" required>Description</Label>
<Label htmlFor="description" className="form-label" required>
Description
</Label>
<textarea
id="description"
className="form-control form-control-sm"
@ -465,7 +476,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="row my-2 text-start">
<div className="col-md-12">
<Label className="form-label" required>Upload Bill </Label>
<Label className="form-label" required>
Upload Bill{" "}
</Label>
<div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
@ -549,7 +562,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<div className="d-flex justify-content-end gap-3">
{" "}
<button
<button
type="reset"
disabled={isPending || createPending}
onClick={handleClose}
@ -568,7 +581,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
? "Update"
: "Submit"}
</button>
</div>
</form>
</div>

View File

@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import { getColorNameFromHex, getIconByFileType } from "../../utils/appUtils";
import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils";
import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
@ -91,9 +91,7 @@ const ViewExpense = ({ ExpenseId }) => {
const onSubmit = (formData) => {
const Payload = {
...formData,
reimburseDate: moment
.utc(formData.reimburseDate, "DD-MM-YYYY")
.toISOString(),
reimburseDate: localToUtc(formData.reimburseDate),
expenseId: ExpenseId,
comment: formData.comment,
};
@ -398,6 +396,7 @@ const ViewExpense = ({ ExpenseId }) => {
name="reimburseDate"
control={control}
minDate={data?.transactionDate}
maxDate={new Date()}
/>
{errors.reimburseDate && (
<small className="danger-text">

View File

@ -12,13 +12,13 @@ import useMaster from "../../hooks/masterHook/useMaster";
import { useProfile } from "../../hooks/useProfile";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import Avatar from "../../components/common/Avatar";
import { useChangePassword } from "../Context/ChangePasswordContext";
import { useProjects } from "../../hooks/useProjects";
import { useCallback, useEffect, useState } from "react";
import { useProjectName } from "../../hooks/useProjects";
import eventBus from "../../services/eventBus";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants";
import { useAuthModal, useLogout, useModal } from "../../hooks/useAuth";
const Header = () => {
const { profile } = useProfile();
@ -26,10 +26,10 @@ const Header = () => {
const dispatch = useDispatch();
const { data, loading } = useMaster();
const navigate = useNavigate();
const {onOpen} = useAuthModal()
const { onOpen:changePass } = useModal("ChangePassword");
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
// {
// console.log(location.pathname);
// }
const { mutate : logout,isPending:logouting} = useLogout()
const isDashboardPath =
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
@ -59,41 +59,9 @@ const Header = () => {
return role ? role.name : "User";
};
const handleLogout = (e) => {
e.preventDefault();
logout();
};
const logout = async () => {
try {
let data = {
refreshToken: localStorage.getItem("refreshToken"),
};
AuthRepository.logout(data)
.then(() => {
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
localStorage.clear();
clearAllCache();
window.location.href = "/auth/login";
})
.catch(() => {
localStorage.removeItem("jwtToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
localStorage.clear();
clearAllCache();
window.location.href = "/auth/login";
});
} catch (error) {
console.error(
"Error during logout:",
error?.response?.data || error.message
);
}
};
const handleProfilePage = () => {
navigate(`/employee/${profile?.employeeInfo?.id}`);
@ -129,7 +97,7 @@ const Header = () => {
}
}
const { openChangePassword } = useChangePassword();
useEffect(() => {
if (
@ -221,7 +189,8 @@ const Header = () => {
className="navbar-nav-right d-flex align-items-center justify-content-between"
id="navbar-collapse"
>
{showProjectDropdown(location.pathname) && (
<div className="d-flex align-items-center">
{showProjectDropdown(location.pathname) && (
<div className="align-items-center">
<i className="rounded-circle bx bx-building-house bx-sm-lg bx-md me-2"></i>
<div className="btn-group">
@ -279,6 +248,7 @@ const Header = () => {
</div>
</div>
)}
</div>
<ul className="navbar-nav flex-row align-items-center ms-md-auto">
<li className="nav-item dropdown-shortcuts navbar-dropdown dropdown me-2 me-xl-0">
@ -426,6 +396,15 @@ const Header = () => {
</li>
<li>
<div className="dropdown-divider"></div>
</li>
<li onClick={()=>onOpen()}>
{" "}
<a
className="dropdown-item cusor-pointer"
>
<i className="bx bx-transfer-alt me-2"></i>
<span className="align-middle">Switch Workspace</span>
</a>
</li>
<li onClick={handleProfilePage}>
<a
@ -445,7 +424,7 @@ const Header = () => {
<span className="align-middle">Settings</span>
</a>
</li>
<li onClick={openChangePassword}>
<li onClick={changePass}>
{" "}
<a
aria-label="go to profile"
@ -455,6 +434,8 @@ const Header = () => {
<span className="align-middle">Change Password</span>
</a>
</li>
<li>
<div className="dropdown-divider"></div>
</li>
@ -462,11 +443,10 @@ const Header = () => {
<a
aria-label="click to log out"
className="dropdown-item cusor-pointer"
href="/logout"
onClick={handleLogout}
onClick={()=>logout()}
>
<i className="bx bx-power-off me-2"></i>
<span className="align-middle">Log Out</span>
{logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
<span className="align-middle">SignOut</span></>}
</a>
</li>
</ul>

View File

@ -1,9 +1,268 @@
import React from 'react'
import React, { useMemo, useState } from "react";
import { useProjectAssignedServices } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import {
useOrganizationType,
useServices,
} from "../../hooks/masterHook/useMaster";
import Label from "../common/Label";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { assignedOrgToProject } from "./OrganizationSchema";
import {
useAssignOrgToProject,
useAssignOrgToTenant,
useOrganizationModal,
} from "../../hooks/useOrganization";
const AssignOrg = ({ setStep }) => {
const { isOpen, orgData, startStep, onOpen, flowType, prevStep, onClose } =
useOrganizationModal();
const selectedProject = useSelectedProject();
const { data: masterService, isLoading: isMasterserviceLoading } =
useServices();
const { data: projectServices, isLoading } =
useProjectAssignedServices(selectedProject);
const { data: orgType, isLoading: orgLoading } = useOrganizationType();
const { mutate: AssignToProject, isPending: isPendingProject } =
useAssignOrgToProject(() => onClose());
const { mutate: AssignToTenant, isPending: isPendingTenat } =
useAssignOrgToTenant(() => {
onClose();
});
const isPending = isPendingProject || isPendingTenat;
const mergedServices = useMemo(() => {
if (!masterService || !projectServices) return [];
const combined = [...masterService?.data, ...projectServices];
return combined.filter(
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
);
}, [masterService, projectServices]);
const resolver =
flowType === "default" ? undefined : zodResolver(assignedOrgToProject);
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
resolver,
defaultValues: {
organizationTypeId: "",
serviceIds: [],
},
});
const onSubmit = (formData) => {
if (flowType === "default") {
const payload = orgData.id;
AssignToTenant(payload);
} else {
const payload = {
...formData,
projectId: selectedProject,
organizationId: orgData.id,
parentOrganizationId: null,
};
AssignToProject(payload);
}
};
const handleEdit = () => {
onOpen({ startStep: 4, orgData });
};
const handleBack = () => {
if (prevStep === 1 && flowType === "assign") {
onOpen({ startStep: prevStep });
} else if (prevStep === 1 && flowType !== "assign") {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: 2 });
}
};
if (isMasterserviceLoading || isLoading)
return <div className="text-center">Loading....</div>;
const AssignOrg = () => {
return (
<div>AssignOrg</div>
)
}
<div className="row text-black text-start mb-3">
{/* Organization Info Display */}
<div className="col-12 mb-3">
<div className="d-flex justify-content-between align-items-center text-start mb-1">
<div className="d-flex flex-row gap-2 align-items-center text-wrap">
<img
src="/public/assets/img/orgLogo.png"
alt="logo"
width={40}
height={40}
/> <p className="fw-semibold fs-6 m-0">{orgData.name}</p>
</div>
<div className="text-end">
<button
type="button"
onClick={handleEdit}
className="btn btn-link p-0"
>
<i className="bx bx-edit text-secondary"></i>
</button>
</div>
</div>
</div>
<div className="d-flex text-secondary mb-2"> <i className="bx bx-sm bx-info-circle me-1" /> Organization Info</div>
{/* Contact Info */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className="bx bx-sm bx-user me-1" /> Contact Person :
</label>
<div className="text-muted">{orgData.contactPerson}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-phone'></i> Contact Number :
</label>
<div className="text-muted">{orgData.contactNumber}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-envelope'></i> Email Address :
</label>
<div className="text-muted">{orgData.email}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ maxWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-barcode"></i>
Service Provider Id (SPRID) :
</label>
<div className="text-muted">{orgData.sprid}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-1 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-map'></i> Address :
</label>
<div className="text-muted text-start">{orgData.address}</div>
</div>
</div>
export default AssignOrg
{/* Form */}
<div className="text-black text-start">
<form onSubmit={handleSubmit(onSubmit)}>
{/* Show fields only if flowType is NOT default */}
{flowType !== "default" && (
<>
{/* Organization Type */}
<div className="mb-3 text-start">
<Label htmlFor="organizationTypeId" className="mb-3 fw-semibold" required>
Organization Type
</Label>
<div className="d-flex flex-wrap gap-3 mt-1">
{orgType?.data.map((type) => (
<div
key={type.id}
className="form-check d-flex align-items-center gap-2 p-0 m-0"
>
<input
type="radio"
id={`organizationType-${type.id}`}
value={type.id}
{...register("organizationTypeId")}
className="form-check-input m-0"
/>
<label
className="form-check-label m-0"
htmlFor={`organizationType-${type.id}`}
>
{type.name}
</label>
</div>
))}
</div>
{errors.organizationTypeId && (
<span className="text-danger">
{errors.organizationTypeId.message}
</span>
)}
</div>
{/* Services */}
<div className="mb-3">
<Label htmlFor="serviceIds" className="mb-3 fw-semibold" required>
Select Services
</Label>
{mergedServices?.map((service) => (
<div key={service.id} className="form-check mb-3">
<input
type="checkbox"
value={service.id}
{...register("serviceIds")}
className="form-check-input"
/>
<label className="form-check-label">{service.name}</label>
</div>
))}
{errors.serviceIds && (
<div className="text-danger small">
{errors.serviceIds.message}
</div>
)}
</div>
</>
)}
{/* Buttons: Always visible */}
<div className="d-flex justify-content-between mt-5">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={handleBack}
disabled={isPending}
>
<i className="bx bx-chevron-left"></i>Back
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending
? "Please wait..."
: flowType === "default"
? "Assign to Organization"
: "Assign to Project"}
</button>
</div>
</form>
</div>
</div>
);
};
export default AssignOrg;

View File

@ -0,0 +1,212 @@
import React, { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
useCreateOrganization,
useOrganization,
useOrganizationModal,
useUpdateOrganization,
} from "../../hooks/useOrganization";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Label from "../common/Label";
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
import { zodResolver } from "@hookform/resolvers/zod";
import SelectMultiple from "../common/SelectMultiple";
const ManagOrg = () => {
const { data: service, isLoading } = useGlobalServices();
const { flowType, orgData, startStep, onOpen, onClose, prevStep } =
useOrganizationModal();
const {
data: organization,
isLoading: organizationLoading,
isError,
error,
} = useOrganization(orgData?.id);
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
// Create & Update mutations
const { mutate: createOrganization, isPending: isCreating } =
useCreateOrganization(() => {
reset(defaultOrganizationValues);
onOpen({ startStep: 1 });
onClose();
});
const { mutate: updateOrganization, isPending: isUpdating } =
useUpdateOrganization(() => {
reset(defaultOrganizationValues);
onOpen({ startStep: 1 });
onClose();
});
// Prefill form if editing
useEffect(() => {
if (organization) {
reset({
name: organization.name || "",
contactPerson: organization.contactPerson || "",
contactNumber: organization.contactNumber || "",
email: organization.email || "",
serviceIds: organization.services?.map((s) => s.id) || [],
address: organization.address || "",
});
}
}, [organization, reset, service?.data]);
const onSubmit = (formData) => {
let payload = { ...formData };
if (organization?.id) {
updateOrganization({
orgId: organization.id,
payload: { ...payload, id: organization.id },
});
} else {
createOrganization(payload);
}
};
const handleBack = () => {
if (flowType === "edit") {
onClose();
return;
}
if (flowType === "assign") {
if (prevStep === 1) {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: prevStep ?? 2 });
}
return;
}
onOpen({ startStep: 2 });
};
return (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-1 text-start">
<Label htmlFor="name" required>
Organization Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactPerson" required>
Contact Person
</Label>
<input
className="form-control form-control-sm"
{...register("contactPerson")}
/>
{errors.contactPerson && (
<span className="danger-text">{errors.contactPerson.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
className="form-control form-control-sm"
{...register("contactNumber")}
/>
{errors.contactNumber && (
<span className="danger-text">{errors.contactNumber.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="email" required>
Email Address
</Label>
<input
className="form-control form-control-sm"
{...register("email")}
/>
{errors.email && (
<span className="danger-text">{errors.email.message}</span>
)}
</div>
<div className="mb-1 text-start">
<SelectMultiple
name="serviceIds"
label="Select Service"
options={service?.data}
labelKey="name"
valueKey="id"
IsLoading={isLoading}
/>
{errors.serviceIds && (
<span className="danger-text">{errors.serviceIds.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="address" required>
Address
</Label>
<textarea
className="form-control form-control-sm"
{...register("address")}
rows={2}
/>
{errors.address && (
<span className="danger-text">{errors.address.message}</span>
)}
</div>
<div className="d-flex justify-content-between gap-2 my-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={handleBack}
>
{flowType === "edit" ? (
"Close"
) : (
<>
<i className="bx bx-chevron-left"></i>Back
</>
)}
</button>
<div>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isCreating || isUpdating || isLoading}
>
{isCreating || isUpdating
? "Please Wait..."
: orgData
? "Update"
: "Submit"}
</button>
</div>
</div>
</form>
</FormProvider>
);
};
export default ManagOrg;

View File

@ -1,471 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Modal from "../common/Modal";
import {
useCreateOrganization,
useOrganizationBySPRID,
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple";
import { useServices } from "../../hooks/masterHook/useMaster";
const ManageOrganization = ({
projectOrganizations = ["ee"],
organizationId = null,
}) => {
const [step, setStep] = useState(1);
const orgModal = useOrganizationModal();
const { data: masterService, isLoading } = useServices();
const [searchText, setSearchText] = useState();
const [SPRID, setSPRID] = useState("");
const { data: orgList, isLoading: orgLoading } = useOrganizationsList(
20,
1,
true,
searchText
);
const { data: OrgListbySPRID, isLoading: isLoadingBySPRID } =
useOrganizationBySPRID(SPRID);
const [Organization, setOrganization] = useState({});
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
console.log(masterService);
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
const { mutate: CreateOrganization, isPending } = useCreateOrganization(
() => {
reset(defaultOrganizationValues);
orgModal.onClose();
setStep(1); // reset to first step
}
);
const onSubmit = (OrgPayload) => {
CreateOrganization(OrgPayload);
};
const RenderTitle = useMemo(() => {
if (organizationId) {
return "Update Organization";
}
if (step === 1) {
return projectOrganizations && projectOrganizations !== null
? "Add Organization"
: "Find Organization";
}
if (step === 2) {
return "Organization Details";
}
if (step === 3) {
return "Create Organization";
}
return "Manage Organization"; // fallback
}, [step, orgModal?.orgData, organizationId]);
const contentBody = (
<div>
{/* ---------- STEP 1: Service Provider- Form Own Tenant list ---------- */}
{step === 1 && (
<div className="d-block">
<div className="text-start mb-1">
<Label className="text-secondary">Find Organization</Label>
<input
type="text"
value={SPRID}
className="form-control form-control-sm w-auto"
placeholder="Enter Organization"
aria-describedby="search-label"
/>
</div>
<div className="py-2 text-tiny text-center">
<div className="d-flex flex-column gap-2 border-0 bg-none">
{orgList?.map((org) => (
<div className="list-group-item list-group-item-action d-flex align-items-center cursor-pointer border-0">
<div className="d-flex align-items-center justify-content-center me-3">
<i className="bx bx-building-house bx-md text-primary"></i>
</div>
<div className="w-100">
<div className="d-flex justify-content-between">
<div className="user-info text-start">
<h6 className="mb-1 fw-normal">{org.name}</h6>
<small className="text-body-secondary">
{org.contactPerson}
</small>
<div className="user-status">
<small>In Meeting</small>
</div>
</div>
<div className="add-btn">
<button
className="btn btn-primary btn-xs"
onClick={() => {
setOrganization(org);
setStep(3);
}}
>
Add
</button>
</div>
</div>
</div>
</div>
))}
</div>
{orgModal.orgData && (
<p className="text-secondary">
Don't have required organization, Please find using{" "}
<span
className="text-mutes cursor-pointer text-decoration-underline"
onClick={() => setStep(2)}
>
SPRID
</span>
</p>
)}
</div>
<div
className={`d-flex ${
projectOrganizations
? "justify-content-end"
: "justify-content-between"
} text-secondary mt-3`}
>
{!projectOrganizations && (
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => setStep(1)}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
)}
<button
type="button"
className="btn btn-xs btn-secondary"
onClick={() => setStep(4)}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button>
</div>
</div>
)}
{/* ---------- STEP 1: Service Provider From Own Other Tenant ---------- */}
{step === 2 && (
<div className="d-block">
<div className="text-start mb-1">
<Label className="text-secondary">Find Organization</Label>
<input
type="text"
className="form-control form-control-sm w-auto"
placeholder="Enter Servicee Provider Id"
aria-describedby="search-label"
/>
</div>
{/* ======== org list ======*/}
<div className="d-flex flex-column gap-2 border-0 bg-none">
{OrgListbySPRID?.map((org) => (
<div className="list-group-item list-group-item-action d-flex align-items-center cursor-pointer border-0">
<div className="d-flex align-items-center justify-content-center me-3">
<i className="bx bx-building-house bx-md text-primary"></i>
</div>
<div className="w-100">
<div className="d-flex justify-content-between">
<div className="user-info text-start">
<h6 className="mb-1 fw-normal">Icing sweet gummies</h6>
<small className="text-body-secondary">15 minutes</small>
<div className="user-status">
<small>In Meeting</small>
</div>
</div>
<div className="add-btn">
<button
className="btn btn-primary btn-xs"
onClick={() => setStep(3)}
>
Add
</button>
</div>
</div>
</div>
</div>
))}
</div>
<div className="d-flex justify-content-between gap-2 mt-3">
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => setStep(1)}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
<button
type="button"
className="btn btn-xs btn-secondary"
onClick={() => setStep(4)}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button>
</div>
</div>
)}
{/* ---------- STEP 2: Existing Organization Details ---------- */}
{step === 3 && (
<div className="row text-black mb-3">
<div className="col-12 mb-3"></div>
<div className="text-start mb-2">
<div className="text-muted">{Organization.name}</div>
</div>
{/* Row 1 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Constact Person :
</label>
<div className="text-muted">{Organization.name}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Contact Number :
</label>
<div className="text-muted">{Organization.contactNumber}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Email Address :
</label>
<div className="text-muted">{Organization.email}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold text-start text-wrap"
style={{ maxWidth: "130px" }}
>
Service provider Id (SPRID) :
</label>
<div className="text-muted">{Organization.sprid}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-1 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Address :
</label>
<div className="text-muted text-start">
{Organization.address}
</div>
</div>
</div>
<div className="text-black text-start">
<div className="mb-2">
<Label className="fs-6">Add Services</Label>
<ul className="list-group list-group-flush">
{masterService.data &&
masterService.data?.map((serv) => (
<li className="list-group-item py-1">
<input
type="checkbox"
className="form-check-input me-2"
/>
{serv.name}
</li>
))}
</ul>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => setStep(1)}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
<button type="button" className="btn btn-sm btn-primary">
Add
</button>
</div>
</div>
</div>
)}
{/* ---------- STEP 3: Add New Organization ---------- */}
{step === 4 && (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-1 text-start">
<Label htmlFor="name" required>
Organization Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactPerson" required>
Contact Person
</Label>
<input
className="form-control form-control-sm"
{...register("contactPerson")}
/>
{errors.contactPerson && (
<span className="danger-text">
{errors.contactPerson.message}
</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
className="form-control form-control-sm"
{...register("contactNumber")}
/>
{errors.contactNumber && (
<span className="danger-text">
{errors.contactNumber.message}
</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="email" required>
Email Address
</Label>
<input
className="form-control form-control-sm"
{...register("email")}
/>
{errors.email && (
<span className="danger-text">{errors.email.message}</span>
)}
</div>
<div className="mb-1 text-start">
<SelectMultiple
name="serviceIds"
label="Services"
required
valueKey="id"
options={services?.data || []}
/>
{errors.serviceIds && (
<span className="danger-text">{errors.serviceIds.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="address" required>
Address
</Label>
<textarea
className="form-control form-control-sm"
{...register("address")}
rows={2}
/>
{errors.address && (
<span className="danger-text">{errors.address.message}</span>
)}
</div>
<div className="d-flex justify-content-between gap-2 my-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setStep(1)}
>
Back
</button>
<div>
<button
type="button"
className="btn btn-sm btn-secondary me-2"
onClick={orgModal.onClose}
disabled={isPending || isLoading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending || isLoading}
>
{isPending ? "Please Wait..." : "Submit"}
</button>
</div>
</div>
</form>
</FormProvider>
)}
</div>
);
return (
<Modal
isOpen={orgModal.isOpen}
onClose={orgModal.onClose}
title={RenderTitle}
body={contentBody}
/>
);
};
export default ManageOrganization;

View File

@ -1,193 +0,0 @@
const ManageOrganization1 = ({
projectOrganizations = [],
organizationId = null,
}) => {
const [step, setStep] = useState(1); // default = scenario decision
const orgModal = useOrganizationModal();
const { data: services, isLoading } = useServices();
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
const { mutate: CreateOrganization, isPending } = useCreateOrganization(
() => {
reset(defaultOrganizationValues);
orgModal.onClose();
setStep(1); // reset to first step
}
);
// 🔹 Decide first step when modal opens
useEffect(() => {
if (orgModal.isOpen) {
if (organizationId) {
setStep(3); // update flow show org details directly
} else if (projectOrganizations && projectOrganizations.length > 0) {
setStep(1); // Scenario 1 from current tenant list
} else {
setStep(2); // Scenario 2 search with SPRID
}
}
}, [orgModal.isOpen, organizationId, projectOrganizations]);
const onSubmit = (OrgPayload) => {
CreateOrganization(OrgPayload);
};
const RenderTitle = useMemo(() => {
if (organizationId) return "Update Organization";
if (step === 1) return "Add Organization"; // current tenant
if (step === 2) return "Find Organization"; // search with SPRID
if (step === 3) return "Organization Details";
if (step === 4) return "Create Organization";
return "Manage Organization";
}, [step, organizationId]);
const contentBody = (
<div>
{/* ---------- STEP 1: From Current Tenant Organizations ---------- */}
{step === 1 && (
<div className="d-block">
<div className="list-group mt-3">
{projectOrganizations.map((org, idx) => (
<div
key={idx}
className="list-group-item list-group-item-action cursor-pointer"
onClick={() => setStep(3)}
>
<i className="bx bx-building-house me-2"></i>
{org}
</div>
))}
</div>
<div className="d-flex justify-content-between text-secondary mt-3">
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => setStep(2)} // jump to SPRID search
>
<i className="bx bx-search-alt"></i> Find with SPRID
</button>
<button
type="button"
className="btn btn-xs btn-secondary"
onClick={() => setStep(4)}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button>
</div>
</div>
)}
{/* ---------- STEP 2: Search by Service Provider ID ---------- */}
{step === 2 && (
<div className="d-block">
<div className="text-start mb-1">
<Label className="text-secondary">Enter Service Provider ID</Label>
<input
type="text"
className="form-control form-control-sm w-auto"
placeholder="SPR - ID"
/>
</div>
{/* Example SPR results */}
<div className="list-group mt-3">
<div
className="list-group-item list-group-item-action cursor-pointer"
onClick={() => setStep(3)}
>
<i className="bx bx-building-house me-2"></i>
Sample Organization (SPRID)
</div>
</div>
<div className="d-flex justify-content-between gap-2 mt-3">
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => setStep(1)}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
<button
type="button"
className="btn btn-xs btn-secondary"
onClick={() => setStep(4)}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button>
</div>
</div>
)}
{/* ---------- STEP 3: Organization Details ---------- */}
{step === 3 && (
<div>
<p className="text-muted small">
Show organization details here (from SPR or tenant list). User
selects services and clicks Add.
</p>
<div className="mb-2">
<Label>Services Offered</Label>
<ul className="list-group">
<li className="list-group-item">
<input type="checkbox" className="form-check-input me-2" />
Service 1
</li>
<li className="list-group-item">
<input type="checkbox" className="form-check-input me-2" />
Service 2
</li>
</ul>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setStep(1)}
>
Back
</button>
<button type="button" className="btn btn-sm btn-primary">
Add
</button>
</div>
</div>
)}
{/* ---------- STEP 4: Create New Organization ---------- */}
{step === 4 && (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
{/* same form as your code, unchanged */}
{/* ... */}
</form>
</FormProvider>
)}
</div>
);
return (
<Modal
isOpen={orgModal.isOpen}
onClose={orgModal.onClose}
title={RenderTitle}
body={contentBody}
/>
);
};
export default ManageOrganization;

View File

@ -0,0 +1,24 @@
/* Default: hide scrollbar */
.scrollable-tbody {
max-height: 350px;
overflow-y: auto;
scrollbar-width: none; /* Firefox */
}
.scrollable-tbody::-webkit-scrollbar {
width: 0; /* Chrome, Safari */
}
/* On hover: show scrollbar */
.scrollable-tbody:hover {
scrollbar-width: thin; /* Firefox */
}
.scrollable-tbody:hover::-webkit-scrollbar {
width: 6px; /* Adjust width */
}
.scrollable-tbody:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
.scrollable-tbody:hover::-webkit-scrollbar-track {
background: transparent;
}

View File

@ -0,0 +1,157 @@
import { useState } from "react";
import {
useAssignOrgToTenant,
useOrganizationBySPRID,
useOrganizationModal,
} from "../../hooks/useOrganization";
import Label from "../common/Label";
import { useDebounce } from "../../utils/appUtils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { spridSchema } from "./OrganizationSchema";
import { OrgCardSkeleton } from "./OrganizationSkeleton";
import { useQueryClient } from "@tanstack/react-query";
// Zod schema: only allow exactly 4 digits
const OrgPickerFromSPId = ({ title, placeholder }) => {
const { onClose, startStep, flowType, onOpen, prevStep,orgData } =
useOrganizationModal();
const clientQuery = useQueryClient()
const {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm({
resolver: zodResolver(spridSchema),
defaultValues: { spridSearchText: "" },
});
const [SPRID, setSPRID] = useState("");
const { data, isLoading, isError, error, refetch } =
useOrganizationBySPRID(SPRID);
const onSubmit = (formdata) => {
setSPRID(formdata.spridSearchText);
};
const handleCrateOrg = () => {
clientQuery.removeQueries({queryKey:["organization"]})
onOpen({ startStep: 4,orgData:null })
};
const SP = watch("spridSearchText");
return (
<div className="d-block">
<form
className="d-flex flex-row gap-6 text-start align-items-center"
onSubmit={handleSubmit(onSubmit)}
>
<div className="d-flex flex-row align-items-center gap-2">
<Label className="text-secondary">Search by SPRID</Label>
<input
type="search"
{...register("spridSearchText")}
className="form-control form-control-sm w-auto"
placeholder="Enter SPRID"
maxLength={4}
/>
</div>
<button type="submit" className="btn btn-sm btn-primary">
<i className="bx bx-sm bx-search-alt-2"></i> Search
</button>
</form>
<div className="text-start danger-text">
{" "}
{errors.spridSearchText && (
<p className="text-danger small mt-1">
{errors.spridSearchText.message}
</p>
)}
</div>
{/* ---- Organization list ---- */}
{isLoading ? (
<OrgCardSkeleton />
) : data && data?.data.length > 0 ? (
<div className="py-2 text-tiny text-center">
<div className="d-flex flex-column gap-2 border-0 bg-none">
{data.data.map((org) => (
<div className="d-flex flex-row gap-2 text-start text-black ">
<div className="mt-1">
<img
src="/public/assets/img/orgLogo.png"
alt="logo"
width={50}
height={50}
/>
</div>
<div className="d-flex flex-column p-0 m-0 cursor-pointer">
<span className="fs-6 fw-semibold">{org.name}</span>
<div className="d-flex gap-2">
<small
className=" fw-semibold text-uppercase"
style={{ letterSpacing: "1px" }}
>
SPRID :{" "}
</small>
<small className="fs-6">{org.sprid}</small>
</div>
<div className="d-flex flex-row gap-2">
<small className="text-small fw-semibold">Address:</small>
<div className="d-flex text-wrap">{org.address}</div>
</div>
<div className="m-0 p-0">
{" "}
<button
type="submit"
className="btn btn-sm btn-primary"
onClick={() => onOpen({ startStep: 3, orgData: org })}
>
Select
</button>
</div>
</div>
</div>
))}
</div>
</div>
) : SPRID ? (
<div className="py-3 text-center text-secondary">
No organization found for "{SPRID}"
</div>
) : null}
<div className="py-12 text-center text-tiny text-black">
<small className="d-block text-secondary">
Do not have SPRID or could not find organization ?
</small>
<button
type="button"
className="btn btn-sm btn-primary mt-3"
onClick={handleCrateOrg}
>
<i className="bx bx-plus-circle me-2"></i>
Create New Organization
</button>
</div>
{/* ---- Footer buttons ---- */}
<div className={`d-flex text-secondary mt-3`}>
{flowType !== "default" && (
<button
type="button"
className="btn btn-xs btn-outline-secondary"
onClick={() => onOpen({ startStep: prevStep })}
>
<i className="bx bx-chevron-left"></i> Back
</button>
)}
</div>
</div>
);
};
export default OrgPickerFromSPId;

View File

@ -0,0 +1,182 @@
import { useState } from "react";
import {
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Label from "../common/Label";
import Pagination from "../common/Pagination";
import "./OrgPicker.css"
const OrgPickerfromTenant = ({ title }) => {
const [searchText, setSearchText] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useOrganizationsList(
ITEMS_PER_PAGE - 10,
1,
true,
null,
searchText
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const { isOpen, orgData, startStep, onOpen, flowType, prevStep } =
useOrganizationModal();
const handleBack = () => {
if (prevStep == 1 && flowType == "assign") {
onOpen({ startStep: prevStep });
} else if (prevStep == 1 && flowType == "assign") {
onOpen({ startStep: 1 });
} else {
onOpen({ startStep: 2 });
}
};
const contactList = [
{
key: "name",
label: "Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "sprid",
label: "SPRID",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.sprid || "N/A"}
</span>
),
align: "text-center",
},
];
return (
<div className="d-block">
<div className="d-flex align-items-center gap-2 mb-1">
<Label className="mb-0">{title}</Label>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText?.(e.target.value)}
className="form-control form-control-sm w-auto"
placeholder="Enter Organization Name"
/>
</div>
{/* ---- Organization list ---- */}
{isLoading ? (
<div>Loading....</div>
) : data && data?.data?.length > 0 ? (
<div className="dataTables_wrapper no-footer pb-5">
<table className="table dataTable text-nowrap">
<thead>
<tr className="table_header_border">
{contactList.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="sticky-action-column bg-white text-center">
Action
</th>
</tr>
</thead>
</table>
<div
className="scrollable-tbody overflow-y-auto"
style={{ maxHeight: "350px" }}
>
<table className="table dataTable text-nowrap mb-0">
<tbody>
{Array.isArray(data.data) && data.data.length > 0
? data.data.map((row, i) => (
<tr key={i}>
{contactList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
<td className="sticky-action-column p-0 bg-white">
<div className="p-1">
<span
type="submit"
className="btn btn-sm"
onClick={() =>
onOpen({ startStep: 3, orgData: row })
}
>
<i class='bx bx-right-arrow-circle text-primary'></i>
</span>
</div>
</td>
</tr>
))
: null}
</tbody>
</table>
</div>
</div>
) : null}
<div className="d-flex flex-column align-items-center text-center text-wrap text-black gap-2">
<small className="mb-1">
Could not find organization in your database? Please search within the
global database.
</small>
<button
type="button"
className="btn btn-sm btn-primary w-auto"
onClick={() => onOpen({ startStep: 2 })}
>
Search Using SPRID
</button>
</div>
{/* ---- Footer buttons ---- */}
<div
className={`d-flex justify-content-end
text-secondary mt-3`}
>
{flowType == "default" && (
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={handleBack}
>
<i className="bx bx-left-arrow-alt"></i> Back
</button>
)}
{/* <button
type="button"
className="btn btn-sm btn-secondary"
onClick={() => onOpen({ startStep: 4 })}
>
<i className="bx bx-plus-circle me-2"></i>
Add New Organization
</button> */}
</div>
</div>
);
};
export default OrgPickerfromTenant;

View File

@ -0,0 +1,117 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Modal from "../common/Modal";
import {
useCreateOrganization,
useOrganizationBySPRID,
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple";
import { useServices } from "../../hooks/masterHook/useMaster";
import AssignOrg from "./AssignOrg";
import ManagOrg from "./ManagOrg";
import OrgPickerFromSPId from "./OrgPickerFromSPId";
import OrgPickerfromTenant from "./OrgPickerfromTenant";
import ViewOrganization from "./ViewOrganization";
const OrganizationModal = () => {
const { isOpen, orgData, startStep, onOpen, onClose, onToggle } =
useOrganizationModal();
const { data: masterService, isLoading } = useServices();
const [searchText, setSearchText] = useState();
const [SPRID, setSPRID] = useState("");
const [Organization, setOrganization] = useState({});
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const {
handleSubmit,
register,
reset,
formState: { errors },
} = method;
const { mutate: CreateOrganization, isPending } = useCreateOrganization(
() => {
reset(defaultOrganizationValues);
onClose();
}
);
const onSubmit = (OrgPayload) => {
CreateOrganization(OrgPayload);
};
const RenderTitle = useMemo(() => {
if (orgData && startStep === 3 ) {
return "Assign Organization";
}
if (startStep === 1) {
return orgData && orgData !== null
? "Add Organization"
: "Choose Organization";
}
if (startStep === 2) {
return "Add Organization";
}
if (startStep === 3) {
return "Assign Organization";
}
if(startStep === 5){
return "Organization Details"
}
return `${orgData ? "Update":"Create"} Organization`;
}, [startStep, orgData]);
const contentBody = (
<div>
{/* ---------- STEP 1: Service Provider- Form Own Tenant list ---------- */}
{startStep === 1 && <OrgPickerfromTenant title="Find Organization" />}
{startStep === 2 && (
<OrgPickerFromSPId
title="Find Organization"
placeholder="Enter Service Provider Id"
projectOrganizations={orgData}
/>
)}
{/* ---------- STEP 2: Existing Organization Details ---------- */}
{startStep === 3 && Organization && (
<AssignOrg Organization={Organization} />
)}
{/* ---------- STEP 3: Add New Organization ---------- */}
{startStep === 4 && <ManagOrg />}
{/* ---------- STEP 3: View Organization ---------- */}
{startStep === 5 && <ViewOrganization orgId={orgData}/>}
</div>
);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={RenderTitle}
body={contentBody}
/>
);
};
export default OrganizationModal;

View File

@ -7,19 +7,19 @@ export const organizationSchema = z.object({
contactNumber: z
.string()
.trim()
.min(7, { message: "Contact number must be at least 7 digits" })
.max(20, { message: "Contact number cannot exceed 12 digits" })
.min(7, { message: "Contact number must be at least 7 digits" })
.max(20, { message: "Contact number cannot exceed 12 digits" })
.regex(phoneRegex, { message: "Invalid phone number" }),
contactPerson: z.string().min(1, { message: "Person name required" }),
address: z.string().min(1, { message: "Address is required!" }),
email: z
.string()
.string().trim()
.min(1, { message: "Email is required" })
.email("Invalid email address"),
serviceIds: z
.array(z.string())
.min(1, { message: "Please insert service id" }),
.min(1, { message: "Service isrequired" }),
});
export const defaultOrganizationValues = {
@ -30,3 +30,28 @@ export const defaultOrganizationValues = {
email: "",
serviceIds: [],
};
export const assignedOrgToProject = z.object({
// projectId: z.string().uuid({ message: "Invalid projectId format" }),
// organizationId: z.string().string({ message: "Invalid organizationId format" }),
// parentOrganizationId: z
// .string()
// .nullable()
// .optional(),
serviceIds: z.preprocess(
(val) => (Array.isArray(val) ? val : []),
z
.array(z.string().uuid({ message: "Invalid serviceId format" }))
.nonempty({ message: "At least one service must be selected" })
),
organizationTypeId: z
.string()
.min(1, { message: "Organization is required" }),
});
export const spridSchema = z.object({
spridSearchText: z
.string()
.regex(/^\d{4}$/, { message: "SPRID must be exactly 4 digits" }),
});

View File

@ -0,0 +1,123 @@
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton mb-2 ${className}`}
style={{
height,
width,
}}
></div>
);
export const OrgCardSkeleton = () => {
return (
<div className="row p-3">
{[...Array(1)].map((_, idx) => (
<div
key={idx}
className="list-group-item d-flex flex-row gap-2 align-items-start border-0 shadow-sm rounded mb-3 p-3"
>
{/* Left: Logo/avatar placeholder */}
<div className="mt-1">
<SkeletonLine height={50} width={50} className="rounded-circle" />
</div>
{/* Right: Info section */}
<div className="d-flex flex-column flex-grow-1 text-start">
{/* Org name */}
<SkeletonLine height={18} width="160px" className="mb-2" />
{/* SPRID */}
<div className="d-flex gap-2 mb-2">
<SkeletonLine height={14} width="60px" />
<SkeletonLine height={14} width="100px" />
</div>
{/* Address */}
<div className="d-flex gap-2">
<SkeletonLine height={14} width="70px" />
<SkeletonLine height={14} width="100%" />
</div>
</div>
</div>
))}
</div>
);
};
export const OrgDetailsSkeleton = () => {
return (
<div className="row text-start p-3">
{/* Header */}
<div className="col-12 mb-3">
<div className="d-flex justify-content-between align-items-center">
{/* Logo + Name */}
<div className="d-flex flex-row gap-2 align-items-center">
<SkeletonLine height={40} width={40} className="rounded-circle" />
<SkeletonLine height={18} width="180px" />
</div>
{/* Status Badge */}
<SkeletonLine height={20} width="70px" className="rounded-pill" />
</div>
</div>
{/* Section Title */}
<div className="d-flex text-secondary mb-2">
<SkeletonLine height={16} width="140px" />
</div>
{/* Contact Person */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="140px" />
</div>
</div>
{/* Contact Number */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="140px" />
</div>
</div>
{/* Email */}
<div className="col-md-12 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="220px" />
</div>
</div>
{/* SPRID */}
<div className="col-6 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="160px" />
</div>
</div>
{/* Employees */}
<div className="col-6 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="60px" />
</div>
</div>
{/* Address */}
<div className="col-12 mb-3">
<div className="d-flex">
<SkeletonLine height={16} width="130px" className="me-2" />
<SkeletonLine height={16} width="100%" />
</div>
</div>
{/* Section Title 2 */}
<div className="d-flex text-secondary mb-2">
<SkeletonLine height={16} width="200px" />
</div>
</div>
);
};

View File

@ -1,10 +1,12 @@
import React from "react";
import { useOrganizationsList } from "../../hooks/useOrganization";
import React, { useState } from "react";
import { useOrganizationModal, useOrganizationsList } from "../../hooks/useOrganization";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Avatar from "../common/Avatar";
import { useDebounce } from "../../utils/appUtils";
import Pagination from "../common/Pagination";
const OrganizationsList = ({searchText}) => {
const [currentPage, setCurrentPage] = useState(1);
const searchString = useDebounce(searchText,500)
const {
data = [],
@ -13,6 +15,7 @@ const OrganizationsList = ({searchText}) => {
isError,
error,
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true,null,searchString);
const {onClose, startStep, flowType, onOpen,orgData } = useOrganizationModal();
const organizationsColumns = [
{
@ -20,7 +23,7 @@ const OrganizationsList = ({searchText}) => {
label: "Organization Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<i className="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
@ -81,11 +84,16 @@ const OrganizationsList = ({searchText}) => {
},
];
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (isFetching && !isFetching) return <div>Loading...</div>;
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
return (
<div className="card px-0 px-sm-4">
<div className="card px-0 px-sm-4 pb-12 pt-5">
<div className="card-datatable table-responsive" id="horizontal-example">
<div className="dataTables_wrapper no-footer px-2">
<table className="table border-top dataTable text-nowrap">
@ -106,8 +114,8 @@ const OrganizationsList = ({searchText}) => {
</tr>
</thead>
<tbody>
{data.length > 0 ? (
data.map((org) => (
{data?.data?.length > 0 ? (
data?.data?.map((org) => (
<tr key={org.id}>
{organizationsColumns.map((col) => (
<td
@ -120,9 +128,9 @@ const OrganizationsList = ({searchText}) => {
</td>
))}
<td className="sticky-action-column ">
<div className="d-flex justify-content-center gap-2 ">
<i className="bx bx-show text-primary cursor-pointer"></i>
<i className="bx bx-edit text-secondary cursor-pointer"></i>
<div className="d-flex justify-content-center gap-2">
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
<i className="bx bx-trash text-danger cursor-pointer"></i>
</div>
</td>
@ -134,12 +142,19 @@ const OrganizationsList = ({searchText}) => {
colSpan={organizationsColumns.length + 1}
className="text-center"
>
<p className="fw-semibold">Not Found</p>
<p className="fw-semibold">{isLoading ? "Loading....":"Not Found Organization"}</p>
</td>
</tr>
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
</div>
</div>

View File

@ -0,0 +1,116 @@
import React from "react";
import { useOrganization } from "../../hooks/useOrganization";
import { OrgDetailsSkeleton } from "./OrganizationSkeleton";
const VieworgDataanization = ({ orgId }) => {
const { data, isLoading, isError, error } = useOrganization(orgId);
if (isLoading) return <OrgDetailsSkeleton />;
if (isError) return <div>{error.message}</div>;
return (
<div className="row text-black text-black text-start ">
{/* Header */}
<div className="col-12 mb-3">
<div className="d-flex justify-content-between align-items-center text-start mb-1">
<div className="d-flex flex-row gap-2 align-items-center text-wrap">
<img
src="/public/assets/img/orgLogo.png"
alt="logo"
width={40}
height={40}
/>{" "}
<p className="fw-semibold fs-6 m-0">{data?.name}</p>
</div>
<div className="text-end">
<span
className={`badge bg-label-${
data?.isActive ? "primary" : "secondary"
} `}
>
{data?.isActive ? "Active" : "In-Active"}{" "}
</span>
</div>
</div>
</div>
<div className="d-flex text-secondary mb-2">
{" "}
<i className="bx bx-sm bx-info-circle me-1" /> Organization Info
</div>
{/* Contact Info */}
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className="bx bx-sm bx-user me-1" /> Contact Person :
</label>
<div className="text-muted">{data?.contactPerson}</div>
</div>
</div>
<div className="col-md-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-phone"></i> Contact Number :
</label>
<div className="text-muted">{data?.contactNumber}</div>
</div>
</div>
<div className="col-md-12 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-envelope"></i> Email Address :
</label>
<div className="text-muted">{data?.email}</div>
</div>
</div>
<div className="col-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ maxWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-barcode"></i>
Service Provider Id (SPRID) :
</label>
<div className="text-muted">{data?.sprid}</div>
</div>
</div>
<div className="col-6 mb-3">
<div className="d-flex">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ maxWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-group"></i>
Employees :
</label>
<div className="text-muted">{data?.activeEmployeeCount}</div>
</div>
</div>
<div className="col-12 mb-3">
<div className="d-flex">
<label
className="form-label me-1 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className="bx bx-sm me-1 bx-map"></i> Address :
</label>
<div className="text-muted text-start">{data?.address}</div>
</div>
</div>
<div className="d-flex text-secondary mb-2">
{" "}
<i className="bx bx-sm bx-briefcase me-1" /> Projects And Services
</div>
</div>
);
};
export default VieworgDataanization;

View File

@ -0,0 +1 @@
useAssignOrgToTenant

View File

@ -42,7 +42,7 @@ const AboutProject = () => {
{IsOpenModal && (
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
<ManageProjectInfo
project={projects_Details}
project={projects_Details?.id}
handleSubmitForm={handleFormSubmit}
onClose={() => setIsOpenModal(false)}
isPending={isPending}

View File

@ -1,24 +1,26 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster from "../../hooks/masterHook/useMaster";
import useMaster, { useServices } from "../../hooks/masterHook/useMaster";
import { useForm, Controller } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { clearCacheKey, getCachedData } from "../../slices/apiDataManager";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
import { TasksRepository } from "../../repositories/ProjectRepository";
import showToast from "../../services/toastService";
import { useProjectDetails } from "../../hooks/useProjects";
import {
useEmployeeForTaskAssign,
useProjectAssignedOrganizationsName,
useProjectAssignedServices,
useProjectDetails,
} from "../../hooks/useProjects";
import eventBus from "../../services/eventBus";
import { useCreateTask } from "../../hooks/useTasks";
import Label from "../common/Label";
const AssignTask = ({ assignData, onClose, setAssigned }) => {
const maxPlanned =
assignData?.workItem?.plannedWork - assignData?.workItem?.completedWork;
const schema = z.object({
const TaskSchema = (maxPlanned) => {
return z.object({
selectedEmployees: z
.array(z.string())
.min(1, { message: "At least one employee must be selected" }),
@ -37,20 +39,26 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
})
),
});
};
const AssignTask = ({ assignData, onClose, setAssigned }) => {
const planned = assignData?.workItem?.plannedWork || 0;
const completed = assignData?.workItem?.completedWork || 0;
const maxPlanned = planned - completed;
const [isHelpVisibleTarget, setIsHelpVisibleTarget] = useState(false);
const helpPopupRefTarget = useRef(null);
const [isHelpVisible, setIsHelpVisible] = useState(false);
const [selectedService, setSelectedService] = useState(null);
const [selectedOrganization, setSelectedOrganization] = useState(null);
const { mutate: assignTask, isPending: isSubmitting } = useCreateTask({
onSuccessCallback: () => {
closedModel();
},
onSuccessCallback: closedModel,
});
const dropdownRef = useRef(null);
const [open, setOpen] = useState(false);
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -63,48 +71,47 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
const infoRef = useRef(null);
const infoRef1 = useRef(null);
// State for search term
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
if (typeof bootstrap !== "undefined") {
if (infoRef.current) {
infoRef.current &&
new bootstrap.Popover(infoRef.current, {
trigger: "focus",
placement: "right",
html: true,
content: `<div>Total Pending tasks of the Activity</div>`,
});
}
if (infoRef1.current) {
infoRef1.current &&
new bootstrap.Popover(infoRef1.current, {
trigger: "focus",
placement: "right",
html: true,
content: `<div>Target task for today</div>`,
});
}
} else {
console.warn("Bootstrap is not available. Popovers might not function.");
}
}, []);
const selectedProject = useSelector(
(store) => store.localVariables.projectId
);
const {
employees,
loading: employeeLoading,
recallEmployeeData,
} = useEmployeesAllOrByProjectId(false, selectedProject, false);
const dispatch = useDispatch();
const { loading } = useMaster();
const { data: jobRoleData } = useMaster();
// Changed to an array to hold multiple selected roles
const selectedProject = useSelectedProject();
const { data: serviceList, isLoading: isServiceLoading } = useProjectAssignedServices(selectedProject);
const { data: organizationList, isLoading: isOrgLoading } =
useProjectAssignedOrganizationsName(selectedProject);
const { data: employees, isLoading: isEmployeeLoading } =
useEmployeeForTaskAssign(
selectedProject,
selectedService,
selectedOrganization
);
const dispatch = useDispatch();
const { loading, data: jobRoleData } = useMaster();
const [selectedRoles, setSelectedRoles] = useState(["all"]);
const [displayedSelection, setDisplayedSelection] = useState("");
const {
handleSubmit,
control,
@ -114,133 +121,98 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
reset,
trigger,
} = useForm({
defaultValues: {
selectedEmployees: [],
description: "",
plannedTask: "",
},
resolver: zodResolver(schema),
defaultValues: { selectedEmployees: [], description: "", plannedTask: "" },
resolver: zodResolver(TaskSchema(maxPlanned)),
});
const handleCheckboxChange = (event, user) => {
const isChecked = event.target.checked;
let updatedSelectedEmployees = watch("selectedEmployees") || [];
const updatedSelectedEmployees = event.target.checked
? [...(watch("selectedEmployees") || []), user.id].filter(
(v, i, a) => a.indexOf(v) === i
)
: (watch("selectedEmployees") || []).filter((id) => id !== user.id);
if (isChecked) {
if (!updatedSelectedEmployees.includes(user.id)) {
updatedSelectedEmployees = [...updatedSelectedEmployees, user.id];
}
} else {
updatedSelectedEmployees = updatedSelectedEmployees?.filter(
(id) => id !== user.id
);
}
setValue("selectedEmployees", updatedSelectedEmployees);
trigger("selectedEmployees");
};
useEffect(() => {
dispatch(changeMaster("Job Role"));
// Initial state should reflect "All Roles" selected
setSelectedRoles(["all"]);
}, [dispatch]);
// Modified handleRoleChange to handle multiple selections
const handleRoleChange = (event, roleId) => {
// If 'all' is selected, clear other selections
if (roleId === "all") {
setSelectedRoles(["all"]);
} else {
setSelectedRoles((prevSelectedRoles) => {
// If "all" was previously selected, remove it
const newRoles = prevSelectedRoles.filter((role) => role !== "all");
if (newRoles.includes(roleId)) {
// If role is already selected, unselect it
return newRoles.filter((id) => id !== roleId);
} else {
// If role is not selected, add it
return [...newRoles, roleId];
}
});
}
setSelectedRoles((prev) => {
if (roleId === "all") return ["all"];
const newRoles = prev.filter((r) => r !== "all");
return newRoles.includes(roleId)
? newRoles.filter((r) => r !== roleId)
: [...newRoles, roleId];
});
};
useEffect(() => {
// Update displayedSelection based on selectedRoles
if (selectedRoles.includes("all")) {
setDisplayedSelection("All Roles");
} else if (selectedRoles.length > 0) {
const selectedRoleNames = selectedRoles.map(roleId => {
const role = jobRoleData?.find(r => String(r.id) === roleId);
return role ? role.name : '';
}).filter(Boolean); // Filter out empty strings for roles not found
setDisplayedSelection(selectedRoleNames.join(', '));
} else {
setDisplayedSelection("Select Roles");
}
setDisplayedSelection(
selectedRoles
.map((id) => jobRoleData?.find((r) => String(r.id) === id)?.name)
.filter(Boolean)
.join(", ")
);
} else setDisplayedSelection("Select Roles");
}, [selectedRoles, jobRoleData]);
const handleSearchChange = (e) => setSearchTerm(e.target.value);
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
// Filter employees first by role, then by search term AND job role name
const filteredEmployees = employees?.filter((emp) => {
const filteredEmployees = employees?.data?.filter((emp) => {
const matchesRole =
selectedRoles.includes("all") || selectedRoles.includes(String(emp.jobRoleId));
// Convert both first and last names and job role name to lowercase for case-insensitive matching
const fullName = `${emp.firstName} ${emp.lastName}`.toLowerCase();
const jobRoleName = jobRoleData?.find((role) => role.id === emp.jobRoleId)?.name?.toLowerCase() || "";
selectedRoles.includes("all") ||
selectedRoles.includes(String(emp.jobRoleId));
const searchLower = searchTerm.toLowerCase();
// Check if the full name OR job role name includes the search term
const matchesSearch = fullName.includes(searchLower) || jobRoleName.includes(searchLower);
return matchesRole && matchesSearch;
const fullName = `${emp.firstName} ${emp.lastName}`.toLowerCase();
const jobRoleName =
jobRoleData
?.find((role) => role.id === emp.jobRoleId)
?.name?.toLowerCase() || "";
return (
matchesRole &&
(fullName.includes(searchLower) || jobRoleName.includes(searchLower))
);
});
// Determine unique job role IDs from the filtered employees (for dropdown options)
const uniqueJobRoleIdsInFilteredEmployees = new Set(
employees?.map(emp => emp.jobRoleId).filter(Boolean)
const jobRolesForDropdown = jobRoleData?.filter((role) =>
new Set(employees?.data?.map((emp) => emp.jobRoleId).filter(Boolean)).has(
role.id
)
);
// Filter jobRoleData to only include roles present in the uniqueJobRoleIdsInFilteredEmployees
const jobRolesForDropdown = jobRoleData?.filter(role =>
uniqueJobRoleIdsInFilteredEmployees.has(role.id)
);
// Calculate the count of selected roles for display
const selectedRolesCount = selectedRoles.includes("all")
? 0 // "All Roles" doesn't contribute to a specific count
? 0
: selectedRoles.length;
const onSubmit = (data) => {
const selectedEmployeeIds = data.selectedEmployees;
const taskTeamWithDetails = selectedEmployeeIds
?.map((empId) => empId)
?.filter(Boolean);
const formattedData = {
taskTeam: taskTeamWithDetails,
plannedTask: data.plannedTask,
description: data.description,
assignmentDate: new Date().toISOString(),
workItemId: assignData?.workItem.id,
};
assignTask({
payload: formattedData,
payload: {
taskTeam: data.selectedEmployees.filter(Boolean),
plannedTask: data.plannedTask,
description: data.description,
assignmentDate: new Date().toISOString(),
workItemId: assignData?.workItem.id,
},
workAreaId: assignData?.workArea?.id,
});
};
const closedModel = () => {
function closedModel() {
reset();
onClose();
};
}
return (
<div className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">
<p className="align-items-center flex-wrap m-0 ">Assign Task</p>
<p className="align-items-center flex-wrap m-0">Assign Task</p>
<div className="container my-3">
<div className="mb-1">
<p className="mb-0">
@ -252,7 +224,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
assignData?.workArea?.areaName,
assignData?.workItem?.activityMaster?.activityName,
]
.filter(Boolean) // Filter out any undefined/null values
.filter(Boolean)
.map((item, index, array) => (
<span key={index} className="d-flex align-items-center">
{item}
@ -268,20 +240,64 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
<div className="form-label text-start">
<div className="row mb-1">
<div className="col-12">
<div className="form-text text-start">
<div className="row text-start">
<div className="col-12 col-md-8 d-flex flex-row gap-3 align-items-center">
<div>
<select
className="form-select form-select-sm"
value={selectedOrganization || ""}
onChange={(e) =>
setSelectedOrganization(e.target.value)
}
>
{isServiceLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">--Select Organization--</option>
{organizationList?.map((org,index) => (
<option key={`${org.id}-${index}`} value={org.id}>
{org.name}
</option>
))}
</>
)}
</select>
</div>
<div>
<select
className="form-select form-select-sm"
value={selectedService || ""}
onChange={(e) => setSelectedService(e.target.value)}
>
{isOrgLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">--Select Service--</option>
{serviceList?.map((service,index) => (
<option key={`${service.id}-${index}`} value={service.id}>
{service.name}
</option>
))}
</>
)}
</select>
</div>
</div>
<div
className="d-flex align-items-center form-text fs-7"
className="col-12 col-md-4 d-flex flex-row gap-3 align-items-center justify-content-end"
ref={dropdownRef}
>
<span className="text-dark">Select Team</span>
{/* Dropdown */}
<div className="dropdown position-relative d-inline-block">
<a
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
className={`dropdown-toggle hide-arrow cursor-pointer ${
selectedRoles.includes("all") ||
selectedRoles.length === 0
? "text-secondary"
: "text-primary"
}`}
}`}
onClick={() => setOpen(!open)}
>
<i className="bx bx-slider-alt ms-2"></i>
@ -290,7 +306,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
{/* Badge */}
{selectedRolesCount > 0 && (
<span
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white"
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white text-tiny"
style={{
fontSize: "0.65rem",
minWidth: "18px",
@ -320,7 +336,9 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
id="checkboxAllRoles"
value="all"
checked={selectedRoles.includes("all")}
onChange={(e) => handleRoleChange(e, e.target.value)}
onChange={(e) =>
handleRoleChange(e, e.target.value)
}
/>
<label
className="form-check-label ms-2"
@ -340,8 +358,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
type="checkbox"
id={`checkboxRole-${role.id}`}
value={role.id}
checked={selectedRoles.includes(String(role.id))}
onChange={(e) => handleRoleChange(e, e.target.value)}
checked={selectedRoles.includes(
String(role.id)
)}
onChange={(e) =>
handleRoleChange(e, e.target.value)
}
/>
<label
className="form-check-label ms-2"
@ -357,14 +379,15 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
</div>
{/* Search Box */}
<input
type="text"
className="form-control form-control-sm ms-auto mb-2 mt-2"
placeholder="Search employees or roles..."
value={searchTerm}
onChange={handleSearchChange}
style={{ maxWidth: "200px" }}
/>
<div>
<input
type="text"
className="form-control form-control-sm ms-auto mb-2 mt-2"
placeholder="Search employees or roles..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
</div>
</div>
</div>
@ -381,19 +404,19 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
>
{selectedRoles?.length > 0 && (
<div className="row">
{employeeLoading ? (
{isEmployeeLoading ? (
<div className="col-12">
<p className="text-center">Loading employees...</p>
</div>
) : filteredEmployees?.length > 0 ? (
filteredEmployees.map((emp) => {
filteredEmployees.map((emp,index) => {
const jobRole = jobRoleData?.find(
(role) => role?.id === emp?.jobRoleId
);
return (
<div
key={emp.id}
key={`${emp.index}-${index}`}
className="col-6 col-md-4 col-lg-3 mb-3"
>
<div className="form-check d-flex align-items-start">
@ -441,7 +464,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
) : (
<div className="col-12">
<p className="text-center">
No employees found for the selected role.
No employees found for the selected filter.
</p>
</div>
)}
@ -456,12 +479,14 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
{watch("selectedEmployees")?.length > 0 && (
<div className="mt-1">
<div className="text-start px-2">
{watch("selectedEmployees")?.map((empId) => {
const emp = employees.find((emp) => emp.id === empId);
{watch("selectedEmployees")?.map((empId,ind) => {
const emp = employees?.data?.find(
(emp) => emp.id === empId
);
return (
emp && (
<span
key={empId}
key={`${empId}-${ind}`}
className="badge rounded-pill bg-label-primary d-inline-flex align-items-center me-1 mb-1"
>
{emp.firstName} {emp.lastName}
@ -506,7 +531,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
{assignData?.workItem?.plannedWork -
assignData?.workItem?.completedWork}
</strong>{" "}
<u>{assignData?.workItem?.activityMaster?.unitOfMeasurement}</u>
<u>
{
assignData?.workItem?.activityMaster
?.unitOfMeasurement
}
</u>
</label>
</label>
</div>
@ -537,8 +567,15 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
className="form-control form-control-sm"
{...field}
/>
<span style={{ paddingLeft: "6px", whiteSpace: "nowrap" }}>
<u>{assignData?.workItem?.activityMaster?.unitOfMeasurement}</u>
<span
style={{ paddingLeft: "6px", whiteSpace: "nowrap" }}
>
<u>
{
assignData?.workItem?.activityMaster
?.unitOfMeasurement
}
</u>
</span>
</div>
)}
@ -546,19 +583,17 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
</div>
{errors.plannedTask && (
<div className="danger-text mt-1">{errors.plannedTask.message}</div>
<div className="danger-text mt-1">
{errors.plannedTask.message}
</div>
)}
</div>
{/* <label
<Label
className="form-text fs-7 m-1 text-lg text-dark"
htmlFor="descriptionTextarea" // Changed htmlFor for better accessibility
htmlFor="descriptionTextarea"
required
>
Description
</label> */}
<Label className="form-text fs-7 m-1 text-lg text-dark"
htmlFor="descriptionTextarea" required>
Description
</Label>
<Controller
name="description"
@ -592,7 +627,6 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
{isSubmitting ? "Please Wait" : "Submit"}
</button>
</div>
</form>
</div>
</div>

View File

@ -32,9 +32,8 @@ const EditActivityModal = ({
building,
floor,
onClose,
} ) =>
{
}) => {
const { activities, loading: loadingActivities } = useActivitiesMaster();
const { categories, loading: loadingCategories } = useWorkCategoriesMaster();
const [selectedActivity, setSelectedActivity] = useState(null);
@ -57,13 +56,12 @@ const EditActivityModal = ({
comment: "",
},
});
const { mutate: UpdateTask, isPending } = useManageTask({
onSuccessCallback: (response) =>
{
showToast( response?.message, "success" )
onClose()
}
} );
const { mutate: UpdateTask, isPending } = useManageTask({
onSuccessCallback: (response) => {
showToast(response?.message, "success")
onClose()
}
});
@ -82,34 +80,33 @@ const { mutate: UpdateTask, isPending } = useManageTask({
[categories]
);
useEffect(() => {
if (!workItem) return;
console.log(workItem)
reset({
activityID: String(
workItem?.workItem?.activityId || workItem?.activityMaster?.id
),
workCategoryId: String(
workItem?.workItem?.workCategoryId || workItem?.workCategoryMaster?.id
),
plannedWork:
workItem?.workItem?.plannedWork || workItem?.plannedWork || 0,
completedWork:
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
comment: workItem?.workItem?.description || workItem?.description || "",
});
}, [workItem?.id,selectedActivity]);
useEffect(() => {
if (!workItem) return;
console.log(workItem)
reset({
activityID: String(
workItem?.workItem?.activityId || workItem?.activityMaster?.id
),
workCategoryId: String(
workItem?.workItem?.workCategoryId || workItem?.workCategoryMaster?.id
),
plannedWork:
workItem?.workItem?.plannedWork || workItem?.plannedWork || 0,
completedWork:
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
comment: workItem?.workItem?.description || workItem?.description || "",
});
}, [workItem?.id, selectedActivity]);
useEffect(() => {
const selected = activities?.find((a) => a.id === activityID);
setSelectedActivity( selected || null );
setSelectedActivity(selected || null);
}, [activityID, activities]);
const onSubmitForm = (data) =>
{
const payload = {
const onSubmitForm = (data) => {
const payload = {
...data,
id: workItem?.workItem?.id ?? workItem?.id,
buildingID: building?.id,
@ -125,9 +122,9 @@ useEffect(() => {
buildingId: building?.id,
floorId: floor?.id,
workAreaId: workArea?.id,
previousCompletedWork:completedTask
previousCompletedWork: completedTask
});
}
}
return (
<form className="row g-2 p-2 p-md-1" onSubmit={handleSubmit(onSubmitForm)}>
<div className="text-center mb-1">
@ -162,14 +159,26 @@ useEffect(() => {
disabled
/>
</div>
<div className="col-12 text-start">
<label className="form-label">Select Service</label>
<input
className="form-control form-control-sm"
value={
workItem?.activityMaster?.activityGroupMaster?.service?.name || ""
}
disabled
/>
</div>
<div className="col-12 text-start">
<label className="form-label">Select Activity</label>
<select
{...register("activityID")}
className="form-select form-select-sm"
disabled
>
<option disabled>Select Activity</option>
<option >Select Activity</option>
{loadingActivities ? (
<option>Loading...</option>
) : (
@ -264,7 +273,7 @@ useEffect(() => {
disabled={isPending}
>
{isPending ? "Please Wait..." : "Edit Task"}
</button>
</button>
</div>
</form>
);

View File

@ -11,7 +11,7 @@ import {
getCachedData,
} from "../../../slices/apiDataManager";
const InfraTable = ({ buildings, projectId}) => {
const InfraTable = ({ buildings, projectId }) => {
const [projectBuilding, setProjectBuilding] = useState([]);
const [expandedBuildings, setExpandedBuildings] = useState([]);
const [showFloorModal, setShowFloorModal] = useState(false);
@ -100,7 +100,7 @@ const InfraTable = ({ buildings, projectId}) => {
No floors have been added yet. Start by adding floors to manage
this building.
</p>
</div>
</td>
</tr>
@ -143,7 +143,7 @@ const InfraTable = ({ buildings, projectId}) => {
// }, [handler]);
return (
<div>
<div className="px-6">
{projectBuilding && projectBuilding.length > 0 && (
<table className="table table-bordered">
<tbody>

View File

@ -3,17 +3,22 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
useActivitiesByGroups,
useActivitiesMaster,
useGroups,
useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster";
import { useManageTask } from "../../../hooks/useProjects";
import { useManageTask, useProjectAssignedOrganizationsName, useProjectAssignedServices } from "../../../hooks/useProjects";
import showToast from "../../../services/toastService";
import Label from "../../common/Label";
import { useSelectedProject } from "../../../slices/apiDataManager";
const taskSchema = z.object({
buildingID: z.string().min(1, "Building is required"),
floorId: z.string().min(1, "Floor is required"),
workAreaId: z.string().min(1, "Work Area is required"),
serviceId: z.string().min(1, "Service is required"),
activityGroupId: z.string().min(1, "Activity Group is required"),
activityID: z.string().min(1, "Activity is required"),
workCategoryId: z.string().min(1, "Work Category is required"),
plannedWork: z.number().min(1, "Planned Work must be greater than 0"),
@ -26,6 +31,8 @@ const defaultModel = {
buildingID: "",
floorId: "",
workAreaId: "",
serviceId: "",
activityGroupId: "",
activityID: "",
workCategoryId: "",
plannedWork: 0,
@ -34,9 +41,42 @@ const defaultModel = {
};
const TaskModel = ({ project, onSubmit, onClose }) => {
const { activities, loading: activityLoading } = useActivitiesMaster();
// const { activities, loading: activityLoading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster();
const projectId = useSelectedProject();
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
const { data: assignedOrganizations, isLoading: orgLoading } = useProjectAssignedOrganizationsName(projectId);
const [selectedService, setSelectedService] = useState("");
const [selectedGroup, setSelectedGroup] = useState("");
const { data: groupsResponse, isLoading: groupsLoading } = useGroups(selectedService);
const groups = groupsResponse?.data ?? [];
const { data: activitiesResponse, isLoading: activitiesLoading } = useActivitiesByGroups(selectedGroup);
const activities = activitiesResponse?.data ?? [];
// Fetch Assigned Organizations (Activity Groups)
const [selectedOrg, setSelectedOrg] = useState("");
const handleServiceChange = (e) => {
const value = e.target.value;
setSelectedService(value);
setSelectedGroup("");
setValue("activityGroupId", "");
setValue("activityID", "");
};
const handleGroupChange = (e) => {
const value = e.target.value;
setSelectedGroup(value);
setValue("activityGroupId", value);
setValue("activityID", "");
};
const {
register,
handleSubmit,
@ -72,7 +112,10 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
const { mutate: CreateTask, isPending } = useManageTask({
onSuccessCallback: (response) => {
showToast(response?.message, "success");
onClose?.();
setValue("activityID",""),
setValue("plannedWork",0),
setValue("completedWork",0)
setValue("comment","")
},
});
useEffect(() => {
@ -96,10 +139,12 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
}, [categories]);
const onSubmitForm = async (data) => {
const payload = [data];
CreateTask({payload:payload,buildingId: data.buildingID,
const payload = [data];
CreateTask({
payload: payload, buildingId: data.buildingID,
floorId: data.floorId,
workAreaId: data.workAreaId, PreviousPlannedWork:0,previousCompletedWork:0});
workAreaId: data.workAreaId, PreviousPlannedWork: 0, previousCompletedWork: 0
});
};
return (
@ -150,6 +195,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
{/* Work Area Selection */}
{selectedFloor && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Work Area</Label>
@ -172,27 +218,68 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
{/* Services Selection */}
{selectedWorkArea && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Activity</Label>
<Label className="form-label" required>Select Service</Label>
<select
className="form-select form-select-sm"
{...register("activityID")}
{...register("serviceId")}
value={selectedService}
// onChange={handleServiceChange}
onChange={(e) => {
handleServiceChange(e);
setValue("serviceId", e.target.value);
}}
>
<option value="">Select Activity</option>
{activityData.map((a) => (
<option key={a.id} value={a.id}>
{a.activityName}
<option value="">Select Service</option>
{servicesLoading && <option>Loading...</option>}
{assignedServices?.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
{errors.activityID && (
<p className="danger-text">{errors.activityID.message}</p>
)}
</div>
)}
{selectedWorkArea && (
{/* Activity Group (Organization) Selection */}
{selectedService && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Activity Group</Label>
<select
className="form-select form-select-sm"
{...register("activityGroupId")}
value={selectedGroup}
onChange={handleGroupChange}
>
<option value="">Select Group</option>
{groupsLoading && <option>Loading...</option>}
{groups?.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
{errors.activityGroupId && <p className="danger-text">{errors.activityGroupId.message}</p>}
</div>
)}
{/* Activity Selection */}
{selectedGroup && (
<div className="col-12 text-start">
<Label className="form-label" required>Select Activity</Label>
<select className="form-select form-select-sm" {...register("activityID")}>
<option value="">Select Activity</option>
{activitiesLoading && <option>Loading...</option>}
{activities?.map((a) => (
<option key={a.id} value={a.id}>{a.activityName}</option>
))}
</select>
{errors.activityID && <p className="danger-text">{errors.activityID.message}</p>}
</div>
)}
{watchActivityId && (
<div className="col-12 text-start">
<label className="form-label">Select Work Category</label>
<select
@ -261,7 +348,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
</div>
)}
<div className="col-12 text-end mt-5">
<div className="col-12 text-end mt-5">
<button
type="button"
className="btn btn-sm btn-label-secondary me-3"
@ -272,9 +359,10 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isSubmitting}
// disabled={isSubmitting}
disabled={isPending}
>
{isSubmitting ? "Please Wait..." : "Add Task"}
{isPending ? "Please Wait..." : "Add Task"}
</button>
</div>
</form>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import WorkItem from "./WorkItem";
import { useProjectDetails, useProjectTasks } from "../../../hooks/useProjects";
import { cacheData } from "../../../slices/apiDataManager";
import { useCurrentService, useProjectDetails, useProjectTasks } from "../../../hooks/useProjects";
import { cacheData, useSelectedProject } from "../../../slices/apiDataManager";
import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../../slices/localVariablesSlice";
import ProjectRepository from "../../../repositories/ProjectRepository";
@ -14,20 +14,20 @@ import {
} from "../../../utils/constants";
import { useParams } from "react-router-dom";
import ProgressBar from "../../common/ProgressBar";
import {formatNumber} from "../../../utils/dateUtils";
import { formatNumber } from "../../../utils/dateUtils";
import { useServices } from "../../../hooks/masterHook/useMaster";
const WorkArea = ({ workArea, floor, forBuilding }) => {
const selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject()
const selectedService = useCurrentService()
const { projects_Details, loading } = useProjectDetails(selectedProject);
const [IsExpandedArea, setIsExpandedArea] = useState(false);
const dispatch = useDispatch();
const [Project, setProject] = useState();
// const { projectId } = useParams();
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, IsExpandedArea);
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, selectedService, IsExpandedArea);
const [workAreaStatus, setWorkAreaStatus] = useState({
completed: 0,
@ -82,9 +82,8 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
aria-controls={`collapse-${workArea.id}`}
>
<i
className={`bx me-2 toggle-icon ${
IsExpandedArea ? "bx-minus-circle" : "bx-plus-circle"
}`}
className={`bx me-2 toggle-icon ${IsExpandedArea ? "bx-minus-circle" : "bx-plus-circle"
}`}
style={{
fontSize: "1.2rem",
color: "black",
@ -105,13 +104,13 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
</span>
</div>
<div className="col-2">
<ProgressBar
completedWork={formatNumber(workArea?.completedWork)}
plannedWork={formatNumber(workArea?.plannedWork)}
className="m-0 text-info"
/>
</div>
<div className="col-2">
<ProgressBar
completedWork={formatNumber(workArea?.completedWork)}
plannedWork={formatNumber(workArea?.plannedWork)}
className="m-0 my-2 " height="6px"rounded showLabel={true}
/>
</div>
</div>
</button>
</p>
@ -121,16 +120,17 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
className="accordion-collapse collapse"
aria-labelledby={`heading-${workArea.id}`}
>
<div className="accordion-body px-1">
{isLoading || ProjectTaskList === undefined ? (
<div className="accordion-body px-6">
{isLoading || ProjectTaskList === undefined ? (
<div className="text-center py-2 text-muted">Loading activities...</div>
) : ProjectTaskList?.length === 0 ? (
) : ProjectTaskList?.length === 0 ? (
<div className="text-center py-2 text-muted">No activities available for this work area.</div>
):ProjectTaskList?.length > 0 ? (
) : ProjectTaskList?.length > 0 ? (
<table className="table table-sm mx-1">
<thead>
<tr>
<th className="infra-activity-table-header-first">Activity</th>
<th className="infra-activity-table-header-second">Service</th>
<th className="infra-activity-table-header d-sm-table-cell d-md-none">
Status
</th>
@ -152,9 +152,9 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
</tr>
</thead>
<tbody className="table-border-bottom-0">
{ProjectTaskList.map((workItem,index) => (
{ProjectTaskList.map((workItem, index) => (
<WorkItem
key={workItem.workItemId || `fallback-${index}`}
key={workItem.workItemId || `fallback-${index}`}
workItem={workItem}
forBuilding={forBuilding}
forFloor={floor}

View File

@ -50,8 +50,8 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
: "Work Area created Successfully",
"success"
);
reset({ id: "0", buildingId: "0", areaName: "", floorId: "0" });
// onClose?.();
setValue("id", "0");
setValue("areaName", "");
},
});
@ -194,7 +194,7 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
<button type="submit" className="btn btn-sm btn-primary" disabled={isPending}>
{isPending ? "Please Wait.." : watchWorkAreaId === "0" ? "Add Work Area" : "Update Work Area"}
</button>
</div>
</form>
);

View File

@ -157,7 +157,17 @@ const WorkItem = ({
: "NA"}
</span>
</td>
<td className="text-center d-sm-table-cell ">
{hasWorkItem
? NewWorkItem?.workItem?.activityMaster?.activityGroupMaster?.service?.name ??
workItem?.activityMaster?.activityGroupMaster?.service?.name ??
"NA"
: "NA"}
{" "}
{hasWorkItem
? NewWorkItem?.workItem?.activityMaster?.activityGroupMaster?.service?.name
: "NA"}
</td>
{/* Status - visible only on small screens */}
<td className="text-center d-sm-table-cell d-md-none">
{hasWorkItem

View File

@ -77,13 +77,7 @@ const ManageProject = () => {
</div>
)}
{isProjectCreated && (
<div>
<h3>Additional Content</h3>
<p>Now that the project is created, you can access this content.</p>
{/* Add more content here */}
</div>
)}
</div>
);
};

View File

@ -1,11 +1,24 @@
import React, { useEffect, useState } from "react";
import { projectSchema, projectDefault } from "./ProjectSchema";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Label from "../common/Label";
import DatePicker from "../common/DatePicker";
import { useCreateProject, useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
const currentDate = new Date().toLocaleDateString('en-CA');
import {
DEFAULT_EMPTY_STATUS_ID,
ITEMS_PER_PAGE,
PROJECT_STATUS,
} from "../../utils/constants";
import {
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import { localToUtc } from "../../utils/appUtils";
const currentDate = new Date().toLocaleDateString("en-CA");
const formatDate = (date) => {
if (!date) {
return currentDate;
@ -14,54 +27,23 @@ const formatDate = (date) => {
if (isNaN(d.getTime())) {
return currentDate;
}
return d.toLocaleDateString('en-CA');
return d.toLocaleDateString("en-CA");
};
const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) => {
const [CurrentProject, setCurrentProject] = useState();
const ManageProjectInfo = ({ project, onClose }) => {
const [addressLength, setAddressLength] = useState(0);
const maxAddressLength = 500;
const { onOpen, startStep, flowType } = useOrganizationModal();
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
const projectSchema = z
.object({
...(project?.id ? { id: z.string().optional() } : {}),
name: z.string().min(1, { message: "Project Name is required" }),
shortName: z.string().optional(),
contactPerson: z
.string()
.min(1, { message: "Contact Person Name is required" })
.regex(/^[A-Za-z\s]+$/, {
message: "Contact Person must contain only letters",
}),
projectAddress: z
.string()
.min(1, { message: "Address is required" })
.max(500, "Address must not exceed 150 characters"),
startDate: z
.string()
.min(1, { message: "Start Date is required" })
.default(currentDate),
endDate: z
.string()
.min(1, { message: "End Date is required" })
.default(currentDate),
projectStatusId: z
.string()
.min(1, { message: "Status is required" })
})
.refine(
(data) => {
const start = new Date(data.startDate);
const end = new Date(data.endDate);
return end >= start;
},
{
path: ["endDate"], // attaches the error to the endDate field
message: "End Date must be greater than Start Date",
}
);
const { projects_Details, loading } = useProjectDetails(project);
const { data, isLoading, isError, error } = useOrganizationsList(
ITEMS_PER_PAGE,
1,
true
);
const { mutate: UpdateProject, isPending } = useUpdateProject(() => {onClose?.()});
const {mutate:CeateProject,isPending:isCreating} = useCreateProject(()=>{onClose?.()})
const {
register,
@ -72,80 +54,66 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
getValues,
} = useForm({
resolver: zodResolver(projectSchema),
defaultValues: {
id: project?.id || "",
name: project?.name || "",
shortName: project?.shortName || "",
contactPerson: project?.contactPerson || "",
projectAddress: project?.projectAddress || "",
startDate: formatDate(project?.startDate) || currentDate,
endDate: formatDate(project?.endDate) || currentDate,
// projectStatusId: String(project?.projectStatusId || "00000000-0000-0000-0000-000000000000"),
projectStatusId: project?.projectStatusId && project.projectStatusId !== DEFAULT_EMPTY_STATUS_ID
? String(project.projectStatusId)
: ACTIVE_STATUS_ID,
},
defaultValues: projectDefault,
mode: "onChange",
});
useEffect(() => {
setCurrentProject(project);
reset(
project
? {
id: project?.id || "",
name: project?.name || "",
shortName: project?.shortName || "",
contactPerson: project?.contactPerson || "",
projectAddress: project?.projectAddress || "",
startDate: formatDate(project?.startDate) || "",
endDate: formatDate(project?.endDate) || "",
projectStatusId: String(project?.projectStatus?.id) || "00000000-0000-0000-0000-000000000000",
}
: {}
);
setAddressLength(project?.projectAddress?.length || 0);
}, [project, reset]);
if (project && projects_Details)
reset({
name: projects_Details?.name || "",
shortName: projects_Details?.shortName || "",
contactPerson: projects_Details?.contactPerson || "",
projectAddress: projects_Details?.projectAddress || "",
startDate: formatDate(projects_Details?.startDate) || "",
endDate: formatDate(projects_Details?.endDate) || "",
projectStatusId:
String(projects_Details?.projectStatus?.id) ||
DEFAULT_EMPTY_STATUS_IDF,
promoterId: projects_Details?.promoter?.id || "",
pmcId: projects_Details?.pmc?.id || "",
});
setAddressLength(projects_Details?.projectAddress?.length || 0);
}, [project, projects_Details, reset,data]);
/**
* Handles the form submission.
* @param {object} updatedProject - The project data from the form.
*/
const onSubmitForm = (updatedProject) => {
handleSubmitForm(updatedProject);
const onSubmitForm = (formData) => {
if (project) {
let payload = {
...formData,
startDate: localToUtc(formData.startDate),
endDate: localToUtc(formData.endDate),
id: project,
};
UpdateProject({ projectId: project, payload: payload });
}else{
let payload = {
...formData,
startDate: localToUtc(formData.startDate),
endDate: localToUtc(formData.endDate),
};
CeateProject(payload)
}
};
const handleCancel = () => {
reset({
id: project?.id || "",
name: project?.name || "",
shortName: project?.shortName || "",
contactPerson: project?.contactPerson || "",
projectAddress: project?.projectAddress || "",
startDate: formatDate(project?.startDate) || currentDate,
endDate: formatDate(project?.endDate) || currentDate,
projectStatusId: String(project?.projectStatus?.id || "00000000-0000-0000-0000-000000000000"),
});
reset(projectDefault);
onClose();
};
const handleOrganizaioFinder = () => {
onClose();
onOpen({ startStep: 2, flowType: "default" });
};
return (
<div className="p-sm-2 p-2">
<div className="text-center mb-2">
<h5 className="mb-2">
{project?.id ? "Edit Project" : "Create Project"}
</h5>
<h5 className="mb-2">{project ? "Edit Project" : "Create Project"}</h5>
</div>
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmitForm)}>
<form
className="row g-2 text-start"
onSubmit={handleSubmit(onSubmitForm)}
>
<div className="col-12 col-md-12">
<Label htmlFor="name" required>
Project Name
@ -225,7 +193,10 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
/>
{errors.startDate && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.startDate.message}
</div>
)}
@ -240,18 +211,21 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
name="endDate"
control={control}
placeholder="DD-MM-YYYY"
minDate={getValues("startDate")} // optional: restrict future dates
minDate={getValues("startDate")} // optional: restrict future dates
className="w-100"
/>
{errors.endDate && (
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.endDate.message}
</div>
)}
</div>
<div className="col-12 col-md-6">
<div className="col-12 ">
<label className="form-label" htmlFor="modalEditUserStatus">
Status
</label>
@ -265,15 +239,11 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
valueAsNumber: false,
})}
>
{/* <option disabled>Status</option>
<option value="b74da4c2-d07e-46f2-9919-e75e49b12731">Active</option> */}
<option value={ACTIVE_STATUS_ID}>Active</option>
<option value="603e994b-a27f-4e5d-a251-f3d69b0498ba">On Hold</option>
<option value="cdad86aa-8a56-4ff4-b633-9c629057dfef">In Progress</option>
<option value="ef1c356e-0fe0-42df-a5d3-8daee355492d">Inactive</option>
<option value="33deaef9-9af1-4f2a-b443-681ea0d04f81">Completed</option>
{PROJECT_STATUS.map((status) => (
<option key={status.id} value={status.id}>
{status.label}
</option>
))}
</select>
{errors.projectStatusId && (
<div
@ -284,6 +254,83 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
</div>
)}
</div>
<div className="col-12 ">
<label className="form-label" htmlFor="modalEditUserStatus">
Promoter
</label>
<select
className="select2 form-select form-select-sm"
aria-label="Default select example"
{...register("promoterId", {
required: "Promoter is required",
valueAsNumber: false,
})}
>
{isLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">Select Promoter</option>
{data?.data?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</>
)}
</select>
{errors.promoterId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.promoterId.message}
</div>
)}
</div>
<div className="col-12 ">
<label className="form-label" htmlFor="modalEditUserStatus">
PMC
</label>
<select
className="select2 form-select form-select-sm"
aria-label="Default select example"
{...register("pmcId", {
required: "Promoter is required",
valueAsNumber: false,
})}
>
{isLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">Select PMC</option>
{data?.data?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</>
)}
</select>
{errors.pmcId && (
<div
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.pmcId.message}
</div>
)}
</div>
<div className="d-flex justify-content-between text-secondary text-tiny text-wrap">
<span>
<i className="bx bx-sm bx-info-circle"></i> Not found PMC and
Pomoter, find through SPRID or create new
</span>
<small className="cursor-pointer" onClick={handleOrganizaioFinder} >
<i className="bx bx-plus-circle text-primary"></i>
</small>
</div>
<div className="col-12 col-md-12">
<Label htmlFor="projectAddress" required>
@ -301,6 +348,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
}}
/>
</div>
<div className="text-end" style={{ fontSize: "12px" }}>
{maxAddressLength - addressLength} characters left
</div>
@ -319,22 +367,21 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
className="btn btn-label-secondary btn-sm me-2"
onClick={handleCancel}
aria-label="Close"
disabled={isPending}
disabled={isPending || isCreating || loading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm"
disabled={isPending}
disabled={isPending || isCreating || loading}
>
{isPending ? "Please Wait..." : project?.id ? "Update" : "Submit"}
{isPending||isCreating ? "Please Wait..." : project ? "Update" : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default ManageProjectInfo;
export default ManageProjectInfo;

View File

@ -1,211 +0,0 @@
import React, { useState, useEffect } from "react";
import EmployeeRepository from "../../repositories/EmployeeRepository";
import { useAllEmployees } from "../../hooks/useEmployees";
import useSearch from "../../hooks/useSearch";
import AssignEmployeeTable from "./AssignEmployeeTable";
import showToast from "../../services/toastService";
import "./MapUser.css";
const MapUsers = ({
projectId,
onClose,
empJobRoles,
onSubmit,
allocation,
assignedLoading,
setAssignedLoading,
}) => {
const {
employeesList,
loading: employeeLoading,
error,
} = useAllEmployees(false);
const [selectedEmployees, setSelectedEmployees] = useState([]);
const [searchText, setSearchText] = useState("");
const handleAllocationData = Array.isArray(allocation) ? allocation : [];
const allocationEmployees = employeesList.map((employee) => {
const allocationItem = handleAllocationData.find(
(alloc) => alloc.employeeId === employee.id
);
return {
...employee,
isActive: allocationItem ? allocationItem.isActive : false,
jobRoleId: allocationItem ? allocationItem.jobRoleId : employee.jobRoleId,
};
});
function parseDate(dateStr) {
return new Date(dateStr.split(".")[0]);
}
const latestAllocations = handleAllocationData.reduce((acc, alloc) => {
const existingAlloc = acc[alloc.employeeId];
if (!existingAlloc) {
acc[alloc.employeeId] = alloc;
} else {
const existingDate = parseDate(
existingAlloc.reAllocationDate || existingAlloc.allocationDate
);
const newDate = parseDate(alloc.reAllocationDate || alloc.allocationDate);
if (newDate > existingDate) {
acc[alloc.employeeId] = alloc;
}
}
return acc;
}, {});
const allocationEmployeesData = employeesList
.map((employee) => {
const allocationItem = latestAllocations[employee.id];
return {
...employee,
isActive: allocationItem ? allocationItem.isActive : false,
};
})
.filter((employee) => employee.isActive === false);
const { filteredData, setSearchQuery } = useSearch(
allocationEmployeesData,
searchText
);
const handleRoleChange = (employeeId, newRoleId) => {
setSelectedEmployees((prevSelectedEmployees) =>
prevSelectedEmployees.map((emp) =>
emp.id === employeeId ? { ...emp, jobRoleId: newRoleId } : emp
)
);
};
const handleCheckboxChange = (employeeId) => {
setSelectedEmployees((prevSelectedEmployees) => {
const updatedEmployees = [...prevSelectedEmployees];
const employeeIndex = updatedEmployees.findIndex(
(emp) => emp.id === employeeId
);
if (employeeIndex !== -1) {
const isSelected = !updatedEmployees[employeeIndex].isSelected;
updatedEmployees[employeeIndex].isSelected = isSelected;
} else {
updatedEmployees.push({
id: employeeId,
isSelected: true,
});
}
return updatedEmployees;
});
};
const handleSubmit = () => {
setAssignedLoading(true);
const selected = selectedEmployees
.filter((emp) => emp.isSelected)
.map((emp) => ({ empID: emp.id, jobRoleId: emp.jobRoleId }));
if (selected.length > 0) {
onSubmit(selected);
setSelectedEmployees([]);
} else {
showToast("Please select Employee", "error");
}
};
return (
<>
<div className="modal-dialog modal-dialog-scrollable mx-sm-auto mx-1 modal-lg modal-simple modal-edit-user">
<div className="modal-content">
<div className="modal-header text-center">
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<p className="m-0 fw-semibold fs-5">Assign Employee</p>
<div className="px-4 mt-4 col-md-4 text-start">
{(filteredData.length > 0 ||
allocationEmployeesData.length > 0) && (
<div className="input-group input-group-sm mb-2">
<input
type="search"
className="form-control"
placeholder="Search employees..."
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
)}
<p className="mb-0 small text-muted fw-semibold">Select Employee</p>
</div>
<div className="modal-body p-sm-4 p-0">
<table
className="datatables-users table border-top dataTable no-footer dtr-column "
id="DataTables_Table_0"
aria-describedby="DataTables_Table_0_info"
style={{ width: "100%" }}
>
<tbody>
{employeeLoading && allocationEmployeesData.length === 0 && (
<tr>
<td>Loading..</td>
</tr>
)}
{!employeeLoading &&
allocationEmployeesData.length === 0 &&
filteredData.length === 0 && (
<tr>
<td>All employee assigned to Project.</td>
</tr>
)}
{!employeeLoading &&
allocationEmployeesData.length > 0 &&
filteredData.length === 0 && (
<tr>
<td>No matching employees found.</td>
</tr>
)}
{(filteredData.length > 0 ||
allocationEmployeesData.length > 0) &&
filteredData.map((emp) => (
<AssignEmployeeTable
key={emp.id}
employee={emp}
jobRoles={empJobRoles}
isChecked={emp.isSelected}
onRoleChange={handleRoleChange}
onCheckboxChange={handleCheckboxChange}
/>
))}
</tbody>
</table>
</div>
<div className="modal-footer mt-5 d-flex justify-content-end gap-0">
<button
type="button"
className="btn btn-sm btn-label-secondary"
data-dismiss="modal"
aria-label="Close"
onClick={onClose}
>
Cancel
</button>
{(filteredData.length > 0 || allocationEmployeesData.length > 0) && (
<button className="btn btn-sm btn-primary" onClick={handleSubmit}>
{assignedLoading ? "Please Wait..." : "Assign to Project"}
</button>
)}
</div>
</div>
</div>
</>
);
};
export default MapUsers;

View File

@ -14,41 +14,15 @@ import {
getProjectStatusName,
} from "../../utils/projectStatus";
import GlobalModel from "../common/GlobalModel";
import { useDispatch } from "react-redux";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useProjectContext } from "../../pages/project/ProjectPage";
const ProjectCard = ({ projectData, recall }) => {
const [ projectInfo, setProjectInfo ] = useState( projectData );
const { projects_Details, loading, error, refetch } = useProjectDetails(
projectInfo?.id,false
);
const [showModal, setShowModal] = useState(false);
const dispatch = useDispatch()
const ProjectCard = ({ project }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const {
mutate: updateProject,
isPending,
isSuccess,
isError,
} = useUpdateProject({
onSuccessCallback: () => {
setShowModal(false);
},
})
useEffect(()=>{
setProjectInfo(projectData);
}, [ projectData ] )
const handleShow = async () => {
try {
const { data } = await refetch();
setShowModal(true);
} catch (err) {
showToast("Failed to load project details", "error");
}
};
const { setMangeProject } = useProjectContext();
const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%";
@ -60,35 +34,17 @@ const ProjectCard = ({ projectData, recall }) => {
const handleClose = () => setShowModal(false);
const handleViewProject = () => {
dispatch(setProjectId(projectInfo.id))
dispatch(setProjectId(project.id));
navigate(`/projects/details`);
};
const handleFormSubmit = (updatedProject) => {
if (projectInfo?.id) {
updateProject({
projectId: projectInfo.id,
updatedData: updatedProject,
});
}
};
const handleViewActivities = () => {
dispatch(setProjectId(project.id));
navigate(`/activities/records?project=${project.id}`);
};
return (
<>
{showModal && projects_Details && (
<GlobalModel isOpen={showModal} closeModal={handleClose}>
<ManageProjectInfo
project={projects_Details}
handleSubmitForm={handleFormSubmit}
onClose={handleClose}
isPending={isPending}
/>
</GlobalModel>
)}
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
<div className={`card cursor-pointer ${isPending ? "bg-light opacity-50 pointer-events-none" : ""}`}>
<div className={`card cursor-pointer`}>
<div className="card-header pb-4">
<div className="d-flex align-items-start">
<div className="d-flex align-items-center">
@ -102,13 +58,11 @@ const ProjectCard = ({ projectData, recall }) => {
<h5
className="mb-0 stretched-link text-heading text-start"
onClick={handleViewProject}
>
{projectInfo.shortName
? projectInfo.shortName
: projectInfo.name}
>
{project?.shortName ? project?.shortName : project?.name}
</h5>
<div className="client-info text-body">
<span>{projectInfo.shortName ? projectInfo.name : ""}</span>
<span>{project?.shortName ? project?.name : ""}</span>
</div>
</div>
</div>
@ -120,23 +74,14 @@ const ProjectCard = ({ projectData, recall }) => {
data-bs-toggle="dropdown"
aria-expanded="false"
>
{loading ? (
<div
className="spinner-border spinner-border-sm text-secondary"
role="status"
>
<span className="visually-hidden">Loading...</span>
</div>
) : (
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
)}
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
@ -150,19 +95,18 @@ const ProjectCard = ({ projectData, recall }) => {
</a>
</li>
<li onClick={handleShow}>
<a className="dropdown-item">
<li>
<a className="dropdown-item" onClick={() =>
setMangeProject({
isOpen: true,
Project: project.id,
})
}>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li
onClick={() =>
navigate(
`/activities/records?project=${projectInfo.id}`
)
}
>
<li onClick={handleViewActivities}>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
@ -180,22 +124,22 @@ const ProjectCard = ({ projectData, recall }) => {
<span className="text-heading fw-medium">
Contact Person:{" "}
</span>
{projectInfo.contactPerson ? projectInfo.contactPerson : "NA"}
{project?.contactPerson ? project.contactPerson : "NA"}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Start Date: </span>
{projectInfo.startDate
? moment(projectInfo.startDate).format("DD-MMM-YYYY")
{project.startDate
? moment(project?.startDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span>
{projectInfo.endDate
? moment(projectInfo.endDate).format("DD-MMM-YYYY")
{project?.endDate
? moment(project?.endDate).format("DD-MMM-YYYY")
: "NA"}
</p>
<p className="mb-0">{projectInfo.projectAddress}</p>
<p className="mb-0">{project?.projectAddress}</p>
</div>
</div>
</div>
@ -205,36 +149,37 @@ const ProjectCard = ({ projectData, recall }) => {
<span
className={
`badge rounded-pill ` +
getProjectStatusColor(projectInfo.projectStatusId)
getProjectStatusColor(project?.projectStatusId)
}
>
{getProjectStatusName(projectInfo.projectStatusId)}
{getProjectStatusName(project?.projectStatusId)}
</span>
</p>{" "}
{getDateDifferenceInDays(projectInfo.endDate, Date()) >= 0 && (
{getDateDifferenceInDays(project?.endDate,new Date() ) >= 0 && (
<span className="badge bg-label-success ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
{project?.endDate &&
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
Days left
</span>
)}
{getDateDifferenceInDays(projectInfo.endDate, Date()) < 0 && (
{getDateDifferenceInDays(project?.endDate, new Date()) < 0 && (
<span className="badge bg-label-danger ms-auto">
{projectInfo.endDate &&
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
{project?.endDate &&
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
Days overdue
</span>
)}
</div>
<div className="d-flex justify-content-between align-items-center mb-2">
<small className="text-body">
Task: {formatNumber(projectInfo.completedWork)} / {formatNumber(projectInfo.plannedWork)}
Task: {formatNumber(project?.completedWork)} /{" "}
{formatNumber(project?.plannedWork)}
</small>
<small className="text-body">
{Math.floor(
getProgressInNumber(
projectInfo.plannedWork,
projectInfo.completedWork
project?.plannedWork,
project?.completedWork
)
) || 0}{" "}
% Completed
@ -246,22 +191,20 @@ const ProjectCard = ({ projectData, recall }) => {
role="progressbar"
style={{
width: getProgress(
projectInfo.plannedWork,
projectInfo.completedWork
project?.plannedWork,
project?.completedWork
),
}}
aria-valuenow={projectInfo.completedWork}
aria-valuenow={project?.completedWork}
aria-valuemin="0"
aria-valuemax={projectInfo.plannedWork}
aria-valuemax={project?.plannedWork}
></div>
</div>
<div className="d-flex align-items-center justify-content-between">
{/* <div className="d-flex align-items-center ">
</div> */}
<div>
<a className="text-muted d-flex " alt="Active team size">
<i className="bx bx-group bx-sm me-1_5"></i>
{projectInfo?.teamSize} Members
{project?.teamSize} Members
</a>
</div>
<div>
@ -278,4 +221,4 @@ const ProjectCard = ({ projectData, recall }) => {
);
};
export default ProjectCard;
export default ProjectCard;

View File

@ -0,0 +1,70 @@
import React from 'react'
import { useProjects } from '../../hooks/useProjects'
import Loader from '../common/Loader'
import ProjectCard from './ProjectCard'
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
return (
<div className="row page-min-h">
{ currentItems.length === 0 && (
<p className="text-center text-muted">No projects found.</p>
)}
{currentItems.map((project) => (
<ProjectCard
key={project.id}
project={project}
/>
))}
{ totalPages > 1 && (
<nav>
<ul className="pagination pagination-sm justify-content-end py-2">
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
<button
className="page-link"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, i) => (
<li
key={i}
className={`page-item ${currentPage === i + 1 && "active"}`}
>
<button
className="page-link"
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</button>
</li>
))}
<li
className={`page-item ${currentPage === totalPages && "disabled"
}`}
>
<button
className="page-link"
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</div>
)
}
export default ProjectCardView

View File

@ -17,113 +17,173 @@ import {
getCachedData,
useSelectedProject,
} from "../../slices/apiDataManager";
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
import {
useCurrentService,
useProjectAssignedServices,
useProjectDetails,
useProjectInfra,
} from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { refreshData } from "../../slices/localVariablesSlice";
import eventBus from "../../services/eventBus";
import {useParams} from "react-router-dom";
import { useParams } from "react-router-dom";
import GlobalModel from "../common/GlobalModel";
import { setService } from "../../slices/globalVariablesSlice";
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
{
// const projectId = useSelector((store)=>store.localVariables.projectId)
const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
const projectId = useSelectedProject();
const selectedService = useCurrentService();
const reloadedData = useSelector((store) => store.localVariables.reload);
const [ expandedBuildings, setExpandedBuildings ] = useState( [] );
const {projectInfra,isLoading,error} = useProjectInfra(projectId)
const [expandedBuildings, setExpandedBuildings] = useState([]);
const { projectInfra, isLoading, error } = useProjectInfra(
projectId,
selectedService
);
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
const [ project, setProject ] = useState( projects_Details );
const [project, setProject] = useState(projects_Details);
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const ManageTask = useHasUserPermission(MANAGE_TASK)
const ManageTask = useHasUserPermission(MANAGE_TASK);
const [showModalFloor, setshowModalFloor] = useState(false);
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
const [showModalTask, setshowModalTask] = useState(false);
const [showModalBuilding, setshowModalBuilding] = useState(false);
const dispatch = useDispatch();
const { data: assignedServices, isLoading: servicesLoading } =
useProjectAssignedServices(projectId);
useEffect(() => {
setProject(projectInfra);
}, [data, projects_Details]);
// useEffect(() => {
// if (reloadedData) {
// refetch();
// dispatch(refreshData(false));
// }
// }, [reloadedData]);
const signalRHandler = (response) => {
setProject(response);
}
};
return (
<>
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding( false )}>
<BuildingModel
project={projectInfra}
onClose={() => setshowModalBuilding( false )}
/>
</GlobalModel>}
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={()=>setshowModalFloor(false)}>
<FloorModel
{showModalBuilding && (
<GlobalModel
isOpen={showModalBuilding}
size="md"
closeModal={() => setshowModalBuilding(false)}
>
<BuildingModel
project={projectInfra}
onClose={()=>setshowModalFloor(false)}
onClose={() => setshowModalBuilding(false)}
/>
</GlobalModel>}
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={()=>setshowModalWorkArea(false)} >
</GlobalModel>
)}
{showModalFloor && (
<GlobalModel
isOpen={showModalFloor}
size="md"
closeModal={() => setshowModalFloor(false)}
>
<FloorModel
project={projectInfra}
onClose={() => setshowModalFloor(false)}
/>
</GlobalModel>
)}
{showModalWorkArea && (
<GlobalModel
isOpen={showModalWorkArea}
size="lg"
closeModal={() => setshowModalWorkArea(false)}
>
<WorkAreaModel
project={projectInfra}
onClose={()=>setshowModalWorkArea(false)}
onClose={() => setshowModalWorkArea(false)}
/>
</GlobalModel>}
{showModalTask && ( <GlobalModel isOpen={showModalTask} size="lg" closeModal={()=>setshowModalTask(false)}>
<TaskModel
</GlobalModel>
)}
{showModalTask && (
<GlobalModel
isOpen={showModalTask}
size="lg"
closeModal={() => setshowModalTask(false)}
>
<TaskModel
project={projectInfra}
onClose={()=>setshowModalTask(false)}
onClose={() => setshowModalTask(false)}
/>
</GlobalModel>)}
</GlobalModel>
)}
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
<div className="card">
<div className="card-body" style={{ padding: "0.5rem" }}>
<div className="align-items-center">
<div className="row ">
<div
className={`col-12 text-end mb-1 `}
className="dataTables_length text-start py-2 px-6 col-md-4 col-12"
id="DataTables_Table_0_length"
>
{ManageInfra && (<>
{!servicesLoading &&
assignedServices?.length > 0 &&
(assignedServices.length > 1 ? (
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={(e) => dispatch(setService(e.target.value))}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
) : (
<h5>{assignedServices[0].name}</h5>
))}
</div>
{/* Buttons Section (aligned to right) */}
<div className="col-md-8 col-12 text-end mb-1">
{ManageInfra && (
<>
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={() => setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Building
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Floors
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
</button>
</>
)}
{(ManageTask || ManageInfra) && (
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={()=>setshowModalBuilding(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Building
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalFloor(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Floors
</button>
<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalWorkArea(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Manage Work Areas
</button></>)}
{(ManageTask || ManageInfra) && (<button
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={()=>setshowModalTask(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Create Tasks
</button>)}
type="button"
className="link-button btn btn-xs rounded-md m-1 btn-primary"
onClick={() => setshowModalTask(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Create Tasks
</button>
)}
</div>
</div>
<div className="row ">
@ -132,11 +192,14 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
<InfraTable
buildings={projectInfra}
projectId={projectId}
// handleFloor={submitData}
// signalRHandler ={signalRHandler}
serviceId={selectedService}
/>
)}
{!isLoading && projectInfra?.length == 0 && <div className="mt-5"><p>No Infra Avaiable</p></div>}
{!isLoading && projectInfra?.length == 0 && (
<div className="mt-5">
<p>No Infra Avaiable</p>
</div>
)}
</div>
</div>
</div>

View File

@ -0,0 +1,285 @@
import React, { useState } from "react";
import { MANAGE_PROJECT, PROJECT_STATUS } from "../../utils/constants";
import { useProjects } from "../../hooks/useProjects";
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
import ProgressBar from "../common/ProgressBar";
import {
getProjectStatusColor,
getProjectStatusName,
} from "../../utils/projectStatus";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProjectContext } from "../../pages/project/ProjectPage";
import usePagination from "../../hooks/usePagination";
const ProjectListView = ({
currentItems,
selectedStatuses,
handleStatusChange,
setCurrentPage,
totalPages,
isLoading,
}) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { setMangeProject } = useProjectContext();
// const { data, isLoading, isError, error } = useProjects();
// check Permissions
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
const projectColumns = [
{
key: "projectName",
label: "Project Name",
className: "text-start py-3",
getValue: (p) => (
<div
className="text-primary cursor-pointer fw-bold py-3"
onClick={() => {
dispatch(setProjectId(p.id));
navigate(`/projects/details`);
}}
>
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
</div>
),
},
{
key: "contactPerson",
label: "Contact Person",
className: "text-start small",
getValue: (p) => `${p?.contactPerson ?? ""}`.trim() || "N/A",
},
{
key: "startDate",
label: "Start Date",
className: "text-center small",
getValue: (p) => formatUTCToLocalTime(p?.startDate) || "N/A",
},
{
key: "deadline",
label: "Deadline",
className: "text-center small",
getValue: (p) => formatUTCToLocalTime(p?.endDate) || "N/A",
},
{
key: "task",
label: "Task",
colSpan: 2,
className: "text-center small",
getValue: (p) => formatNumber(p?.plannedWork) || "0",
},
{
key: "progress",
label: "Progress",
className: "text-start small",
getValue: (p) => (
<ProgressBar
plannedWork={p.plannedWork}
completedWork={p.completedWork}
className="mb-0"
height="6px"
/>
),
},
{
key: "status",
label: "Status",
className: "text-center small",
isFilter: true,
customRender: (_, selectedStatuses, handleStatusChange) => (
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Status <i className="bx bx-filter bx-sm"></i>
</a>
<ul className="dropdown-menu p-2 text-capitalize">
{PROJECT_STATUS.map(({ id, label }) => (
<li key={id}>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={selectedStatuses.includes(id)}
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
</div>
</li>
))}
</ul>
</div>
),
getValue: (p) => (
<span className={`badge ${getProjectStatusColor(p.projectStatusId)}`}>
{getProjectStatusName(p.projectStatusId)}
</span>
),
},
];
const handleViewActivities = (project) => {
dispatch(setProjectId(project));
navigate(`/activities/records?project=${project}`);
};
return (
<div className="card page-min-h py-4 px-6 shadow-sm">
<div
className="table-responsive text-nowrap page-min-h"
>
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
{projectColumns.map((col) => (
<th key={col.key} colSpan={col.colSpan} className={`${col.className} table_header_border`}>
{col.label}
</th>
))}
<th className="text-center py-3">Action</th>
</tr>
</thead>
<tbody>
{currentItems?.map((project) => (
<tr key={project.id}>
{projectColumns.map((col) => (
<td
key={col.key}
colSpan={col.colSpan}
className={`${col.className} py-5`}
style={{ paddingTop: "20px", paddingBottom: "20px" }}
>
{col.getValue
? col.getValue(project)
: project[col.key] || "N/A"}
</td>
))}
<td
className={`mx-2 ${
canManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item cursor-pointer"
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setMangeProject({
isOpen: true,
Project: project.id,
})
}
>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li onClick={() => handleViewActivities(project.id)}>
<a className="dropdown-item cursor-pointer">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
</ul>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{isLoading && (
<div className="py-4">
{" "}
{isLoading && <p className="text-center">Loading...</p>}
{!isLoading && filteredProjects.length === 0 && (
<p className="text-center text-muted">No projects found.</p>
)}
</div>
)}
{!isLoading && currentItems.length === 0 && (
<div className="py-6">
<p className="text-center text-muted">No projects found.</p>
</div>
)}
{!isLoading && totalPages > 1 && (
<nav>
<ul className="pagination pagination-sm justify-content-end py-2">
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
<button
className="page-link"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, i) => (
<li
key={i}
className={`page-item ${currentPage === i + 1 && "active"}`}
>
<button
className="page-link"
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</button>
</li>
))}
<li
className={`page-item ${
currentPage === totalPages && "disabled"
}`}
>
<button
className="page-link"
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</div>
);
};
export default ProjectListView;

View File

@ -1,36 +1,35 @@
import React from 'react'
import AssignRole from './AssignTask'
import React from "react";
import AssignRole from "./AssignTask";
const ProjectModal = ({modalConfig,closeModal}) => {
const ProjectModal = ({ modalConfig, closeModal }) => {
return (
<div
className="modal fade"
id="project-modal"
tabindex="-1"
aria-hidden="true"
role="dialog"
>
<div className="modal-dialog modal-lg modal-simple">
<div className="modal-content">
<div className="modal-body">
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal}
></button>
<div className="text-center mb-2"></div>
{modalConfig?.type === "assignRole" && <AssignRole assignData={modalConfig?.data} onClose={closeModal} />}
</div>
</div>
</div>
</div>
)
}
className="modal fade"
id="project-modal"
tabindex="-1"
aria-hidden="true"
role="dialog"
>
<div className="modal-dialog modal-lg modal-simple">
<div className="modal-content">
<div className="modal-body">
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal}
></button>
<div className="text-center mb-2"></div>
export default ProjectModal
{modalConfig?.type === "assignRole" && (
<AssignRole assignData={modalConfig?.data} onClose={closeModal} />
)}
</div>
</div>
</div>
</div>
);
};
export default ProjectModal;

View File

@ -1,6 +1,4 @@
import React from "react";
import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
DIRECTORY_ADMIN,
DIRECTORY_MANAGER,
@ -13,6 +11,7 @@ import {
VIEW_DOCUMENT,
VIEW_PROJECT_INFRA,
} from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
@ -22,7 +21,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirUser = useHasUserPermission(DIRECTORY_USER);
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT);
const isViewDocuments = useHasUserPermission(VIEW_DOCUMENT);
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
@ -42,8 +41,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
hidden: !(DirAdmin || DireManager || DirUser),
},
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
];
return (
<div className="nav-align-top">

View File

@ -0,0 +1,125 @@
import React from "react";
import { useProjectAssignedOrganizations } from "../../../hooks/useProjects";
import { useSelectedProject } from "../../../slices/apiDataManager";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
const ProjectAssignedOrgs = () => {
const selectedProject = useSelectedProject();
const { data, isLoading, isError, error } =
useProjectAssignedOrganizations(selectedProject);
const orgList = [
{
key: "name",
label: "Organization Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
<i class="bx bx-buildings"></i>
<span
className="text-truncate d-inline-block "
style={{ maxWidth: "150px" }}
>
{org?.name || "N/A"}
</span>
</div>
),
align: "text-start",
},
{
key: "service",
label: "Service Name",
getValue: (org) => (
<div className="d-flex gap-2 py-1 ">
{org?.service?.name}
</div>
),
align: "text-start",
},
{
key: "sprid",
label: "Service Provider Id",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.sprid || "N/A"}
</span>
),
align: "text-center",
},
{
key: "organizationType",
label: "Organization Type",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{org?.organizationType || "N/A"}
</span>
),
align: "text-center",
},
{
key: "assignedDate",
label: "Assigned Date",
getValue: (org) => (
<span
className="text-truncate d-inline-block"
style={{ maxWidth: "200px" }}
>
{/* {org?.assignedDate || "N/A"} */}
{formatUTCToLocalTime(org?.assignedDate)}
</span>
),
align: "text-center",
},
];
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>{error.message}</div>;
return (
<div>
<div className="dataTables_wrapper no-footer mx-5 pb-2 page">
<table className="table dataTable text-nowrap">
<thead>
<tr className="table_header_border">
{orgList.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{Array.isArray(data) && data.length > 0 ? (
data.map((row, i) => (
<tr key={i}>
{orgList.map((col) => (
<td key={col.key} className={col.align}>
{col.getValue(row)}
</td>
))}
</tr>
))
) : (
<tr style={{ height: "200px" }}>
<td
colSpan={orgList.length}
className="text-center align-middle"
>
Not Assigned yet
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default ProjectAssignedOrgs;

View File

@ -0,0 +1,11 @@
import React from 'react'
const ProjectServices = () => {
return (
<div className='row'>
</div>
)
}
export default ProjectServices

View File

@ -1,29 +1,28 @@
import React from "react";
import { useOrganizationModal } from "../../hooks/useOrganization";
import { useSelectedProject } from "../../slices/apiDataManager";
import ProjectAssignedOrgs from "./ProjectOrganization/ProjectAssignedOrgs";
const ProjectOrganizations = () => {
const orgModal = useOrganizationModal();
const selectedProject = useSelectedProject()
const { onOpen, startStep, flowType } = useOrganizationModal();
const selectedProject = useSelectedProject();
return (
<div className="card">
<div className="card-header">
<div className="d-flex justify-content-end px-2">
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={() => orgModal.onOpen(selectedProject)}
>
<i className="bx bx-plus-circle me-2"></i>
Add Organization
</button>
<div className="card pb-10" >
<div className="card-header">
<div className="d-flex justify-content-end px-2">
<button
type="button"
className="link-button btn btn-sm rounded-md link-button-sm m-1 btn-primary"
onClick={() => onOpen({ startStep: 1, flowType: "assign" })}
>
<i className="bx bx-plus-circle me-2"></i>
Add Organization
</button>
</div>
</div>
</div>
<div className="card-body">
<p className="text-secondary">
Not found Organization connected with current Project
</p>
<div className="row">
<ProjectAssignedOrgs />
</div>
</div>
);

View File

@ -10,9 +10,9 @@ import ReactApexChart from "react-apexcharts";
import Chart from "react-apexcharts";
const ProjectOverview = ({ project }) => {
const { projects } = useProjects();
const { data } = useProjects();
const [current_project, setCurrentProject] = useState(
projects.find((pro) => pro.id == project)
data?.find((pro) => pro.id == project)
);
const selectedProject = useSelector(
@ -154,7 +154,7 @@ const ProjectOverview = ({ project }) => {
}, [current_project]);
useEffect(() => {
setCurrentProject(projects.find((pro) => pro.id == selectedProject));
setCurrentProject(data?.find((pro) => pro.id == selectedProject));
if (current_project) {
let val = getProgressInPercentage(
current_project.plannedWork,

View File

@ -0,0 +1,59 @@
import { z } from "zod";
import { DEFAULT_EMPTY_STATUS_ID } from "../../utils/constants";
const currentDate = new Date()
export const projectDefault = {
name: "",
shortName: "",
contactPerson: "",
projectAddress: "",
startDate: currentDate.toISOString().split("T")[0],
endDate: currentDate.toISOString().split("T")[0],
projectStatusId: DEFAULT_EMPTY_STATUS_ID,
promoterId: "",
pmcId: "",
};
export const projectSchema = z
.object({
name: z.string().min(1, { message: "Project Name is required" }),
shortName: z.string().optional(),
contactPerson: z
.string()
.min(1, { message: "Contact Person Name is required" })
.regex(/^[A-Za-z\s]+$/, {
message: "Contact Person must contain only letters",
}),
projectAddress: z
.string()
.min(1, { message: "Address is required" })
.max(500, "Address must not exceed 150 characters"),
startDate: z
.string()
.min(1, { message: "Start Date is required" })
.default(projectDefault),
endDate: z
.string()
.min(1, { message: "End Date is required" })
.default(projectDefault),
projectStatusId: z.string().min(1, { message: "Status is required" }),
promoterId: z.string().min(1, { message: "Promoter is required" }),
pmcId: z.string().min(1, { message: "PMC is required" }),
})
.refine(
(data) => {
const start = new Date(data.startDate);
const end = new Date(data.endDate);
return end >= start;
},
{
path: ["endDate"],
message: "End Date must be greater than Start Date",
}
);

View File

@ -0,0 +1,80 @@
import React, { useState } from "react";
import TeamEmployeeList from "./TeamEmployeeList";
import { useOrganization } from "../../../hooks/useDirectory";
import { useOrganizationsList } from "../../../hooks/useOrganization";
import { useProjectAssignedOrganizationsName } from "../../../hooks/useProjects";
import { useSelectedProject } from "../../../slices/apiDataManager";
const TeamAssignToProject = ({ closeModal }) => {
const [searchText, setSearchText] = useState("");
const [selectedOrg, setSelectedOrg] = useState(null);
const project = useSelectedProject();
const { data, isLoading, isError, error } =
useProjectAssignedOrganizationsName(project);
return (
<div className="container">
<p className="fs-5 fs-seminbod ">Assign Employee To Project </p>
<div className="row align-items-center gx-5">
<div className="col">
<div className="d-flex flex-grow-1 align-items-center gap-2">
{isLoading ? (
<select className="form-select form-select-sm w-100" disabled>
<option value="">Loading...</option>
</select>
) : data?.length === 0 ? (
<p className="mb-0 badge bg-label-secondary">No organizations found</p>
) : (
<>
<label
htmlFor="organization"
className="form-label mb-0 text-nowrap"
>
Select Organization
</label>
<select
id="organization"
className="form-select form-select-sm w-100"
value={selectedOrg || ""}
onChange={(e) => setSelectedOrg(e.target.value)}
>
<option value="">Select</option>
{data.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
</>
)}
</div>
</div>
<div className="col">
<div className="d-flex flex-grow-1 align-items-center gap-2">
<label htmlFor="search" className="form-label mb-0 text-nowrap">
Search Employee
</label>
<input
id="search"
type="search"
className="form-control form-control-sm w-100"
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
</div>
</div>
<div >
<TeamEmployeeList
organizationId={selectedOrg}
searchTerm={searchText}
closeModal={closeModal}
/>
</div>
</div>
);
};
export default TeamAssignToProject;

View File

@ -0,0 +1,251 @@
import React, { useState, useEffect } from "react";
import Avatar from "../../common/Avatar";
import { useDebounce } from "../../../utils/appUtils";
import { useSelectedProject } from "../../../slices/apiDataManager";
import {
useEmployeesByProjectAllocated,
useManageProjectAllocation,
useProjectAssignedServices,
} from "../../../hooks/useProjects";
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
import showToast from "../../../services/toastService";
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
const selectedProject = useSelectedProject();
const debounceSearchTerm = useDebounce(searchTerm, 500);
const {
data: employeesData = [],
isLoading,
isError,
error,
} = useOrganizationEmployees(
selectedProject,
organizationId,
debounceSearchTerm
);
const { projectEmployees, loading: employeeLodaing } =
useEmployeesByProjectAllocated(selectedProject, null);
const { data: jobRoles } = useMaster();
const { data: services } = useProjectAssignedServices(selectedProject);
const [employees, setEmployees] = useState([]);
const { mutate: handleAssignEmployee, isPending } =
useManageProjectAllocation({
onSuccessCallback: () => {
closeModal();
},
onErrorCallback: () => {
closeModal();
},
});
useEffect(() => {
if (employeesData?.data?.length > 0) {
const available = employeesData.data.filter((emp) => {
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
return !projEmp || projEmp.isActive === false;
});
setEmployees(
available.map((emp) => ({
...emp,
isChecked: false,
jobRole: emp?.jobRoleId || null,
serviceId: "",
errors: {},
}))
);
}
}, [employeesData, projectEmployees, organizationId]);
const handleCheckboxChange = (index) => {
setEmployees((prev) => {
const newArr = [...prev];
newArr[index].isChecked = !newArr[index].isChecked;
newArr[index].errors = {};
return newArr;
});
};
const handleSelectChange = (index, field, value) => {
setEmployees((prev) => {
const newArr = [...prev];
newArr[index][field] = value;
newArr[index].errors[field] = "";
return newArr;
});
};
const onSubmit = () => {
const checkedEmployees = employees.filter((emp) => emp.isChecked);
setEmployees((prev) => prev.map((emp) => ({ ...emp, errors: {} })));
if (checkedEmployees.length === 0) {
showToast("Select at least one employee", "info");
return;
}
let hasError = false;
const newEmployees = employees.map((emp) => {
const empErrors = {};
if (emp.isChecked) {
if (!emp.jobRole) {
empErrors.jobRole = "Job role is required";
hasError = true;
}
if (!emp.serviceId) {
empErrors.serviceId = "Service is required";
hasError = true;
}
}
return { ...emp, errors: empErrors };
});
setEmployees(newEmployees);
if (hasError) return; // stop submit if validation fails
const payload = checkedEmployees.map((emp) => ({
employeeId: emp.id,
jobRoleId: emp.jobRole,
serviceId: emp.serviceId,
projectId: selectedProject,
status: true,
}));
handleAssignEmployee({ payload,actionType:"assign"} );
setEmployees((prev) =>
prev.map((emp) => ({
...emp,
isChecked: false,
jobRole: "",
serviceId: "",
errors: {},
}))
);
};
if (isLoading) {
return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ;
}
if (isError) {
return (
<div className="page-min-h d-flex justify-content-center align-items-center ">
{error?.status === 400 ? (
<p className="m-0">Enter employee you want to find.</p>
) : (
<p className="m-0 dange-text">Something went wrong. Please try again later.</p>
)}
</div>
);
}
if (employees.length === 0) {
return(<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>) ;
}
return (
<div className=" position-relative">
<table className="table" style={{ maxHeight: "80px", overflowY: "auto" }}>
<thead className=" position-sticky top-0">
<tr>
<th>Employee</th>
<th>Service</th>
<th>Job Role</th>
<th>Select</th>
</tr>
</thead>
<tbody>
{employees.map((emp, index) => (
<tr key={emp.id}>
<td>
<div className="d-flex align-items-center">
<Avatar firstName={emp.firstName} lastName={emp.lastName} />
<span className="ms-2 fw-semibold">
{emp.firstName} {emp.lastName}
</span>
</div>
</td>
<td>
<select
value={emp.serviceId}
disabled={!emp.isChecked}
onChange={(e) =>
handleSelectChange(index, "serviceId", e.target.value)
}
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
emp.errors.serviceId ? "is-invalid" : ""
}`}
>
<option value="">Select Service</option>
{services?.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
{emp.errors.serviceId && (
<div className="danger-text">{emp.errors.serviceId}</div>
)}
</td>
<td>
<select
value={emp.jobRole}
disabled={!emp.isChecked}
onChange={(e) =>
handleSelectChange(index, "jobRole", e.target.value)
}
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
emp.errors.jobRole ? "is-invalid" : ""
}`}
>
<option value="">Select Job Role</option>
{jobRoles?.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
{emp.errors.jobRole && (
<div className="danger-text">{emp.errors.jobRole}</div>
)}
</td>
<td>
<input
type="checkbox"
className="form-check-input"
checked={emp.isChecked}
onChange={() => handleCheckboxChange(index)}
/>
</td>
</tr>
))}
</tbody>
</table>
<div className="position-sticky bottom-0 bg-white d-flex justify-content-end gap-3 z-25 ">
<button
type="button"
className="btn btn-sm btn-label-secondary"
onClick={() => closeModal()}
>
Cancel
</button>
<button onClick={onSubmit} className="btn btn-primary">
{isPending ? "Please Wait..." : "Assign to Project"}
</button>
</div>
</div>
);
};
export default TeamEmployeeList;

View File

@ -0,0 +1,339 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Link, NavLink, useNavigate, useParams } from "react-router-dom";
import showToast from "../../../services/toastService";
import Avatar from "../../common/Avatar";
import moment from "moment";
import ProjectRepository from "../../../repositories/ProjectRepository";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../../slices/localVariablesSlice";
import useMaster from "../../../hooks/masterHook/useMaster";
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
import { ASSIGN_TO_PROJECT } from "../../../utils/constants";
import ConfirmModal from "../../common/ConfirmModal";
import eventBus from "../../../services/eventBus";
import {
useEmployeesByProjectAllocated,
useManageProjectAllocation,
useProjectAssignedServices,
} from "../../../hooks/useProjects";
import { useSelectedProject } from "../../../slices/apiDataManager";
import GlobalModel from "../../common/GlobalModel";
import TeamAssignToProject from "./TeamAssignToProject";
const Teams = () => {
const selectedProject = useSelectedProject();
const dispatch = useDispatch();
const [AssigTeam, setAssignTeam] = useState(false);
const [employees, setEmployees] = useState([]);
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [deleteEmployee, setDeleteEmplyee] = useState(null);
const [searchTerm, setSearchTerm] = useState(""); // State for search term
const [selectedService, setSelectedService] = useState(null);
const [activeEmployee, setActiveEmployee] = useState(false);
const { data: assignedServices, isLoading: servicesLoading } =
useProjectAssignedServices(selectedProject);
const { data: empJobRoles, loading } = useMaster();
const handleToggleActive = (e) => setActiveEmployee(e.target.checked);
const handleServiceChange = (e) => {
setSelectedService(e.target.value);
};
const navigate = useNavigate();
const HasAssignUserPermission = useHasUserPermission(ASSIGN_TO_PROJECT);
const [IsDeleteModal, setIsDeleteModal] = useState(false);
const {
projectEmployees,
loading: employeeLodaing,
refetch,
} = useEmployeesByProjectAllocated(
selectedProject,
selectedService,
null,
activeEmployee
);
const {
mutate: submitAllocations,
isPending,
isSuccess,
isError,
} = useManageProjectAllocation({
onSuccessCallback: () => {
setSelectedEmployee(null);
},
onErrorCallback: () => {
setSelectedEmployee(null);
},
});
const handleDelete = (employee) => {
let payload = [
{
employeeId: employee?.employeeId,
jobRoleId: employee?.jobRoleId,
projectId: selectedProject,
serviceId: employee?.serviceId,
status: false,
},
];
submitAllocations({ payload: payload, actionType: "remove" });
};
const getJobRole = (jobRoleId) => {
if (loading) return "Loading...";
if (!Array.isArray(empJobRoles)) return "Unassigned";
if (!jobRoleId) return "Unassigned";
const role = empJobRoles.find((b) => b.id == jobRoleId);
return role ? role.name : "Unassigned";
};
const filteredEmployees = useMemo(() => {
if (!projectEmployees) return [];
let filtered = projectEmployees;
if (activeEmployee) {
filtered = projectEmployees.filter((emp) => !emp.isActive);
}
// Apply search filter if present
if (searchTerm?.trim()) {
const lower = searchTerm.toLowerCase();
filtered = filtered.filter((emp) => {
const fullName = `${emp.firstName ?? ""} ${emp.lastName ?? ""}`.toLowerCase();
const jobRole = getJobRole(emp?.jobRoleId)?.toLowerCase();
return fullName.includes(lower) || jobRole.includes(lower);
});
}
return filtered;
}, [projectEmployees, searchTerm, activeEmployee]);
const handleSearch = (e) => setSearchTerm(e.target.value);
const employeeHandler = useCallback(
(msg) => {
if (filteredEmployees.some((emp) => emp.employeeId == msg.employeeId)) {
refetch();
}
},
[filteredEmployees, refetch]
);
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
return (
<>
{AssigTeam && (
<GlobalModel
size="lg"
isOpen={AssigTeam}
closeModal={() => setAssignTeam(false)}
>
<TeamAssignToProject closeModal={() => setAssignTeam(false)} />
</GlobalModel>
)}
<ConfirmModal
type="delete"
header={"Remove Employee"}
message={"Are you sure you want to remove?"}
isOpen={!!selectedEmployee}
loading={isPending}
onSubmit={() => handleDelete(selectedEmployee)}
onClose={() => setSelectedEmployee(null)}
/>
<div className="card card-action mb-6">
<div className="card-body">
<div className="row align-items-center justify-content-between mb-4 g-3">
<div className="col-md-6 col-12 algin-items-center">
<div className="d-flex flex-wrap align-items-center gap-3">
<div>
{!servicesLoading && (
<>
{(!assignedServices || assignedServices.length === 0) && (
<span className="badge bg-label-secondary">
Not Service Assigned
</span>
)}
{assignedServices?.length === 1 && (
<span className="badge bg-label-secondary">
{assignedServices[0].name}
</span>
)}
{assignedServices?.length > 1 && (
<select
className="form-select form-select-sm"
aria-label="Select Service"
value={selectedService}
onChange={handleServiceChange}
>
<option value="">All Services</option>
{assignedServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
)}
</>
)}
</div>
<div className="form-check form-switch d-flex align-items-center text-nowrap">
<input
type="checkbox"
className="form-check-input"
id="activeEmployeeSwitch"
checked={activeEmployee}
onChange={handleToggleActive}
/>
<label
className="form-check-label ms-2"
htmlFor="activeEmployeeSwitch"
>
{activeEmployee ? "Active Employees" : "Include Inactive Employees"}
</label>
</div>
</div>
</div>
<div className="col-md-6 col-12 d-flex justify-content-md-end align-items-center justify-content-start gap-3">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search by Name or Role"
aria-controls="DataTables_Table_0"
style={{ maxWidth: "200px" }}
value={searchTerm}
onChange={handleSearch}
/>
{HasAssignUserPermission && (
<button
type="button"
className="btn btn-primary btn-sm text-nowrap"
onClick={() => setAssignTeam(true)}
>
<i className="bx bx-plus-circle me-1"></i>
Assign Employee
</button>
)}
</div>
</div>
<div className="table-responsive text-nowrap modal-min-h">
{employeeLodaing && <p>Loading..</p>}
{projectEmployees && projectEmployees.length > 0 && (
<table className="table ">
<thead>
<tr>
<th>
<div className="text-start ms-5">Name</div>
</th>
<th>Services</th>
<th>Organization</th>
<th>Assigned Date</th>
{activeEmployee && <th>Release Date</th>}
<th>Project Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody className="table-border-bottom-0">
{filteredEmployees &&
filteredEmployees
.sort((a, b) =>
(a.firstName || "").localeCompare(b.firstName || "")
)
.map((emp) => (
<tr key={emp.id}>
<td>
<div className="d-flex justify-content-start align-items-center">
<Avatar
firstName={emp.firstName}
lastName={emp.lastName}
/>
<div className="d-flex flex-column">
<a
onClick={() =>
navigate(
`/employee/${emp.employeeId}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{emp.firstName}{" "}
{emp.lastName}
</span>
</a>
</div>
</div>
</td>
<td>{emp.serviceName || "N/A"}</td>
<td>{emp.organizationName || "N/A"}</td>
<td>
{moment(emp.allocationDate).format("DD-MMM-YYYY")}
</td>
{activeEmployee && (
<td>
{emp.reAllocationDate
? moment(emp.reAllocationDate).format(
"DD-MMM-YYYY"
)
: "Present"}
</td>
)}
<td>
<span className="badge bg-label-primary me-1">
{getJobRole(emp.jobRoleId)}
</span>
</td>
<td>
{emp.isActive ? (
<button
aria-label="Delete"
type="button"
title="Remove from project"
className="btn p-0 dropdown-toggle hide-arrow"
onClick={() => setSelectedEmployee(emp)}
>
<i className="bx bx-trash me-1 text-danger"></i>
</button>
) : (
<span>Not in project</span>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
{!employeeLodaing && filteredEmployees.length === 0 && (
<div className="text-center text-muted py-3 d-flex justify-content-center align-items-center py-12">
<p>
{activeEmployee
? "No active employees assigned to the project"
: "No inactive employees assigned to the project"}
</p>
</div>
)}
</div>
</div>
</div>
</>
);
};
export default Teams;

View File

@ -1,420 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import MapUsers from "./MapUsers";
import { Link, NavLink, useNavigate, useParams } from "react-router-dom";
import showToast from "../../services/toastService";
import Avatar from "../common/Avatar";
import moment from "moment";
import ProjectRepository from "../../repositories/ProjectRepository";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster from "../../hooks/masterHook/useMaster";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { ASSIGN_TO_PROJECT } from "../../utils/constants";
import ConfirmModal from "../common/ConfirmModal";
import eventBus from "../../services/eventBus";
import {
useEmployeesByProjectAllocated,
useManageProjectAllocation,
} from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
const Teams = () => {
// const {projectId} = useParams()
// const projectId = useSelector((store)=>store.localVariables.projectId)
const projectId = useSelectedProject();
const dispatch = useDispatch();
const { data, loading } = useMaster();
const [isModalOpen, setIsModelOpen] = useState(false);
const [error, setError] = useState("");
const [empJobRoles, setEmpJobRoles] = useState(null);
const [employees, setEmployees] = useState([]);
const [filteredEmployees, setFilteredEmployees] = useState([]);
const [removingEmployeeId, setRemovingEmployeeId] = useState(null);
const [assignedLoading, setAssignedLoading] = useState(false);
const [activeEmployee, setActiveEmployee] = useState(true);
const [deleteEmployee, setDeleteEmplyee] = useState(null);
const [searchTerm, setSearchTerm] = useState(""); // State for search term
const navigate = useNavigate();
const HasAssignUserPermission = useHasUserPermission(ASSIGN_TO_PROJECT);
const [IsDeleteModal, setIsDeleteModal] = useState(false);
const {
projectEmployees,
loading: employeeLodaing,
refetch,
} = useEmployeesByProjectAllocated(projectId);
const {
mutate: submitAllocations,
isPending,
isSuccess,
isError,
} = useManageProjectAllocation({
onSuccessCallback: () => {
setRemovingEmployeeId(null);
setAssignedLoading(false);
setDeleteEmplyee(null);
closeDeleteModal();
},
onErrorCallback: () => {
closeDeleteModal();
},
});
const removeAllocation = (item) => {
setRemovingEmployeeId(item.id);
submitAllocations({
items: [
{
empID: item.employeeId,
jobRoleId: item.jobRoleId,
projectId: projectId,
status: false,
},
],
added: false,
});
};
const handleEmpAlicationFormSubmit = (allocaionObj) => {
let items = allocaionObj.map((item) => {
return {
empID: item.empID,
jobRoleId: item.jobRoleId,
projectId: projectId,
status: true,
};
});
submitAllocations({ items, added: true });
setActiveEmployee(true);
setFilteredEmployees(employees.filter((emp) => emp.isActive));
const dropdown = document.querySelector(
'select[name="DataTables_Table_0_length"]'
);
if (dropdown) dropdown.value = "true";
};
const getRole = (jobRoleId) => {
if (loading) return "Loading...";
if (!Array.isArray(empJobRoles)) return "Unassigned";
if (!jobRoleId) return "Unassigned";
const role = empJobRoles.find((b) => b.id == jobRoleId);
return role ? role.name : "Unassigned";
};
const openModel = () => {
setIsModelOpen(true);
};
const onModelClose = () => {
setIsModelOpen(false);
const modalElement = document.getElementById("user-model");
if (modalElement) {
modalElement.classList.remove("show");
modalElement.style.display = "none";
document.body.classList.remove("modal-open");
document.querySelector(".modal-backdrop").remove();
}
const modalBackdropElement = document.querySelector(".modal-backdrop");
if (modalBackdropElement) {
modalBackdropElement.remove();
}
document.body.style.overflow = "auto";
};
useEffect(() => {
dispatch(changeMaster("Job Role"));
}, [dispatch]);
useEffect(() => {
if (projectEmployees) {
setEmployees(projectEmployees);
//setFilteredEmployees(projectEmployees?.filter((emp) => emp.isActive));
const filtered = projectEmployees.filter((emp) => emp.isActive);
setFilteredEmployees(filtered);
}
}, [projectEmployees, employeeLodaing]);
useEffect(() => {
if (data) {
setEmpJobRoles(data);
}
}, [data]);
const filterAndSearchEmployees = useCallback(() => {
const statusFiltered = employees.filter((emp) =>
activeEmployee ? emp.isActive : !emp.isActive
);
if (searchTerm === "") {
setFilteredEmployees(statusFiltered);
return;
}
const lowercasedSearchTerm = searchTerm.toLowerCase();
const searchedAndFiltered = statusFiltered.filter((item) => {
const fullName =
`${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
const roleName = getRole(item.jobRoleId).toLowerCase();
return (
fullName.includes(lowercasedSearchTerm) ||
roleName.includes(lowercasedSearchTerm)
);
});
setFilteredEmployees(searchedAndFiltered);
}, [employees, activeEmployee, searchTerm, getRole]);
useEffect(() => {
filterAndSearchEmployees();
}, [employees, activeEmployee, searchTerm, filterAndSearchEmployees]);
const handleFilterEmployee = (e) => {
const filterValue = e.target.value;
// if (filterValue === "true") {
// setActiveEmployee(true);
// setFilteredEmployees(employees.filter((emp) => emp.isActive));
// } else {
// setFilteredEmployees(employees.filter((emp) => !emp.isActive));
// setActiveEmployee(false);
// }
setActiveEmployee(filterValue === "true");
setSearchTerm("");
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
};
const deleteModalOpen = (item) => {
setDeleteEmplyee(item);
setIsDeleteModal(true);
};
const closeDeleteModal = () => setIsDeleteModal(false);
const handler = useCallback(
(msg) => {
if (msg.projectIds.some((item) => item === projectId)) {
refetch();
}
},
[projectId, refetch]
);
useEffect(() => {
eventBus.on("assign_project_all", handler);
return () => eventBus.off("assign_project_all", handler);
}, [handler]);
const employeeHandler = useCallback(
(msg) => {
if (filteredEmployees.some((item) => item.employeeId == msg.employeeId)) {
refetch();
}
},
[filteredEmployees, refetch]
);
useEffect(() => {
eventBus.on("employee", employeeHandler);
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
return (
<>
<div
className="modal fade"
id="user-model"
tabIndex="-1"
aria-labelledby="userModalLabel"
aria-hidden="true"
>
<MapUsers
projectId={projectId}
onClose={onModelClose}
empJobRoles={empJobRoles}
onSubmit={handleEmpAlicationFormSubmit}
allocation={employees}
assignedLoading={assignedLoading}
setAssignedLoading={setAssignedLoading}
></MapUsers>
</div>
{IsDeleteModal && (
<ConfirmModal
isOpen={IsDeleteModal}
type="delete"
header="Removed Employee"
message="Are you sure you want delete?"
onSubmit={() => removeAllocation(deleteEmployee)}
onClose={closeDeleteModal}
loading={isPending}
/>
)}
<div className="card card-action mb-6">
<div className="card-body">
<div className="row d-flex justify-content-between mb-4">
<div className="col-md-6 col-12 d-flex align-items-center">
<div className="dataTables_filter d-inline-flex align-items-center ms-2">
<input
type="search"
className="form-control form-control-sm me-4"
placeholder="Search by Name or Role"
aria-controls="DataTables_Table_0"
value={searchTerm}
onChange={handleSearch}
/>
</div>
</div>
<div className="col-md-6 col-12 d-flex justify-content-end align-items-center">
<div
className="dataTables_length text-start py-2 px-2"
id="DataTables_Table_0_length"
>
<label>
<select
name="DataTables_Table_0_length"
aria-controls="DataTables_Table_0"
className="form-select form-select-sm"
onChange={handleFilterEmployee}
// value={false}
aria-label=""
defaultValue="true"
>
<option value="true">Active Employee</option>
<option value="false">In-Active Employee</option>
</select>
</label>
</div>
<button
type="button"
className={`link-button btn-primary btn-sm ${
HasAssignUserPermission ? "" : "d-none"
}`}
data-bs-toggle="modal"
data-bs-target="#user-model"
>
<i className="bx bx-plus-circle me-2"></i>
Assign Employee
</button>
</div>
</div>
<div className="table-responsive text-nowrap">
{employeeLodaing && <p>Loading..</p>}
{!employeeLodaing &&
filteredEmployees &&
filteredEmployees.length > 0 && (
<table className="table ">
<thead>
<tr>
<th>
<div className="text-start ms-5">Name</div>
</th>
<th>Assigned Date</th>
{!activeEmployee && <th>Release Date</th>}
<th>Project Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody className="table-border-bottom-0">
{filteredEmployees &&
filteredEmployees.map((item) => (
<tr key={item.id}>
<td>
<div className="d-flex justify-content-start align-items-center">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={() =>
navigate(
`/employee/${item.employeeId}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
</span>
</a>
</div>
</div>
</td>
<td>
{" "}
{moment(item.allocationDate).format(
"DD-MMM-YYYY"
)}{" "}
</td>
{!activeEmployee && (
<td>
{item.reAllocationDate
? moment(item.reAllocationDate).format(
"DD-MMM-YYYY"
)
: "Present"}
</td>
)}
<td>
<span className="badge bg-label-primary me-1">
{getRole(item.jobRoleId)}
</span>
</td>
<td>
{item.isActive && (
<button
aria-label="Delete"
type="button"
title="Remove from project"
className="btn p-0 dropdown-toggle hide-arrow"
onClick={() => deleteModalOpen(item)}
>
{" "}
{removingEmployeeId === item.id ? (
<div
className="spinner-border spinner-border-sm text-primary"
role="status"
>
<span className="visually-hidden">
Loading...
</span>
</div>
) : (
<i className="bx bx-trash me-1 text-danger"></i>
)}
</button>
)}
{!item.isActive && <span>Not in project</span>}
</td>
</tr>
))}
</tbody>
</table>
)}
{!employeeLodaing && filteredEmployees.length === 0 && (
<div className="text-center text-muted py-3">
{activeEmployee
? "No active employees assigned to the project"
: "No inactive employees assigned to the project"}
</div>
)}
</div>
</div>
</div>
</>
);
};
export default Teams;

View File

@ -72,15 +72,15 @@ const EditProfile = ({ TenantId, onClose }) => {
return (
<FormProvider {...methods}>
<form className="row g-6" onSubmit={handleSubmit(onSubmit)}>
<h6>Edit Tenant</h6>
<h5>Edit Tenant</h5>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="firstName" required>First Name</Label>
<input id="firstName" type="text" className="form-control form-control-sm" {...register("firstName")} inputMode='text' />
{errors.firstName && <div className="danger-text">{errors.firstName.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="lastName" required>Last Name</Label>
<input id="lastName" type="text" className="form-control form-control-sm" {...register("lastName")} />
{errors.lastName && <div className="danger-text">{errors.lastName.message}</div>}
@ -88,32 +88,32 @@ const EditProfile = ({ TenantId, onClose }) => {
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="contactNumber" required>Contact Number</Label>
<input id="contactNumber" type="text" className="form-control form-control-sm" {...register("contactNumber")} inputMode="tel"
placeholder="+91 9876543210" />
{errors.contactNumber && <div className="danger-text">{errors.contactNumber.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="domainName" >Domain Name</Label>
<input id="domainName" type="text" className="form-control form-control-sm" {...register("domainName")} />
{errors.domainName && <div className="danger-text">{errors.domainName.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="taxId" >Tax ID</Label>
<input id="taxId" type="text" className="form-control form-control-sm" {...register("taxId")} />
{errors.taxId && <div className="danger-text">{errors.taxId.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="officeNumber" >Office Number</Label>
<input id="officeNumber" type="text" className="form-control form-control-sm" {...register("officeNumber")} />
{errors.officeNumber && <div className="danger-text">{errors.officeNumber.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="industryId" required>Industry</Label>
<select className="form-select form-select-sm" {...register("industryId")}>
{industryLoading ? <option value="">Loading...</option> :
@ -125,7 +125,7 @@ const EditProfile = ({ TenantId, onClose }) => {
{errors.industryId && <div className="danger-text">{errors.industryId.message}</div>}
</div>
<div className="col-sm-6 mt-1">
<div className="col-sm-6 mt-1 text-start">
<Label htmlFor="reference">Reference</Label>
<select className="form-select form-select-sm" {...register("reference")}>
{reference.map((org) => (
@ -134,7 +134,7 @@ const EditProfile = ({ TenantId, onClose }) => {
</select>
{errors.reference && <div className="danger-text">{errors.reference.message}</div>}
</div>
<div className="col-sm-6">
<div className="col-sm-6 text-start">
<Label htmlFor="organizationSize" required>
Organization Size
</Label>
@ -154,19 +154,19 @@ const EditProfile = ({ TenantId, onClose }) => {
)}
</div>
<div className="col-12 mt-1">
<div className="col-12 mt-1 text-start">
<Label htmlFor="billingAddress" required>Billing Address</Label>
<textarea id="billingAddress" className="form-control" {...register("billingAddress")} rows={2} />
{errors.billingAddress && <div className="danger-text">{errors.billingAddress.message}</div>}
</div>
<div className="col-12 mt-1">
<div className="col-12 mt-1 text-start">
<Label htmlFor="description">Description</Label>
<textarea id="description" className="form-control" {...register("description")} rows={2} />
{errors.description && <div className="danger-text">{errors.description.message}</div>}
</div>
<div className="col-sm-12">
<div className="col-sm-12 text-start">
<Label htmlFor="logImage">Logo Image</Label>
<LogoUpload
preview={logoPreview}
@ -176,9 +176,9 @@ const EditProfile = ({ TenantId, onClose }) => {
/>
</div>
<div className="d-flex justify-content-center gap-2 mt-3">
<div className="d-flex justify-content-end gap-2 mt-3">
<button type="button" disabled={isPending} className="btn btn-sm btn-label-secondary" onClick={onClose}>Cancel</button>
<button type="submit" disabled={isPending} className="btn btn-sm btn-primary">{isPending ? "Please Wait..." : "Submit"}</button>
<button type="button" disabled={isPending} className="btn btn-sm btn-secondary" onClick={onClose}>Cancel</button>
</div>
</form>
</FormProvider>

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
const toBase64 = (file) =>
@ -10,11 +10,15 @@ const toBase64 = (file) =>
});
export const LogoUpload = ({ preview, setPreview, fileName, setFileName }) => {
const {
register,
setValue,
formState: { errors },
} = useFormContext();
const { register, setValue, watch, formState: { errors } } = useFormContext();
const logoImage = watch("logoImage");
// Sync preview when the form value changes
useEffect(() => {
if (logoImage && !preview) {
setPreview(logoImage); // Use base64 as preview
}
}, [logoImage, preview, setPreview]);
const handleUpload = async (e) => {
const file = e.target.files?.[0];

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Label from "../common/Label";
import DatePicker from "../common/DatePicker";
@ -6,11 +6,14 @@ import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
import { LogoUpload } from "./LogoUpload";
import { orgSize, reference } from "../../utils/constants";
import moment from "moment";
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
import SelectMultiple from "../common/SelectMultiple";
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
const { data, isError, isLoading: industryLoading } = useIndustries();
const [logoPreview, setLogoPreview] = useState(null);
const [logoName, setLogoName] = useState("");
const { data: services, isLoading: serviceLoading } = useGlobalServices();
const {
register,
control,
@ -42,6 +45,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
"industryId",
"reference",
"logoImage",
"serviceIds",
]);
if (valid) {
@ -53,6 +57,13 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
}
};
useEffect(() => {
const logoImage = getValues("logoImage");
if (logoImage) {
setLogoPreview(logoImage);
setLogoName("Uploaded Logo");
}
}, [getValues]);
return (
@ -181,7 +192,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
</div>
<div className="col-sm-6">
<Label htmlFor="reference">Reference</Label>
<Label htmlFor="reference" required>Reference</Label>
<select
id="reference"
className="form-select shadow-none border py-1 px-2 small"
@ -198,6 +209,20 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
)}
</div>
<div className="col-sm-6">
<SelectMultiple
name="serviceIds"
label="Services"
options={services?.data}
isLoading={serviceLoading}
labelKey="name"
valueKey="id"
/>
{errors.serviceIds && (
<div className="danger-text">{errors.serviceIds.message}</div>
)}
</div>
<div className="col-sm-12">
<Label htmlFor="description">Description</Label>

View File

@ -5,12 +5,12 @@ import GlobalModel from "../common/GlobalModel";
import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
import IconButton from "../common/IconButton";
import { hasUserPermission } from "../../utils/authUtils";
import { MANAGE_TENANTS } from "../../utils/constants";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const Profile = ({ data }) => {
const {setEditTenant} = useTenantDetailsContext()
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS)
const canUpdateTenant = useHasUserPermission(MANAGE_TENANTS)
return (
<>
<div className="container-fuid">

View File

@ -224,7 +224,7 @@ const SubScription = ({ onSubmitSubScription, onNext }) => {
</div>
{Object.keys(errors).length > 0 && (
<div class="alert alert-danger" role="alert">
<div role="alert">
{Object.entries(errors).map(([key, error]) => (
<div key={key} className="danger-text">
{error?.message}

View File

@ -14,7 +14,7 @@ const SkeletonCell = ({ width = "100%", height = 20, style = {} }) => (
export const TenantTableSkeleton = ({ columns, rows = 5 }) => {
return (
<div className="card p-2 mt-3">
<div className="p-2 mt-3">
<div className="card-datatable text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>

View File

@ -38,7 +38,7 @@ const TenantFilterPanel = ({ onApply }) => {
);
// Close popup when navigating to another component
// Close popup when navigating to another component
const location = useLocation();
useEffect(() => {
handleClosePanel();
@ -106,13 +106,13 @@ const TenantFilterPanel = ({ onApply }) => {
<div className="d-flex justify-content-end py-3 gap-2">
<button
type="button"
className="btn btn-label-secondary btn-xs"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear
</button>
<button type="submit" className="btn btn-primary btn-xs" >
<button type="submit" className="btn btn-primary btn-sm" >
Apply
</button>
</div>

View File

@ -33,6 +33,7 @@ export const newTenantSchema = z.object({
organizationSize: z.string().nonempty("Organization size is required"),
industryId: z.string().uuid("Invalid industry ID"),
reference: z.string().nonempty("Reference is required"),
serviceIds: z.array(z.string()).optional(),
});
export const tenantDefaultValues = {
@ -51,13 +52,14 @@ export const tenantDefaultValues = {
organizationSize: "",
industryId: "", // should be a valid UUID if pre-filled
reference: "",
serviceIds:[]
};
export const getSubscriptionSchema = (minUsers) =>
z.object({
planId: z.string().min(1, { message: "Please select Plan" }),
planId: z.string().min(1, { message: "Please select a plan to continue" }),
currencyId: z.string().uuid("Invalid currency"),
maxUsers: z
.number({ invalid_type_error: "Must be a number" })

View File

@ -137,7 +137,7 @@ const TenantsList = ({
return (
<>
<div className="p-2 mt-3">
<div className="card-datatable text-nowrap table-responsive">
<div className=" text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap">
<thead>
<tr className="shadow-sm">

View File

@ -1,14 +1,13 @@
import { useEffect, useRef } from "react";
import { useController } from "react-hook-form";
const DatePicker = ({
name,
control,
placeholder = "DD-MM-YYYY",
className = "",
allowText = false,
maxDate, // removed default new Date()
maxDate,
minDate,
...rest
}) => {
@ -22,43 +21,43 @@ const DatePicker = ({
});
useEffect(() => {
if (inputRef.current) {
flatpickr(inputRef.current, {
dateFormat: "d-m-Y",
allowInput: allowText,
defaultDate: value
? flatpickr.parseDate(value, "Y-m-d")
: null,
maxDate: maxDate ?? undefined, // only applied if passed
minDate: minDate ? new Date(minDate.split("T")[0]) : undefined,
onChange: function (selectedDates) {
if (selectedDates.length > 0) {
// store in YYYY-MM-DD
const formatted = flatpickr.formatDate(selectedDates[0], "Y-m-d");
onChange(formatted);
} else {
onChange("");
}
},
...rest
});
}
if (!inputRef.current) return;
const fp = flatpickr(inputRef.current, {
dateFormat: "d-m-Y",
allowInput: allowText,
defaultDate: value ? new Date(value) : null, // safely convert to Date
maxDate: maxDate ? new Date(maxDate) : undefined,
minDate: minDate ? new Date(minDate) : undefined,
onChange: (selectedDates) => {
if (selectedDates.length > 0) {
onChange(flatpickr.formatDate(selectedDates[0], "Y-m-d"));
} else {
onChange("");
}
},
...rest
});
return () => {
fp.destroy(); // clean up on unmount
};
}, [inputRef, value, allowText, maxDate, minDate, rest, onChange]);
const displayValue = value ? flatpickr.formatDate(new Date(value), "d-m-Y") : "";
return (
<div className={` position-relative ${className}`}>
<div className={`position-relative ${className}`}>
<input
type="text"
className="form-control form-control-sm "
className="form-control form-control-sm"
placeholder={placeholder}
defaultValue={
value
? flatpickr.formatDate(
flatpickr.parseDate(value, "Y-m-d"),
"d-m-Y"
)
: ""
}
value={displayValue}
onChange={(e) => {
if (allowText) {
onChange(e.target.value); // allow manual typing if enabled
}
}}
ref={(el) => {
inputRef.current = el;
ref(el);
@ -70,7 +69,7 @@ const DatePicker = ({
<span
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
onClick={() => {
if (inputRef.current && inputRef.current._flatpickr) {
if (inputRef.current?._flatpickr) {
inputRef.current._flatpickr.open();
}
}}

View File

@ -1,27 +1,39 @@
import React, { useEffect, useRef } from "react";
import { useController, useFormContext, useWatch } from "react-hook-form";
import { useSelector } from "react-redux";
const DateRangePicker = ({
md,
sm,
md=12,
sm=6,
onRangeChange,
DateDifference = 7,
endDateMode = "yesterday",
}) => {
const inputRef = useRef(null);
const persistedRange = useSelector(
(store) => store.localVariables.attendance.defaultDateRange
);
useEffect(() => {
const endDate = new Date();
if (endDateMode === "yesterday") {
endDate.setDate(endDate.getDate() - 1);
let startDate, endDate;
if (persistedRange?.startDate && persistedRange?.endDate) {
startDate = new Date(persistedRange.startDate);
endDate = new Date(persistedRange.endDate);
} else {
endDate = new Date();
if (endDateMode === "yesterday") {
endDate.setDate(endDate.getDate() - 1);
}
endDate.setHours(0, 0, 0, 0);
startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - (DateDifference - 1));
startDate.setHours(0, 0, 0, 0);
}
endDate.setHours(0, 0, 0, 0);
const startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - (DateDifference - 1));
startDate.setHours(0, 0, 0, 0);
const fp = flatpickr(inputRef.current, {
const fp = flatpickr(inputRef.current, {
mode: "range",
dateFormat: "Y-m-d",
altInput: true,
@ -41,32 +53,32 @@ const DateRangePicker = ({
endDate: endDate.toLocaleDateString("en-CA"),
});
return () => {
fp.destroy();
};
}, [onRangeChange, DateDifference, endDateMode]);
return () => fp.destroy();
}, [onRangeChange, DateDifference, endDateMode, persistedRange]);
const handleIconClick = () => {
if (inputRef.current) {
inputRef.current._flatpickr.open(); // directly opens flatpickr
if (inputRef.current?._flatpickr) {
inputRef.current._flatpickr.open();
}
};
return (
<div className={`col-${sm} col-sm-${md} px-1`}>
<input
type="text"
className="form-control form-control-sm ps-2 pe-5 me-4"
placeholder="From to End"
id="flatpickr-range"
ref={inputRef}
/>
<div className={`position-relative w-auto justify-content-center`}>
<input
type="text"
className="form-control form-control-sm w-100 pe-8 "
placeholder="From to End"
id="flatpickr-range"
ref={inputRef}
/>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 end-0 translate-middle-y me-2 "
onClick={handleIconClick}
/>
</div>
<i
className="bx bx-calendar calendar-icon cursor-pointer position-relative top-50 translate-middle-y " onClick={handleIconClick}
style={{ right: "22px", bottom: "-8px" }}
></i>
</div>
);
};
@ -76,6 +88,7 @@ export default DateRangePicker;
export const DateRangePicker1 = ({
startField = "startDate",
endField = "endDate",
@ -148,10 +161,20 @@ export const DateRangePicker1 = ({
}, []);
useEffect(() => {
if (defaultRange && resetSignal !== undefined) {
if (resetSignal !== undefined) {
if (defaultRange) {
applyDefaultDates();
} else {
setValue(startField, "", { shouldValidate: true });
setValue(endField, "", { shouldValidate: true });
if (inputRef.current?._flatpickr) {
inputRef.current._flatpickr.clear();
}
}
}, [resetSignal, defaultRange]);
}
}, [resetSignal, defaultRange, setValue, startField, endField]);
const start = getValues(startField);
const end = getValues(endField);

View File

@ -4,10 +4,13 @@ import { useDebounce } from "../../utils/appUtils";
import { useController } from "react-hook-form";
import Avatar from "./Avatar";
const EmployeeSearchInput = ({ control, name, projectId,placeholder }) => {
const EmployeeSearchInput = ({
control,
name,
projectId,
placeholder,
forAll,
}) => {
const {
field: { onChange, value, ref },
fieldState: { error },
@ -17,17 +20,20 @@ const EmployeeSearchInput = ({ control, name, projectId,placeholder }) => {
const [showDropdown, setShowDropdown] = useState(false);
const debouncedSearch = useDebounce(search, 500);
const {
data: employees,
isLoading,
} = useEmployeesName(projectId, debouncedSearch);
const { data: employees, isLoading } = useEmployeesName(
projectId,
debouncedSearch,
forAll
);
useEffect(() => {
if (value && !search) {
const found = employees?.data?.find((emp) => emp.id === value);
if (found) setSearch(found.firstName + " " + found.lastName);
if (value && employees?.data) {
const found = employees.data.find((emp) => emp.id === value);
if (found && forAll) {
setSearch(found.firstName + " " + found.lastName);
}
}
}, [value, employees]);
}, [value, employees?.data]);
const handleSelect = (employee) => {
onChange(employee.id);
@ -46,7 +52,7 @@ const EmployeeSearchInput = ({ control, name, projectId,placeholder }) => {
onChange={(e) => {
setSearch(e.target.value);
setShowDropdown(true);
onChange("");
onChange("");
}}
onFocus={() => {
if (search) setShowDropdown(true);
@ -61,28 +67,27 @@ const EmployeeSearchInput = ({ control, name, projectId,placeholder }) => {
{isLoading ? (
<li className="list-group-item">
<a>Searching...</a>
</li>
) : (
employees?.data?.map((emp) => (
<li
key={emp.id}
className="list-group-item list-group-item-action py-1 px-1"
style={{ cursor: "pointer" }}
onClick={() => handleSelect(emp)}
>
<div className="d-flex align-items-center px-0">
<Avatar
size="xs"
classAvatar="m-0 me-2"
firstName={emp.firstName}
lastName={emp.lastName}
/>
<span className="text-muted">
{`${emp?.firstName} ${emp?.lastName}`.trim()}
</span>
</div>
</li>
<li
key={emp.id}
className="list-group-item list-group-item-action py-1 px-1"
style={{ cursor: "pointer" }}
onClick={() => handleSelect(emp)}
>
<div className="d-flex align-items-center px-0">
<Avatar
size="xs"
classAvatar="m-0 me-2"
firstName={emp.firstName}
lastName={emp.lastName}
/>
<span className="text-muted">
{`${emp?.firstName} ${emp?.lastName}`.trim()}
</span>
</div>
</li>
))
)}
</ul>

View File

@ -0,0 +1,65 @@
import React, { useEffect, useRef, useState } from "react";
const HoverPopup = ({ title, content, children }) => {
const [visible, setVisible] = useState(false);
const triggerRef = useRef(null);
const popupRef = useRef(null);
// Toggle on hover or click
const handleMouseEnter = () => setVisible(true);
const handleClick = () => setVisible((prev) => !prev);
// Hide on outside click
useEffect(() => {
const handleDocumentClick = (e) => {
if (
!popupRef.current?.contains(e.target) &&
!triggerRef.current?.contains(e.target)
) {
setVisible(false);
}
};
if (visible) {
document.addEventListener("click", handleDocumentClick);
}
return () => {
document.removeEventListener("click", handleDocumentClick);
};
}, [visible]);
return (
<div
className="d-inline-block position-relative"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
{visible && (
<div
ref={popupRef}
className="bg-white border rounded shadow-sm py-1 px-2 text-start text-lowercase"
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
zIndex: 1050,
minWidth: "220px",
marginTop: "8px",
}}
>
{title && <h6 className="mb-2 text-capitalize">{title}</h6>}
<div className="text-capitalize">{content}</div>
</div>
)}
</div>
);
};
export default HoverPopup;

View File

@ -1,4 +1,3 @@
import { useCallback } from "react";
const Modal = ({
@ -7,16 +6,14 @@ const Modal = ({
title,
body,
disabled,
size="md",
position="top",
size = "md",
position = "top",
}) => {
const handleClose = useCallback(() => {
if (disabled) return;
onClose();
}, [disabled, onClose]);
if (!isOpen) return null;
return (
@ -26,26 +23,24 @@ const Modal = ({
tabIndex="-1"
role="dialog"
>
<div className={`modal-dialog modal-${size} modal-dialog-${position}`} role="document">
<div className="modal-content text-white shadow-lg">
<div
className={`modal-dialog modal-${size} modal-dialog-${position}`}
role="document"
>
<div className="modal-content text-white shadow-lg">
{/* Header */}
<div className="modal-header pb-2 border-0">
<div className="modal-header justify-content-center pb-2 border-0">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="btn-close btn-close-white"
onClick={handleClose}
aria-label="Close"
>
</button>
></button>
</div>
{/* Body */}
<div className="modal-body pt-0">{body}</div>
</div>
</div>
</div>

View File

@ -3,35 +3,65 @@ import React from "react";
const ProgressBar = ({
plannedWork = 100,
completedWork = 0,
height = "8px",
height = "6px",
className = "mb-4",
rounded = true,
showLabel = true,
}) => {
const getProgress = (planned, completed) => {
if (!planned || planned === 0) return "0%";
return `${Math.min((completed / planned) * 100, 100).toFixed(2)}%`;
if (!planned || planned === 0) return 0;
return Math.min((completed / planned) * 100, 100);
};
const progressStyle = {
width: getProgress(plannedWork, completedWork),
const percentage = getProgress(plannedWork, completedWork);
const progressBarStyle = {
width: ` ${percentage.toFixed(2)}%`,
transition: "width 0.4s ease",
};
const containerStyle = {
height,
display: "flex",
alignItems: "center",
gap: "8px",
};
return (
<div
className={`progress ${className} ${rounded ? "rounded" : ""}`}
style={{ height }}
className={`d-flex align-items-center ${className}`}
style={containerStyle}
>
<div
className={`progress-bar ${rounded ? "rounded" : ""}`}
role="progressbar"
style={progressStyle}
aria-valuenow={completedWork}
aria-valuemin="0"
aria-valuemax={plannedWork}
></div>
<div className="flex-grow-1">
<div
className={`progress ${rounded ? "rounded" : ""}`}
style={{ height, backgroundColor: "#f0f0f0" }}
>
<div
className={`progress-bar ${rounded ? "rounded" : ""}`}
role="progressbar"
style={progressBarStyle}
aria-valuenow={completedWork}
aria-valuemin="0"
aria-valuemax={plannedWork}
/>
</div>
</div>
{showLabel && (
<span
className="fw-semibold text-secondary"
style={{
minWidth: "45px",
textAlign: "right",
fontSize: "0.8rem",
}}
>
{percentage.toFixed(2)}%
</span>
)}
</div>
);
};
export default ProgressBar;

View File

@ -8,24 +8,24 @@ const SelectMultiple = ({
name,
options = [],
label = "Select options",
labelKey = "name",
labelKey = "name",
valueKey = "id",
placeholder = "Please select...",
IsLoading = false,
IsLoading = false,required = false
}) => {
const { setValue, watch } = useFormContext();
const selectedValues = watch(name) || [];
const { setValue, watch,register } = useFormContext();
useEffect(() => {
register(name, { value: [] });
}, [register, name]);
const selectedValues = watch(name) || [];
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const containerRef = useRef(null);
const dropdownRef = useRef(null);
const [dropdownStyles, setDropdownStyles] = useState({
top: 0,
left: 0,
width: 0,
});
const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 });
useEffect(() => {
const handleClickOutside = (e) => {
@ -64,10 +64,14 @@ const SelectMultiple = ({
setValue(name, updated, { shouldValidate: true });
};
const filteredOptions = options.filter((item) => {
const label = getLabel(item);
return label?.toLowerCase().includes(searchText.toLowerCase());
});
const filteredOptions = (options || []).filter((item) => {
const label = getLabel(item);
return (
typeof label === "string" &&
label.toLowerCase().includes(searchText.toLowerCase())
);
});
const dropdownElement = (
<div
@ -105,14 +109,8 @@ const SelectMultiple = ({
return (
<div
key={valueVal}
className={`multi-select-dropdown-option ${
isChecked ? "selected" : ""
}`}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
}}
className={`multi-select-dropdown-option ${isChecked ? "selected" : ""}`}
style={{ display: "flex", alignItems: "center", padding: "4px 8px" }}
>
<input
type="checkbox"
@ -141,12 +139,9 @@ const SelectMultiple = ({
return (
<>
<div
ref={containerRef}
className="multi-select-dropdown-container"
style={{ position: "relative" }}
>
<label>{label}</label>
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
<label className="form-label mb-1">{label}</label>
<Label className={name} required={required}></Label>
<div
className="multi-select-dropdown-header"
@ -155,9 +150,7 @@ const SelectMultiple = ({
>
<span
className={
selectedValues.length > 0
? "placeholder-style-selected"
: "placeholder-style"
selectedValues.length > 0 ? "placeholder-style-selected" : "placeholder-style"
}
>
<div className="selected-badges-container">
@ -166,10 +159,7 @@ const SelectMultiple = ({
const found = options.find((opt) => opt[valueKey] === val);
const label = found ? getLabel(found) : "";
return (
<span
key={val}
className="badge badge-selected-item mx-1 mb-1"
>
<span key={val} className="badge badge-selected-item mx-1 mb-1">
{label}
</span>
);

View File

@ -0,0 +1,143 @@
import React, { useState } from "react";
import { useImageGalleryFilter } from "../../hooks/useImageGallery";
import { useSelectedProject } from "../../slices/apiDataManager";
import { FormProvider, useForm } from "react-hook-form";
import Label from "../common/Label";
import { DateRangePicker1 } from "../common/DateRangePicker";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultGalleryFilterValue, gallerySchema } from "./GallerySchema";
import SelectMultiple from "../common/SelectMultiple";
import { localToUtc } from "../../utils/appUtils";
const GalleryFilterPanel = ({ onApply }) => {
const selectedProject = useSelectedProject();
const [resetKey, setResetKey] = useState(0);
const { data, isLoading, isError, error } =
useImageGalleryFilter(selectedProject);
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const methods = useForm({
resolver: zodResolver(gallerySchema),
defaultValues: defaultGalleryFilterValue,
});
const {
handleSubmit,
register,
setValue,reset,
formState: { errors },
} = methods;
const onSubmit = (formData) => {
onApply({
...formData,
startDate: localToUtc(formData.startDate),
endDate: localToUtc(formData.endDate),
});
closePanel()
};
const onClear=()=>{
reset(defaultGalleryFilterValue);
setResetKey((prev) => prev + 1);
closePanel()
}
if (isLoading) return <div>Loading....</div>;
if (isError) return <div>{error.message}</div>;
return (
<div className="d-block text-start">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<Label>Select Date:</Label>
<DateRangePicker1
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
maxDate={new Date()}
/>
</div>
<div className="mb-2">
<SelectMultiple
name="buildingIds"
label="Select building:"
options={data?.buildings}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="floorIds"
label="Select Floor:"
options={data?.floors}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="workAreaIds"
label="Select Work Area:"
options={data?.workAreas}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="workCategoryIds"
label="Select Work Category:"
options={data?.workCategories}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="activityIds"
label="Select Activity:"
options={data?.activities}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="uploadedByIds"
label="Select Uploaded By:"
options={data?.UploadedBys}
labelKey="name"
valueKey="id"
/>
</div>
<div className="mb-2">
<SelectMultiple
name="serviceIds"
label="Select Service:"
options={data?.services}
labelKey="name"
valueKey="id"
/>
</div>
<div className="d-flex flex-row gap-3 justify-content-end">
<button className="btn btn-sm btn-label-secondary" onClick={onClear}>Cancel</button>
<button type="submit" className="btn btn-sm btn-primary">
Apply
</button>
</div>
</form>
</FormProvider>
</div>
);
};
export default GalleryFilterPanel;

View File

@ -0,0 +1,25 @@
import { z } from "zod";
export const gallerySchema = z.object({
buildingIds: z.array(z.string()).optional(),
floorIds: z.array(z.string()).optional(),
workAreaIds: z.array(z.string()).optional(),
activityIds: z.array(z.string()).optional(),
workCategoryIds: z.array(z.string()).optional(),
startDate: z.string().optional(),
endDate: z.string().optional(),
uploadedByIds: z.array(z.string()).optional(),
serviceIds: z.array(z.string()).optional(),
});
export const defaultGalleryFilterValue = {
buildingIds: [],
floorIds: [],
workAreaIds: [],
activityIds: [],
workCategoryIds:[],
startDate: null,
endDate: null,
uploadedByIds:[],
serviceIds: [],
};

View File

@ -0,0 +1,169 @@
import React, { useRef, useState, useCallback, useEffect } from "react";
import moment from "moment";
import Avatar from "../../components/common/Avatar";
import { useGalleryContext } from "../../pages/Gallary/ImageGallaryPage";
import useImageGallery from "../../hooks/useImageGallery";
import { useSelectedProject } from "../../slices/apiDataManager";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import Pagination from "../common/Pagination";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader from "../common/Loader";
const ImageGalleryListView = ({filter}) => {
const [hoveredImage, setHoveredImage] = useState(null);
const selectedProject = useSelectedProject();
const [currentPage, setCurrentPage] = useState(1);
const { setOpenGallery } = useGalleryContext();
const { data, isLoading, isError, error } = useImageGallery(
selectedProject,
currentPage,
10,
filter
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
if (!data?.data?.length && !isLoading) {
return (
<p className="text-center text-muted mt-5">
{selectedProject ? " No images match the selected filters.":"Please Select Project!"}
</p>
);
}
if (isLoading) {
return (
<div className="page-min-h d-flex justify-content-center align-items-center">
<Loader />
</div>
);
}
return (
<div className="main-content">
<div className="activity-section">
{data?.data?.map((batch) => {
if (!batch.documents?.length) return null;
const doc = batch.documents[0];
const userName = `${doc.uploadedBy?.firstName || ""} ${
doc.uploadedBy?.lastName || ""
}`.trim();
const date = formatUTCToLocalTime(doc.uploadedAt);
// const hasArrows = batch.documents.length > scrollThreshold;
return (
<div key={batch.batchId} className="grouped-section">
<div className="group-heading">
<div className="d-flex align-items-center mb-1">
<Avatar
size="xs"
firstName={doc.uploadedBy?.firstName}
lastName={doc.uploadedBy?.lastName}
className="me-2"
/>
<div className="d-flex flex-column align-items-start">
<strong className="user-name-text">{userName}</strong>
<span className="text-muted small">{date}</span>
</div>
</div>
<div className="location-line text-secondary">
<div className="d-flex align-items-center flex-wrap gap-1 text-secondary">
<span className="d-flex align-items-center">
<span>{batch.buildingName}</span>
<i className="bx bx-chevron-right " />
</span>
<span className="d-flex align-items-center">
<span>{batch.floorName}</span>
<i className="bx bx-chevron-right m" />
</span>
<span className="d-flex align-items-center ">
<span>{batch.workAreaName || "Unknown"}</span>
<i className="bx bx-chevron-right " />
<span>{batch.activityName}</span>
</span>
</div>
{batch.workCategoryName && (
<span className="badge bg-label-primary ms-2">
{batch.workCategoryName}
</span>
)}
</div>
</div>
<div className="image-group-wrapper">
{/* {hasArrows && (
<button className="scroll-arrow left-arrow" onClick={() => scrollLeft(batch.batchId)}>
</button>
)} */}
<div
className="image-group-horizontal"
// ref={(el) => (imageGroupRefs.current[batch.batchId] = el)}
>
{batch.documents.map((d, i) => {
const hoverDate = moment().format("DD MMMM, YYYY");
const hoverTime = moment(d.uploadedAt).format("hh:mm A");
return (
<div
key={d.id}
className="image-card"
onMouseEnter={() => setHoveredImage(d)}
onMouseLeave={() => setHoveredImage(null)}
onClick={() =>
setOpenGallery({
isOpen: true,
data: { data: batch, index: i },
})
}
>
<div className="image-wrapper">
<img src={d.url} alt={`Image ${i + 1}`} />
</div>
{hoveredImage === d && (
<div className="image-hover-description">
<p>
<strong>Date:</strong> {hoverDate}
</p>
<p>
<strong>Time:</strong> {hoverTime}
</p>
<p>
<strong>Activity:</strong> {batch.activityName}
</p>
</div>
)}
</div>
);
})}
</div>
{/* {hasArrows && (
<button className="scroll-arrow right-arrow" onClick={() => scrollRight(batch.batchId)}>
</button>
)} */}
</div>
</div>
);
})}
</div>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data?.totalPages}
onPageChange={paginate}
/>
)}
</div>
);
};
export default ImageGalleryListView;

View File

@ -0,0 +1,111 @@
import React, { useState, useEffect } from "react";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
const ViewGallery = ({ batch, index }) => {
const [loading, setLoading] = useState(true);
const [currentIndex, setCurrentIndex] = useState(index);
console.log(batch);
useEffect(() => {
setCurrentIndex(index);
}, [index, batch]);
if (!batch || !batch.documents || batch.documents.length === 0) return null;
const image = batch.documents[currentIndex];
if (!image) return null;
const fullName = `${image.uploadedBy?.firstName || ""} ${
image.uploadedBy?.lastName || ""
}`.trim();
const date = formatUTCToLocalTime(image.uploadedAt);
const buildingName = batch.buildingName;
const floorName = batch.floorName;
const workAreaName = batch.workAreaName;
const activityName = batch.activityName;
const batchComment = batch.comment;
const handlePrev = () => {
setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1));
};
const handleNext = () => {
setCurrentIndex((prevIndex) =>
Math.min(batch.documents.length - 1, prevIndex + 1)
);
};
const hasPrev = currentIndex > 0;
const hasNext = currentIndex < batch.documents.length - 1;
return (
<div>
{loading && <p>Loading...</p>}
<div className="position-relative d-flex justify-content-center align-items-center">
{hasPrev && (
<button
className="btn btn-icon btn-outline-primary rounded-circle position-absolute start-0 top-50 translate-middle-y shadow"
onClick={handlePrev}
>
<i className="bx bx-chevron-left fs-4"></i>
</button>
)}
<img
src={image.url}
alt="Preview"
className="img-fluid rounded"
style={{
maxHeight: "500px",
width: "100%",
objectFit: "contain",
}}
onLoad={() => setLoading(false)}
/>
{hasNext && (
<button
className="btn btn-icon btn-outline-primary position-absolute end-0 top-50 translate-middle-y rounded-circle shadow"
onClick={handleNext}
>
<i className="bx bx-chevron-right fs-4"></i>
</button>
)}
</div>
{/* Details */}
<div className="mt-3 text-start small">
<p className="mb-1">
<i className="bx bxs-user me-2"></i>
<span className="text-muted">Uploaded By: </span>
<span className="fw-semibold">{fullName}</span>
</p>
<p className="mb-1">
<i className="bx bxs-calendar me-2"></i>
<span className="text-muted">Date: </span>
<span className="fw-semibold">{date}</span>
</p>
<p className="mb-1">
<i className="bx bx-map me-2"></i>
<span className="text-muted">Location: </span>
<span className="fw-semibold">
{buildingName} <i className="bx bx-chevron-right"></i> {floorName}{" "}
<i className="bx bx-chevron-right"></i> {workAreaName || "Unknown"}{" "}
<i className="bx bx-chevron-right"></i> {activityName}
</span>
</p>
<p className="mb-0">
<i className="bx bx-comment-dots me-2"></i>
<span className="text-muted">Comment: </span>
<span className="fw-semibold">{batchComment}</span>
</p>
</div>
</div>
);
};
export default ViewGallery;

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