Compare commits

..

191 Commits

Author SHA1 Message Date
96bcdffdca added optional chain for n=menu 2025-12-05 18:45:19 +05:30
f8963ef476 Merge pull request 'At Assigned Employee when selecting a Job role it change according to master now show correct data.' (#520) from Assigned_Employee_Job_Role into main
Reviewed-on: #520
merged
2025-11-26 12:20:28 +00:00
9ad3b8726c At Assigned Employee when selecting a Job role it change according to master now show correct data. 2025-11-25 18:09:32 +05:30
d4582c101a Merge pull request 'Project_Branch_Management : New Feature Service Project - Branch Management' (#519) from Project_Branch_Management into main
Reviewed-on: #519
merged
2025-11-25 09:42:47 +00:00
c6af020c85 changed fs-6 and body-font-size 2025-11-25 15:09:25 +05:30
822ff1a7e4 UI Alignment in Service Card view. 2025-11-25 11:20:19 +05:30
92d17167b1 added zoom in-out 2025-11-25 10:11:43 +05:30
8ec62827d5 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-24 17:58:26 +05:30
965fce1587 Merge pull request 'Service_Project_ListView :- Creating List view for service Project.' (#518) from Service_Project_ListView into Project_Branch_Management
Reviewed-on: #518
Merged
2025-11-24 11:16:57 +00:00
bb37fd7044 set align for Service Project List card 2025-11-24 11:16:57 +00:00
03d60bf66d Adding search functionality in Projectpage. 2025-11-24 11:16:57 +00:00
071e5956a7 Changes. 2025-11-24 11:16:57 +00:00
c669eb90c3 Correction in List view alignment. 2025-11-24 11:16:57 +00:00
4d2e37f52e Adding Message for Card and list view in Services and Infra 2025-11-24 11:16:57 +00:00
6dde240926 Creating a list view for Services. 2025-11-24 11:16:57 +00:00
482b5a1680 Adding lIst view. 2025-11-24 11:16:57 +00:00
098090cf69 Merge pull request 'Status Dropdown Should Auto-Close After Job Status Update' (#516) from Kartik_Bug#1773 into Project_Branch_Management
Reviewed-on: #516
mergd
2025-11-24 11:05:51 +00:00
5747d4ec71 Status Dropdown Should Auto-Close After Job Status Update 2025-11-24 10:35:03 +05:30
48f314eac4 added signalR for project Branch 2025-11-21 15:28:31 +05:30
18e739f3ab fixed collection header ui 2025-11-21 14:51:31 +05:30
a5329f1a2a added new selectproject field inside ManageRecurring 2025-11-21 14:25:32 +05:30
8b4a9d2d1c added new project filed inside ManagePaymentRequest 2025-11-21 14:08:52 +05:30
4506f740eb Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 13:13:50 +05:30
f001dff5b0 Creating new icon for Un-archive 2025-11-21 13:09:14 +05:30
33d94f6f06 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 12:58:52 +05:30
0ac1141579 changed filed for project and employee now not need project for select employee 2025-11-21 12:58:47 +05:30
4d8af5da91 added pre-fill updating 2025-11-21 12:57:54 +05:30
fead3a37a6 Adding Pending in Loading in Job list. 2025-11-21 12:31:10 +05:30
98e9a8b625 When we in Archive tab then canvas will be open now it work correctly. 2025-11-21 12:21:22 +05:30
2781da8906 Change the logic according to archieve show by status. 2025-11-21 12:04:36 +05:30
d13c48dd39 Change the Ui for Archieve jobs. 2025-11-21 11:47:55 +05:30
ee4ef18594 Implementing Restore Functionality in Jobs. 2025-11-21 11:39:36 +05:30
742337a3d0 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 11:12:03 +05:30
5abab06b0c removed console error 2025-11-21 11:11:44 +05:30
17a9f4a9b1 Implementing Project change 2025-11-21 11:10:55 +05:30
de088fcfc2 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 09:46:32 +05:30
8e12eb6797 Adding Switch button on Jobs. 2025-11-21 09:45:28 +05:30
14c1da7888 added restore 2025-11-20 22:01:25 +05:30
753d8c4a2c Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-20 17:09:09 +05:30
766d19744c added suggestion for branch Type inside service Projects 2025-11-20 17:09:04 +05:30
caaadc4c08 Implementing Validation when we status will be done or closed then Archieve button will be shown. 2025-11-20 16:38:25 +05:30
1531fbe6f2 Implementing Archieve functionality in Jobs. 2025-11-20 16:16:20 +05:30
9f5a167613 added copy button for localtion url 2025-11-20 14:49:54 +05:30
fa923d4c3a added popup for branch view 2025-11-20 14:39:54 +05:30
bd43475d12 Merge pull request 'Advance_Payment_List :- Implementing advance Payment new API.' (#514) from Advance_Payment_List into Project_Branch_Management
Reviewed-on: #514
merged
2025-11-20 09:08:07 +00:00
521d46bdee Merge pull request 'Adding Billed To field in Manage Collection.' (#513) from Collection_Service_Project into Project_Branch_Management
Reviewed-on: #513
merged
2025-11-20 07:23:25 +00:00
604bb68dc2 Adding Billed To field in Manage Collection. 2025-11-20 07:23:25 +00:00
047e563505 added mutiple contact person inside branch 2025-11-20 12:49:56 +05:30
e4f053ee65 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-20 12:41:39 +05:30
7105a3640f Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-20 12:36:40 +05:30
d167c57ab0 Implementing the Update api for branches. 2025-11-20 12:34:45 +05:30
d3c006279c Increasing the size of Service details page. 2025-11-20 10:29:42 +05:30
195a0c83bb Correction in api for details. 2025-11-20 10:00:06 +05:30
8f86b05d35 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-20 09:51:29 +05:30
dd716187da Implementing edit api for branches. 2025-11-20 09:51:23 +05:30
5b86a2f64f removed console error 2025-11-20 09:50:49 +05:30
e99d49b83e added view comments 2025-11-20 09:26:02 +05:30
0210e17170 added branch details 2025-11-19 22:58:41 +05:30
e7fddd41c2 added branch for projects 2025-11-19 21:28:01 +05:30
7e4dffff34 Implementing Get api. 2025-11-19 18:47:30 +05:30
e7a68aeab7 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 18:32:03 +05:30
8a5d6158c2 Implementing Creating API. 2025-11-19 18:31:17 +05:30
50269aead2 added selectFiled for branch 2025-11-19 18:28:54 +05:30
0ede07b0d5 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:36:37 +05:30
df24b18a27 At the time of hit in cancel form will be closed. 2025-11-19 16:36:32 +05:30
f5d89f2bab Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:36:01 +05:30
15978b2ac7 fixed useClient implmention 2025-11-19 16:35:53 +05:30
2dbf5dc109 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:31:12 +05:30
7cc8cc99c2 Creating a list view and modal for Branches. 2025-11-19 16:30:51 +05:30
c04ee8dab5 configure branch related api inside service repo. and made hooks for branch 2025-11-19 16:28:37 +05:30
c52f85ee4a added Advance payment flag inside paymentRequest list and view details 2025-11-19 15:44:54 +05:30
c718269dfd added spinner in service profile page 2025-11-19 15:35:39 +05:30
268bd2875f Changing the position of Status in Collection list view. 2025-11-19 14:26:12 +05:30
7773b7a43b Adding Status and UI implementation in Collection. 2025-11-19 12:59:57 +05:30
25003d912e add green color class for onfieldwork.com 2025-11-19 12:35:13 +05:30
a3be63b74f Implementing api for Advance payment all data. 2025-11-19 12:22:38 +05:30
54420c70d9 change landing page logo to marco from ON 2025-11-19 12:15:08 +05:30
4b5f8756b3 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-19 11:27:51 +05:30
10e54637d5 Adding list view in Advance payment list. 2025-11-19 10:47:11 +05:30
3fe533c8e3 addd serach poject field both projects 2025-11-19 10:42:12 +05:30
5b91c13b85 adding list view in advance payment list. 2025-11-19 09:46:41 +05:30
ed87e3f11b removed conole error 2025-11-18 18:28:38 +05:30
e276836653 added prevent to take action after close ticket 2025-11-18 18:13:49 +05:30
523d6baea6 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-18 17:56:21 +05:30
9e47f10440 updated padding of multi- select tag 2025-11-18 17:56:17 +05:30
54d89e1429 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-18 17:38:30 +05:30
018f132501 Adding Navigate functionality in ServiceprjectTeams. 2025-11-18 17:31:51 +05:30
f3eed11540 Merge pull request 'Service_Management_CardView :- Creating a card for Services project.' (#512) from Service_Management_CardView into Service_Project_Managment
Reviewed-on: #512
merged
2025-11-18 11:56:35 +00:00
836b34c237 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Management_CardView 2025-11-18 17:24:04 +05:30
bf92ffcb3e added job status during update job 2025-11-18 17:22:52 +05:30
dd380f6820 Edit button add in About project for Services. 2025-11-18 17:20:53 +05:30
f997c2f2cc Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Management_CardView 2025-11-18 17:01:22 +05:30
1c5d62f5ed Correction in HoverPopup when it we select single the all will be open. 2025-11-18 16:57:48 +05:30
37dbbf53f3 fixed table align 2025-11-18 16:56:11 +05:30
2cb9b41f98 Increasing the size of datepicker at TaskReport filter panel. 2025-11-18 16:38:57 +05:30
e6e27beb6f Creating Card for ServiceProject. 2025-11-18 16:35:05 +05:30
9822ae91ec Adding AdvancePayment a list view. 2025-11-18 15:56:16 +05:30
3753d01f7d prevent to change status once job status is done 2025-11-18 14:25:13 +05:30
c8a599fc8f Adding Extra fields in Service Project Card. 2025-11-18 12:47:39 +05:30
1c2674d435 Filteration using status in Services and Infra. 2025-11-18 12:24:41 +05:30
31d340c645 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Management_CardView 2025-11-18 12:15:25 +05:30
61e210c305 Filtering according to Dropdown filter. 2025-11-18 12:14:35 +05:30
dd41600045 modify Job view Ui 2025-11-18 11:58:57 +05:30
72052a4708 Creating a ServiceCard view. 2025-11-18 11:50:29 +05:30
4dfc0fa7ca Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-18 11:08:44 +05:30
3cfac9a2bb fixed - useProject (infra) params 2025-11-18 11:08:33 +05:30
b14353dd4f Adding Organization form at Create Service. 2025-11-18 10:10:44 +05:30
8de1230f79 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 20:26:33 +05:30
9b65e0a320 added update job 2025-11-17 20:26:28 +05:30
70ed617b9b Adding Payment_processed at the place of payment_Processed. 2025-11-17 19:21:40 +05:30
d4bb7373f2 Adding Plus icon in Create Services. 2025-11-17 19:12:34 +05:30
f4f14127b7 Merge pull request 'We are creating filter chips in the Payment Request filter panel.' (#508) from Katik_Task#1746 into Service_Project_Managment
Reviewed-on: #508
2025-11-17 13:12:58 +00:00
c8bbf6b8a8 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Katik_Task#1746 2025-11-17 18:40:18 +05:30
36be17daa7 Merge pull request 'Hide “Advance Payment” Mode When Creating Expense After Payment Processed (Mobile & Web)' (#509) from Kartik_Bug#1735 into Service_Project_Managment
Reviewed-on: #509
2025-11-17 13:06:01 +00:00
d6b2ed9f84 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 18:35:49 +05:30
088e5e0368 UI Implementation at Job List 2025-11-17 18:35:42 +05:30
977e5c77a7 Merge pull request 'Invoice list should display according to the selected date range in Collection module.' (#510) from Kartik_Bug#1505 into Service_Project_Managment
Reviewed-on: #510
2025-11-17 13:03:50 +00:00
2b1d3e5890 Merge branch 'Service_Project_Managment' into Kartik_Bug#1505 2025-11-17 13:03:42 +00:00
a61e621ce2 Merge pull request 'Kartik_Bug#1737 :- UI Implementation in Expense Weidget.' (#507) from Kartik_Bug#1737 into Service_Project_Managment
Reviewed-on: #507
2025-11-17 13:01:33 +00:00
0bd1cea703 1) Handle blank space in menu bar
2) Breadcrum  "Home" link to "Dashboard" on pages where it was missing
2025-11-17 18:30:13 +05:30
d6ef713370 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 18:28:20 +05:30
da391368a6 setup job data for update 2025-11-17 18:28:15 +05:30
6860ba3dff Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 18:25:48 +05:30
849567f999 UI changes in View Jobs. 2025-11-17 18:25:42 +05:30
507bc68da9 Added the server timeout for signalR 2025-11-17 18:12:35 +05:30
767475c7e5 Adding Create Service Project and Create Infra project project at ManageProject. 2025-11-17 17:53:01 +05:30
5f649cd917 Added Job Id in Jobs. 2025-11-17 17:50:41 +05:30
ea6686560d added missing job id 2025-11-17 17:46:13 +05:30
1358cd749a fixed error during came merged 2025-11-17 17:09:35 +05:30
d95ba4c800 added changes status fun 2025-11-17 17:06:05 +05:30
78eafd2e5a Showing ProjectName at the breadcrum in Services Project. 2025-11-17 16:15:36 +05:30
6598ec9f73 Adding placeholder and adding in TagInput a background color. 2025-11-17 15:44:03 +05:30
679ae95b8c Removing project selection at Job Creation. 2025-11-17 15:21:31 +05:30
4f03cbb305 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 13:34:09 +05:30
d342550b0f added refresh Manage Job form whenever closing after editing and creating job 2025-11-17 13:34:04 +05:30
4f7ad519cf Change label in Advance Payment in Total balance to currenct balance and hide project selection dropdown in PR and Advance. 2025-11-17 13:11:22 +05:30
c311324bad Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 12:45:01 +05:30
937531afb6 updated PmsEmployeeInputTag for getting all exiten employee for update 2025-11-17 12:44:56 +05:30
b68f8306fd added confirmation modal inside serviceproject employee list 2025-11-17 12:44:01 +05:30
7470a6836f UI alignment at View Managejob Ticket in jobs. 2025-11-17 12:33:19 +05:30
12184091cd UI alignment at view Jobs. 2025-11-17 11:29:44 +05:30
8761c128e6 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-17 10:57:51 +05:30
8eb34addcd added serviceproject list 2025-11-17 10:57:48 +05:30
b568188a3e Adding TDS Calculation in ViewExpense. 2025-11-17 10:31:41 +05:30
a21bec943d Added GST number in Organization. 2025-11-17 10:05:02 +05:30
3d1d8264ad Merge pull request 'Service_Project_Management_Bugs :- Changes for Jobs' (#511) from Service_Project_Management_Bugs into Service_Project_Managment
Reviewed-on: #511
Merged
2025-11-17 04:16:04 +00:00
d831d08aaf Adding validation in Contact number and adding trim functioanlity. 2025-11-17 04:16:04 +00:00
8553b6bee7 At New Project popup show the Active selected all the time when popup open. 2025-11-17 04:16:04 +00:00
e47262123d Added columns in jobs list and implementing functionality for daysleft. 2025-11-17 04:16:04 +00:00
852ebd7dd0 Correction in UI in ProjectPage. 2025-11-17 04:16:04 +00:00
fc1235caef added allocation and deallocation of service Project team 2025-11-17 00:42:50 +05:30
7e646ca3e8 add new EmployeeSearch for multiple 2025-11-14 20:07:28 +05:30
92bcfb73c6 Merge branch 'migrate_main' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-14 16:47:40 +05:30
770123119c Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-14 16:39:57 +05:30
7190a3c190 added offcanavs for create and update 2025-11-14 16:39:46 +05:30
b399977946 Invoice list should display according to the selected date range in Collection module. 2025-11-14 15:39:40 +05:30
314dd67118 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-14 15:12:23 +05:30
74e3b11174 Adding some space in Payment Request list. 2025-11-14 15:12:07 +05:30
dab3c5fe52 fixed attachement error 2025-11-14 14:58:50 +05:30
d1bcc9981c Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-14 13:04:29 +05:30
86819dde03 added comment, attchment 2025-11-14 13:04:15 +05:30
64d96d46d0 Correction in UI in Dashboard weidget. 2025-11-14 12:58:47 +05:30
05f45a66d8 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Bug#1737 2025-11-14 11:59:06 +05:30
0b38196847 Hide “Advance Payment” Mode When Creating Expense After Payment Processed (Mobile & Web) 2025-11-14 11:52:12 +05:30
74e645a41e Increase the size of datepicker at gallery filter panel and remove unwanted PaidBy field in ViewPayment Request. 2025-11-14 11:23:10 +05:30
598601c515 We are creating filter chips in the Payment Request filter panel. 2025-11-14 10:22:27 +05:30
297e0712bc integrated job details api and optimized form 2025-11-14 02:51:29 +05:30
9b7988d6d8 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-13 19:50:41 +05:30
844a6f59d1 added fetch api of jobs 2025-11-13 19:50:19 +05:30
c1be1dafc8 Changes in Payment Request List view adding Due Date, Created at and Payee. 2025-11-13 17:45:05 +05:30
14ed888fe8 UI implementation has been done in Expense 2025-11-13 17:22:54 +05:30
b13034a995 Expense widget - Layout change 2025-11-13 15:14:07 +05:30
c4b589460a Correction in EmpReportingManger. 2025-11-13 12:33:50 +05:30
8453a09426 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Service_Project_Managment 2025-11-13 11:34:34 +05:30
60421d1028 Adding Navigate functionality in ManageReporting. 2025-11-13 11:34:05 +05:30
2746e9d2bb added Job tab 2025-11-13 11:00:51 +05:30
651779adea Merge branch 'Organization_Hierarchy' into Service_Project_Managment 2025-11-13 10:20:48 +05:30
910565e003 Merge branch 'Organization_Hierarchy' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Hierarchy 2025-11-13 10:07:48 +05:30
a40f96fe72 removed unneeded border and replace toggle for activtive - inactie 2025-11-13 10:07:40 +05:30
cd467efcb3 Correction in EmpReporting Manger. 2025-11-13 10:05:41 +05:30
559d6cdaac Adding Employee details at mange Reporting popup. 2025-11-13 09:54:54 +05:30
3500adc1eb Change the icon in Employee List manage Reporting button. 2025-11-13 09:45:41 +05:30
d7f00898a0 added delete service project 2025-11-12 19:38:04 +05:30
1bba9ab68a added created, fetch and edit operration 2025-11-12 18:12:37 +05:30
5c1b14bd38 added create service project and separated service project and orgnaization project 2025-11-12 14:18:43 +05:30
fa944f5abe Removing unwanted placeholder. 2025-11-12 12:14:09 +05:30
4351fafed7 Adding Card in Profile related to Reporting Manger. 2025-11-11 18:59:58 +05:30
94413b9beb Implementing ManageReporting at Employee. 2025-11-11 18:00:34 +05:30
867ee92151 Adding popup in employees for ManageReporting. 2025-11-11 11:11:13 +05:30
1195cae2d7 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Organization_Hierarchy 2025-11-11 09:45:48 +05:30
34b0fa3d43 add images 2025-11-10 10:06:09 +05:30
131 changed files with 8481 additions and 1638 deletions

View File

@ -3,6 +3,16 @@
--bs-nav-link-font-size: 0.7375rem;
--bg-border-color :#f8f6f6
}
.offcanvas.offcanvas-wide {
width: 700px !important; /* adjust as needed */
}
.sticky-section {
position: sticky;
top: var(--sticky-top, 0px) !important;
z-index: 1025;
}
/* ===========================% Background_Colors %========================================================== */
.bg-light-primary {
background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);

View File

@ -89,7 +89,7 @@
);
--bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.875rem;
--bs-body-font-size: 0.85rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.375;
--bs-body-color: #646e78;
@ -9060,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] {
}
.table th {
color: var(--bs-heading-color);
font-size: 0.8125rem;
font-size: 0.8025rem;
letter-spacing: 0.2px;
text-transform: uppercase;
}
@ -20345,7 +20345,7 @@ li:not(:first-child) .dropdown-item,
}
.fs-6 {
font-size: 0.9375rem !important;
font-size: 0.8375rem !important;
}
.fs-tiny {
@ -32560,9 +32560,7 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
.bg-blue {
background-color:var(--bs-blue)
}
.text-blue{
color:var(--bs-blue)
}
.bg-indigo {
background-color:var(--bs-indigo)
}
@ -32574,4 +32572,10 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
}
.text-red{
color:var(--bs-red)
}
.text-blue{
color:var(--bs-blue)
}
.text-green{
color:var(--bs-green)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -5,21 +5,24 @@ import { useAuthModal, useModal } from "./hooks/useAuth";
import SwitchTenant from "./pages/authentication/SwitchTenant";
import ChangePasswordPage from "./pages/authentication/ChangePassword";
import NewCollection from "./components/collections/ManageCollection";
import ServiceProjectTeamAllocation from "./components/ServiceProject/ServiceProjectTeam/ServiceProjectTeamAllocation";
const ModalProvider = () => {
const { isOpen, onClose } = useOrganizationModal();
const { isOpen: isAuthOpen } = useAuthModal();
const {isOpen:isChangePass} = useModal("ChangePassword")
const {isOpen:isCollectionNew} = useModal("newCollection");
const { isOpen: isChangePass } = useModal("ChangePassword");
const { isOpen: isCollectionNew } = useModal("newCollection");
const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation");
return (
<>
{isOpen && <OrganizationModal />}
{isAuthOpen && <SwitchTenant />}
{isChangePass && <ChangePasswordPage /> }
{isCollectionNew && <NewCollection/>}
{isChangePass && <ChangePasswordPage />}
{isCollectionNew && <NewCollection />}
{isServiceTeamAllocation && <ServiceProjectTeamAllocation />}
</>
);
};
export default ModalProvider;
export default ModalProvider;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo } from "react";
import { useExpenseTransactions } from "../../hooks/useExpense";
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense";
import Error from "../common/Error";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader, { SpinnerLoader } from "../common/Loader";
@ -11,11 +11,10 @@ import { employee } from "../../data/masters";
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
import { formatFigure } from "../../utils/appUtils";
const AdvancePaymentList = ({ employeeId }) => {
const { setBalance } = useAdvancePaymentContext();
const AdvancePaymentList = ({ employeeId, searchString }) => {
const { setBalance } = useAdvancePaymentContext();
const { data, isError, isLoading, error, isFetching } =
useExpenseTransactions(employeeId, { enabled: !!employeeId });
const records = Array.isArray(data) ? data : [];
let currentBalance = 0;
@ -85,7 +84,7 @@ const AdvancePaymentList = ({ employeeId }) => {
key: "date",
label: (
<>
Date
Date
</>
),
align: "text-start",

View File

@ -0,0 +1,100 @@
import React from 'react'
import Avatar from "../../components/common/Avatar"; // <-- ADD THIS
import { useExpenseAllTransactionsList } from '../../hooks/useExpense';
import { useNavigate } from 'react-router-dom';
import { formatFigure } from '../../utils/appUtils';
const AdvancePaymentList1 = ({ searchString }) => {
const { data, isError, isLoading, error } =
useExpenseAllTransactionsList(searchString);
const rows = data || [];
const navigate = useNavigate();
const columns = [
{
key: "employee",
label: "Employee Name",
align: "text-start",
customRender: (r) => (
<div className="d-flex align-items-center gap-2" onClick={() => navigate(`/advance-payment/${r.id}`)}
style={{ cursor: "pointer" }}>
<Avatar firstName={r.firstName} lastName={r.lastName} />
<span className="fw-medium">
{r.firstName} {r.lastName}
</span>
</div>
),
},
{
key: "jobRoleName",
label: "Job Role",
align: "text-start",
customRender: (r) => (
<span className="fw-semibold">
{r.jobRoleName}
</span>
),
},
{
key: "balanceAmount",
label: "Balance (₹)",
align: "text-end",
customRender: (r) => (
<span className="fw-semibold fs-6">
{formatFigure(r.balanceAmount, {
// type: "currency",
currency: "INR",
})}
</span>
),
},
];
if (isLoading) return <p className="text-center py-4">Loading...</p>;
if (isError) return <p className="text-center py-4 text-danger">{error.message}</p>;
return (
<div className="card-datatable" id="payment-request-table">
<div className="mx-2">
<table className="table border-top dataTable text-nowrap align-middle">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className={`sorting ${col.align}`}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.length > 0 ? (
rows.map((row) => (
<tr key={row.id} className="align-middle" style={{ height: "40px" }}>
{columns.map((col) => (
<td key={col.key} className={`d-table-cell ${col.align} py-3`}>
{col.customRender
? col.customRender(row)
: col.getValue(row)}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center border-0 py-3">
No Employees Found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
export default AdvancePaymentList1;

View File

@ -54,6 +54,7 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
<div className="mb-3 w-100">
<label className="fw-semibold">Choose Date Range:</label>
<DateRangePicker1
className="w-100"
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="dateFrom"
endField="dateTo"

View File

@ -202,6 +202,7 @@ const TaskReportList = () => {
<span>
Total Pending{" "}
<HoverPopup
id="total_pending_task"
title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>}
>
@ -213,6 +214,7 @@ const TaskReportList = () => {
<span>
Reported/Planned{" "}
<HoverPopup
id="reportes_and_planned_task"
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
>

View File

@ -100,9 +100,9 @@ const AttendanceOverview = () => {
};
return (
<div className="bg-white p-4 rounded shadow d-flex flex-column h-100">
<div className="bg-white px-4 rounded shadow d-flex flex-column h-100">
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-3">
<div className="d-flex mt-2 justify-content-between align-items-center mb-3">
<div className="card-title mb-0 text-start">
<h5 className="mb-1 fw-bold">Attendance Overview</h5>
<p className="card-subtitle">Role-wise present count</p>

View File

@ -12,11 +12,11 @@ import Teams from "./Teams";
import TasksCard from "./Tasks";
import ProjectCompletionChart from "./ProjectCompletionChart";
import ProjectProgressChart from "./ProjectProgressChart";
import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceChart";
import AttendanceOverview from "./AttendanceOverview";
import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject";
import ProjectStatistics from "../Project/ProjectStatistics";
const Dashboard = () => {
@ -29,16 +29,16 @@ const Dashboard = () => {
<div className="row gy-4">
{isAllProjectsSelected && (
<div className="col-sm-6 col-lg-4">
<Projects />
<Projects />
</div>
)}
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<Teams />
<Teams />
</div>
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<TasksCard/>
<TasksCard />
</div>
{isAllProjectsSelected && (
@ -46,32 +46,31 @@ const Dashboard = () => {
<ProjectCompletionChart />
</div>
)}
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<ProjectOverview />
</div>
)}
<div className="col-xxl-6 col-lg-6">
<ProjectProgressChart />
</div>
<div className="col-12 col-xl-8">
<div className="card h-100">
<ExpenseAnalysis />
</div>
</div>
<div className="col-12 col-xl-4 col-md-6">
<div className="card h-100">
<ExpenseStatus />
</div>
</div>
{!isAllProjectsSelected && (
<div className="col-12 col-md-6 mb-sm-0 mb-4">
<AttendanceOverview />
</div>
)}
{!isAllProjectsSelected && (
<div className="col-xxl-4 col-lg-4">
<ProjectStatistics />
</div>
)}
<div className="col-12 col-xl-4 col-md-6">
<div className="card ">
<ExpenseStatus />
</div>
</div>
<div className="col-12 col-xl-8">
<div className="card h-100">
<ExpenseAnalysis />
</div>
</div>
<div className="col-12 col-md-6">
<ExpenseByProject />
</div>

View File

@ -7,11 +7,12 @@ import { FormProvider, useForm } from "react-hook-form";
import { formatCurrency, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects";
import { SpinnerLoader } from "../common/Loader";
import flatColors from "../Charts/flatColor";
const ExpenseAnalysis = () => {
const projectId = useSelectedProject();
const [projectName, setProjectName] = useState("All Project");
const { projectNames, loading } = useProjectName();
const { projectNames } = useProjectName();
const methods = useForm({
defaultValues: { startDate: "", endDate: "" },
@ -50,7 +51,7 @@ const ExpenseAnalysis = () => {
labels,
legend: { show: false },
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
colors: flatColors,
plotOptions: {
pie: {
donut: {
@ -79,22 +80,19 @@ const ExpenseAnalysis = () => {
return (
<>
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
<div className="text-start ">
<div className="text-start">
<h5 className="mb-1 card-title">Expense Breakdown</h5>
{/* <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p> */}
<p className="card-subtitle m-0">{projectName}</p>
</div>
<div className="text-end text-sm-end ">
<div className="text-end text-sm-end">
<FormProvider {...methods}>
<DateRangePicker1 />
</FormProvider>
</div>
</div>
{/* Card body */}
<div className="card-body position-relative">
{isLoading && (
<div
@ -114,7 +112,6 @@ const ExpenseAnalysis = () => {
</div>
)}
{!isLoading && report.length > 0 && (
<>
{isFetching && (
@ -123,50 +120,59 @@ const ExpenseAnalysis = () => {
</div>
)}
<div className="d-flex justify-content-center mb-3">
<Chart
options={donutOptions}
series={series}
type="donut"
width="100%"
height={320}
/>
</div>
<div className="row">
{/* Chart Column */}
<div className="col-12 col-lg-6 d-flex justify-content-center mt-5 mb-3 mb-lg-0">
<Chart
options={donutOptions}
series={series}
type="donut"
width="70%"
height={320}
/>
</div>
<div className="mb-2 w-100">
<div className="row g-2">
{report.map((item, idx) => (
<div
className="col-12 col-sm-6 d-flex align-items-start"
key={idx}
>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{
backgroundColor:
donutOptions.colors[idx % donutOptions.colors.length],
}}
>
<i className="bx bx-receipt fs-4"></i>
</span>
{/* Data/Legend Column */}
<div className="col-12 mt-6 col-lg-6">
<div className="row g-4">
{report.map((item, idx) => (
<div
className="col-6"
key={idx}
style={{
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`,
}}
>
<div className="d-flex flex-column text-start">
<small
className="fw-semibold text-wrap text-dark"
style={{
fontSize: "0.8rem",
whiteSpace: "normal",
wordBreak: "break-word",
lineHeight: "1.2",
}}
>
{item.projectName}
</small>
<span
className="fw-semibold text-muted"
style={{
fontSize: "0.75rem",
}}
>
{formatCurrency(item.totalApprovedAmount)}
</span>
</div>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-semibold">{item.projectName}</small>
<span className="fw-semibold text-muted ms-1">
{formatCurrency(item.totalApprovedAmount)}
</span>
</div>
</div>
))}
))}
</div>
</div>
</div>
</>
)}
</div>
{/* Header */}
</>
);
};

View File

@ -92,7 +92,7 @@ const ExpenseByProject = () => {
<div className="card shadow-sm h-100 rounded ">
{/* Header */}
<div className="card-header">
<div className="d-flex justify-content-start align-items-center mb-1 mt-1">
<div className="d-flex justify-content-between align-items-center mb-1 mt-1">
<div className="text-start">
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
<p className="card-subtitle m-0">{projectName}</p>

View File

@ -103,7 +103,7 @@ const ExpenseStatus = () => {
</div>
<div>
<small
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-xl"
} text-gray-500`}
>
{item?.count || 0}
@ -122,7 +122,7 @@ const ExpenseStatus = () => {
{isManageExpense && (
<div
className="d-flex justify-content-between align-items-center cursor-pointer"
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
onClick={() => handleNavigate(EXPENSE_STATUS.payment_processed)}
>
<div className="d-block">
<span
@ -137,7 +137,7 @@ const ExpenseStatus = () => {
</div>
<div className="d-flex align-items-center gap-2">
<span
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-xl" : "text-3xl"
} text-md`}
>
{formatCurrency(data?.totalAmount || 0)}

View File

@ -1,12 +1,17 @@
import React from "react";
import React, { useState } from "react";
import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
const ProjectCompletionChart = () => {
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
// Bar chart logic
const [currentPage, setCurrentPage] = useState(1);
const {
data: projects,
isLoading: loading,
isError,
error,
} = useProjectCompletionStatus();
const projectNames = projects?.map((p) => p.name) || [];
const projectProgress =
projects?.map((p) => {

View File

@ -23,7 +23,7 @@ import Label from "../common/Label";
const ManageContact = ({ contactId, closeModal }) => {
// fetch master data
const { buckets, loading: bucketsLoaging } = useBuckets();
const { data:projects, loading: projectLoading } = useProjects();
const { data: projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { organizationList } = useOrganization();
@ -205,12 +205,12 @@ const ManageContact = ({ contactId, closeModal }) => {
<Label htmlFor={"organization"} required>
Organization
</Label>
<InputSuggestions
organizationList={organizationList}
value={watch("organization") || ""}
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
error={errors.organization?.message}
/>
<InputSuggestions
organizationList={organizationList}
value={watch("organization") || ""}
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
error={errors.organization?.message}
/>
</div>
</div>
@ -408,6 +408,7 @@ const ManageContact = ({ contactId, closeModal }) => {
label="Tags"
options={contactTags}
isRequired={true}
placeholder="Enter Tag"
/>
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
@ -482,7 +483,7 @@ const ManageContact = ({ contactId, closeModal }) => {
<button className="btn btn-sm btn-primary" type="submit" disabled={isPending}>
{isPending ? "Please Wait..." : "Submit"}
</button>
</div>
</form>
</FormProvider>

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import EmpOverview from "./EmpOverview";
import { useProjectsAllocationByEmployee } from "../../hooks/useProjects";
import EmpReportingManager from "./EmpReportingManager";
const EmpDashboard = ({ profile }) => {
const {
@ -16,6 +17,7 @@ const EmpDashboard = ({ profile }) => {
{" "}
<EmpOverview profile={profile}></EmpOverview>
</div>
<div className="col col-sm-6 pt-5">
<div className="card ">
<div className="card-body">
@ -80,6 +82,13 @@ const EmpDashboard = ({ profile }) => {
</div>
</div>
</div>
<div className="col-12 col-sm-6 pt-5">
<EmpReportingManager
employeeId={profile?.id}
employee={profile}
/>
</div>
</div>
</>
);

View File

@ -0,0 +1,87 @@
import React, { useState } from "react";
import { useOrganizationHierarchy } from "../../hooks/useEmployees";
import GlobalModel from "../common/GlobalModel";
import ManageReporting from "./ManageReporting";
import Avatar from "../common/Avatar";
import { SpinnerLoader } from "../common/Loader";
const EmpReportingManager = ({ employeeId, employee }) => {
const { data, isLoading } = useOrganizationHierarchy(employeeId);
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
if (isLoading)
return (
<div className="d-flex justify-content-center py-5">
<SpinnerLoader />
</div>
);
// Safe access to primary and secondary managers
const primaryManager = data?.find((d) => d.isPrimary)?.reportTo;
const secondaryManagers = data?.filter((d) => !d.isPrimary).map((d) => d.reportTo) || [];
return (
<div className="row">
<div className="col-12 mb-4">
<div className="card">
<div className="card-body">
<small className="card-text text-uppercase text-body-secondary small d-block text-start mb-3">
Reporting Manager
</small>
<div className="text-start">
{/* Primary Manager */}
<div className="row mb-2 align-items-start">
<div className="col-auto text-end pe-3"><i className="bx bx-user-circle me-1"></i>Primary Manager<span style={{ marginLeft: "50px" }}>:</span></div>
<div className="col text-wrap">
{primaryManager
? `${primaryManager.firstName || ""} ${primaryManager.lastName || ""}`
: "NA"}
</div>
</div>
{/* Secondary Managers */}
{secondaryManagers?.length > 0 && (
<div className="row mb-3 align-items-start">
<div className="col-auto text-end pe-3"><i className="bx bx-group me-1"></i>Secondary Managers<span style={{ marginLeft: "25px" }}>:</span></div>
<div className="col text-wrap">
{secondaryManagers
.map((m) => `${m.firstName || ""} ${m.lastName || ""}`)
.join(", ")}
</div>
</div>
)}
</div>
{/* Manage Reporting Button */}
<div className="mt-5 text-end" >
<button
className="btn btn-sm btn-primary"
onClick={() => setShowManageReportingModal(true)}
>
<i className="bx bx-network-chart me-1"></i> Manage Reporting
</button>
</div>
</div>
</div>
</div>
{/* Manage Reporting Modal */}
{showManageReportingModal && (
<GlobalModel
isOpen={showManageReportingModal}
closeModal={() => setShowManageReportingModal(false)}
>
<ManageReporting
employee={employee}
employeeId={employeeId}
onClosed={() => setShowManageReportingModal(false)}
/>
</GlobalModel>
)}
</div>
);
};
export default EmpReportingManager;

View File

@ -3,8 +3,8 @@ import { z } from "zod"
const mobileNumberRegex = /^[0-9]\d{9}$/;
export const employeeSchema =
z.object({
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" }),
@ -90,35 +90,46 @@ export const employeeSchema =
.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),
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"],
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
}
export const ManageReportingSchema = z.object({
primaryNotifyTo: z.array(z.string()).min(1, "Primary Reporting Manager is required"),
secondaryNotifyTo: z.array(z.string()).optional(),
});
export const defatEmployeeObj = {
firstName: "",
middleName: "",
lastName: "",
email: "",
currentAddress: "",
birthDate: "",
joiningDate: "",
emergencyPhoneNumber: "",
emergencyContactPerson: "",
aadharNumber: "",
gender: "",
panNumber: "",
permanentAddress: "",
phoneNumber: "",
jobRoleId: null,
organizationId:"",
hasApplicationAccess:false
}
export const defaultManageReporting = {
primaryNotifyTo: [],
secondaryNotifyTo: [],
};

View File

@ -0,0 +1,188 @@
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../common/Label";
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
import { useManageEmployeeHierarchy, useOrganizationHierarchy } from "../../hooks/useEmployees";
import { ManageReportingSchema, defaultManageReporting } from "./EmployeeSchema";
import Avatar from "../common/Avatar";
import { useNavigate } from "react-router-dom";
const ManageReporting = ({ onClosed, employee, employeeId }) => {
const {
handleSubmit,
control,
reset,
formState: { errors },
watch,
} = useForm({
resolver: zodResolver(ManageReportingSchema),
defaultValues: defaultManageReporting,
});
const navigate = useNavigate();
const { data, isLoading } = useOrganizationHierarchy(employeeId);
// mutation hook
const { mutate: manageHierarchy, isPending } = useManageEmployeeHierarchy(
employeeId,
onClosed
);
const primaryValue = watch("primaryNotifyTo");
const secondaryValue = watch("secondaryNotifyTo");
// Prefill hierarchy data
useEffect(() => {
if (data && Array.isArray(data)) {
const primary = data.find((r) => r.isPrimary);
const secondary = data.filter((r) => !r.isPrimary);
reset({
primaryNotifyTo: primary ? [primary.reportTo.id] : [],
secondaryNotifyTo: secondary.map((r) => r.reportTo.id),
});
}
}, [data, reset]);
const handleClose = () => {
reset(defaultManageReporting);
onClosed();
};
const onSubmit = (formData) => {
// Build set of currently selected IDs
const selectedIds = new Set([
...(formData.primaryNotifyTo || []),
...(formData.secondaryNotifyTo || []),
]);
// Build payload including previous assignments, setting isActive true/false accordingly
const payload = (data || []).map((item) => ({
reportToId: item.reportTo.id,
isPrimary: item.isPrimary,
isActive: selectedIds.has(item.reportTo.id),
}));
// Add any new IDs that were not previously assigned
if (formData.primaryNotifyTo?.length) {
const primaryId = formData.primaryNotifyTo[0];
if (!data?.some((d) => d.reportTo.id === primaryId)) {
payload.push({
reportToId: primaryId,
isPrimary: true,
isActive: true,
});
}
}
if (formData.secondaryNotifyTo?.length) {
formData.secondaryNotifyTo.forEach((id) => {
if (!data?.some((d) => d.reportTo.id === id)) {
payload.push({
reportToId: id,
isPrimary: false,
isActive: true,
});
}
});
}
manageHierarchy(payload);
};
const handleClick = () => {
handleClose();
navigate(`/employee/${employee.id}`);
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
<h5 className="m-0 py-1 mb-4">Reporting Manager</h5>
{/* Employee Info */}
<div className="d-flex align-items-center justify-content-start mb-4 ps-0">
<div className="me-1 mb-1">
<Avatar size="sm" firstName={employee.firstName} lastName={employee.lastName} />
</div>
{/* Employee Name + Role */}
<div className="d-flex flex-column">
<div className="d-flex align-items-center gap-2">
<span className="fw-semibold text-dark fs-6 me-1 cursor-pointer" onClick={handleClick}>
{`${employee.firstName || ""} ${employee.middleName || ""} ${employee.lastName || ""}`.trim() || "Employee Name NA"}
</span>
{/* External Link Icon (Navigate to Employee Profile) */}
<i
className="bx bx-link-external text-primary"
style={{ fontSize: "1rem", cursor: "pointer" }}
title="View Profile"
onClick={handleClick}
></i>
</div>
<div className="text-start">
{employee.jobRole && (
<small className="text-muted">{employee.jobRole}</small>
)}
</div>
</div>
</div>
{/* Primary Reporting Manager */}
<div className="mb-4 text-start">
<Label className="form-label" required>
Primary Reporting Manager
</Label>
<PmsEmployeeInputTag
control={control}
name="primaryNotifyTo"
placeholder={primaryValue?.length > 0 ? "" : "Search and select primary manager"}
forAll={true}
disabled={primaryValue?.length > 0}
/>
{errors.primaryNotifyTo && (
<div className="text-danger small mt-1">
{errors.primaryNotifyTo.message}
</div>
)}
</div>
{/* Secondary Reporting Manager */}
<div className="mb-4 text-start">
<Label className="form-label">Secondary Reporting Manager</Label>
<PmsEmployeeInputTag
control={control}
name="secondaryNotifyTo"
placeholder="Search and add secondary managers"
forAll={true}
/>
</div>
{/* Buttons */}
<div className="d-flex justify-content-end gap-3 mt-3 mb-3">
<button
type="button"
onClick={handleClose}
className="btn btn-label-secondary btn-sm"
disabled={isPending}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm"
disabled={isPending}
>
{isPending ? "Saving..." : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default ManageReporting;

View File

@ -20,7 +20,7 @@ const Filelist = ({ files, removeFile, expenseToEdit,sm=6,md=4 }) => {
<i
className={`bx ${getIconByFileType(
file?.contentType
)} fs-3 text-primary`}
)} fs-3 `}
style={{ minWidth: "30px" }}
></i>

View File

@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
import { formatFileSize, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects";
import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster, {
@ -32,6 +32,11 @@ import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist";
import { DEFAULT_CURRENCY } from "../../utils/constants";
import SelectEmployeeServerSide, {
SelectProjectField,
} from "../common/Forms/SelectFieldServerSide";
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const {
@ -40,6 +45,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
error: ExpenseErrorLoad,
} = useExpense(expenseToEdit);
const [expenseCategory, setExpenseCategory] = useState();
const [selectedEmployees, setSelectedEmployees] = useState([]);
const dispatch = useDispatch();
const {
expenseCategories,
@ -83,11 +89,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
loading: StatusLoadding,
error: stausError,
} = useExpenseStatus();
const {
data: employees,
isLoading: EmpLoading,
isError: isEmployeeError,
} = useEmployeesNameByProject(selectedproject);
// const {
// data: employees,
// isLoading: EmpLoading,
// isError: isEmployeeError,
// } = useEmployeesNameByProject(selectedproject);
const files = watch("billAttachments");
const onFileChange = async (e) => {
@ -150,6 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
}
};
const { mutate: AllocationTeam, isPending1 } =
useAllocationServiceProjectTeam(() => {
setSelectedEmployees([]);
setSeletingEmp({
employee: null,
isOpen: false,
});
});
useEffect(() => {
if (expenseToEdit && data) {
reset({
@ -180,7 +194,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
: [],
});
}
}, [data, reset, employees]);
}, [data, reset]);
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
handleClose()
);
@ -223,7 +237,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start">
<div className="col-md-6">
{/* <div className="col-md-6">
<Label className="form-label" required>
Select Project
</Label>
@ -245,6 +259,23 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}
</div> */}
<div className="col-12 col-md-6 mb-2">
<SelectProjectField
label="Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
/>
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}
</div>
<div className="col-md-6">
@ -307,14 +338,28 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
{/* <Label className="form-label" required>
Paid By{" "}
</Label>
<EmployeeSearchInput
</Label> */}
{/* <EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
forAll={expenseToEdit ? true : false}
forAll={true}
/> */}
<AppFormController
name="paidById"
control={control}
render={({ field }) => (
<SelectEmployeeServerSide
label="Paid By" required
value={field.value}
onChange={field.onChange}
isFullObject={false} // because using ID
/>
)}
/>
</div>
</div>
@ -423,10 +468,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.gstNumber.message}</small>
)}
</div>
</div>
<div className="row">
<div className="col-md-6 text-start ">
<div className="row">
<div className="col-md-6 text-start ">
<Label htmlFor="currencyId" className="form-label" required>
Select Currency
</Label>
@ -452,24 +496,26 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.currencyId.message}</small>
)}
</div>
{expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start">
<Label className="form-label" required>No. of Persons</Label>
<input
type="number"
id="noOfPersons"
className="form-control form-control-sm"
{...register("noOfPersons")}
inputMode="numeric"
/>
{errors.noOfPersons && (
<small className="danger-text">
{errors.noOfPersons.message}
</small>
)}
</div>
)}
</div>
{expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start">
<Label className="form-label" required>
No. of Persons
</Label>
<input
type="number"
id="noOfPersons"
className="form-control form-control-sm"
{...register("noOfPersons")}
inputMode="numeric"
/>
{errors.noOfPersons && (
<small className="danger-text">
{errors.noOfPersons.message}
</small>
)}
</div>
)}
</div>
<div className="row my-2 text-start">
<div className="col-md-12">

View File

@ -1,54 +1,80 @@
import { useState } from "react";
const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0);
const [scale, setScale] = useState(1);
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3));
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4));
const resetAll = () => {
setRotation(0);
setScale(1);
};
return (
<>
<div className="d-flex justify-content-start">
<>
<div className="d-flex justify-content-start gap-3 mb-2">
<i
className="bx bx-rotate-right cursor-pointer"
className="bx bx-rotate-right cursor-pointer fs-4"
title="Rotate"
onClick={() => setRotation((prev) => prev + 90)}
></i>
</div>
<div
className="position-relative d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "80vh" }}
>
{loading && (
<div className="text-secondary text-center mb-2">Loading...</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
<img
src={imageUrl}
alt="Full View"
className="img-fluid"
style={{
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg)`,
transition: "transform 0.3s ease",
}}
onLoad={() => setLoading(false)}
/>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
title="Zoom In"
onClick={zoomIn}
></i>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
title="Zoom Out"
onClick={zoomOut}
></i>
</div>
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
<button
className="btn btn-outline-secondary"
onClick={() => setRotation(0)}
title="Reset Rotation"
>
<i className="bx bx-reset"></i> Reset
</button>
<div
className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden"
style={{ minHeight: "80vh" }}
>
{loading && (
<div className="text-secondary text-center mb-2">
Loading...
</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
<img
src={imageUrl}
alt="Full View"
className="img-fluid"
style={{
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg) scale(${scale})`,
transition: "transform 0.3s ease",
cursor: "grab",
}}
onLoad={() => setLoading(false)}
/>
</div>
<div className="position-absolute bottom-0 start-0 m-2">
<button
className="btn btn-outline-secondary"
onClick={resetAll}
>
<i className="bx bx-reset"></i> Reset
</button>
</div>
</div>
</div>
</>
</>
);
};
export default PreviewDocument;

View File

@ -10,6 +10,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
import {
calculateTDSPercentage,
formatCurrency,
formatFigure,
getColorNameFromHex,
@ -54,12 +55,22 @@ const ViewExpense = ({ ExpenseId }) => {
setValue,
reset,
control,
watch,
formState: { errors },
} = useForm({
resolver: zodResolver(ActionSchema),
defaultValues: defaultActionValues,
});
const baseAmount = Number(watch("baseAmount")) || 0;
const taxAmount = Number(watch("taxAmount")) || 0;
const tdsPercentage = Number(watch("tdsPercentage")) || 0;
const { grossAmount, tdsAmount, netPayable } = useMemo(() => {
return calculateTDSPercentage(baseAmount, taxAmount, tdsPercentage);
}, [baseAmount, taxAmount, tdsPercentage]);
const userPermissions = useSelector(
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
);
@ -132,9 +143,8 @@ const ViewExpense = ({ ExpenseId }) => {
<span>{data?.expenseUId}</span>
</div>{" "}
<span
className={`badge bg-label-${
getColorNameFromHex(data?.status?.color) || "secondary"
}`}
className={`badge bg-label-${getColorNameFromHex(data?.status?.color) || "secondary"
}`}
t
>
{data?.status?.name}
@ -142,7 +152,7 @@ const ViewExpense = ({ ExpenseId }) => {
</div>
{/* Row 1 */}
<div className="row text-start">
<div className="row text-start">
<div className="col-md-6 mb-3">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
@ -264,28 +274,28 @@ const ViewExpense = ({ ExpenseId }) => {
</div> */}
{/* Row 5 */}
<div className="row text-start">
<div className="row text-start">
<div className="col-md-6 mb-3">
<label
className="form-label me-2 mb-0 fw-semibold text-start"
style={{ minWidth: "130px" }}
>
Created At :
</label>
</div>
<div className="col-md-6 mb-3">
</div>
<div className="col-md-6 mb-3">
<small className="text-muted">
{formatUTCToLocalTime(data?.createdAt, true)}
</small>
</div>
</div>
</div>
{/* Created & Paid By */}
{data.createdBy && (
@ -307,9 +317,8 @@ const ViewExpense = ({ ExpenseId }) => {
lastName={data.createdBy?.lastName}
/>
<span className="text-muted">
{`${data.createdBy?.firstName ?? ""} ${
data.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
{`${data.createdBy?.firstName ?? ""} ${data.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
@ -337,31 +346,30 @@ const ViewExpense = ({ ExpenseId }) => {
</span>
</div>
</div> */}
<div className="row text-start">
<div className="col-md-6 text-start mb-3">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By :
</label>
</div>
<div className="col-md-6 text-start mb-3">
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={data.paidBy?.firstName}
lastName={data.paidBy?.lastName}
/>
<span className="text-muted">
{`${data.paidBy?.firstName ?? ""} ${
data.paidBy?.lastName ?? ""
<div className="row text-start">
<div className="col-md-6 text-start mb-3">
<label
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By :
</label>
</div>
<div className="col-md-6 text-start mb-3">
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0 me-1"
firstName={data.paidBy?.firstName}
lastName={data.paidBy?.lastName}
/>
<span className="text-muted">
{`${data.paidBy?.firstName ?? ""} ${data.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</span>
</div>
</div>
</div>
{/* Description */}
<div className="col-12 text-start mb-2">
@ -493,19 +501,7 @@ const ViewExpense = ({ ExpenseId }) => {
projectId={null}
/>
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label">TDS Percentage</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("tdsPercentage")}
/>
{errors.tdsPercentage && (
<small className="danger-text">
{errors.tdsPercentage.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
Base Amount
@ -536,28 +532,53 @@ const ViewExpense = ({ ExpenseId }) => {
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label">TDS Percentage</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("tdsPercentage")}
/>
{errors.tdsPercentage && (
<small className="danger-text">
{errors.tdsPercentage.message}
</small>
)}
</div>
<div className="col-12 d-flex align-items-center gap-4 mb-2 mt-1">
<div>
<span className="fw-semibold">TDS Amount: </span>
<span className="badge bg-label-secondary">{tdsAmount.toFixed(2)}</span>
</div>
<div>
<span className="fw-semibold">Net Payable: </span>
<span className="badge bg-label-secondary">{netPayable.toFixed(2)}</span>
</div>
</div>
</div>
)}
<div className="col-12 mb-3 text-start mt-1">
{((nextStatusWithPermission.length > 0 &&
!IsRejectedExpense) ||
(IsRejectedExpense && isCreatedBy)) && (
<>
<Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">
{errors.comment.message}
</small>
)}
</>
)}
<>
<Label className="form-label me-2 mb-0" required>
Comment
</Label>
<textarea
className="form-control form-control-sm"
{...register("comment")}
rows="2"
/>
{errors.comment && (
<small className="danger-text">
{errors.comment.message}
</small>
)}
</>
)}
{nextStatusWithPermission?.length > 0 &&
(!IsRejectedExpense || isCreatedBy) && (

View File

@ -46,8 +46,15 @@ const Header = () => {
pathname
);
const isExpensePage = /^\/expenses$/.test(pathname);
const isPaymentRequest = /^\/payment-request$/.test(pathname);
const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
const isAdvancePayment = /^\/advance-payment$/.test(pathname);
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.test(pathname);
const isAdvancePayment1 =
/^\/advance-payment(\/[0-9a-fA-F-]{36})?$/.test(pathname);
return !(isDirectoryPath || isProfilePage || isExpensePage);
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage || isAdvancePayment1);
};
const allowedProjectStatusIds = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
@ -59,11 +66,7 @@ const Header = () => {
if (!Array.isArray(roles)) return "User";
let role = roles.find((role) => role.id === joRoleId);
return role ? role.name : "User";
};
};
const handleProfilePage = () => {
navigate(`/employee/${profile?.employeeInfo?.id}`);

View File

@ -25,22 +25,19 @@ const Sidebar = () => {
/>
</span> */}
<a
href="/"
className="app-brand-link fw-bold navbar-brand text-green fs-6"
>
<small className="app-brand-link fw-bold navbar-brand text-green fs-6">
<span className="app-brand-logo demo">
<img src="/img/brand/marco.png" width="50" />
</span>
<span className="text-blue">OnField</span>
<span>Work</span>
<span className="text-dark">.com</span>
</a>
</small>
</Link>
<a className="layout-menu-toggle menu-link text-large ms-auto">
<small className="layout-menu-toggle menu-link text-large ms-auto">
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
</a>
</small>
</div>
<div className="menu-inner-shadow"></div>
@ -61,7 +58,7 @@ const Sidebar = () => {
</>
)}
{data &&
data?.data.map((section) => (
data?.data?.map((section) => (
<React.Fragment
key={section.id || section.header || section.items[0]?.id}
>
@ -99,7 +96,7 @@ const MenuItem = (item) => {
className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
target={item.link?.includes("http") ? "_blank" : undefined}
>
<i className={`menu-icon tf-icons ${item.icon}`}></i>
{item.icon && <i className={`menu-icon tf-icons ${item.icon}`}></i>}
<div>{item.name}</div>
{item.available === false && (
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">

View File

@ -135,6 +135,19 @@ const ManagOrg = () => {
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="gstNumber">
GST Number
</Label>
<input
className="form-control form-control-sm"
{...register("gstNumber")}
/>
{errors.gstNumber && (
<span className="danger-text">{errors.gstNumber.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="email" required>
Email Address
@ -199,8 +212,8 @@ const ManagOrg = () => {
{isCreating || isUpdating
? "Please Wait..."
: orgData
? "Update"
: "Submit"}
? "Update"
: "Submit"}
</button>
</div>
</div>

View File

@ -20,6 +20,7 @@ export const organizationSchema = z.object({
serviceIds: z
.array(z.string())
.min(1, { message: "Service isrequired" }),
gstNumber: z.string().optional(),
});
export const defaultOrganizationValues = {
@ -29,6 +30,7 @@ export const defaultOrganizationValues = {
address: "",
email: "",
serviceIds: [],
gstNumber : ""
};
export const assignedOrgToProject = z.object({

View File

@ -24,7 +24,7 @@ import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useSelector } from "react-redux";
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
import { localToUtc } from "../../utils/appUtils";
import { calculateTDSPercentage, localToUtc } from "../../utils/appUtils";
import { usePaymentMode } from "../../hooks/masterHook/useMaster";
import Filelist from "../Expenses/Filelist";
@ -40,6 +40,7 @@ const ActionPaymentRequest = ({ requestId }) => {
error: PaymentModeError,
} = usePaymentMode();
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
const [imageLoaded, setImageLoaded] = useState({});
@ -69,15 +70,10 @@ const ActionPaymentRequest = ({ requestId }) => {
const taxAmount = watch("taxAmount") || 0;
const tdsPercentage = watch("tdsPercentage") || 0;
const grossAmount = baseAmount + taxAmount;
const tdsAmount = useMemo(() => (baseAmount * tdsPercentage) / 100, [
baseAmount,
tdsPercentage,
]);
const netPayable = useMemo(() => grossAmount - tdsAmount, [grossAmount, tdsAmount]);
const { grossAmount, tdsAmount, netPayable } = useMemo(() => {
return calculateTDSPercentage(baseAmount, taxAmount, tdsPercentage);
}, [baseAmount, taxAmount, tdsPercentage]);
const userPermissions = useSelector(
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
@ -180,6 +176,16 @@ const ActionPaymentRequest = ({ requestId }) => {
const newFiles = files.filter((_, i) => i !== index);
setValue("billAttachments", newFiles, { shouldValidate: true });
};
const filteredPaymentModes = useMemo(() => {
return PaymentModes?.filter((mode) => {
if (mode.name === "Advance Payment" && data?.isAdvancePayment === false) {
return false;
}
return true;
}) || [];
}, [PaymentModes, data]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
@ -224,7 +230,7 @@ const ActionPaymentRequest = ({ requestId }) => {
{PaymentModeLoading ? (
<option disabled>Loading...</option>
) : (
PaymentModes?.map((payment) => (
filteredPaymentModes?.map((payment) => (
<option key={payment.id} value={payment.id}>
{payment.name}
</option>
@ -428,6 +434,7 @@ const ActionPaymentRequest = ({ requestId }) => {
<span className="badge bg-label-secondary">{netPayable.toFixed(2)}</span>
</div>
</div>
</div>
)}

View File

@ -29,6 +29,7 @@ import Filelist from "../Expenses/Filelist";
import InputSuggestions from "../common/InputSuggestion";
import { useProfile } from "../../hooks/useProfile";
import { blockUI } from "../../utils/blockUI";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
const {
@ -234,10 +235,10 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
{/* Project and Category */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>
{/* <Label className="form-label" required>
Select Project
</Label>
<select
</Label> */}
{/* <select
className="form-select form-select-sm"
{...register("projectId")}
disabled={
@ -254,7 +255,23 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
</option>
))
)}
</select>
</select> */}
<SelectProjectField
label="Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
disabled={
data?.recurringPayment?.isVariable && !isDraft && !isProcessed
}
/>
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}

View File

@ -0,0 +1,60 @@
import React, { useMemo } from "react";
const PaymentRequestFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
const data = filterData?.data || filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const addGroup = (ids, list, label, key) => {
if (!ids?.length) return;
const items = ids.map((id) => ({
id,
name: list?.find((i) => i.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
addGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds");
addGroup(filters.currencyIds, data.currency, "Currency", "currencyIds");
addGroup(filters.expenseCategoryIds, data.expenseCategory, "Expense Category", "expenseCategoryIds");
addGroup(filters.payees, data.payees, "Payees", "payees");
addGroup(filters.projectIds, data.projects, "Projects", "projectIds");
addGroup(filters.statusIds, data.status, "Status", "statusIds");
return chips;
}, [filters, filterData]);
if (!filterChips.length) return null;
return (
<div className="d-flex flex-wrap align-items-center gap-2">
{filterChips.map((chipGroup) => (
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
<span className="fw-semibold me-2">{chipGroup.label}:</span>
{chipGroup.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
>
<span>{item.name}</span>
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chipGroup.key, item.id)}
/>
</span>
))}
</div>
))}
</div>
);
};
export default PaymentRequestFilterChips;

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useEffect, useState, useMemo, forwardRef, useImperativeHandle } from "react";
import { FormProvider, useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "./PaymentRequestSchema";
import { defaultPaymentRequestFilter, SearchPaymentRequestSchema } from "./PaymentRequestSchema";
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
@ -13,7 +13,7 @@ import moment from "moment";
import { usePaymentRequestFilter } from "../../hooks/useExpense";
import { useLocation, useNavigate, useParams } from "react-router-dom";
const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata, clearFilter }, ref) => {
const { status } = useParams();
const navigate = useNavigate();
const selectedProjectId = useSelector(
@ -38,10 +38,23 @@ const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
const [resetKey, setResetKey] = useState(0);
const dynamicDefaultFilter = useMemo(() => {
return {
...defaultPaymentRequestFilter,
projectIds: defaultPaymentRequestFilter.projectIds || [],
statusIds: status ? [status] : defaultPaymentRequestFilter.statusIds || [],
createdByIds: defaultPaymentRequestFilter.createdByIds || [],
currencyIds: defaultPaymentRequestFilter.currencyIds || [],
expenseCategoryIds: defaultPaymentRequestFilter.expenseCategoryIds || [],
payees: defaultPaymentRequestFilter.payees || [],
startDate: defaultPaymentRequestFilter.startDate,
endDate: defaultPaymentRequestFilter.endDate,
};
}, [status, selectedProjectId]);
const methods = useForm({
resolver: zodResolver(SearchPaymentRequestSchema),
defaultValues: defaultPaymentRequestFilter,
defaultValues: dynamicDefaultFilter,
});
const { control, handleSubmit, reset, setValue, watch } = methods;
@ -52,12 +65,28 @@ const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
};
const handleGroupChange = (e) => {
const group = groupByList.find((g) => g.id === e.target.value);
if (group) setSelectedGroup(group);
};
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
// Reset specific field
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
}
},
getValues: methods.getValues, // optional, to read current filter state
}));
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const onSubmit = (formData) => {
onApply({
@ -179,7 +208,7 @@ const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
))}
</div>
</div>
</div>
</div>
<div className="d-flex justify-content-end py-3 gap-2">
<button
@ -197,6 +226,6 @@ const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
</FormProvider>
</>
);
};
});
export default PaymentRequestFilterPanel;

View File

@ -20,8 +20,9 @@ import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import Error from "../common/Error";
import Pagination from "../common/Pagination";
import PaymentRequestFilterChips from "./PaymentRequestFilterChips";
const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter, search, groupBy = "submittedBy" }) => {
const { setManageRequest, setVieRequest } = usePaymentRequestContext();
const navigate = useNavigate();
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -44,9 +45,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
displayField = "Status";
break;
case "submittedBy":
key = `${item?.createdBy?.firstName ?? ""} ${
item.createdBy?.lastName ?? ""
}`.trim();
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
}`.trim();
displayField = "Submitted By";
break;
case "project":
@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
key: "paymentRequestUID",
label: "Request ID",
align: "text-start mx-2",
getValue: (e) => e.paymentRequestUID || "N/A",
getValue: (e) => <div className="d-flex"><span>{e.paymentRequestUID || "N/A"}</span> {e.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xxs" >Adv</span>}</div>,
},
{
key: "title",
@ -93,40 +93,25 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
align: "text-start",
getValue: (e) => e.title || "N/A",
},
// { key: "payee", label: "Payee", align: "text-start" },
{
key: "SubmittedBy",
label: "Submitted By",
align: "text-start",
getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => (
<div
className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}
>
<Avatar
size="xs"
classAvatar="m-0"
firstName={e.createdBy?.firstName}
lastName={e.createdBy?.lastName}
/>
<span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
),
},
{
key: "createdAt",
label: "Submitted On",
label: "Created At",
align: "text-start",
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
},
{
key: "payee",
label: "Payee",
align: "text-start",
getValue: (e) => e.payee || "N/A",
},
{
key: "dueDate",
label: "Due Date",
align: "text-start",
getValue: (e) => formatUTCToLocalTime(e?.dueDate),
},
{
key: "amount",
label: "Amount",
@ -143,9 +128,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
align: "text-center",
getValue: (e) => (
<span
className={`badge bg-label-${
getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
}`}
className={`badge bg-label-${getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
}`}
>
{e?.expenseStatus?.name || "Unknown"}
</span>
@ -171,8 +155,8 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const header = [
"Request ID",
"Request Title",
"Submitted By",
"Submitted On",
"Created At",
"Due Date",
"Amount",
"Status",
"Action",
@ -181,10 +165,10 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
const grouped = groupBy
? Object.fromEntries(
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(
([keyA], [keyB]) => keyA.localeCompare(keyB)
)
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(
([keyA], [keyB]) => keyA.localeCompare(keyB)
)
)
: { All: data?.data ?? [] };
const IsGroupedByDate = [
@ -240,6 +224,14 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
)}
<div className="card page-min-h table-responsive px-sm-4">
<div className="card-datatable mx-2" id="payment-request-table ">
<div className="col-12 mb-2 mt-2">
<PaymentRequestFilterChips
filters={filters}
filterData={filterData}
removeFilterChip={removeFilterChip}
clearFilter={clearFilter}
/>
</div>
<table className="table border-top dataTable text-nowrap align-middle">
<thead>
<tr>
@ -270,7 +262,7 @@ const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
</td>
</tr>
{items?.map((paymentRequest) => (
<tr key={paymentRequest.id}>
<tr key={paymentRequest.id} style={{ height: "40px" }}>
{paymentRequestColumns.map(
(col) =>
(col.isAlwaysVisible || groupBy !== col.key) && (

View File

@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<div className="col-12 col-sm-6 ">
<div className="row ">
<div className="col-12 d-flex justify-content-between mb-6">
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span></div>
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span> {data.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xs" >Advance</span>}</div>
<span
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
}`}
@ -199,7 +199,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<div className="row text-start">
<div className="col-6 mb-3">
<label className="form-label me-2 mb-0 fw-semibold text-start">
Supplier:
Payee:
</label>
</div>
<div className="col-6 mb-3">
@ -271,28 +271,7 @@ const ViewPaymentRequest = ({ requestId }) => {
</div>
</div>
</div>
)}
{data?.paidBy && (
<div className="col-6 text-start">
<div className="d-flex flex-column gap-2 text-start">
<label className="form-label me-2 mb-0 fw-semibold">
Paid By:
</label>
<div className="d-flex align-items-center ">
<Avatar
size="xs"
classAvatar="m-0"
firstName={data?.paidBy?.firstName}
lastName={data?.paidBy?.lastName}
/>
<span className="text-muted">
{`${data?.paidBy?.firstName ?? ""} ${data?.paidBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
</div>
</div>
)}
)}
<div className="text-start my-2">
<label className="fw-semibold form-label">Description : </label>

View File

@ -82,7 +82,6 @@ const EditActivityModal = ({
useEffect(() => {
if (!workItem) return;
console.log(workItem)
reset({
activityID: String(
workItem?.workItem?.activityId || workItem?.activityMaster?.id

View File

@ -108,7 +108,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
return (
<div className="p-sm-2 p-2">
<div className="text-center mb-2">
<h5 className="mb-2">{project ? "Edit Project" : "Create Project"}</h5>
<h5 className="mb-2">{project ? "Edit Infra Project" : "Create Infra Project"}</h5>
</div>
<form
className="row g-2 text-start"

View File

@ -1,6 +1,10 @@
import React, { useEffect, useState } from "react";
import moment from "moment";
import { formatNumber, getDateDifferenceInDays } from "../../utils/dateUtils";
import {
formatNumber,
formatUTCToLocalTime,
getDateDifferenceInDays,
} from "../../utils/dateUtils";
import { useNavigate } from "react-router-dom";
import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
import ManageProjectInfo from "./ManageProjectInfo";
@ -17,12 +21,18 @@ import GlobalModel from "../common/GlobalModel";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useProjectContext } from "../../pages/project/ProjectPage";
import { useActiveInActiveServiceProject } from "../../hooks/useServiceProject";
import ConfirmModal from "../common/ConfirmModal";
const ProjectCard = ({ project }) => {
const ProjectCard = ({ project, isCore = true }) => {
const [deleteProject, setDeleteProject] = useState({
project: null,
isOpen: false,
});
const dispatch = useDispatch();
const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const { setMangeProject } = useProjectContext();
const { setMangeProject, setManageServiceProject } = useProjectContext();
const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%";
@ -34,15 +44,49 @@ const ProjectCard = ({ project }) => {
const handleClose = () => setShowModal(false);
const handleViewProject = () => {
dispatch(setProjectId(project.id));
navigate(`/projects/details`);
if (isCore) {
dispatch(setProjectId(project.id));
navigate(`/projects/details`);
} else {
navigate(`/service-projects/${project.id}`);
}
};
const handleViewActivities = () => {
dispatch(setProjectId(project.id));
navigate(`/activities/records?project=${project.id}`);
};
const handleManage = () => {
if (isCore) {
setMangeProject({
isOpen: true,
Project: project.id,
});
} else {
setManageServiceProject({
isOpen: true,
project: project.id,
});
}
};
const { mutate: DeleteProject, isPending } = useActiveInActiveServiceProject(
() => setDeleteProject({ project: null, isOpen: false })
);
const handleActiveInactive = (projectId) => {
DeleteProject(projectId, false);
};
return (
<>
<ConfirmModal
type="delete"
header="Delete Project"
message="Are you sure you want delete?"
onSubmit={handleActiveInactive}
onClose={() => setDeleteProject({ project: null, isOpen: false })}
loading={isPending}
paramData={project.id}
isOpen={deleteProject.isOpen}
/>
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
<div className={`card cursor-pointer`}>
<div className="card-header pb-4">
@ -96,22 +140,31 @@ const ProjectCard = ({ project }) => {
</li>
<li>
<a className="dropdown-item" onClick={() =>
setMangeProject({
isOpen: true,
Project: project.id,
})
}>
<a className="dropdown-item" onClick={handleManage}>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
<li onClick={handleViewActivities}>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
{isCore && (
<li onClick={handleViewActivities}>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
)}
{!isCore && (
<li
onClick={() =>
setDeleteProject({ project: project, isOpen: true })
}
>
<a className="dropdown-item">
<i className="bx bx-trash me-2"></i>
<span className="align-left">Delete</span>
</a>
</li>
)}
</ul>
</div>
</div>
@ -124,21 +177,29 @@ const ProjectCard = ({ project }) => {
<span className="text-heading fw-medium">
Contact Person:{" "}
</span>
{project?.contactPerson ? project.contactPerson : "NA"}
{isCore
? project?.contactPerson
? project.contactPerson
: "NA"
: project.contactName}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Start Date: </span>
{project.startDate
? moment(project?.startDate).format("DD-MMM-YYYY")
: "NA"}
<span className="text-heading fw-medium">
{isCore ? "Start Date:" : "Start Date"}{" "}
</span>
{formatUTCToLocalTime(
isCore ? project?.startDate : project.assignedDate
)}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span>
{isCore && (
<p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span>
{project?.endDate
? moment(project?.endDate).format("DD-MMM-YYYY")
: "NA"}
</p>
{project?.endDate
? moment(project?.endDate).format("DD-MMM-YYYY")
: "NA"}
</p>
)}
<p className="mb-0">{project?.projectAddress}</p>
</div>
</div>
@ -155,7 +216,7 @@ const ProjectCard = ({ project }) => {
{getProjectStatusName(project?.projectStatusId)}
</span>
</p>{" "}
{getDateDifferenceInDays(project?.endDate,new Date() ) >= 0 && (
{getDateDifferenceInDays(project?.endDate, new Date()) >= 0 && (
<span className="badge bg-label-success ms-auto">
{project?.endDate &&
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
@ -165,7 +226,7 @@ const ProjectCard = ({ project }) => {
{getDateDifferenceInDays(project?.endDate, new Date()) < 0 && (
<span className="badge bg-label-danger ms-auto">
{project?.endDate &&
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
Days overdue
</span>
)}

View File

@ -1,70 +1,34 @@
import React from 'react'
import { useProjects } from '../../hooks/useProjects'
import Loader from '../common/Loader'
import ProjectCard from './ProjectCard'
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
import React from "react";
import { useProjects } from "../../hooks/useProjects";
import Loader from "../common/Loader";
import ProjectCard from "./ProjectCard";
import Pagination from "../common/Pagination";
const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
return (
<div className="row page-min-h">
{data?.length === 0 && (
<div
className="col-12 d-flex justify-content-center align-items-center"
style={{ minHeight: "250px" }}
>
<p className="text-center text-muted m-0">No Infra projects found.</p>
</div>
)}
<div className="row page-min-h">
{data?.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
{ currentItems.length === 0 && (
<p className="text-center text-muted">No projects found.</p>
)}
{currentItems.map((project) => (
<ProjectCard
key={project.id}
project={project}
/>
))}
<div className="col-12 d-flex justify-content-start mt-3">
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={paginate}
/>
</div>
</div>
);
};
{ 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
export default ProjectCardView;

View File

@ -13,15 +13,9 @@ import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProjectContext } from "../../pages/project/ProjectPage";
import usePagination from "../../hooks/usePagination";
import Pagination from "../common/Pagination";
const ProjectListView = ({
currentItems,
selectedStatuses,
handleStatusChange,
setCurrentPage,
totalPages,
isLoading,
}) => {
const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { setMangeProject } = useProjectContext();
@ -132,152 +126,120 @@ const ProjectListView = ({
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}>
<div className="table-responsive text-nowrap">
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
{projectColumns.map((col) => (
<td
<th
key={col.key}
colSpan={col.colSpan}
className={`${col.className} py-5`}
style={{ paddingTop: "20px", paddingBottom: "20px" }}
className={`${col.className} table_header_border`}
>
{col.getValue
? col.getValue(project)
: project[col.key] || "N/A"}
</td>
{col.label}
</th>
))}
<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>
<th className="text-center py-3">Action</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{data?.length > 0 ? (
data?.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>
))
) : (
<tr
className="no-hover"
style={{
pointerEvents: "none",
backgroundColor: "transparent",
}}
>
<td
colSpan={projectColumns.length + 1}
className="text-center align-middle"
style={{ height: "300px", borderBottom: "none" }}
>
No Infra projects available
</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>
)}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
paginate={paginate}
/>
</div>
);
};

View File

@ -104,7 +104,6 @@ const hasChanges =
permission: payloadPermissions,
};
console.log("Final payload:", payload);
updatePermission(payload);
};

View File

@ -9,7 +9,7 @@ import {
import ReactApexChart from "react-apexcharts";
import Chart from "react-apexcharts";
const ProjectOverview = ({ project }) => {
const ProjectStatistics = ({ project }) => {
const { data } = useProjects();
const [current_project, setCurrentProject] = useState(
data?.find((pro) => pro.id == project)
@ -133,10 +133,10 @@ const ProjectOverview = ({ project }) => {
},
},
stroke: {
lineCap: "round",
lineCap: "round",
},
labels: ["Progress"],
series: [percentage],
labels: ["Progress"],
series: [percentage],
};
};
const [radialPercentage, setRadialPercentage] = useState(75); // Initial percentage
@ -165,7 +165,7 @@ const ProjectOverview = ({ project }) => {
}, [selectedProject]);
return (
<div className="card" style={{ minHeight: "490px" }}>
<div className="card h-100">
<div className="card-header text-start">
<h5 className="card-action-title mb-0">
{" "}
@ -173,78 +173,78 @@ const ProjectOverview = ({ project }) => {
<span className="ms-2 fw-bold">Project Statistics</span>
</h5>
</div>
<div className="card-body">
<ul className="list-unstyled m-0 p-0">
<li className="d-flex flex-wrap">
<div className="w-100 d-flex flex-wrap">
{/* Centered Chart */}
<div className="w-100 d-flex justify-content-center mb-3">
<div >
<Chart
options={radialBarOptions}
series={radialBarOptions.series}
type="radialBar"
height="100%"
/>
</div>
</div>
{/* Info Section */}
<div className="mb-2" style={{ flex: "1 1 auto" }}>
<div>
{/* Tasks Planned */}
<div className="d-flex align-items-center mb-3">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-primary">
<i className="bx bx-check text-primary fs-4"></i>
</span>
<div className="card-body">
<ul className="list-unstyled m-0 p-0">
<li className="d-flex flex-wrap">
<div className="w-100 d-flex flex-wrap">
{/* Centered Chart */}
<div className="w-100 d-flex justify-content-center mb-3">
<div >
<Chart
options={radialBarOptions}
series={radialBarOptions.series}
type="radialBar"
height="100%"
/>
</div>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Tasks Planned</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.plannedWork)}
</h5>
{/* Info Section */}
<div className="mb-2" style={{ flex: "1 1 auto" }}>
<div>
{/* Tasks Planned */}
<div className="d-flex align-items-center mb-3">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-primary">
<i className="bx bx-check text-primary fs-4"></i>
</span>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Tasks Planned</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.plannedWork)}
</h5>
</div>
</div>
{/* Tasks Completed */}
<div className="d-flex align-items-center mb-3">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-info">
<i className="bx bx-star text-info fs-4"></i>
</span>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Tasks Completed</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.completedWork)}
</h5>
</div>
</div>
{/* Team Size */}
<div className="d-flex align-items-center">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-primary">
<i className="bx bx-group text-primary fs-4"></i>
</span>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Current Team Size</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.teamSize)}
</h5>
</div>
</div>
</div>
</div>
</div>
{/* Tasks Completed */}
<div className="d-flex align-items-center mb-3">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-info">
<i className="bx bx-star text-info fs-4"></i>
</span>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Tasks Completed</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.completedWork)}
</h5>
</div>
</div>
{/* Team Size */}
<div className="d-flex align-items-center">
<div className="avatar me-2">
<span className="avatar-initial rounded-2 bg-label-primary">
<i className="bx bx-group text-primary fs-4"></i>
</span>
</div>
<div className="d-flex flex-column text-start">
<small className="fw-bold">Current Team Size</small>
<h5 className="mb-0">
{FormattedNumber(current_project?.teamSize)}
</h5>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
);
};
export default ProjectOverview;
export default ProjectStatistics;

View File

@ -10,11 +10,13 @@ import {
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
import showToast from "../../../services/toastService";
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
import { useDispatch } from "react-redux";
import { changeMaster } from "../../../slices/localVariablesSlice";
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
const selectedProject = useSelectedProject();
const debounceSearchTerm = useDebounce(searchTerm, 500);
const dispatch = useDispatch();
const {
data: employeesData = [],
isLoading,
@ -45,6 +47,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
});
useEffect(() => {
dispatch(changeMaster("Job Role"));
if (employeesData?.data?.length > 0) {
const available = employeesData.data.filter((emp) => {
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
@ -119,7 +122,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
status: true,
}));
handleAssignEmployee({ payload,actionType:"assign"} );
handleAssignEmployee({ payload, actionType: "assign" });
setEmployees((prev) =>
prev.map((emp) => ({
@ -132,26 +135,26 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
);
};
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 (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 (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>) ;
}
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 (
@ -183,9 +186,8 @@ if (employees.length === 0) {
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" : ""
}`}
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) => (
@ -205,9 +207,8 @@ if (employees.length === 0) {
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" : ""
}`}
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) => (

View File

@ -27,6 +27,7 @@ import InputSuggestions from "../common/InputSuggestion";
import { useEmployeesName } from "../../hooks/useEmployees";
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
import HoverPopup from "../common/HoverPopup";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
const {
@ -131,10 +132,9 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
}
}, [currencyData, requestToEdit, setValue]);
const StrikeDate = watch("strikeDate")
const StrikeDate = watch("strikeDate");
const onSubmit = (fromdata) => {
console.log(fromdata);
let payload = {
...fromdata,
strikeDate: fromdata.strikeDate
@ -164,10 +164,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
{/* Project and Category */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>
Select Project
</Label>
<select
{/* <select
className="form-select form-select-sm"
{...register("projectId")}
>
@ -181,7 +178,19 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</option>
))
)}
</select>
</select> */}
<SelectProjectField
label="Select Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
/>
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}
@ -236,20 +245,24 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</div>
<div className="col-md-6 mt-2">
<div className="d-flex justify-content-start align-items-center gap-2">
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label htmlFor="isVariable" className="form-label mb-0" required>
Payment Type
</Label>
<HoverPopup
title="Payment Type"
id="payment_type"
content={
<p>
Choose whether the payment amount varies or remains fixed each cycle.
<br />
<strong>Is Variable:</strong> Amount changes per cycle.
<br />
<strong>Fixed:</strong> Amount stays constant.
</p>
<div className=" w-50">
<p>
Choose whether the payment amount varies or remains fixed
each cycle.
<br />
<strong>Is Variable:</strong> Amount changes per cycle.
<br />
<strong>Fixed:</strong> Amount stays constant.
</p>
</div>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
@ -270,7 +283,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
checked={field.value === true}
onChange={() => field.onChange(true)}
/>
<Label htmlFor="isVariableTrue" className="form-check-label">
<Label
htmlFor="isVariableTrue"
className="form-check-label"
>
Is Variable
</Label>
</div>
@ -283,7 +299,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
checked={field.value === false}
onChange={() => field.onChange(false)}
/>
<Label htmlFor="isVariableFalse" className="form-check-label">
<Label
htmlFor="isVariableFalse"
className="form-check-label"
>
Fixed
</Label>
</div>
@ -295,7 +314,6 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<small className="danger-text">{errors.isVariable.message}</small>
)}
</div>
</div>
{/* Date and Amount */}
@ -388,13 +406,15 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</Label>
<HoverPopup
title="Frequency"
id="frequency"
content={
<p>
Defines how often payments or billing occur, such as monthly, quarterly, or annually.
Defines how often payments or billing occur, such as
monthly, quarterly, or annually.
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -443,21 +463,26 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
{/* Payment Buffer Days and End Date */}
<div className="row my-2 text-start">
<div className="col-md-6">
<div className="d-flex justify-content-start align-items-center gap-2">
<Label htmlFor="paymentBufferDays" className="form-label mb-0" required>
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label
htmlFor="paymentBufferDays"
className="form-label mb-0 "
required
>
Payment Buffer Days
</Label>
<HoverPopup
title="Payment Buffer Days"
id="payment_buffer_days"
content={
<p>
Number of extra days allowed after the due date before payment is considered late.
Number of extra days allowed after the due date before
payment is considered late.
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -478,21 +503,21 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
)}
</div>
<div className="col-md-6">
<div className="d-flex justify-content-start align-items-center gap-2">
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label htmlFor="endDate" className="form-label mb-0" required>
End Date
</Label>
<HoverPopup
title="End Date"
id="end_date"
content={
<p>
The date when the last payment in the recurrence occurs.
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -507,10 +532,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<small className="danger-text">{errors.endDate.message}</small>
)}
</div>
</div>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="notifyTo" className="form-label" required>
@ -569,8 +592,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
{createPending || isPending
? "Please wait...."
: requestToEdit
? "Update"
: "Save as Draft"}
? "Update"
: "Save as Draft"}
</button>
</div>
</form>

View File

@ -166,7 +166,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
}
);
};
console.log("Tanish",filteredData)
return (
<>
{IsDeleteModalOpen && (
@ -203,7 +203,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
<tr
key={recurringExpense.id}
className="align-middle"
style={{ height: "50px" }}
style={{ height: "40px" }}
>
{recurringExpenseColumns.map((col) => (
<td

View File

@ -0,0 +1,272 @@
import React, { useState } from "react";
import {
daysLeft,
getJobStatusBadge,
getNextBadgeColor,
} from "../../utils/appUtils";
import { useServiceProjectJobs, useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE, JOBS_STATUS_IDS } from "../../utils/constants";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { SpinnerLoader } from "../common/Loader";
import { useParams } from "react-router-dom";
import ProjectPage from "../../pages/project/ProjectPage";
import { useServiceProjectJobContext } from "./Jobs";
import ConfirmModal from "../common/ConfirmModal";
const JobList = ({ isArchive }) => {
const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
const { mutate: UpdateJob,isPending } = useUpdateServiceProjectJob(() => {
});
const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProjectJobs(
ITEMS_PER_PAGE,
1,
true,
projectId,
isArchive
);
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
const [archiveJobId, setArchiveJobId] = useState(null);
const [isArchiveAction, setIsArchiveAction] = useState(true);
const handleArchive = () => {
const payload = [
{
op: "replace",
path: "/isArchive",
value: isArchiveAction,
},
];
UpdateJob({
id: archiveJobId,
payload,
isArchiveAction,
});
setIsArchiveModalOpen(false);
setArchiveJobId(null);
};
const jobGrid = [
{
key: "jobTicketUId",
label: "Job Id",
getValue: (e) => <span>{e?.jobTicketUId || "N/A"}</span>,
align: "text-start",
},
{
key: "title",
label: "Title",
getValue: (e) => (
<span
className={`fw-semibold text-truncate d-inline-block ${!isArchive ? "cursor-pointer" : ""}`}
style={{
maxWidth: "100%",
width: "210px",
}}
onClick={() => {
if (!isArchive) {
setSelectedJob({ showCanvas: true, job: e?.id });
}
}}
title={e?.title}
>
{e?.title}
</span>
),
isAlwaysVisible: true,
className: "text-start",
},
{
key: "dueDate",
label: "Due On",
getValue: (e) => formatUTCToLocalTime(e.startDate),
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
{
key: "status",
label: "Status",
getValue: (e) => {
return (
<span className={`badge ${getJobStatusBadge(e?.status?.id)}`}>
{e?.status?.displayName}
</span>
);
},
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
{
key: "daysLeft",
label: "Days Left",
getValue: (e) => {
const { days, color } = daysLeft(e.startDate, e.dueDate);
return (
<span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"}
</span>
);
},
isAlwaysVisible: true,
className: "text-start d-none d-sm-table-cell",
},
];
const canArchive = (statusId) => {
const closedId = JOBS_STATUS_IDS.find((s) => s.label === "Closed")?.id;
const reviewDoneId = JOBS_STATUS_IDS.find((s) => s.label === "Review Done")?.id;
return statusId === closedId || statusId === reviewDoneId;
};
return (
<>
{isArchiveModalOpen && (
<ConfirmModal
isOpen={isArchiveModalOpen}
type={isArchiveAction ? "archive" : "Un-archive"}
header={isArchiveAction ? "Archive Job" : "Restore Job"}
message={
isArchiveAction
? "Are you sure you want to archive this job?"
: "Are you sure you want to restore this job?"
}
onSubmit={handleArchive}
onClose={() => setIsArchiveModalOpen(false)}
loading={isPending}
/>
)}
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
<table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
aria-describedby="DataTables_Table_0_info"
>
<thead>
<tr>
{jobGrid.map((col) => (
<th
key={col.key}
className={`${col.align || "text-center"} ${col.className || ""
}`}
scope="col"
>
<div className={col.className}>{col.label}</div>
</th>
))}
<th className="sorting_disabled text-center" aria-label="Actions">
Actions
</th>
</tr>
</thead>
<tbody>
{Array.isArray(data?.data) && data.data.length > 0 ? (
data.data.map((row, i) => (
<tr key={i} className="text-start">
{jobGrid.map((col) => (
<td
key={col.key}
className={col.className}
// onClick={() =>
// setSelectedJob({ showCanvas: true, job: row?.id })
// }
onClick={() => {
if (!isArchive) {
setSelectedJob({ showCanvas: true, job: e?.id });
}
}}
>
{col.getValue(row)}
</td>
))}
<td>
<div className="dropdown text-center">
<button
className="btn btn-icon dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
{!isArchive && (
<>
<button
className="dropdown-item py-1"
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id })
}
>
<i className="bx bx-detail bx-sm"></i> View
</button>
<button
className="dropdown-item py-1"
onClick={() =>
setManageJob({ isOpen: true, jobId: row?.id })
}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
</>
)}
{isArchive && (
<button
className="dropdown-item py-1"
onClick={() => {
setArchiveJobId(row.id);
setIsArchiveAction(false);
setIsArchiveModalOpen(true);
}}
>
<i className="bx bx-reset bx-sm"></i> Restore
</button>
)}
{!isArchive && canArchive(row?.status?.id) && (
<button
className="dropdown-item py-1"
onClick={() => {
setArchiveJobId(row.id);
setIsArchiveAction(true);
setIsArchiveModalOpen(true);
}}
>
<i className="bx bx-archive bx-sm"></i> Archive
</button>
)}
</div>
</div>
</td>
</tr>
))
) : (
<tr style={{ height: "200px" }}>
<td
colSpan={jobGrid.length + 1}
className="text-center border-0 align-middle"
>
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
</td>
</tr>
)}
</tbody>
</table>
</div>
</>
);
};
export default JobList;

View File

@ -0,0 +1,96 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import JobList from "./JobList";
import { useNavigate } from "react-router-dom";
import { useServiceProjects } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import OffcanvasComponent from "../common/OffcanvasComponent";
import showToast from "../../services/toastService";
import ManageJob from "./ServiceProjectJob/ManageJob";
import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
import GlobalModel from "../common/GlobalModel";
import PreviewDocument from "../Expenses/PreviewDocument";
export const JonContext = createContext();
export const useServiceProjectJobContext = () => {
const context = useContext(JonContext);
if (!context) {
showToast("Something went wrong", "warning");
window.location = "/dashboard";
}
return context;
};
const Jobs = () => {
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
const [showArchive, setShowArchive] = useState(false);
const [showCanvas, setShowCanvas] = useState(false);
const [selectedProject, setSelectedProject] = useState(null);
const [selectJob, setSelectedJob] = useState({
showCanvas: false,
job: null,
});
const navigate = useNavigate();
const { data } = useServiceProjects(ITEMS_PER_PAGE, 1);
const contextProvider = {
setSelectedJob,
setSelectedProject,
setManageJob,
manageJob,
};
return (
<>
<JonContext.Provider value={contextProvider}>
<OffcanvasComponent
id="customCanvas"
title="Job"
placement="end"
show={selectJob.showCanvas}
onClose={() => setSelectedJob({ showCanvas: false, job: null })}
>
<ManageJobTicket Job={selectJob} />
</OffcanvasComponent>
<OffcanvasComponent
id="customCanvas1"
title={`${manageJob.jobId ? "Update a Job" : "Create a new Job"} `}
placement="end"
show={manageJob.isOpen}
onClose={() => setManageJob({ isOpen: false, jobId: null })}
>
<ManageJob Job={manageJob.jobId} />
</OffcanvasComponent>
<div className="card page-min-h my-2 px-7 pb-4">
<div className="row align-items-center py-4">
{/* LEFT — Tabs */}
<div className="col-12 col-md-6 text-start">
<button
type="button"
className={`btn btn-sm ${showArchive ? "btn-secondary" : "btn-outline-secondary"}`}
onClick={() => setShowArchive(!showArchive)}
>
<i className="bx bx-archive bx-sm me-1 mt-1"></i> Archived
</button>
</div>
{/* RIGHT — New Job button */}
<div className="col-12 col-md-6 d-flex justify-content-md-end mt-2 mt-md-0">
<button
className="btn btn-sm btn-primary"
onClick={() => setManageJob({ isOpen: true, jobId: null })}
>
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
</button>
</div>
</div>
{/* Job List */}
<JobList filterByProject={selectedProject} isArchive={showArchive} />
</div>
</JonContext.Provider>
</>
);
};
export default Jobs;

View File

@ -0,0 +1,328 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { defaultProjectValues } from "./ServiceProjectSchema";
import Label from "../common/Label";
import { FormProvider, useForm } from "react-hook-form";
import {
useGlobalServices,
useServices,
} from "../../hooks/masterHook/useMaster";
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
import DatePicker from "../common/DatePicker";
import SelectMultiple from "../common/SelectMultiple";
import { projectSchema } from "./ServiceProjectSchema";
import {
useOrganization,
useOrganizationModal,
useOrganizationsList,
} from "../../hooks/useOrganization";
import { error } from "pdf-lib";
import {
useCreateServiceProject,
useServiceProject,
useUpdateServiceProject,
} from "../../hooks/useServiceProject";
const ManageServiceProject = ({ serviceProjectId, onClose }) => {
const {
data: projectdata,
isLoading: isProjectLoading,
isProjectError,
} = useServiceProject(serviceProjectId);
const [searchText, setSearchText] = useState("");
const methods = useForm({
resolver: zodResolver(projectSchema),
defaultValues: defaultProjectValues,
});
const {
register,
handleSubmit,
control,
reset,
formState: { errors },
} = methods;
const { data, isError, isLoading } = useServices();
const { data: organization, isLoading: isLoadingOrganization } =
useOrganizationsList(ITEMS_PER_PAGE, 1, true);
const { mutate: CreateServiceProject, isPending } = useCreateServiceProject(
() => {
reset();
onClose();
}
);
const { mutate: UpdateServiceProject, isPending: isUpdating } =
useUpdateServiceProject(() => {
reset();
onClose();
});
const onSubmit = (formData) => {
if (serviceProjectId) {
let existingServiceIds = projectdata?.services?.map((s) => s.id) || [];
const oldServices = projectdata.services.map((service) => ({
serviceId: service.id,
isActive: formData.services.includes(service.id),
}));
const newServices = formData.services
.filter((s) => !existingServiceIds.includes(s))
.map((service) => ({ serviceId: service, isActive: true }));
formData.services = [...oldServices, ...newServices];
} else {
formData.services = formData.services.map((service) => ({
serviceId: service,
isActive: true,
}));
}
if (serviceProjectId) {
let payload = { ...formData, id: serviceProjectId };
UpdateServiceProject({ id: serviceProjectId, payload });
} else {
CreateServiceProject(formData);
}
};
useEffect(() => {
if (projectdata) {
const activeServiceIds = (projectdata.services || [])
.map((s) => s.serviceId || s.id)
.filter(Boolean);
reset({
name: projectdata.name,
shortName: projectdata.shortName,
clientId: projectdata.client.id,
assignedDate: projectdata.assignedDate,
address: projectdata.address,
statusId: projectdata.status.id,
contactEmail: projectdata.contactEmail,
contactPhone: projectdata.contactPhone,
contactName: projectdata.contactName,
services: activeServiceIds,
});
}
}, [projectdata]);
const { onOpen: openOrgModal } = useOrganizationModal();
return (
<FormProvider {...methods}>
<form className="px-3 py-2" onSubmit={handleSubmit(onSubmit)}>
<div className=" text-center">
<h5>{serviceProjectId ? "Update Service Project" : "Create Service Project"}</h5>
</div>
<div className="row text-start">
<div className="col-12 mb-2">
<Label htmlFor="name" required>
Client
</Label>
<div className="d-flex align-items-center gap-2">
<select
className="select2 form-select form-select-sm flex-grow-1"
aria-label="Default select example"
{...register("clientId", {
required: "Client is required",
valueAsNumber: false,
})}
>
{isLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">Select Client</option>
{organization?.data?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</>
)}
</select>
<i
className="bx bx-plus-circle bx-xs cursor-pointer text-primary"
onClick={() => {
onClose();
openOrgModal({ startStep: 2 }); // Step 4 = ManagOrg
}}
/>
</div>
{errors?.clientId && (
<span className="danger-text">{errors.clientId.message}</span>
)}
</div>
<div className="col-12 mb-2">
<Label htmlFor="name" required>
Project Name
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("name")}
placeholder="Enter Project Name.."
/>
{errors?.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Short Name (Short Project Name)
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("shortName")}
placeholder="Enter Project Short Name.."
/>
{errors?.shortName && (
<span className="danger-text">{errors.shortName.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Select Status
</Label>
<select
className="form-select form-select-sm"
{...register("statusId")}
>
<option>Select Service</option>
{PROJECT_STATUS?.map((status) => (
<option key={status.id} value={status.id}>{status.label}</option>
))}
</select>
{errors?.statusId && (
<span className="danger-text">{errors.statusId.message}</span>
)}
</div>
<div className="col-12 mb-2">
<SelectMultiple
options={data?.data}
isLoading={isLoading}
name="services"
labelKey="name"
valueKey="id"
label="Select Service"
/>
{errors?.services && (
<span className="danger-text">{errors.services.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Contact Person
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("contactName")}
placeholder="Enter Employee name.."
onInput={(e) => {
e.target.value = e.target.value.replace(/[^A-Za-z ]/g, "");
}}
/>
{errors?.contactName && (
<span className="danger-text">{errors.contactName.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Contact Email
</Label>
<input
type="text"
className="form-control form-control-sm"
{...register("contactEmail")}
placeholder="Enter Employee Email.."
/>
{errors?.contactEmail && (
<span className="danger-text">{errors.contactEmail.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Contact Number
</Label>
<input
type="text"
maxLength={10}
className="form-control form-control-sm"
{...register("contactPhone")}
placeholder="Enter Employee Contact.."
onInput={(e) => {
e.target.value = e.target.value.replace(/[^0-9]/g, "");
}}
/>
{errors?.contactPhone && (
<span className="danger-text">{errors.contactPhone.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Assign Date
</Label>
<DatePicker
name="assignedDate"
className="w-100"
control={control}
/>
</div>
<div className="col-12 col-md-12 mb-2">
<Label htmlFor="address" required>
Project Address
</Label>
<div className="input-group">
<textarea
name="address"
className="form-control"
// maxLength={maxAddressLength}
{...register("address")}
placeholder="Enter Project Address.."
// onChange={(e) => {
// setAddressLength(e.target.value.length);
// }}
/>
</div>
<div className="text-end" style={{ fontSize: "12px" }}>
{/* {maxAddressLength - addressLength} characters left */}
</div>
{errors.address && (
<div className="danger-text text-start">
{errors.address.message}
</div>
)}
</div>
</div>
<div className="d-flex justify-content-end gap-4">
<button
className="btn btn-sm btn-outline-secondary"
disabled={isPending || isUpdating}
onClick={onClose}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending || isUpdating}
>
{isPending || isUpdating
? "Please wait..."
: serviceProjectId
? "Update"
: "Submit"}
</button>
</div>
</form>
</FormProvider>
);
};
export default ManageServiceProject;

View File

@ -0,0 +1,95 @@
import React from 'react'
import { formatUTCToLocalTime } from '../../utils/dateUtils'
const ServiceProfile = ({data,setIsOpenModal}) => {
return (
<div className="card mb-4 h-100">
<div className="card-header text-start">
<h5 className="card-action-title mb-0 ps-1">
{" "}
<i className="fa fa-building rounded-circle text-primary"></i>
<span className="ms-2 fw-bold">Project Profile</span>
</h5>
</div>
<div className="card-body">
<ul className="list-unstyled my-3 ps-0 text-start">
<li className="d-flex mb-3">
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
<i className="bx bx-cog"></i>
<span className="fw-medium mx-2 text-nowrap">Name:</span>
</div>
<div className="flex-grow-1 text-start text-wrap">
{data.name}
</div>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-fingerprint"></i>
<span className="fw-medium mx-2">Nick Name:</span>
</div>
<span>{data.shortName}</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-check"></i>
<span className="fw-medium mx-2">Assign Date:</span>
</div>
<span>
{data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"}
</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-trophy"></i>
<span className="fw-medium mx-2">Status:</span>
</div>
<span>{data?.status.status}</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-user"></i>
<span className="fw-medium mx-2">Contact:</span>
</div>
<span>{data.contactName}</span>
</li>
<li className="d-flex mb-3">
{/* Label section with icon */}
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
<i className="bx bx-flag mt-1"></i>
<span className="fw-medium mx-2 text-nowrap">Address:</span>
</div>
{/* Content section that wraps nicely */}
<div className="flex-grow-1 text-start text-wrap">
{data.address}
</div>
</li>
<li className="d-flex justify-content-center mt-4">
<a className="d-flex justify-content-center mt-4">
<button
type="button"
className="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#edit-project-modal"
onClick={() => setIsOpenModal(true)}
>
Modify Details
</button>
</a>
</li>
</ul>
</div>
</div>
)
}
export default ServiceProfile

View File

@ -0,0 +1,86 @@
import React, { useState } from "react";
import { useBranchDetails } from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../../common/Loader";
import Error from "../../common/Error";
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
const BranchDetails = ({ branch }) => {
const [copied, setCopied] = useState(false);
const { data, isLoading, isError, error } = useBranchDetails(branch);
if (isLoading) return <BranchDetailsSkeleton />;
if (isError) return <Error error={error} />;
let contactInfo = [];
try {
contactInfo = JSON.parse(data?.contactInformation || "[]");
} catch (e) {}
const googleMapUrl = data?.googleMapUrl || data?.locationLink;
const handleCopy = async () => {
if (!googleMapUrl) return;
await navigator.clipboard.writeText(googleMapUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="w-100">
<div className="d-flex mb-2 align-items-center">
<i className="bx bx-buildings bx-sm me-2 text-primary"></i>
<span className="fw-semibold">Branch Details</span>
</div>
<DetailRow label="Branch Name" value={data?.branchName} />
<DetailRow label="Type" value={data?.branchType} />
<DetailRow label="Address" value={data?.address} />
{/* Contact persons */}
{contactInfo.map((person, index) => (
<div key={index} className="mb-2">
<div className="fw-medium text-primary">{person.contactPerson}</div>
<DetailRow label="Role" value={person.designation} />
<DetailRow label="Emails" value={person.contactEmails.join(", ")} />
<DetailRow label="Phone" value={person.contactNumbers.join(", ")} />
</div>
))}
{/* Map Link */}
{googleMapUrl && (
<div className="mt-2">
<a
href={googleMapUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary text-decoration-underline"
>
View on Google Maps
</a>
<button
className="btn btn-xs btn-secondry border ms-2"
onClick={handleCopy}
>
<i className={`bx bx-xs me-1 ${copied ? "bxs-copy-alt":"bx-copy-alt"} `} ></i> {copied ? "Copied!" : "Copy"}
</button>
</div>
)}
</div>
);
};
const DetailRow = ({ label, value }) => (
<div className="d-flex mb-1">
<div className="text-secondary" style={{ width: "90px", flexShrink: 0 }}>
{label}:
</div>
<div className="" style={{ wordBreak: "break-word" }}>
{value || "N/A"}
</div>
</div>
);
export default BranchDetails;

View File

@ -0,0 +1,358 @@
import React, { useEffect } from "react";
import { useProjectName } from "../../../hooks/useProjects";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../../common/Label";
import {
useBranchDetails,
useBranchTypes,
useCreateBranch,
useServiceProjects,
useUpdateBranch,
} from "../../../hooks/useServiceProject";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { useParams } from "react-router-dom";
import { BranchSchema, defaultBranches } from "../ServiceProjectSchema";
import InputSuggessionField from "../../common/Forms/InputSuggesstionField";
import InputSuggestions from "../../common/InputSuggestion";
const ManageBranch = ({ closeModal, BranchToEdit = null }) => {
const {
data,
isLoading,
isError,
error: requestError,
} = useBranchDetails(BranchToEdit);
const { data: branchTypes } = useBranchTypes();
const [contacts, setContacts] = React.useState([
{
contactPerson: "",
designation: "",
contactEmails: [""],
contactNumbers: [""],
},
]);
const { projectId } = useParams();
const schema = BranchSchema();
const {
register,
control,
watch,
handleSubmit,
setValue,
reset,
formState: { errors },
} = useAppForm({
resolver: zodResolver(schema),
defaultValues: {
...defaultBranches,
projectId: projectId || "",
},
});
const handleClose = () => {
reset();
closeModal();
};
useEffect(() => {
if (BranchToEdit && data) {
reset({
branchName: data.branchName || "",
projectId: data.project?.id || projectId || "",
address: data.address || "",
branchType: data.branchType || "",
googleMapUrl: data.googleMapUrl || "",
});
if (data.contactInformation) {
try {
setContacts(JSON.parse(data.contactInformation));
} catch {
setContacts([]);
}
}
}
}, [data, reset]);
const { mutate: CreateServiceBranch, isPending: createPending } =
useCreateBranch(() => {
handleClose();
});
const { mutate: ServiceBranchUpdate, isPending } = useUpdateBranch(() =>
handleClose()
);
const onSubmit = (formdata) => {
let payload = {
...data,
...formdata,
projectId,
contactInformation: JSON.stringify(contacts), // important
};
if (BranchToEdit) {
ServiceBranchUpdate({ id: data.id, payload });
} else {
CreateServiceBranch(payload);
}
};
return (
<div className="container p-3">
<h5 className="m-0">
{BranchToEdit ? "Update Branch" : "Create Branch"}
</h5>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="branchName" className="form-label" required>
Branch Name
</Label>
<input
type="text"
id="branchName"
className="form-control form-control-sm"
{...register("branchName")}
placeholder="Enter Branch"
/>
{errors.branchName && (
<small className="danger-text">{errors.branchName.message}</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="branchType" className="form-label" required>
Branch Type
</Label>
<InputSuggestions
organizationList={branchTypes}
value={watch("branchType") || ""}
onChange={(val) =>
setValue("branchType", val, { shouldValidate: true })
}
error={errors.branchType?.message}
/>
</div>
</div>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="googleMapUrl" className="form-label">
Google Map URL
</Label>
<input
type="text"
id="googleMapUrl"
className="form-control form-control-sm"
{...register("googleMapUrl")}
/>
{errors.googleMapUrl && (
<small className="danger-text">
{errors.googleMapUrl.message}
</small>
)}
</div>
</div>
<div className="row my-2 text-start">
<div className="col-12">
<Label className="form-label" required>
Contact Persons
</Label>
{contacts.map((item, index) => (
<div key={index} className="border rounded p-2 mb-3">
<div className="d-flex justify-content-end py-1">
{" "}
<div className="col-md-1 d-flex align-items-center">
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() =>
setContacts(contacts.filter((_, i) => i !== index))
}
></i>
</div>
</div>
<div className="row mb-2">
<div className=" col-md-6">
<Label className="form-label">Contact Name</Label>
<input
type="text"
placeholder="Contact Name"
className="form-control form-control-sm"
value={item.contactPerson}
onChange={(e) => {
const list = [...contacts];
list[index].contactPerson = e.target.value;
setContacts(list);
}}
/>
</div>
<div className="col-md-6">
<Label className="form-label">Designation</Label>
<input
type="text"
placeholder="Designation"
className="form-control form-control-sm"
value={item.designation}
onChange={(e) => {
const list = [...contacts];
list[index].designation = e.target.value;
setContacts(list);
}}
/>
</div>
</div>
{/* Numbers Section */}
<Label className="form-label">Contact Numbers</Label>
{item.contactNumbers.map((num, numIndex) => (
<div
key={numIndex}
className="d-flex gap-2 mb-2 align-items-center"
>
<input
type="text"
placeholder="Number"
className="form-control form-control-sm"
maxLength={10}
value={num}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ""); // remove non-digit characters
const list = [...contacts];
list[index].contactNumbers[numIndex] = value;
setContacts(list);
}}
/>
{/* Show PLUS only on last row */}
{numIndex === item.contactNumbers.length - 1 ? (
<i
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactNumbers.push("");
setContacts(list);
}}
></i>
) : (
<i
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactNumbers.splice(numIndex, 1);
setContacts(list);
}}
></i>
)}
</div>
))}
<hr />
{/* Emails Section */}
<Label className="form-label">Contact Emails</Label>
{item.contactEmails.map((email, emailIndex) => (
<div
key={emailIndex}
className="d-flex gap-2 mb-2 align-items-center"
>
<input
type="email"
placeholder="Email"
className="form-control form-control-sm"
value={email}
onChange={(e) => {
const list = [...contacts];
list[index].contactEmails[emailIndex] = e.target.value;
setContacts(list);
}}
/>
{/* Show PLUS only on the last row */}
{emailIndex === item.contactEmails.length - 1 ? (
<i
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactEmails.push("");
setContacts(list);
}}
></i>
) : (
<i
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactEmails.splice(emailIndex, 1);
setContacts(list);
}}
></i>
)}
</div>
))}
</div>
))}
<button
type="button"
className="btn btn-sm btn-primary mt-2"
onClick={() =>
setContacts([
...contacts,
{
contactPerson: "",
designation: "",
contactEmails: [""], // important
contactNumbers: [""], // important
},
])
}
>
<i className="bx bx-plus"></i> Add Contact
</button>
</div>
</div>
<div className="row my-2 text-start">
<div className="col-12">
<Label htmlFor="address" className="form-label" required>
Address
</Label>
<textarea
id="address"
className="form-control form-control-sm"
{...register("address")}
/>
{errors.address && (
<small className="danger-text">{errors.address.message}</small>
)}
</div>
</div>
<div className="d-flex justify-content-end gap-3">
<button
type="reset"
onClick={handleClose}
className="btn btn-label-secondary btn-sm mt-3"
>
Cancel
</button>
<button type="submit" className="btn btn-primary btn-sm mt-3">
{isPending ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default ManageBranch;

View File

@ -0,0 +1,283 @@
import React, { useState } from "react";
import GlobalModel from "../../common/GlobalModel";
import ManageBranch from "./ManageBranch";
import { useBranches, useDeleteBranch } from "../../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../../utils/constants";
import { useDebounce } from "../../../utils/appUtils";
import { useParams } from "react-router-dom";
import Pagination from "../../common/Pagination";
import ConfirmModal from "../../common/ConfirmModal";
import { SpinnerLoader } from "../../common/Loader";
const ServiceBranch = () => {
const { projectId } = useParams();
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [showInactive, setShowInactive] = useState(false);
const [manageState, setManageState] = useState({
IsOpen: false,
branchId: null,
});
const { mutate: DeleteBranch, isPending } = useDeleteBranch();
const [deletingId, setDeletingId] = useState(null);
const [search, setSearch] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(search, 500);
const { data, isLoading, isError, error } = useBranches(
projectId,
!showInactive,
ITEMS_PER_PAGE - 12,
currentPage,
debouncedSearch
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const columns = [
{
key: "branchName",
label: "Name",
align: "text-start",
getValue: (e) => e?.branchName || "N/A",
},
];
const handleDelete = (id) => {
setDeletingId(id);
DeleteBranch(
{ id, isActive: showInactive },
{
onSettled: () => {
setDeletingId(null);
setIsDeleteModalOpen(false);
},
}
);
};
return (
<>
{IsDeleteModalOpen && (
<ConfirmModal
isOpen={IsDeleteModalOpen}
type={!showInactive ? "delete" : "undo"}
header={!showInactive ? "Delete Branch" : "Restore Branch"}
message={
!showInactive
? "Are you sure you want delete?"
: "Are you sure you want restore?"
}
onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)}
loading={isPending}
paramData={deletingId}
/>
)}
<div className="card h-100 table-responsive px-sm-4">
<div className="card-datatable" id="payment-request-table">
{/* Header Section */}
<div className="row align-items-center justify-content-between mt-3 mx-1">
<div className="col-md-4 col-sm-12 ms-n3 text-start ">
<h5 className="mb-0">
<i className="bx bx-buildings text-primary"></i>
<span className="ms-2 fw-bold">Branchs</span>
</h5>
</div>
{/* Flex container for toggle + button */}
<div className="col-md-8 col-sm-12 text-end">
<div className="d-flex justify-content-end gap-2">
<div className="form-check form-switch d-inline-flex align-items-center">
<input
type="checkbox"
className="form-check-input mt-1"
id="inactiveEmployeesCheckbox"
checked={showInactive}
onChange={() => setShowInactive(!showInactive)}
/>
<label
htmlFor="inactiveEmployeesCheckbox"
className="ms-2 text-xs"
>
{!showInactive ? "Show Deleted" : "Hide Deleted"}
</label>
</div>
<div className="d-flex justify-content-end">
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() =>
setManageState({
IsOpen: true,
branchId: null,
})
}
>
<i className="bx bx-sm bx-plus-circle me-2"></i>
Add Branch
</button>
</div>
</div>
</div>
</div>
<div className="mx-2 mt-3">
<table className="table border-top text-nowrap align-middle table-borderless">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{isLoading && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center py-5"
style={{
height: "200px",
verticalAlign: "middle",
}}
>
<div className="d-flex justify-content-center align-items-center w-100 h-100">
<SpinnerLoader />
</div>
</td>
</tr>
)}
{isError && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center text-danger py-5"
>
{error?.message || "Error loading branches"}
</td>
</tr>
)}
{!isLoading &&
!isError &&
data?.data?.length > 0 &&
data.data.map((branch) => (
<tr key={branch.id} style={{ height: "35px" }}>
{columns.map((col) => (
<td key={col.key} className={`${col.align} py-3`}>
{col.getValue(branch)}
</td>
))}
<td className="text-center">
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
{!showInactive ? (
<>
<li
onClick={() =>
setManageState({
IsOpen: true,
branchId: branch.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
Modify
</a>
</li>
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(branch.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i>
Delete
</a>
</li>
</>
) : (
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(branch.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-undo text-danger me-2"></i>
Restore
</a>
</li>
)}
</ul>
</div>
</td>
</tr>
))}
{!isLoading &&
!isError &&
(!data?.data || data.data.length === 0) && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center py-12"
>
No Branch Found
</td>
</tr>
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
{manageState.IsOpen && (
<GlobalModel
isOpen
size="md"
closeModal={() =>
setManageState({ IsOpen: false, branchId: null })
}
>
<ManageBranch
key={manageState.branchId ?? "new"}
BranchToEdit={manageState.branchId}
closeModal={() =>
setManageState({ IsOpen: false, branchId: null })
}
/>
</GlobalModel>
)}
</div>
</div>
</>
);
};
export default ServiceBranch;

View File

@ -0,0 +1,104 @@
import SelectField from "../../common/Forms/SelectField";
import Error from "../../common/Error";
import { z } from "zod";
import {
AppFormController,
AppFormProvider,
useAppForm,
} from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDispatch, useSelector } from "react-redux";
import { closePopup } from "../../../slices/localVariablesSlice";
import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
export const ChangeStatusSchema = z.object({
statusId: z.string().min(1, { message: "Please select status" }),
});
const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
const { data, isLoading, isError, error } = useJobStatus(statusId, projectId);
const dispatch = useDispatch();
const methods = useAppForm({
resolver: zodResolver(ChangeStatusSchema),
defaultValues: { statusId: "" },
});
const {
control,
handleSubmit,
reset,
formState: { errors },
} = methods;
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
handleClose();
});
const onSubmit = (formData) => {
const payload =
[
{
op: "replace",
path: "/statusId",
value: formData.statusId,
},
];
UpdateStatus({ id: jobId, payload });
};
const handleClose = () => {
dispatch(closePopup(popUpId));
};
return (
<AppFormProvider {...methods}>
<div className="d-flex mb-2">
<span className="fs-6 fw-medium">
Change Status
</span>
</div>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<AppFormController
name="statusId"
control={control}
render={({ field }) => (
<SelectField
label="Select Status"
options={data ?? []}
placeholder="Choose a Status"
required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isLoading}
className="m-0"
/>
)}
/>
{errors.statusId && (
<small className="danger-text">{errors.statusId.message}</small>
)}
</div>
<div className="d-flex flex-row justify-content-end gap-3">
<button
type="button"
onClick={handleClose}
className="btn btn-label-secondary btn-sm"
>
Cancel
</button>
<button type="submit" className="btn btn-primary btn-sm">
{isPending ? "Please wait" : "Save"}
</button>
</div>
</form>
</AppFormProvider>
);
};
export default ChangeStatus;

View File

@ -0,0 +1,218 @@
import React, { useEffect, useState } from "react";
import Avatar from "../../common/Avatar";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { JobCommentSchema } from "../ServiceProjectSchema";
import {
useAddCommentJob,
useJobComments,
} from "../../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../../utils/constants";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
import Filelist from "../../Expenses/Filelist";
import { formatFileSize, getIconByFileType } from "../../../utils/appUtils";
const JobComments = ({ data }) => {
const {
register,
handleSubmit,
watch,
reset,
setValue,
formState: { errors },
} = useAppForm({
resolver: zodResolver(JobCommentSchema),
defaultValues: { comment: "", attachments: [] },
});
const {
data: comments,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useJobComments(data?.id, ITEMS_PER_PAGE, 1);
const jobComments = comments?.pages?.flatMap((p) => p?.data ?? []) ?? [];
const { mutate: AddComment, isPending } = useAddCommentJob(() => reset());
const onSubmit = (formData) => {
formData.jobTicketId = data?.id;
AddComment(formData);
};
useEffect(() => {
document.documentElement.style.setProperty("--sticky-top", `-25px`);
}, []);
const files = watch("attachments");
const onFileChange = async (e) => {
const newFiles = Array.from(e.target.files);
if (newFiles.length === 0) return;
const existingFiles = Array.isArray(watch("attachments"))
? watch("attachments")
: [];
const parsedFiles = await Promise.all(
newFiles.map(async (file) => {
const base64Data = await toBase64(file);
return {
fileName: file.name,
base64Data,
contentType: file.type,
fileSize: file.size,
description: "",
isActive: true,
};
})
);
const combinedFiles = [
...existingFiles,
...parsedFiles.filter(
(newFile) =>
!existingFiles?.some(
(f) =>
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
)
),
];
setValue("attachments", combinedFiles, {
shouldDirty: true,
shouldValidate: true,
});
};
const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(",")[1]);
reader.onerror = (error) => reject(error);
});
const removeFile = (index) => {
const newFiles = files.filter((_, i) => i !== index);
setValue("attachments", newFiles, { shouldValidate: true });
};
return (
<div className="row">
<div className="sticky-section bg-white p-3">
<h6 className="m-0 fw-semibold mb-3">Add Comment</h6>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex">
<Avatar firstName={"A"} lastName={"D"} />
<div className="flex-grow-1">
<textarea
className="form-control"
rows={3}
placeholder="Write your comment..."
{...register("comment")}
></textarea>
{errors?.comment && (
<small className="danger-text">
{errors?.comment?.message}
</small>
)}
</div>
</div>
<div className="flex-grow-1 ms-10 mt-2">
{files?.length > 0 && (
<Filelist files={files} removeFile={removeFile} />
)}
</div>
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
<div
onClick={() => document.getElementById("attachments").click()}
className="cursor-pointer"
style={{ whiteSpace: "nowrap" }}
>
<input
type="file"
accept=".pdf,.jpg,.jpeg,.png"
id="attachments"
multiple
className="d-none"
{...register("attachments")}
onChange={(e) => {
onFileChange(e);
e.target.value = "";
}}
/>
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
Add Attachment
</div>
<button
className="btn btn-primary btn-sm px-1 py-1" // smaller padding + slightly smaller font
type="submit"
disabled={!watch("comment")?.trim() || isPending}
>
Send
</button>
</div>
</form>
</div>
<div className="card-body p-0">
<div className="list-group p-0 m-0">
{jobComments?.map((item) => {
const user = item?.createdBy;
return (
<div className="d-flex align-items-start mt-2 mx-0 px-0">
<Avatar
size="xs"
firstName={user?.firstName}
lastName={user?.lastName}
/>
<div className="w-100">
<div className="d-flex flex-row align-items-center gap-3 w-100">
<span className="fw-semibold">
{user?.firstName} {user?.lastName}
</span>
<span className="text-secondary">
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
</span>
</div>
<div className="text-muted text-secondary">
{user?.jobRoleName}
</div>
<div className="text-wrap">
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
{item.attachments?.map((file) => (
<div className="d-flex align-items-center">
<i
className={`bx bx-xxl ${getIconByFileType(
file?.contentType
)} fs-3`}
></i>
<div className="d-flex flex-column">
<p className="m-0">{file.fileName}</p>
<small className="text-secondary">
{formatFileSize(file.fileSize)}
</small>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export default JobComments;

View File

@ -0,0 +1,59 @@
import React from "react";
import Avatar from "../../common/Avatar";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
const JobStatusLog = ({ data }) => {
return (
<div className="card shadow-none p-0">
<div className="card-body p-0">
<div className="list-group">
{data?.map((item) => (
<div
key={item.id}
className="list-group-item border-0 border-bottom p-1"
>
<div className="d-flex justify-content-between">
<div>
<span className="fw-semibold">
{item.nextStatus?.displayName ??
item.status?.displayName ??
"Status"}
</span>
</div>
<span className="text-secondary">
{formatUTCToLocalTime(item?.updatedAt,true)}
</span>
</div>
<div className="d-flex align-items-start mt-2 mx-0 px-0">
<Avatar
firstName={item.updatedBy?.firstName}
lastName={item.updatedBy?.lastName}
/>
<div className="">
<div className="d-flex flex-row gap-3">
<span className="fw-semibold">
{item.updatedBy?.firstName} {item.updatedBy?.lastName}
</span>
<span className="text-secondary">
{/* <em>{formatUTCToLocalTime(item?.createdAt, true)}</em> */}
</span>
</div>
<div className="text-muted text-secondary">
{item?.updatedBy?.jobRoleName}
</div>
<div className="text-wrap">
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default JobStatusLog;

View File

@ -0,0 +1,288 @@
import React, { useEffect, useState } from "react";
import Breadcrumb from "../../common/Breadcrumb";
import Label from "../../common/Label";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultJobValue, jobSchema } from "../ServiceProjectSchema";
import {
useBranches,
useCreateServiceProjectJob,
useJobTags,
useServiceProjectJobDetails,
useServiceProjects,
useUpdateServiceProjectJob,
} from "../../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../../utils/constants";
import DatePicker from "../../common/DatePicker";
import PmsEmployeeInputTag from "../../common/PmsEmployeeInputTag";
import TagInput from "../../common/TagInput";
import { localToUtc } from "../../../utils/appUtils";
import SelectField from "../../common/Forms/SelectField";
import {
AppFormController,
AppFormProvider,
useAppForm,
} from "../../../hooks/appHooks/useAppForm";
import { useParams } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
import { useServiceProjectJobContext } from "../Jobs";
import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide";
const ManageJob = ({ Job }) => {
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
const { projectId } = useParams();
const dispatch = useDispatch();
const methods = useAppForm({
resolver: zodResolver(jobSchema),
defaultValues: defaultJobValue,
});
const {
register,
control,
watch,
handleSubmit,
reset,
setValue,
formState: { errors },
} = methods;
const {
data: JobTags,
isLoading: isTagLoading,
isError: isTagError,
error: tagError,
} = useJobTags();
const {
data: JobData,
isLoading: isJobLoading,
isError: isJobError,
error: jobError,
} = useServiceProjectJobDetails(Job);
const { data, isLoading, isError, error } = useJobStatus(
JobData?.status?.id,
projectId
);
const { mutate: CreateJob, isPending } = useCreateServiceProjectJob(() => {
reset();
});
const { mutate: UpdateJob, isPending: Updating } = useUpdateServiceProjectJob(
() => {
setManageJob({ isOpen: false, jobId: null });
setSelectedJob({ showCanvas: true, job: Job });
}
);
const onSubmit = (formData) => {
if (Job) {
const existingEmployeeIds = JobData?.assignees?.map((e) => e.id) || [];
const oldEmployees = JobData.assignees.map((e) => ({
employeeId: e.id,
isActive: formData.assignees.includes(e.id),
}));
const newEmployees = formData.assignees
.filter((id) => !existingEmployeeIds.includes(id))
.map((id) => ({
employeeId: id,
isActive: true,
}));
const updatedEmployees = [...oldEmployees, ...newEmployees];
const payload = [
{
op: "replace",
path: "/title",
value: formData.title,
},
{
op: "replace",
path: "/description",
value: formData.description,
},
{
op: "replace",
path: "/startDate",
value: localToUtc(formData.startDate) ?? JobData.startDate,
},
{
op: "replace",
path: "/dueDate",
value: localToUtc(formData.dueDate) ?? JobData.dueDate,
},
{
op: "replace",
path: "/tags",
value: formData.tags,
},
{
op: "replace",
path: "/assignees",
value: updatedEmployees,
},
{
op: "replace",
path: "/statusId",
value: formData.statusId,
},
];
UpdateJob({ id: Job, payload });
} else {
formData.assignees = formData.assignees.map((emp) => ({
employeeId: emp,
isActive: true,
}));
formData.startDate = localToUtc(formData.startDate);
formData.dueDate = localToUtc(formData.dueDate);
formData.projectId = projectId;
CreateJob(formData);
}
};
useEffect(() => {
if (!JobData && !Job) {
reset({
...defaultJobValue,
projectId: projectId,
});
return;
}
if (!JobData || !Job) return;
const assignedEmployees = (JobData.assignees || []).map((e) => e.id);
reset({
title: JobData.title ?? "",
description: JobData.description ?? "",
projectId: JobData.project?.id ?? projectId,
assignees: assignedEmployees,
startDate: JobData.startDate ?? null,
dueDate: JobData.dueDate ?? null,
tags: JobData.tags ?? [],
statusId: JobData.status.id,
projectBranchId : JobData?.projectBranch?.id
});
}, [JobData, Job, projectId]);
return (
<div className="container">
<AppFormProvider {...methods}>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12 mb-2 ">
<Label required>Title</Label>
<input
type="text"
{...register("title")}
className="form-control form-control"
placeholder="Enter Title"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>Start Date</Label>
<DatePicker
name="startDate"
control={control}
placeholder="DD-MM-YYYY"
className="w-full"
size="md"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>Due Date</Label>
<DatePicker
control={control}
minDate={watch("startDate")}
name="dueDate"
className="w-full"
size="md"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label>Select Employee</Label>
<PmsEmployeeInputTag
control={control}
name="assignees"
placeholder="Select employee"
/>
</div>
{Job && (
<div className="col-12 col-md-6 mb-2 mb-md-4">
<AppFormController
name="statusId"
control={control}
render={({ field }) => (
<SelectField
label="Select Status"
options={data ?? []}
placeholder="Choose a Status"
required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isLoading}
className="m-0"
/>
)}
/>
{errors.statusId && (
<small className="danger-text">{errors.statusId.message}</small>
)}
</div>
)}
<div className="col-12 col-md-6 mb-2 mb-md-4">
<TagInput
options={JobTags?.data}
name="tags"
label="Tag"
placeholder="Enter Tag"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<SelectFieldSearch
label="Select Branch"
placeholder="Select Branch"
value={watch("projectBranchId")}
onChange={(val) => setValue("projectBranchId", val)}
valueKey="id"
labelKey="branchName"
hookParams={[projectId, true, 10, 1]}
useFetchHook={useBranches}
isMultiple={false}
disabled={Job}
/>
</div>
<div className="col-12">
<Label required>Description</Label>
<textarea
{...register("description")}
className="form-control form-control-sm"
rows={3}
/>
</div>
<div className="d-flex flex-row-reverse my-4">
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending || Updating}
>
{isPending || Updating
? "Please wait..."
: Job
? "Update"
: "Submit"}
</button>
</div>
</form>
</AppFormProvider>
</div>
);
};
export default ManageJob;

View File

@ -0,0 +1,256 @@
import React, { useEffect, useRef } from "react";
import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../../common/Loader";
import Error from "../../common/Error";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
import Avatar from "../../common/Avatar";
import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup";
import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils";
import HoverPopup from "../../common/HoverPopup";
import ChangeStatus from "./ChangeStatus";
import { useParams } from "react-router-dom";
import { STATUS_JOB_CLOSED } from "../../../utils/constants";
import Tooltip from "../../common/Tooltip";
import BranchDetails from "../ServiceProjectBranch/BranchDetails";
import { JobDetailsSkeleton } from "../ServiceProjectSeketon";
import JobComments from "./JobComments";
import JobStatusLog from "./JobStatusLog";
const ManageJobTicket = ({ Job }) => {
const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
Job?.job
);
const drawerRef = useRef();
const tabsData = [
{
id: "comment",
title: "Comments",
icon: "bx bx-comment me-2",
active: true,
content: <JobComments data={data} />,
},
{
id: "history",
title: "History",
icon: "bx bx-time me-2",
active: false,
content: <JobStatusLog data={data?.updateLogs} />,
},
];
if (isLoading) return <JobDetailsSkeleton />;
if (isError)
return (
<div>
<Error error={error} />
</div>
);
return (
<div
className=" text-start position-relative"
ref={drawerRef}
style={{ overflow: "visible" }}
>
<div className="col-12">
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
<p className="mb-0">
<span className="fw-medium me-1">Job Id :</span>
{data?.jobTicketUId || "N/A"}
</p>
<div className="d-flex flex-column align-items-end gap-3 mb-3">
<div className="d-flex flex-row gap-2 position-relative">
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
{data?.status?.displayName}
</span>
{STATUS_JOB_CLOSED !== data?.status?.id && (
<HoverPopup
id="STATUS_CHANEG"
Mode="click"
className=""
align="right"
content={
<ChangeStatus
statusId={data?.status?.id}
projectId={projectId}
jobId={Job?.job}
popUpId="STATUS_CHANEG"
/>
}
>
<Tooltip
text={"Change Status"}
placement="left"
children={
<i className="bx bx-edit bx-sm cursor-pointer"></i>
}
/>
</HoverPopup>
)}
</div>
</div>
</div>
<div className="d-flex flex-wrap">
<p>{data?.description || "N/A"}</p>
</div>
<div className="d-flex justify-content-between align-items-center mb-4">
<div className="d-flex flex-row gap-1 text-secondry">
<i className="bx bx-calendar"></i>{" "}
<span>
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
</span>
</div>
<div className="d-flex flex-row gap-2 text-wraps">
{data?.tags?.map((tag, ind) => (
<span
key={`${ind}0-${tag?.name}`}
className="badge bg-label-primary"
>
{tag?.name}
</span>
))}
</div>
</div>
<div className="d-flex justify-content-md-between ">
<div className="d-flex flex-row gap-5">
<span className="text-secondry">
<i className="bx bx-calendar"></i> Start Date :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>{" "}
<i className="bx bx-right-arrow-alt"></i>{" "}
<span className="text-secondry">
<i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>
</div>
{data?.dueDate &&
(() => {
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
return (
<span>
<span className="text-secondry me-1">Days Left:</span>
<span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"}
</span>
</span>
);
})()}
</div>
{data?.projectBranch && (
<div className="d-flex gap-3 my-2 position-relative" ref={drawerRef} >
<span className="text-secondary">
<i className="bx bx-buildings"></i> Branch Name:
</span>
<HoverPopup
id="BRANCH_DETAILS"
Mode="click"
align="auto"
boundaryRef={drawerRef}
content={<BranchDetails branch={data?.projectBranch?.id} />}
>
<span className="text-decoration-underline cursor-pointer">
{data?.projectBranch?.branchName}
</span>
</HoverPopup>
</div>
)}
<div className="border-top my-1">
<p className="m-0 py-1">
<i className="bx bx-group"></i> Peoples
</p>
{/* Created By */}
<div className="d-flex justify-content-between align-items-start w-100">
<p className="text-secondary m-0 me-3">Created By</p>
<div className="flex-grow-1 d-flex align-items-center gap-2">
<Avatar
size="xs"
firstName={data?.createdBy?.firstName}
lastName={data?.createdBy?.lastName}
/>
<div className="d-flex flex-column">
<p className="m-0 text-truncate">
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
</p>
<small className="text-secondary text-xs">
{data?.createdBy?.jobRoleName}
</small>
</div>
</div>
</div>
{/* Assigned To */}
<div className="d-flex flex-column flex-md-row align-items-start w-100 mt-2">
<p className="text-secondary m-0 me-3">Assigned To</p>
<div className="flex-grow-1">
<div className="d-flex flex-wrap gap-3">
{data?.assignees?.map((emp) => (
<div key={emp.id} className="d-flex align-items-center">
<Avatar
size="xs"
firstName={emp.firstName}
lastName={emp.lastName}
/>
<div className="d-flex flex-column ms-2 text-truncate">
<span className="text-truncate">
{emp.firstName} {emp.lastName}
</span>
<small className="text-secondary text-xs text-truncate">
{emp.jobRoleName}
</small>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
<ul className="nav nav-tabs" role="tablist">
{tabsData.map((tab) => (
<li className="nav-item" key={tab.id}>
<button
type="button"
className={`nav-link ${tab.active ? "active" : ""}`}
data-bs-toggle="tab"
data-bs-target={`#tab-${tab.id}`}
role="tab"
>
<i className={tab.icon} /> {tab.title}
</button>
</li>
))}
</ul>
<div className="tab-content p-1 px-sm-3">
{tabsData.map((tab) => (
<div
key={tab.id}
className={`tab-pane fade ${tab.active ? "show active" : ""}`}
id={`tab-${tab.id}`}
role="tabpanel"
>
{tab.content}
</div>
))}
</div>
</div>
</div>
);
};
export default ManageJobTicket;

View File

@ -0,0 +1,80 @@
import React from "react";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { JobCommentSchema } from "../ServiceProjectSchema";
import Avatar from "../../common/Avatar";
import Filelist from "../../Expenses/Filelist";
const UpdateJobComment = () => {
const {
register,
handleSubmit,
watch,
reset,
setValue,
formState: { errors },
} = useAppForm({
resolver: zodResolver(JobCommentSchema),
defaultValues: { comment: "", attachments: [] },
});
const onSubmit = () => {};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex">
<Avatar firstName={"A"} lastName={"D"} />
<div className="flex-grow-1">
<textarea
className="form-control"
rows={3}
placeholder="Write your comment..."
{...register("comment")}
></textarea>
{errors?.comment && (
<small className="danger-text">{errors?.comment?.message}</small>
)}
</div>
</div>
{/* <div className="flex-grow-1 ms-10 mt-2">
{files?.length > 0 && (
<Filelist files={} removeFile={removeFile} />
)}
</div> */}
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
<div
onClick={() => document.getElementById("attachments").click()}
className="cursor-pointer"
style={{ whiteSpace: "nowrap" }}
>
<input
type="file"
accept=".pdf,.jpg,.jpeg,.png"
id="attachments"
multiple
className="d-none"
{...register("attachments")}
onChange={(e) => {
onFileChange(e);
e.target.value = "";
}}
/>
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
Add Attachment
</div>
<button
className="btn btn-primary btn-sm px-1 py-1"
type="submit"
disabled={!watch("comment")?.trim() || isPending}
>
Submit
</button>
</div>
</form>
</div>
);
};
export default UpdateJobComment;

View File

@ -0,0 +1,46 @@
import React from "react";
const ServiceProjectNav = ({ onPillClick, activePill }) => {
const ProjectTab = [
{ key: "profile", icon: "bx bx-user", label: "Profile" },
{
key: "jobs",
icon: "bx bx-briefcase-alt",
label: "Jobs",
link: "/service/job",
},
{ key: "teams", icon: "bx bx-group", label: "Teams" },
{
key: "directory",
icon: "bx bxs-contact",
label: "Directory",
},
];
return (
<div className="table-responsive">
<div className="nav-align-top">
<ul className="nav nav-tabs">
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
<li key={tab.key} className="nav-item cursor-pointer">
<a
className={`nav-link ${
activePill === tab.key ? "active cursor-pointer" : ""
} fs-6`}
onClick={(e) => {
e.preventDefault();
onPillClick(tab.key);
}}
>
<i className={`${tab.icon} bx-sm me-1_5`}></i>
<span className="d-none d-md-inline ">{tab.label}</span>
</a>
</li>
))}
</ul>
</div>
</div>
);
};
export default ServiceProjectNav;

View File

@ -0,0 +1,48 @@
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import { useServiceProject } from "../../hooks/useServiceProject";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import ManageServiceProject from "./ManageServiceProject";
import GlobalModel from "../common/GlobalModel";
import { SpinnerLoader } from "../common/Loader";
import ServiceBranch from "./ServiceProjectBranch/ServiceBranch";
import ServiceProfile from "./ServiceProfile";
const ServiceProjectProfile = () => {
const { projectId } = useParams();
const [IsOpenModal, setIsOpenModal] = useState(false);
const { data, isLoading, isError, error } = useServiceProject(projectId);
if (isLoading)
return (
<div className="py-8">
<SpinnerLoader />
</div>
);
return (
<>
{IsOpenModal && (
<GlobalModel
isOpen={IsOpenModal}
closeModal={() => setIsOpenModal(false)}
>
<ManageServiceProject
serviceProjectId={projectId}
onClose={() => setIsOpenModal(false)}
/>
</GlobalModel>
)}
<div className="row py-2">
<div className="col-md-6 col-lg-5 order-2 mb-6">
<ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
</div>
<div className="col-md-6 col-lg-7 order-2 mb-6">
<ServiceBranch />
</div>
</div>
</>
);
};
export default ServiceProjectProfile;

View File

@ -0,0 +1,166 @@
import { z } from "zod";
import { PROJECT_STATUS } from "../../utils/constants";
//#region Service Project
export const projectSchema = z.object({
name: z.string().trim().min(1, "Name is required"),
shortName: z.string().trim().min(1, "Short name is required"),
clientId: z.string().trim().min(1, { message: "Client is required" }),
services: z
.array(z.string())
.min(1, { message: "At least one service required" }),
address: z.string().trim().min(1, "Address is required"),
assignedDate: z.string().trim().min(1, { message: "Date is required" }),
statusId: z.string().trim().min(1, "Status is required"),
contactName: z
.string()
.trim()
.min(1, "Contact name is required")
.regex(/^[A-Za-z\s]+$/, "Contact name can contain only letters"),
contactPhone: z
.string()
.trim()
.regex(/^\d+$/, "Phone number must contain only digits")
.min(7, "Invalid phone number")
.max(15, "Phone number too long"),
contactEmail: z.string().trim().email("Invalid email address"),
});
export const defaultProjectValues = {
name: "",
shortName: "",
clientId: "",
services: [],
address: "",
assignedDate: "",
statusId: PROJECT_STATUS.find((s) => s.label === "Active")?.id,
contactName: "",
contactPhone: "",
contactEmail: "",
};
//#endregion
//#region JobSchema
export const TagSchema = z.object({
name: z.string().min(1, "Tag name is required"),
isActive: z.boolean().default(true),
});
export const jobSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
projectId: z.string().min(1, "Project is required"),
assignees: z.array(z.string()).optional(),
startDate: z.string(),
dueDate: z.string(),
tags: z.array(TagSchema).optional().default([]),
statusId: z.string().optional().nullable(),
projectBranchId: z.string().optional().nullable(),
});
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = [
"application/pdf",
"image/png",
"image/jpg",
"image/jpeg",
];
export const JobCommentSchema = z.object({
comment: z.string().min(1, { message: "Please leave comment" }),
attachments: z
.array(
z.object({
fileName: z.string().min(1),
base64Data: z.string().nullable(),
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}),
documentId: z.string().optional(),
fileSize: z.number().max(MAX_FILE_SIZE),
description: z.string().optional(),
isActive: z.boolean().default(true),
})
)
.optional()
.default([]),
});
export const defaultJobValue = {
title: "",
description: "",
projectId: "",
assignees: [],
statusId: null,
startDate: null,
dueDate: null,
tags: [],
branchId: null,
};
//#endregion
//#region Branch
export const BranchSchema = () =>
z.object({
projectId: z
.string()
.trim()
.min(1, { message: "Project is required" }),
branchName: z
.string()
.trim()
.min(1, { message: "Branch Name is required" }),
contactInformation: z.string().optional(),
address: z
.string()
.trim()
.min(1, { message: "Address is required" }),
branchType: z
.string()
.trim()
.min(1, { message: "Branch Type is required" }),
googleMapUrl: z
.string()
});
export const defaultBranches = {
branchName: "",
projectId: "",
contactInformation: "",
address: "",
branchType: "",
googleMapUrl: "",
};
//#endregion

View File

@ -0,0 +1,138 @@
import React from "react";
const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => (
<div
className={`skeleton ${className}`}
style={{
height,
width,
borderRadius: "4px",
}}
></div>
);
export const BranchDetailsSkeleton = () => {
return (
<div className="w-100">
<div className="d-flex mb-3">
<SkeletonLine height={22} width="280px" />
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} width="90%" />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8 d-flex gap-2 align-items-center">
<SkeletonLine height={16} width="60%" />
<SkeletonLine height={16} width="20px" />
</div>
</div>
</div>
);
};
export const JobDetailsSkeleton = () => {
return (
<div className="row text-start">
<div className="col-12">
{/* Title */}
<SkeletonLine height={24} width="50%" />
{/* Job ID + Status */}
<div className="d-flex justify-content-between align-items-start flex-wrap mb-3 mt-2">
<SkeletonLine height={18} width="30%" />
<div className="d-flex flex-row gap-2">
<SkeletonLine height={22} width="70px" />
<SkeletonLine height={22} width="22px" />
</div>
</div>
{/* Description */}
<SkeletonLine height={40} width="100%" />
{/* Created Date */}
<div className="d-flex my-3">
<SkeletonLine height={16} width="40%" />
</div>
{/* Start / Due Date */}
<div className="d-flex justify-content-between mb-4">
<SkeletonLine height={16} width="50%" />
<SkeletonLine height={22} width="70px" />
</div>
{/* Branch Name */}
<div className="d-flex flex-row gap-3 my-2">
<SkeletonLine height={16} width="30%" />
<SkeletonLine height={16} width="40%" />
</div>
{/* Created By */}
<div className="row align-items-center my-3">
<div className="col-12 col-md-auto mb-2">
<SkeletonLine height={16} width="80px" />
</div>
<div className="col d-flex align-items-center gap-2">
<SkeletonLine height={30} width="30px" /> {/* Avatar */}
<SkeletonLine height={16} width="40%" />
</div>
</div>
{/* Assigned To */}
<div className="row mt-2">
<div className="col-12 col-md-auto mb-2">
<SkeletonLine height={16} width="90px" />
</div>
</div>
{/* Tabs */}
<div className="mt-4">
<div className="d-flex gap-3 mb-3">
<SkeletonLine height={35} width="80px" />
<SkeletonLine height={35} width="80px" />
<SkeletonLine height={35} width="80px" />
</div>
<SkeletonLine height={150} width="100%" />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react'
import { useModal } from '../../../hooks/useAuth'
import { useParams } from 'react-router-dom';
import ServiceProjectTeamList from './ServiceProjectTeamList';
const ProjectTeam = () => {
const {onOpen} = useModal("ServiceTeamAllocation");
const {projectId}= useParams();
return (
<div className='card page-min-h mt-2 px-4'>
<div className='row text-end'>
<div className='col-12'>
<div className='p-2'><button onClick={()=>onOpen({projectId:projectId})} className='btn btn-sm btn-primary'>Manage Employee</button></div>
</div>
</div>
<ServiceProjectTeamList/>
</div>
)
}
export default ProjectTeam

View File

@ -0,0 +1,229 @@
import React, { useEffect, useState } from "react";
import moment from "moment";
import { formatNumber, formatUTCToLocalTime, getDateDifferenceInDays } from "../../../utils/dateUtils";
import { useNavigate } from "react-router-dom";
import ManageProjectInfo from "../../Project/ManageProjectInfo";
import ProjectRepository from "../../../repositories/ProjectRepository";
import { cacheData, getCachedData } from "../../../slices/apiDataManager";
import showToast from "../../../services/toastService";
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../../utils/constants";
import GlobalModel from "../../common/GlobalModel";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../../slices/localVariablesSlice";
import { useProjectContext } from "../../../pages/project/ProjectPage";
import { useActiveInActiveServiceProject } from "../../../hooks/useServiceProject";
import ConfirmModal from "../../common/ConfirmModal";
import { getProjectStatusColor, getProjectStatusName } from "../../../utils/projectStatus";
const ServiceProjectCard = ({ project, isCore = true }) => {
const [deleteProject, setDeleteProject] = useState({
project: null,
isOpen: false,
});
const dispatch = useDispatch();
const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const { setMangeProject, setManageServiceProject } = useProjectContext();
const handleClose = () => setShowModal(false);
const handleViewProject = () => {
if (isCore) {
dispatch(setProjectId(project.id));
navigate(`/projects/details`);
} else {
navigate(`/service-projects/${project.id}`);
}
};
const handleManage = () => {
if (isCore) {
setMangeProject({
isOpen: true,
Project: project.id,
});
} else {
setManageServiceProject({
isOpen: true,
project: project.id,
});
}
};
const { mutate: DeleteProject, isPending } = useActiveInActiveServiceProject(
() => setDeleteProject({ project: null, isOpen: false })
);
const handleActiveInactive = (projectId) => {
DeleteProject(projectId, false);
};
return (
<>
<ConfirmModal
type="delete"
header="Delete Project"
message="Are you sure you want delete?"
onSubmit={handleActiveInactive}
onClose={() => setDeleteProject({ project: null, isOpen: false })}
loading={isPending}
paramData={project.id}
isOpen={deleteProject.isOpen}
/>
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
<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">
<div className="avatar me-4">
<i
className="rounded-circle bx bx-building-house"
style={{ fontSize: "xx-large" }}
></i>
</div>
<div className="me-2">
<h5
className="mb-0 stretched-link text-heading text-start"
onClick={handleViewProject}
>
{project?.shortName ? project?.shortName : project?.name}
</h5>
<div className="client-info text-body text-start">
<span>{project?.shortName ? project?.name : ""}</span>
</div>
</div>
</div>
<div className={`ms-auto ${!ManageProject && "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"
onClick={handleViewProject}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a className="dropdown-item" onClick={handleManage}>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
{!isCore && (
<li
onClick={() =>
setDeleteProject({ project: project, isOpen: true })
}
>
<a className="dropdown-item">
<i className="bx bx-trash me-2"></i>
<span className="align-left">Delete</span>
</a>
</li>
)}
</ul>
</div>
</div>
</div>
</div>
{/* Card View */}
<div className="card-body pb-1">
<div className="d-flex align-items-center flex-wrap">
<div className="text-start mb-4 ms-2">
<div className="ms-11 mb-3">
<p className="mb-0">
<span
className={
"badge rounded-pill " +
getProjectStatusColor(project?.status?.id)
}
>
{getProjectStatusName(project?.status?.id)}
</span>
</p>
</div>
<p className="mb-1">
<span className="text-heading fw-medium">Contact Person: </span>
{project?.contactName}
</p>
<p className="mb-1">
<span className="text-heading fw-medium">Assigned Date: </span>
{formatUTCToLocalTime(project?.assignedDate)}
</p>
<p className="mb-0">
<span className="text-heading fw-medium">Address: </span>
{project?.address}
</p>
<p className="mb-0">{project?.projectAddress}</p>
</div>
</div>
</div>
<div className="card-body border-top text-start ms-2">
<div className="mt-n3">
<p className="mb-0">
<i className="bx bx-group bx-sm me-1_5"></i>
{project?.teamMemberCount} Employees
</p>
</div>
{/* Heading */}
<div className="mt-3">
<h5 className="mb-3 text-heading fw-semibold">Jobs</h5>
{/* Job details */}
<div className="d-flex flex-column">
<p className="mb-2">
<i className="bx bx-briefcase bx-sm me-1"></i>
{project?.assignedJobsCount} Assigned Jobs
</p>
<p className="mb-2">
<i className="bx bx-task bx-sm me-1"></i>
{project?.activeJobsCount} Active Jobs
</p>
<p className="mb-2">
<i className="bx bx-time-five bx-sm me-1"></i>
{project?.jobsPassedDueDateCount} Job Passes Due Date
</p>
<p className="mb-2">
<i className="bx bx-pause-circle bx-sm me-1"></i>
{project?.onHoldJobsCount} On Hold Jobs
</p>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default ServiceProjectCard;

View File

@ -0,0 +1,209 @@
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";
import Pagination from "../../common/Pagination";
const ServiceProjectList = ({
data,
currentPage,
totalPages,
paginate,
isCore = true,
}) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { setMangeProject, setManageServiceProject } = useProjectContext();
const handleClose = () => setShowModal(false);
// 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(`/service-projects/${p.id}`);
}}
>
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
</div>
),
},
{
key: "client.contactPerson",
label: "Contact Person",
className: "text-start small",
getValue: (p) => p.client?.contactPerson || "N/A",
},
{
key: "assignedDate",
label: "Assign Date",
className: "text-center small",
getValue: (p) => formatUTCToLocalTime(p.assignedDate),
},
{
key: "status",
label: "Status",
className: "text-center small",
getValue: (p) => (
<span className={`badge ${getProjectStatusColor(p.status?.id)}`}>
{p.status?.status}
</span>
),
},
];
const handleViewProject = (p) => {
if (isCore) {
dispatch(setProjectId(p.id));
navigate(`/projects/details`);
} else {
navigate(`/service-projects/${p.id}`);
}
};
const handleManage = (p) => {
if (isCore) {
setMangeProject({
isOpen: true,
Project: p.id,
});
} else {
setManageServiceProject({
isOpen: true,
project: p.id,
});
}
};
return (
<div>
<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>
{data?.length > 0 ? (
data.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"
onClick={() => handleViewProject(project)}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a
className="dropdown-item"
onClick={() => handleManage(project)}
>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
</ul>
</div>
</td>
</tr>
))
) : (
<tr
className="no-hover"
style={{
pointerEvents: "none",
backgroundColor: "transparent",
}}
>
<td
colSpan={projectColumns.length + 1}
className="text-center align-middle"
style={{ height: "300px", borderBottom: "none" }}
>
No Service projects available
</td>
</tr>
)}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
paginate={paginate}
/>
</div>
</div>
);
};
export default ServiceProjectList;

View File

@ -0,0 +1,267 @@
import React, { useEffect, useState } from "react";
import { useModal } from "../../../hooks/useAuth";
import Modal from "../../common/Modal";
import Label from "../../common/Label";
import { useTeamRole } from "../../../hooks/masterHook/useMaster";
import SelectField from "../../common/Forms/SelectField";
import SelectFieldServerSide from "../../common/Forms/SelectFieldServerSide";
import SelectEmployeeServerSide from "../../common/Forms/SelectFieldServerSide";
import Avatar from "../../common/Avatar";
import { useParams } from "react-router-dom";
import { EmployeeChip } from "../../common/Chips";
import {
useAllocationServiceProjectTeam,
useServiceProjectTeam,
} from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../../common/Loader";
import showToast from "../../../services/toastService";
import ConfirmModal from "../../common/ConfirmModal";
const ServiceProjectTeamAllocation = () => {
const { isOpen, onClose, data: Project } = useModal("ServiceTeamAllocation");
const [deletingEmp, setSeletingEmp] = useState({
employee: null,
isOpen: false,
});
const {
data: Team,
isLoading: isTeamLoading,
isError: isTeamError,
error: teamError,
} = useServiceProjectTeam(Project?.projectId, true);
const [isAddEmployee, setIsAddEmployee] = useState(false);
const [nonDuplicateEmployee, setNonDuplicateEmployee] = useState([]);
const [selectedTeam, setSelectTeam] = useState(null);
const [selectedEmployees, setSelectedEmployees] = useState([]);
const { data, isLoading } = useTeamRole();
const TeamAllocationColumns = [
{
key: "team",
label: "Role",
getValue: (e) => e?.teamName || "-",
},
];
const handleRemove = (id) => {
setSelectedEmployees((prev) => prev.filter((e) => e.id !== id));
};
const { mutate: AllocationTeam, isPending } = useAllocationServiceProjectTeam(
() => {
setSelectedEmployees([]);
setSeletingEmp({
employee: null,
isOpen: false,
});
}
);
const AssignEmployee = () => {
let payload = selectedEmployees.map((e) => {
return {
projectId: Project?.projectId,
employeeId: e.id,
teamRoleId: selectedTeam,
isActive: true,
};
});
AllocationTeam({ payload, isActive: true });
};
const handleDeAllocation = (emp) => {
let payload = [
{
projectId: Project?.projectId,
employeeId: emp.employee.id,
teamRoleId: emp.teamRole.id,
isActive: false,
},
];
AllocationTeam({ payload: payload, isActive: false });
};
useEffect(() => {
if (selectedEmployees?.length > 0 && !selectedTeam) {
handleRemove(selectedEmployees[0]?.id);
showToast(`Please select a role`, "warning");
}
if (Team?.length > 0 && selectedEmployees?.length > 0) {
setNonDuplicateEmployee((prev) => {
let updated = [...prev];
selectedEmployees.forEach((selectedEmployee) => {
const alreadyAllocated = Team.some(
(e) =>
e?.employee?.id === selectedEmployee?.id &&
e?.teamRole?.id === selectedTeam
);
if (alreadyAllocated) {
handleRemove(selectedEmployee?.id);
showToast(
`${selectedEmployee.firstName} ${selectedEmployee.lastName} already allocated with same role`,
"warning"
);
}
});
return updated;
});
}
}, [Team, selectedEmployees, selectedTeam]);
const TeamAllocationBody = (
<>
<ConfirmModal
type="delete"
header="Remove Employee"
message="Are you sure you want remove?"
onSubmit={handleDeAllocation}
onClose={() => setSeletingEmp({ employee: null, isOpen: false })}
loading={isPending}
paramData={deletingEmp.employee}
isOpen={deletingEmp.isOpen}
/>
<div className=" text-start">
<div className="row">
<div className="d-flex justify-content-end">
{!isAddEmployee && (
<button
className="btn btn-sm btn-primary"
onClick={() => setIsAddEmployee(!isAddEmployee)}
>
<i className="bx bx-plus-circle me-2"></i>Add Employee
</button>
)}
</div>
{isAddEmployee && (
<>
<div className="col-12 col-md-6 mb-3">
<SelectField
label="Select Role"
required
options={data}
value={selectedTeam}
labelKey="name"
valueKey="id"
onChange={(e) => {
setSelectedEmployees([]);
setSelectTeam(e);
}}
isLoading={isLoading}
/>
</div>
<div className="col-12 col-md-6 mb-3">
<SelectEmployeeServerSide
label="Select Employee"
isMultiple={true}
isFullObject={true}
value={selectedEmployees}
onChange={setSelectedEmployees}
/>
</div>
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
{selectedEmployees.map((e) => (
<EmployeeChip key={`${e.id}-emp`} handleRemove={handleRemove} employee={e} />
))}
</div>
</>
)}
</div>
{isAddEmployee && (
<div className="d-flex justify-content-end my-2">
{" "}
<button
type="button"
className="btn btn-label-secondary btn-sm me-2"
onClick={() => setIsAddEmployee(false)}
aria-label="Close"
disabled={isPending}
>
Cancel
</button>
<button
className="btn btn-sm btn-primary"
disabled={isPending}
onClick={AssignEmployee}
>
{isPending && !deletingEmp.employee ? "Please wait..." : "Save"}
</button>
</div>
)}
<div className="col-12">
<table className="table text-center">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{Team?.length > 0 ? (
Team?.map((emp) => (
<tr key={emp?.id}>
<td className="w-min">
<div className="d-flex align-items-center gap-2">
<Avatar
size="xs"
firstName={emp?.employee?.firstName}
lastName={emp?.employee?.lastName}
/>
<span className="fw-medium">
{emp?.employee?.firstName} {emp?.employee?.lastName}
</span>
</div>
</td>
<td>{emp?.teamRole?.name}</td>
<td>
{deletingEmp?.employee?.id === emp.id && isPending ? (
<div className="spinner-border spinner-border-sm"></div>
) : (
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() =>
setSeletingEmp({ employee: emp, isOpen: true })
}
></i>
)}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={3}
className="text-center text-muted py-4 border-0"
>
{isTeamLoading ? (
<SpinnerLoader />
) : (
<p className="m-0 py-4">No Records Found</p>
)}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</>
);
return (
<Modal
size="lg"
title={"Team Allocation"}
isOpen={isOpen}
onClose={onClose}
body={TeamAllocationBody}
/>
);
};
export default ServiceProjectTeamAllocation;

View File

@ -0,0 +1,95 @@
import React from "react";
import Avatar from "../../common/Avatar";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
import { useServiceProjectTeam } from "../../../hooks/useServiceProject";
import { useNavigate, useParams } from "react-router-dom";
import { SpinnerLoader } from "../../common/Loader";
const ServiceProjectTeamList = () => {
const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProjectTeam(
projectId,
true
);
const navigate = useNavigate();
const servceProjectColmen = [
{
key: "employeName",
label: "Name",
getValue: (e) => (
<div className="d-flex align-items-center cursor-pointer"
onClick={() => navigate(`/employee/${e.employee.id}`)}>
{" "}
<Avatar size="xs"
firstName={e.employee.firstName}
lastName={e.employee.lastName}
/>
<small>{`${e.employee.firstName} ${e.employee.lastName}`}</small>
</div>
),
align: "text-start",
},
{
key: "teamRole",
label: "Role",
getValue: (e) => {
return (
<div className="d-flex align-itmes-center">
<span>{`${e.teamRole.name}`}</span>
</div>
);
},
align: "text-start",
},
{
key: "assignedAt",
label: "assigned Date",
getValue: (e) =>
formatUTCToLocalTime(e.assignedAT)
,
align: "text-center",
},
];
return (
<div className="table-responsive">
<table className="table align-middle mb-0">
<thead>
<tr>
{servceProjectColmen.map((col) => (
<th key={col.key} className={col.align}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map((row) => (
<tr key={row.id}>
{servceProjectColmen.map((col) => (
<td key={col.key} className={col.align}>{col.getValue(row)}</td>
))}
</tr>
))
) : (
<tr>
<td
colSpan={servceProjectColmen.length}
className="text-center py-4 border-0"
>
{isLoading ? (
<SpinnerLoader />
) : (
<div className="py-8">No Records Available</div>
)}
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default ServiceProjectTeamList;

View File

@ -104,8 +104,7 @@ const SubScriptionHistory = ({ tenantId }) => {
</button>
<button
className="dropdown-item py-1"
onClick={() =>
console.log("Download clicked for", item.id)
onClick={() =>{}
}
>
<i className="bx bx-cloud-download bx-sm"></i> Download

View File

@ -93,11 +93,9 @@ const TenantForm = () => {
};
const onSubmitTenant = (data) => {
console.log("Tenant Data:", data);
};
const onSubmitSubScription = (data) => {
console.log("Subscription Data:", data);
};
const newTenantConfig = [

View File

@ -72,7 +72,7 @@ const AddPayment = ({ onClose }) => {
<div className="col-12 col-md-6 mb-2">
<Label required>Transaction Date </Label>
<DatePicker
<DatePicker className="w-100"
name="paymentReceivedDate"
control={control}
minDate={

View File

@ -29,18 +29,17 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
const searchDebounce = useDebounce(searchString, 500);
const { data, isLoading, isError, error } = useCollections(
ITEMS_PER_PAGE,
currentPage,
selectedProject,
searchDebounce,
localToUtc(fromDate),
localToUtc(toDate),
isPending,
ITEMS_PER_PAGE,
currentPage,
true,
selectedProject,
searchDebounce
isPending
);
const { setProcessedPayment, setAddPayment, setViewCollection } =
useCollectionContext();
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
@ -113,6 +112,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
),
align: "text-center",
},
{
key: "status",
label: "Status",
getValue: (col) => (
<span className={`badge bg-label-${col?.isActive ? "primary" : "danger"}`}>
{col?.isActive ? "Active" : "Inactive"}
</span>
),
align: "text-center",
},
{
key: "amount",
label: "Total Amount",
@ -129,6 +138,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
),
align: "text-end",
},
{
key: "balance",
label: "Balance",
@ -186,84 +196,84 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
canViewCollection ||
canEditCollection ||
canCreate) && (
<td
className="sticky-action-column text-center"
style={{ padding: "12px 8px" }}
>
<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>
<td
className="sticky-action-column text-center"
style={{ padding: "12px 8px" }}
>
<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">
{/* View */}
<ul className="dropdown-menu dropdown-menu-end">
{/* View */}
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() => setViewCollection(row.id)}
>
<i className="bx bx-show me-2 text-primary"></i>
<span>View</span>
</a>
</li>
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() => setViewCollection(row.id)}
>
<i className="bx bx-show me-2 text-primary"></i>
<span>View</span>
</a>
</li>
{/* Only if not completed */}
{!row?.markAsCompleted && (
<>
{/* Add Payment */}
{(isAdmin || canAddPayment) && (
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setAddPayment({
isOpen: true,
invoiceId: row.id,
})
}
>
<i className="bx bx-wallet me-2 text-warning"></i>
<span>Add Payment</span>
</a>
</li>
)}
{/* Only if not completed */}
{!row?.markAsCompleted && (
<>
{/* Add Payment */}
{(isAdmin || canAddPayment) && (
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setAddPayment({
isOpen: true,
invoiceId: row.id,
})
}
>
<i className="bx bx-wallet me-2 text-warning"></i>
<span>Add Payment</span>
</a>
</li>
)}
{/* Mark Payment */}
{isAdmin && (
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setProcessedPayment({
isOpen: true,
invoiceId: row.id,
})
}
>
<i className="bx bx-check-circle me-2 text-success"></i>
<span>Mark Payment</span>
</a>
</li>
)}
</>
)}
</ul>
</div>
</td>
)}
{/* Mark Payment */}
{isAdmin && (
<li>
<a
className="dropdown-item cursor-pointer"
onClick={() =>
setProcessedPayment({
isOpen: true,
invoiceId: row.id,
})
}
>
<i className="bx bx-check-circle me-2 text-success"></i>
<span>Mark Payment</span>
</a>
</li>
)}
</>
)}
</ul>
</div>
</td>
)}
</tr>
))
) : (

View File

@ -27,7 +27,7 @@ const Comment = ({ invoice }) => {
AddComment(payload);
};
return (
<div className="row pt-1 px-2">
<div className="row pt-1 px-2 pb-3">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="col-12">
<textarea

View File

@ -16,10 +16,16 @@ import {
import { formatFileSize, localToUtc } from "../../utils/appUtils";
import { useCollectionContext } from "../../pages/collections/CollectionPage";
import { formatDate } from "../../utils/dateUtils";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useOrganizationsList } from "../../hooks/useOrganization";
const ManageCollection = ({ collectionId, onClose }) => {
const { data, isError, isLoading, error } = useCollection(collectionId);
const { projectNames, projectLoading } = useProjectName(true);
const { data: organization, isLoading: isLoadingOrganization } =
useOrganizationsList(ITEMS_PER_PAGE, 1, true);
const methods = useForm({
resolver: zodResolver(newCollection),
defaultValues: defaultCollection,
@ -138,6 +144,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
taxAmount: data?.taxAmount,
basicAmount: data?.basicAmount,
description: data?.description,
billedToId: data?.billedToId,
attachments: data.attachments
? data.attachments.map((doc) => ({
fileName: doc.fileName,
@ -163,36 +170,61 @@ const ManageCollection = ({ collectionId, onClose }) => {
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
<div className="row px-md-1 px-0">
<div className="col-12 col-md-6 mb-2">
<Label className="form-label" required>
Select Project
</Label>
<select
className="form-select form-select-sm"
{...register("projectId")}
>
<option value="">Select Project</option>
{projectLoading ? (
<option>Loading...</option>
) : (
projectNames?.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))
)}
</select>
<div className="col-12 col-md-6 mb-2">
<SelectProjectField
label="Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
/>
{errors.projectId && (
<small className="danger-text">
{errors.projectId.message}
</small>
<small className="danger-text">{errors.projectId.message}</small>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label htmlFor="name" required>
Bill To
</Label>
<div className="d-flex align-items-center gap-2">
<select
className="select2 form-select form-select flex-grow-1"
aria-label="Default select example"
{...register("billedToId", {
required: "Client is required",
valueAsNumber: false,
})}
>
{isLoading ? (
<option>Loading...</option>
) : (
<>
<option value="">Select Client</option>
{organization?.data?.map((org) => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</>
)}
</select>
</div>
{errors?.clientId && (
<span className="danger-text">{errors.billedToId.message}</span>
)}
</div>
<div className="col-12 col-md-6 mb-2">
<Label required>Title</Label>
<input
type="text"
className="form-control form-control-sm"
className="form-control form-control"
{...register("title")}
/>
{errors.title && (
@ -203,7 +235,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
<Label required>Invoice Number</Label>
<input
type="text"
className="form-control form-control-sm"
className="form-control form-control"
{...register("invoiceNumber")}
/>
{errors.invoiceId && (
@ -216,7 +248,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
<Label required>E-Invoice Number</Label>
<input
type="text"
className="form-control form-control-sm"
className="form-control form-control"
{...register("eInvoiceNumber")}
/>
{errors.invoiceId && (
@ -232,6 +264,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
name="invoiceDate"
control={control}
maxDate={new Date()}
size="md"
/>
{errors.invoiceDate && (
<small className="danger-text">
@ -246,6 +279,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
name="exceptedPaymentDate"
control={control}
minDate={watch("invoiceDate")}
size="md"
/>
{errors.exceptedPaymentDate && (
<small className="danger-text">
@ -260,6 +294,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
name="clientSubmitedDate"
control={control}
maxDate={new Date()}
size="md"
/>
{errors.exceptedPaymentDate && (
<small className="danger-text">
@ -275,7 +310,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
<input
type="number"
id="basicAmount"
className="form-control form-control-sm"
className="form-control form-control"
min="1"
step="0.01"
inputMode="decimal"
@ -294,7 +329,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
<input
type="number"
id="taxAmount"
className="form-control form-control-sm"
className="form-control form-control"
min="1"
step="0.01"
inputMode="decimal"
@ -313,7 +348,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
</Label>
<textarea
id="description"
className="form-control form-control-sm"
className="form-control form-control"
{...register("description")}
rows="2"
></textarea>

View File

@ -25,7 +25,6 @@ const ViewCollection = ({ onClose }) => {
if (isLoading) return <CollectionDetailsSkeleton />;
if (isError) return <div>{error.message}</div>;
return (
<div className="container p-3">
<p className="fs-5 fw-semibold">Collection Details</p>
@ -43,9 +42,8 @@ const ViewCollection = ({ onClose }) => {
<div>
{" "}
<span
className={`badge bg-label-${
data?.isActive ? "primary" : "danger"
}`}
className={`badge bg-label-${data?.isActive ? "primary" : "danger"
}`}
>
{data?.isActive ? "Active" : "Inactive"}
</span>
@ -214,9 +212,8 @@ const ViewCollection = ({ onClose }) => {
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item">
<button
className={`nav-link ${
activeTab === "payments" ? "active" : ""
}`}
className={`nav-link ${activeTab === "payments" ? "active" : ""
}`}
onClick={() => setActiveTab("payments")}
type="button"
>
@ -225,9 +222,8 @@ const ViewCollection = ({ onClose }) => {
</li>
<li className="nav-item">
<button
className={`nav-link ${
activeTab === "details" ? "active" : ""
}`}
className={`nav-link ${activeTab === "details" ? "active" : ""
}`}
onClick={() => setActiveTab("details")}
type="button"
>

View File

@ -19,6 +19,7 @@ export const newCollection = z.object({
invoiceDate: z.string().min(1, { message: "Date is required" }),
description: z.string().trim().optional(),
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
billedToId: z.string().min(1, { message: "Date is required" }),
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
invoiceNumber: z
.string()
@ -75,6 +76,7 @@ export const defaultCollection = {
taxAmount: "",
basicAmount: "",
description: "",
billedToId:"",
attachments: [],
};

View File

@ -0,0 +1,44 @@
import React from "react";
export const EmployeeChip = ({ handleRemove, employee }) => {
return (
<span
key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
{employee?.photo ? (
<span className="tagify__tag__avatar-wrap me-1">
<img
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }}
/>
</span>
) : (
<div className="avatar avatar-xs me-2">
<span className="avatar-initial roemployeended-circle bg-label-secondary">
{employee?.firstName?.[0] || ""}
{employee?.lastName?.[0] || ""}
</span>
</div>
)}
<div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text">
{employee?.firstName} {employee?.lastName}
</span>
</div>
</div>
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
);
};

View File

@ -17,9 +17,13 @@ const ConfirmModal = ({
case "delete":
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
case "success":
return <i className="bx bx-check-circle text-success" style={{ fontSize: "60px" }}></i>;
case "warning":
return <i className="bx bx-error-circle text-warning" style={{ fontSize: "60px" }}></i>;
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
case "archive":
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
case "Un-archive":
return <i className="bx bx-archive-out text-warning" style={{ fontSize: "60px" }}></i>;
case "undo":
return <i className="bx bx-undo text-info" style={{ fontSize: "50px" }}></i>;
default:
return null;
}

View File

@ -4,6 +4,7 @@ import { useController } from "react-hook-form";
const DatePicker = ({
name,
control,
size="sm",
placeholder = "DD-MM-YYYY",
className = "",
allowText = false,
@ -51,7 +52,7 @@ const DatePicker = ({
<div className={`position-relative ${className} w-max `}>
<input
type="text"
className="form-control form-control-sm"
className={`form-control form-control form-control-${size}`}
placeholder={placeholder}
value={displayValue}
onChange={(e) => {

View File

@ -0,0 +1,90 @@
import React from "react";
import Avatar from "./Avatar";
import Tooltip from "./Tooltip";
import HoverPopup from "./HoverPopup";
import { map } from "zod";
const EmployeeAvatarGroup = ({ employees = [] }) => {
const visibleEmployees = employees.slice(0, 3);
const remainingEmployees = employees.slice(3);
const remainingCount = employees.length - visibleEmployees.length;
return (
<div className="d-flex align-items-center avatar-group my-1 text-capitalize ">
{visibleEmployees.map((emp, i) => (
<div key={i} className="avatar avatar-sm me-1">
<HoverPopup title={"Employee"} content={<EmployeeDetails e={emp} />}>
{emp.avatarUrl ? (
<img
src={emp.avatarUrl}
alt={`${emp.firstName} ${emp.lastName}`}
className="rounded-circle pull-up"
title={`${emp.firstName} ${emp.lastName}`}
width="40"
height="40"
/>
) : (
<Avatar
firstName={emp.firstName}
lastName={emp.lastName}
classAvatar="pull-up"
/>
)}
</HoverPopup>
</div>
))}
{remainingCount > 0 && (
<div className="avatar p-1">
<div className={`avatar avatar-sm `}>
<span className="avatar-initial rounded-circle pull-up text-dark border">
<HoverPopup
title={`More ${remainingCount} emplopyee`}
content={<Remaning emp={remainingEmployees} />}
>
+{remainingCount}
</HoverPopup>
</span>
</div>
</div>
)}
</div>
);
};
export default EmployeeAvatarGroup;
const EmployeeDetails = ({ e }) => {
return (
<a className="list-group-item list-group-item-action d-flex justify-content-between text-capitalize">
<div className="li-wrapper d-flex justify-content-start align-items-start">
<Avatar firstName={e?.firstName} lastName={e?.lastName} />
<div className="list-content">
<p className="mb-0">{`${e?.firstName}' ${e?.lastName} `}</p>
<small className="text-body-secondary">
{e?.jobRoleName ?? "employee"}
</small>
</div>
</div>
</a>
);
};
export const Remaning = ({ emp }) => {
return (
<div className="w-100">
{emp?.map((e) => (
<a className="list-group-item list-group-item-action d-flex justify-content-between text-capitalize">
<div className="li-wrapper d-flex justify-content-start align-items-start">
<Avatar firstName={e?.firstName} lastName={e?.lastName} />
<div className="list-content">
<p className="mb-0">{`${e?.firstName} ${e?.lastName} `}</p>
<small className="text-body-secondary">
{e?.jobRoleName ?? "employee"}
</small>
</div>
</div>
</a>
))}
</div>
);
};

View File

@ -7,6 +7,7 @@ import Avatar from "./Avatar";
const EmployeeSearchInput = ({
control,
name,
size = "sm",
projectId,
placeholder,
forAll,
@ -46,7 +47,7 @@ const EmployeeSearchInput = ({
<input
type="text"
ref={ref}
className={`form-control form-control-sm`}
className={`form-control form-control-sm-${size}`}
placeholder={placeholder}
value={search}
onChange={(e) => {

View File

@ -0,0 +1,91 @@
import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
const InputSuggessionField = ({
suggesstionList = [],
value,
onChange,
error,
disabled = false,
label = "Label",
placeholder = "Please Enter",
required = false,
isLoading = false,
}) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = suggesstionList.find((opt) => opt === value);
const displayText = selectedOption ? selectedOption : placeholder;
const handleSelect = (option) => {
onChange(option);
setOpen(false);
};
const toggleDropdown = () => setOpen((prev) => !prev);
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={toggleDropdown}
disabled={isLoading}
>
<span
className={`text-truncate ${!selectedOption ? "text-muted" : ""}`}
>
{isLoading ? "Loading..." : displayText}
</span>
</button>
{open && !isLoading && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
{suggesstionList.map((option, i) => (
<li key={i}>
<button
type="button"
className={`dropdown-item ${option === value ? "active" : ""}`}
onClick={() => handleSelect(option)}
>
{option}
</button>
</li>
))}
</ul>
)}
</div>
);
};
export default InputSuggessionField;

View File

@ -0,0 +1,94 @@
import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
const SelectField = ({
label = "Select",
options = [],
placeholder = "Select Option",
required = false,
value,
onChange,
valueKey = "id",
labelKey = "name",
isLoading = false,
}) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = options.find((opt) => opt[valueKey] === value);
const displayText = selectedOption ? selectedOption[labelKey] : placeholder;
const handleSelect = (option) => {
onChange(option[valueKey]);
setOpen(false);
};
const toggleDropdown = () => setOpen((prev) => !prev);
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={toggleDropdown}
disabled={isLoading}
>
<span
className={`text-truncate ${!selectedOption ? "text-muted" : ""}`}
>
{isLoading ? "Loading..." : displayText}
</span>
</button>
{open && !isLoading && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
{options.map((option, i) => (
<li key={i}>
<button
type="button"
className={`dropdown-item ${
option[valueKey] === value ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{option[labelKey]}
</button>
</li>
))}
</ul>
)}
</div>
);
};
export default SelectField;

View File

@ -0,0 +1,574 @@
import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
import { useDebounce } from "../../../utils/appUtils";
import { useEmployeesName } from "../../../hooks/useEmployees";
import { useProjectBothName } from "../../../hooks/useProjects";
import EmployeeRepository from "../../../repositories/EmployeeRepository";
const SelectEmployeeServerSide = ({
label = "Select",
placeholder = "Select Employee",
required = false,
value = null,
onChange,
valueKey = "id",
isFullObject = false,
isMultiple = false,
projectId = null,
isAllEmployee = false,
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const [forcedSelected, setForcedSelected] = useState(null);
const { data, isLoading } = useEmployeesName(
projectId,
debounce,
isAllEmployee
);
const options = data?.data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (emp) => {
if (!emp) return "";
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
};
let selectedSingle = null;
if (!isMultiple) {
if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && value)
selectedSingle =
options.find((o) => o[valueKey] === value) || forcedSelected;
}
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleSelect = (option) => {
if (!isMultiple) {
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
setOpen(false);
} else {
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
updated = exists
? selectedList.filter((e) => e[valueKey] !== option[valueKey])
: [...selectedList, option];
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
useEffect(() => {
if (!value || isFullObject) return;
const exists = options.some((o) => o[valueKey] === value);
if (exists) return;
const loadSingleEmployee = async () => {
try {
const emp = await EmployeeRepository.getEmployeeName(
null,
null,
true,
value
);
setForcedSelected(emp.data[0]);
} catch (err) {
console.error("Failed to load selected employee", err);
}
};
loadSingleEmployee();
}, [value, options, isFullObject, valueKey]);
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
padding: 0,
}}
>
<li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</li>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]} className="px-1 rounded">
<button
type="button"
className={`dropdown-item rounded ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export default SelectEmployeeServerSide;
export const SelectProjectField = ({
label = "Select",
placeholder = "Select Project",
required = false,
value = null,
onChange,
valueKey = "id",
isFullObject = false,
isMultiple = false,
isAllProject = false,
disabled
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useProjectBothName(debounce);
const options = data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (project) => {
if (!project) return "";
return `${project.name || ""}`.trim();
};
let selectedSingle = null;
if (!isMultiple) {
if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && value)
selectedSingle = options.find((o) => o[valueKey] === value);
}
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
/** Main button label */
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
/** -----------------------------
* HANDLE OUTSIDE CLICK
* ----------------------------- */
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => {
if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
} else {
// MULTIPLE SELECT
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
if (exists) {
// remove
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
disabled={disabled}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "2px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
<div className="p-1">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</div>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]} className="px-1 rounded w-full">
<button
type="button"
className={`dropdown-item rounded d-block text-truncate w-100 ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export const SelectFieldSearch = ({
label = "Select",
placeholder = "Select ",
required = false,
value = null,
onChange,
valueKey = "id",
labelKey = "name",
disabled = false,
isFullObject = false,
isMultiple = false,
hookParams,
useFetchHook,
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useFetchHook(...hookParams, debounce);
const options = data?.data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (entity) => {
if (!entity) return "";
return `${entity[labelKey] || ""}`.trim();
};
/** -----------------------------
* SELECTED OPTION (SINGLE)
* ----------------------------- */
let selectedSingle = null;
if (!isMultiple) {
if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && value)
selectedSingle = options.find((o) => o[valueKey] === value);
}
/** -----------------------------
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
/** Main button label */
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
/** -----------------------------
* HANDLE OUTSIDE CLICK
* ----------------------------- */
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// MERGED OPTIONS TO ENSURE SELECTED VALUE APPEARS EVEN IF NOT IN SEARCH RESULT
const [mergedOptions, setMergedOptions] = useState([]);
useEffect(() => {
let finalList = [...options];
if (!isMultiple && value && !isFullObject) {
// already selected option inside options?
const exists = options.some((o) => o[valueKey] === value);
// if selected item not found, try to get from props (value) as fallback
if (!exists && typeof value === "object") {
finalList.unshift(value);
}
}
if (isMultiple && Array.isArray(value)) {
value.forEach((val) => {
const id = isFullObject ? val[valueKey] : val;
const exists = options.some((o) => o[valueKey] === id);
if (!exists && typeof val === "object") {
finalList.unshift(val);
}
});
}
setMergedOptions(finalList);
}, [options, value]);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => {
if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
} else {
// MULTIPLE SELECT
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
if (exists) {
// remove
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
disabled={disabled}
onClick={() => setOpen((prev) => !prev)}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "2px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
<div className="p-1">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
disabled={disabled}
/>
</div>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]}>
<button
type="button"
className={`dropdown-item ${isActive ? "active" : ""}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};

View File

@ -28,7 +28,6 @@ const CommentEditor = () => {
const [value, setValue] = useState("");
const handleSubmit = () => {
console.log("Comment:", value);
// Submit or handle content
};

View File

@ -1,68 +1,197 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
closePopup,
openPopup,
togglePopup,
} from "../../slices/localVariablesSlice";
/**
* align: "auto" | "left" | "right"
* boundaryRef: optional ref to the drawer/container element to use as boundary
*/
const HoverPopup = ({
id,
title,
content,
children,
className = "",
Mode = "hover",
align = "auto",
boundaryRef = null,
}) => {
const dispatch = useDispatch();
const visible = useSelector((s) => s.localVariables.popups[id] || false);
const HoverPopup = ({ title, content, children }) => {
const [visible, setVisible] = useState(false);
const triggerRef = useRef(null);
const popupRef = useRef(null);
// Toggle popup on hover or click
const handleMouseEnter = () => setVisible(true);
const handleClick = () => setVisible((prev) => !prev);
const handleMouseEnter = () => {
if (Mode === "hover") dispatch(openPopup(id));
};
const handleMouseLeave = () => {
if (Mode === "hover") dispatch(closePopup(id));
};
const handleClick = (e) => {
if (Mode === "click") {
e.stopPropagation();
dispatch(togglePopup(id));
}
};
// Hide popup on outside click
// Close on outside click when using click mode
useEffect(() => {
const handleDocumentClick = (e) => {
if (Mode !== "click" || !visible) return;
const handler = (e) => {
if (
!popupRef.current?.contains(e.target) &&
!triggerRef.current?.contains(e.target)
popupRef.current &&
!popupRef.current.contains(e.target) &&
triggerRef.current &&
!triggerRef.current.contains(e.target)
) {
setVisible(false);
dispatch(closePopup(id));
}
};
if (visible) document.addEventListener("click", handleDocumentClick);
return () => document.removeEventListener("click", handleDocumentClick);
}, [visible]);
document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler);
}, [Mode, visible, dispatch, id]);
return (
// Positioning effect: respects align prop and stays inside boundary (drawer)
useEffect(() => {
if (!visible || !popupRef.current || !triggerRef.current) return;
// run in next frame so DOM/layout settles
requestAnimationFrame(() => {
const popup = popupRef.current;
// choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
const boundaryEl =
(boundaryRef && boundaryRef.current) || popup.parentElement;
if (!boundaryEl) return;
const boundaryRect = boundaryEl.getBoundingClientRect();
const triggerRect = triggerRef.current.getBoundingClientRect();
// reset styles first
popup.style.left = "";
popup.style.right = "";
popup.style.transform = "";
popup.style.top = "";
const popupRect = popup.getBoundingClientRect();
const parentRect = boundaryRect; // alias
// Convert trigger center to parent coordinates
const triggerCenterX =
triggerRect.left + triggerRect.width / 2 - parentRect.left;
// preferred left so popup center aligns to trigger center:
const preferredLeft = triggerCenterX - popupRect.width / 2;
// Helpers to set styles in parent's coordinate system:
const setLeft = (leftPx) => {
popup.style.left = `${leftPx}px`;
popup.style.right = "auto";
popup.style.transform = "none";
};
const setRight = (rightPx) => {
popup.style.left = "auto";
popup.style.right = `${rightPx}px`;
popup.style.transform = "none";
};
// If user forced align:
if (align === "left") {
// align popup's left to parent's left (0)
setLeft(0);
return;
}
if (align === "right") {
// align popup's right to parent's right (0)
setRight(0);
return;
}
if (align === "center") {
popup.style.left = "50%";
popup.style.right = "auto";
popup.style.transform = "translateX(-50%)";
return;
}
// align === "auto": try preferred centered position, but flip fully if overflow
// clamp preferredLeft to boundaries so it doesn't render partially outside
const leftIfCentered = Math.max(
0,
Math.min(preferredLeft, parentRect.width - popupRect.width)
);
// if centered fits, use it
if (leftIfCentered === preferredLeft) {
setLeft(leftIfCentered);
return;
}
// if centering would overflow right -> stick popup fully to left (left=0)
if (preferredLeft > parentRect.width - popupRect.width) {
// place popup so its right aligns to parent's right
// i.e., left = parent width - popup width
setLeft(parentRect.width - popupRect.width);
return;
}
// if centering would overflow left -> stick popup fully to left=0
if (preferredLeft < 0) {
setLeft(0);
return;
}
// fallback center
setLeft(leftIfCentered);
});
}, [visible, align, boundaryRef]);
return (
<div
className="d-inline-block position-relative" // <-- ADD THIS !!
style={{
maxWidth: "calc(700px - 100px)",
width: "100%",
wordWrap: "break-word",
overflow: "visible", // also make sure popup isn't clipped
}}
>
<div
className="d-inline-block position-relative"
className="d-inline-block"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setVisible(false)}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
{visible && (
<div
ref={popupRef}
className="bg-white border rounded shadow-sm p-3 text-start"
style={{
position: "absolute",
top: "100%",
left: "50%",
transform: "translateX(-50%)",
zIndex: 1050,
minWidth: "240px",
maxWidth: "300px",
marginTop: "8px",
whiteSpace: "normal",
}}
>
{title && (
<h6 className="mb-2 fw-semibold text-dark" style={{ fontSize: "0.9rem" }}>
{title}
</h6>
)}
<div className="text-muted" style={{ fontSize: "0.85rem", lineHeight: "1.4" }}>
{content}
</div>
</div>
)}
</div>
);
{visible && (
<div
ref={popupRef}
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
style={{
zIndex: 2000,
top: "100%",
width: "max-content",
minWidth: "120px",
}}
onClick={(e) => e.stopPropagation()}
>
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
<div>{content}</div>
</div>
)}
</div>
);
};
export default HoverPopup;

View File

@ -5,14 +5,13 @@ const InputSuggestions = ({
value,
onChange,
error,
disabled=false
disabled = false,
}) => {
const [filteredList, setFilteredList] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const handleInputChange = (e) => {
const val = e.target.value;
onChange(val);
const matches = organizationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
@ -26,7 +25,7 @@ const InputSuggestions = ({
};
return (
<div className="position-relative">
<div className="mb-3 position-relative">
<input
className="form-control form-control-sm"
value={value}
@ -39,19 +38,19 @@ const InputSuggestions = ({
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
borderRadius: "0px",
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
className="ropdown-item"
style={{
cursor: "pointer",
padding: "5px 12px",
@ -59,17 +58,15 @@ const InputSuggestions = ({
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
className={`dropdown-item ${
org === value ? "active" : ""
}`}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}

View File

@ -40,7 +40,7 @@ const Modal = ({
</div>
{/* Body */}
<div className="modal-body pt-0">{body}</div>
<div className="modal-body pt-0 text-black">{body}</div>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
@ -99,7 +99,7 @@
.multi-select-dropdown-option.selected {
background-color: #dbe7ff;
color: #0d6efd;
color: #696cff;
}
.multi-select-dropdown-option input[type="checkbox"] {

View File

@ -0,0 +1,75 @@
import React, { useEffect, useRef } from "react";
const OffcanvasComponent = ({
id = "globalOffcanvas",
title = "Offcanvas Title",
placement = "start", // start | end | top | bottom
children,
show = false,
onClose = () => { },
}) => {
const offcanvasRef = useRef(null);
const bsInstance = useRef(null);
useEffect(() => {
if (!offcanvasRef.current) return;
// initialize once
bsInstance.current = bootstrap.Offcanvas.getOrCreateInstance(offcanvasRef.current);
const el = offcanvasRef.current;
const handleHide = () => onClose();
el.addEventListener("hidden.bs.offcanvas", handleHide);
return () => {
el.removeEventListener("hidden.bs.offcanvas", handleHide);
};
}, [onClose]);
// react to `show` changes
useEffect(() => {
if (!bsInstance.current) return;
if (show) bsInstance.current.show();
else bsInstance.current.hide();
}, [show]);
return (
<div >
<div
ref={offcanvasRef}
id={id}
className={`offcanvas offcanvas offcanvas-${placement} offcanvas-wide`}
tabIndex="-1"
aria-labelledby={`${id}-label`}
>
<div className="offcanvas-header">
<div className="d-flex align-items-center gap-3">
<i
className="bx bx-lg bx-left-arrow-alt cursor-pointer"
data-bs-dismiss="offcanvas"
aria-label="Close"
></i>
<i className="bx bx-briefcase text-primary"></i>
<h5 className="offcanvas-title mb-0" id={`${id}-label`}>
{title}
</h5>
</div>
<button
type="button"
className="btn-close"
data-bs-dismiss="offcanvas"
aria-label="Close"
></button>
</div>
<div className="offcanvas-body ">{children}</div>
</div>
</div>
);
};
export default OffcanvasComponent;

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useMemo } from "react";
import { useController } from "react-hook-form";
import { useDebounce } from "../../utils/appUtils";
import { useEmployeesName } from "../../hooks/useEmployees";
import { useEmployeesName, useUserCache } from "../../hooks/useEmployees";
import Avatar from "./Avatar";
const PmsEmployeeInputTag = ({
@ -11,15 +11,17 @@ const PmsEmployeeInputTag = ({
projectId,
forAll,
isApplicationUser = false,
disabled,
}) => {
const {
field: { value = [], onChange },
} = useController({ name, control });
const { getUser, addToCache, userCache } = useUserCache();
const [search, setSearch] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [filteredUsers, setFilteredUsers] = useState([]);
const [userCache, setUserCache] = useState({});
const dropdownRef = useRef(null);
const inputRef = useRef(null);
const activeIndexRef = useRef(-1);
@ -31,26 +33,28 @@ const PmsEmployeeInputTag = ({
forAll
);
// Keep both filtered list and cache updated
useEffect(() => {
if (employees?.data?.length) {
setFilteredUsers(employees.data);
activeIndexRef.current = -1;
// cache all fetched users by id
setUserCache((prev) => {
const updated = { ...prev };
employees.data.forEach((u) => {
updated[u.id] = u;
});
return updated;
});
employees.data.forEach((u) => addToCache(u.id, u));
} else {
setFilteredUsers([]);
}
}, [employees]);
// close dropdown when clicking outside
// load selected employees not in filtered list
useEffect(() => {
if (!Array.isArray(value) || value.length === 0) return;
value.forEach(async (id) => {
if (!userCache[id]) {
await getUser(id); // fetch and cache
}
});
}, [value]);
useEffect(() => {
const onDocClick = (e) => {
if (
@ -65,7 +69,6 @@ const PmsEmployeeInputTag = ({
return () => document.removeEventListener("mousedown", onDocClick);
}, []);
// select a user
const handleSelect = (user) => {
if (value.includes(user.id)) return;
const updated = [...value, user.id];
@ -75,13 +78,11 @@ const PmsEmployeeInputTag = ({
setTimeout(() => inputRef.current?.focus(), 0);
};
// remove selected user
const handleRemove = (id) => {
const updated = value.filter((uid) => uid !== id);
onChange(updated);
};
// keyboard navigation
const onInputKeyDown = (e) => {
if (!showDropdown) return;
const max = Math.max(0, filteredUsers.length - 1);
@ -102,7 +103,6 @@ const PmsEmployeeInputTag = ({
}
};
// scroll active dropdown item into view
const scrollToActive = () => {
const wrapper = dropdownRef.current?.querySelector(
".tagify__dropdown__wrapper"
@ -119,24 +119,21 @@ const PmsEmployeeInputTag = ({
}
};
// resolve user details by ID (for rendering tags)
// FIX: allow null (not found yet)
const resolveUserById = (id) => {
return userCache[id] || filteredUsers.find((u) => u.id === id);
return userCache[id] || filteredUsers.find((u) => u.id === id) || null;
};
// main visible users list (memoized)
const visibleUsers = useMemo(() => {
const baseList = isApplicationUser
? (filteredUsers || []).filter((u) => u?.email)
: filteredUsers || [];
// also include selected users even if missing from current API
const selectedUsers =
Array.isArray(value) && value.length
? value.map((uid) => userCache[uid]).filter(Boolean)
: [];
// merge unique
const merged = [
...selectedUsers,
...baseList.filter((u) => !selectedUsers.some((s) => s.id === u.id)),
@ -147,10 +144,9 @@ const PmsEmployeeInputTag = ({
return (
<div
className="tagify form-control d-flex align-items-center flex-wrap position-relative "
className="tagify form-control d-flex align-items-center flex-wrap position-relative"
ref={dropdownRef}
>
{/* Selected tags (chips) */}
{value.map((id) => {
const u = resolveUserById(id);
if (!u) return null;
@ -187,10 +183,8 @@ const PmsEmployeeInputTag = ({
<button
type="button"
className="tagify__tag__removeBtn"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(id)}
aria-label={`Remove ${u.firstName}`}
title="Remove"
/>
</span>
);
@ -203,7 +197,7 @@ const PmsEmployeeInputTag = ({
id="TagifyUserList"
name="TagifyUserList"
className="tagify__input flex-grow-1 border-0 bg-transparent"
placeholder={placeholder || "Type to search users..."}
placeholder={placeholder}
onChange={(e) => {
setSearch(e.target.value);
setShowDropdown(true);
@ -215,6 +209,7 @@ const PmsEmployeeInputTag = ({
autoComplete="off"
aria-expanded={showDropdown}
aria-haspopup="listbox"
disabled={disabled}
/>
{showDropdown && (

View File

@ -8,24 +8,29 @@ const SelectMultiple = ({
name,
options = [],
label = "Select options",
labelKey = "name",
labelKey = "name",
valueKey = "id",
placeholder = "Please select...",
IsLoading = false,required = false
IsLoading = false,
required = false,
}) => {
const { setValue, watch,register } = useFormContext();
const { setValue, watch, register } = useFormContext();
useEffect(() => {
register(name, { value: [] });
}, [register, name]);
register(name, { value: [] });
}, [register, name]);
const selectedValues = watch(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) => {
@ -65,13 +70,12 @@ const selectedValues = watch(name) || [];
};
const filteredOptions = (options || []).filter((item) => {
const label = getLabel(item);
return (
typeof label === "string" &&
label.toLowerCase().includes(searchText.toLowerCase())
);
});
const label = getLabel(item);
return (
typeof label === "string" &&
label.toLowerCase().includes(searchText.toLowerCase())
);
});
const dropdownElement = (
<div
@ -97,7 +101,7 @@ const selectedValues = watch(name) || [];
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="multi-select-dropdown-search-input"
style={{ width: "100%", padding: 4 }}
style={{ width: "100%", }}
/>
</div>
@ -109,8 +113,14 @@ const selectedValues = watch(name) || [];
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"
@ -139,7 +149,11 @@ const selectedValues = watch(name) || [];
return (
<>
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
<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>
@ -150,7 +164,9 @@ const selectedValues = watch(name) || [];
>
<span
className={
selectedValues.length > 0 ? "placeholder-style-selected" : "placeholder-style"
selectedValues.length > 0
? "placeholder-style-selected"
: "placeholder-style"
}
>
<div className="selected-badges-container">
@ -159,7 +175,10 @@ const selectedValues = watch(name) || [];
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 bg-label-primary mx-1 py-2"
>
{label}
</span>
);

View File

@ -2,7 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import React, { useEffect, useState } from "react";
import Label from "./Label";
const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) => {
const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = false, options = [] }) => {
const { setValue, watch } = useFormContext();
const tags = watch(name) || [];
const [input, setInput] = useState("");
@ -33,29 +33,29 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] })
}
};
const handleChange = (e) => {
const val = e.target.value;
setInput(val);
const handleChange = (e) => {
const val = e.target.value;
setInput(val);
if (val) {
setSuggestions(
options
.filter((opt) => {
const label = typeof opt === "string" ? opt : opt.name;
return (
label.toLowerCase().includes(val.toLowerCase()) &&
!tags.some((t) => t.name === label)
);
})
.map((opt) => ({
name: typeof opt === "string" ? opt : opt.name,
isActive: true,
}))
);
} else {
setSuggestions([]);
}
};
if (val) {
setSuggestions(
options
.filter((opt) => {
const label = typeof opt === "string" ? opt : opt.name;
return (
label.toLowerCase().includes(val.toLowerCase()) &&
!tags.some((t) => t.name === label)
);
})
.map((opt) => ({
name: typeof opt === "string" ? opt : opt.name,
isActive: true,
}))
);
} else {
setSuggestions([]);
}
};
const handleSuggestionClick = (sugg) => {
handleAdd(sugg);
@ -65,13 +65,13 @@ const handleChange = (e) => {
return (
<>
<label htmlFor={name} className="form-label">
<Label htmlFor={name} className="form-label" required={required}>
{label}
</label>
</Label>
<div
className="form-control form-control-sm p-1"
style={{ minHeight: "38px", position: "relative" }}
style={{ minHeight: "33px", position: "relative" }}
>
<div className="d-flex flex-wrap align-items-center gap-1">
{tags.map((tag, index) => (
@ -105,6 +105,9 @@ const handleChange = (e) => {
outline: "none",
flex: 1,
minWidth: "120px",
backgroundColor: "white",
color: "black"
}}
/>
</div>

View File

@ -60,7 +60,7 @@ const GalleryFilterPanel = ({ onApply }) => {
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<Label>Select Date:</Label>
<DateRangePicker1
<DateRangePicker1 className="w-100"
placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="startDate"
endField="endDate"

View File

@ -0,0 +1,6 @@
import { useForm, Controller,FormProvider } from "react-hook-form";
export const useAppForm = (config) => useForm(config);
export const AppFormProvider = FormProvider;
export const AppFormController = Controller;

View File

@ -296,6 +296,26 @@ export const useOrganizationType = () => {
});
};
export const useJobStatus=(statusId,projectId)=>{
return useQuery({
queryKey:["Job_Staus",statusId,projectId],
queryFn:async()=>{
const resp = await MasterRespository.getJobStatus(statusId,projectId);
return resp.data;
},
enabled:!!statusId && !!projectId
})
}
export const useTeamRole=()=>{
return useQuery({
queryKey:["Team_Role"],
queryFn:async()=>{
const resp = await MasterRespository.getTeamRole();
return resp.data;
}
})
}
//#region ==Get Masters==
const fetchMasterData = async (masterType) => {
switch (masterType) {
@ -385,6 +405,8 @@ const useMaster = () => {
export default useMaster;
//#endregion
// ================================Mutation====================================
//#region Job Role
@ -435,6 +457,9 @@ export const useCreateJobRole = (onSuccessCallback) => {
//#endregion Job Role
//#region Application Role
export const useCreateApplicationRole = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -480,6 +505,10 @@ export const useUpdateApplicationRole = (onSuccessCallback) => {
};
//#endregion
//#region Create work Category
export const useCreateWorkCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -525,6 +554,11 @@ export const useUpdateWorkCategory = (onSuccessCallback) => {
};
//#endregion
//#region Contact Category
export const useCreateContactCategory = (onSuccessCallback) => {
@ -575,6 +609,10 @@ export const useUpdateContactCategory = (onSuccessCallback) => {
//#endregion
//#region Contact Tag
export const useCreateContactTag = (onSuccessCallback) => {
@ -621,6 +659,10 @@ export const useUpdateContactTag = (onSuccessCallback) => {
};
//#endregion
//#region Expense Category
export const useCreateExpenseCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -666,6 +708,11 @@ export const useUpdateExpenseCategory = (onSuccessCallback) => {
//#endregion
//#region Payment Mode
export const useCreatePaymentMode = (onSuccessCallback) => {
@ -712,6 +759,10 @@ export const useUpdatePaymentMode = (onSuccessCallback) => {
//#endregion
// Services-------------------------------
// export const useCreateService = (onSuccessCallback) => {
@ -793,6 +844,10 @@ export const useUpdateService = (onSuccessCallback) => {
//#endregion
//#region Activity Grouph
export const useCreateActivityGroup = (onSuccessCallback) => {
@ -857,6 +912,10 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
//#endregion
//#region Activities
export const useCreateActivity = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -911,6 +970,11 @@ export const useUpdateActivity = (onSuccessCallback) => {
//#endregion
//#region Expense Status
export const useCreateExpenseStatus = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -954,6 +1018,10 @@ export const useUpdateExpenseStatus = (onSuccessCallback) => {
};
//#endregion
//#region Document-Category
export const useCreateDocumentCatgory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -1000,6 +1068,11 @@ export const useUpdateDocumentCategory = (onSuccessCallback) => {
};
//#endregion
//#region Document-Type
export const useCreateDocumentType = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -1044,6 +1117,10 @@ export const useUpdateDocumentType = (onSuccessCallback) => {
};
//#endregion
//#region Payment Adjustment Head
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
const queryClient = useQueryClient();
@ -1087,6 +1164,10 @@ export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
};
//#endregion
//#region ==Delete Master==
export const useDeleteMasterItem = () => {
const queryClient = useQueryClient();
@ -1122,6 +1203,8 @@ export const useDeleteMasterItem = () => {
//#endregion
export const useDeleteServiceGroup = () => {
const queryClient = useQueryClient();

View File

@ -25,16 +25,24 @@ import showToast from "../services/toastService";
export const useModal = (modalType) => {
const dispatch = useDispatch();
const isOpen = useSelector(
(state) => state.localVariables.modals[modalType]?.isOpen
const modalState = useSelector(
(state) => state.localVariables.modals[modalType] || {}
);
const onOpen = (data = {}) => dispatch(openModal({ modalType, data }));
const { isOpen = false, data = null } = modalState;
const onOpen = (payload = {}) =>
dispatch(openModal({ modalType, data: payload }));
const onClose = () => dispatch(closeModal({ modalType }));
const onToggle = () => dispatch(toggleModal({ modalType }));
return { isOpen, onOpen, onClose, onToggle };
return { isOpen, data, onOpen, onClose, onToggle };
};
export const useSubscription = (frequency) => {
return useQuery({
queryKey: ["subscriptionPlans", frequency],

View File

@ -2,39 +2,80 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CollectionRepository } from "../repositories/ColllectionRepository";
import showToast from "../services/toastService";
// export const useCollections = (
// pageSize,
// pageNumber,
// fromDate,
// toDate,
// isPending,
// isActive,
// projectId,
// searchString
// ) => {
// return useQuery({
// queryKey: [
// "collections",
// pageSize,
// pageNumber,
// fromDate,
// toDate,
// isPending,
// isActive,
// projectId,
// searchString,
// ],
// queryFn: async () => {
// const response = await CollectionRepository.getCollections(
// pageSize,
// pageNumber,
// fromDate,
// toDate,
// isPending,
// isActive,
// projectId,
// searchString
// );
// return response.data;
// },
// keepPreviousData: true,
// staleTime: 1000 * 60 * 1,
// });
// };
export const useCollections = (
pageSize,
pageNumber,
projectId,
searchString,
fromDate,
toDate,
isPending,
pageSize,
pageNumber,
isActive,
projectId,
searchString
isPending
) => {
return useQuery({
queryKey: [
"collections",
pageSize,
pageNumber,
fromDate,
toDate,
isPending,
isActive,
projectId,
searchString,
fromDate,
toDate,
pageSize,
pageNumber,
isActive,
isPending,
],
queryFn: async () => {
const response = await CollectionRepository.getCollections(
pageSize,
pageNumber,
projectId,
searchString,
fromDate,
toDate,
isPending,
pageSize,
pageNumber,
isActive,
projectId,
searchString
isPending
);
return response.data;
},
@ -44,15 +85,14 @@ export const useCollections = (
});
};
export const useCollection =(collectionId)=>{
export const useCollection = (collectionId) => {
return useQuery({
queryKey:["collection",collectionId],
queryFn:async()=> {
queryKey: ["collection", collectionId],
queryFn: async () => {
const resp = await CollectionRepository.getCollection(collectionId);
return resp.data
},
enabled:!!collectionId
enabled: !!collectionId
})
}
@ -133,11 +173,11 @@ export const useAddComment = (onSuccessCallBack) => {
});
};
export const useUpdateCollection =(onSuccessCallBack)=>{
const client = useQueryClient();
export const useUpdateCollection = (onSuccessCallBack) => {
const client = useQueryClient();
return useMutation({
mutationFn:async({collectionId,payload})=> await CollectionRepository.updateCollection(collectionId,payload),
onSuccess: () => {
mutationFn: async ({ collectionId, payload }) => await CollectionRepository.updateCollection(collectionId, payload),
onSuccess: () => {
client.invalidateQueries({ queryKey: ["collections"] });
client.invalidateQueries({ queryKey: ["collection"] });
showToast("Collection Updated Successfully", "success");

View File

@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query";
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]);
const [isLineChartLoading, setLoading] = useState(false);
@ -18,11 +17,13 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
try {
const payload = {
days,
FromDate: FromDate || '',
FromDate: FromDate || "",
projectId: projectId || null,
};
const response = await GlobalRepository.getDashboardProgressionData(payload);
const response = await GlobalRepository.getDashboardProgressionData(
payload
);
setDashboard_Data(response.data);
} catch (err) {
setError("Failed to fetch dashboard data.");
@ -38,123 +39,6 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
return { dashboard_data, loading: isLineChartLoading, error };
};
// export const useDashboard_AttendanceData = (date, projectId) => {
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
// const [isLineChartLoading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
// setDashboard_AttendanceData(response.data);
// } catch (err) {
// setError("Failed to fetch dashboard data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
// if (date && projectId !== null) {
// fetchData();
// }
// }, [date, projectId]);
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
// };
// 🔹 Dashboard Projects Card Data Hook
// export const useDashboardProjectsCardData = () => {
// const [projectsCardData, setProjectsData] = useState([]);
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchProjectsData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardProjectsCardData();
// setProjectsData(response.data);
// } catch (err) {
// setError("Failed to fetch projects card data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
// fetchProjectsData();
// }, []);
// return { projectsCardData, loading, error };
// };
// 🔹 Dashboard Teams Card Data Hook
// export const useDashboardTeamsCardData = (projectId) => {
// const [teamsCardData, setTeamsData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchTeamsData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
// setTeamsData(response.data || {});
// } catch (err) {
// setError("Failed to fetch teams card data.");
// console.error("Error fetching teams card data:", err);
// setTeamsData({});
// } finally {
// setLoading(false);
// }
// };
// fetchTeamsData();
// }, [projectId]);
// return { teamsCardData, loading, error };
// };
// export const useDashboardTasksCardData = (projectId) => {
// const [tasksCardData, setTasksData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchTasksData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
// setTasksData(response.data);
// } catch (err) {
// setError("Failed to fetch tasks card data.");
// console.error(err);
// setTasksData({});
// } finally {
// setLoading(false);
// }
// };
// fetchTasksData();
// }, [projectId]);
// return { tasksCardData, loading, error };
// };
export const useAttendanceOverviewData = (projectId, days) => {
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
const [loading, setLoading] = useState(false);
@ -167,7 +51,10 @@ export const useAttendanceOverviewData = (projectId, days) => {
setError("");
try {
const response = await GlobalRepository.getAttendanceOverview(projectId, days);
const response = await GlobalRepository.getAttendanceOverview(
projectId,
days
);
setAttendanceOverviewData(response.data);
} catch (err) {
setError("Failed to fetch attendance overview data.");
@ -182,7 +69,6 @@ export const useAttendanceOverviewData = (projectId, days) => {
return { attendanceOverviewData, loading, error };
};
// -------------------Query----------------------------
// export const useDashboard_Data = (days, FromDate, projectId)=>{
@ -199,39 +85,47 @@ export const useAttendanceOverviewData = (projectId, days) => {
// }
// })
// }
export const useProjectCompletionStatus = () => {
return useQuery({
queryKey: ["projectCompletionStatus"],
queryFn: async () => {
const resp = await await GlobalRepository.getProjectCompletionStatus();
return resp.data;
},
});
};
export const useDashboard_AttendanceData = (date, projectId) => {
return useQuery({
queryKey: ["dashboardAttendances", date, projectId],
queryFn: async () => {
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
const resp = await await GlobalRepository.getDashboardAttendanceData(
date,
projectId
);
return resp.data;
}
})
}
},
});
};
export const useDashboardTeamsCardData = (projectId) => {
return useQuery({
queryKey: ["dashboardTeams", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId);
return resp.data;
}
})
}
},
});
};
export const useDashboardTasksCardData = (projectId) => {
return useQuery({
queryKey: ["dashboardTasks", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
const resp = await GlobalRepository.getDashboardTasksCardData(projectId);
return resp.data;
}
})
}
},
});
};
// export const useAttendanceOverviewData = (projectId, days) => {
// return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId],
@ -247,29 +141,30 @@ export const useDashboardProjectsCardData = () => {
return useQuery({
queryKey: ["dashboardProjects"],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardProjectsCardData();
return resp.data;
}
})
}
},
});
};
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
const hasBothDates = !!startDate && !!endDate;
const noDatesSelected = !startDate && !endDate;
const shouldFetch =
noDatesSelected ||
hasBothDates;
const shouldFetch = noDatesSelected || hasBothDates;
return useQuery({
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
const resp = await GlobalRepository.getExpenseData(
projectId,
startDate,
endDate
);
return resp.data;
},
enabled: shouldFetch,
refetchOnWindowFocus: true, // refetch when you come back
refetchOnMount: "always", // always refetch on remount
refetchOnWindowFocus: true, // refetch when you come back
refetchOnMount: "always", // always refetch on remount
staleTime: 0,
});
};
@ -280,17 +175,20 @@ export const useExpenseStatus = (projectId) => {
queryFn: async () => {
const resp = await GlobalRepository.getExpenseStatus(projectId);
return resp.data;
}
})
}
},
});
};
export const useExpenseDataByProject = (projectId, categoryId, months) => {
return useQuery({
queryKey: ["expenseByProject", projectId, categoryId, months],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
const resp = await GlobalRepository.getExpenseDataByProject(
projectId,
categoryId,
months
);
return resp.data;
},
});
};
};

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState,useCallback } from "react";
import { cacheData, getCachedData } from "../slices/apiDataManager";
import { RolesRepository } from "../repositories/MastersRepository";
import EmployeeRepository from "../repositories/EmployeeRepository";
@ -9,6 +9,36 @@ import { useSelector } from "react-redux";
import { store } from "../store/store";
import { queryClient } from "../layouts/AuthLayout";
export const useUserCache = () => {
const [userCache, setUserCache] = useState({});
const addToCache = (id, user) => {
setUserCache((prev) => ({ ...prev, [id]: user }));
};
const getUser = async (id) => {
if (userCache[id]) return userCache[id];
try {
const res = await EmployeeRepository.getEmployeeProfile(id);
if (res?.data) {
addToCache(id, res.data);
return res.data;
}
} catch (err) {
console.error("User not found", id);
return null;
}
};
return {
userCache,
getUser,
addToCache,
};
};
// Query ---------------------------------------------------------------------------
export const useEmployee = (employeeId) => {
@ -201,7 +231,7 @@ export const useEmployeesName = (projectId, search, allEmployee) => {
queryFn: async () =>
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
staleTime: 5 * 60 * 1000, // Optional: cache for 5 minutes
staleTime: 5 * 60 * 1000,
});
};
@ -341,3 +371,41 @@ export const useUpdateEmployeeRoles = ({
error: mutation.error,
};
};
export const useOrganizationHierarchy = (employeeId) => {
return useQuery({
queryKey: ["organizationHierarchy", employeeId],
queryFn: async () => {
const resp = await EmployeeRepository.getOrganizaionHierarchy(employeeId);
return resp.data;
},
enabled: !!employeeId,
});
};
export const useManageEmployeeHierarchy = (employeeId, onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
return await EmployeeRepository.manageOrganizationHierarchy(
employeeId,
payload
);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["organizationHierarchy", employeeId],
});
showToast("Reporting hierarchy updated successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Something went wrong, please try again!",
"error"
);
},
});
};

View File

@ -438,6 +438,15 @@ export const useExpenseTransactions = (employeeId)=>{
keepPreviousData:true,
})
}
export const useExpenseAllTransactionsList = (searchString) => {
return useQuery({
queryKey: ["transaction", searchString],
queryFn: async () => {
const resp = await ExpenseRepository.getAllTranctionList(searchString);
return resp.data;
},
});
};
//#endregion
// ---------------------------Put Post Recurring Expense---------------------------------------

View File

@ -20,13 +20,17 @@ export const useCurrentService = () => {
// ------------------------------Query-------------------
export const useProjects = () => {
export const useProjects = (pageSize, pageNumber,searchString) => {
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
return useQuery({
queryKey: ["ProjectsList"],
queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
queryFn: async () => {
const response = await ProjectRepository.getProjectList();
return response.data;
const response = await ProjectRepository.getProjectList(
pageSize,
pageNumber,
searchString,
);
return response?.data;
},
enabled: !!loggedUser,
});
@ -153,7 +157,7 @@ export const useProjectsAllocationByEmployee = (employeeId) => {
return { projectList, loading: isLoading, error, refetch };
};
export const useProjectName = (provideAll=false) => {
export const useProjectName = (provideAll = false) => {
const {
data = [],
isLoading,
@ -161,7 +165,7 @@ export const useProjectName = (provideAll=false) => {
refetch,
isError,
} = useQuery({
queryKey: ["basicProjectNameList",provideAll],
queryKey: ["basicProjectNameList", provideAll],
queryFn: async () => {
const res = await ProjectRepository.projectNameList(provideAll);
return res.data || res;
@ -179,6 +183,19 @@ export const useProjectName = (provideAll=false) => {
};
};
export const useProjectBothName = (searchString) => {
return useQuery({
queryKey: ["basic_bothProject", searchString],
queryFn: async () => {
const res = await ProjectRepository.projectNameListAll(searchString);
return res.data || res;
},
onError: (error) => {
showToast(error.message || "Error while Fetching project Name", "error");
},
});
};
export const useProjectInfra = (projectId, serviceId) => {
const {
data: projectInfra,
@ -337,7 +354,7 @@ export const useEmployeeForTaskAssign = (
// -- -------------Mutation-------------------------------
export const useCreateProject = ( onSuccessCallback ) => {
export const useCreateProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
@ -368,7 +385,7 @@ export const useCreateProject = ( onSuccessCallback ) => {
});
};
export const useUpdateProject = ( onSuccessCallback ) => {
export const useUpdateProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
const { mutate, isPending, isSuccess, isError } = useMutation({
mutationFn: async ({ projectId, payload }) => {
@ -395,7 +412,6 @@ export const useUpdateProject = ( onSuccessCallback ) => {
},
onError: (error) => {
console.log(error);
showToast(error?.message || "Error while updating project", "error");
},
});

View File

@ -0,0 +1,423 @@
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { ServiceProjectRepository } from "../repositories/ServiceProjectRepository";
import showToast from "../services/toastService";
//#region Service Project
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
pageNumber,
searchString
);
return response.data;
},
});
};
export const useServiceProject = (projectId) => {
return useQuery({
queryKey: ["serviceProject", projectId],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProject(
projectId
);
return response.data;
},
enabled: !!projectId,
});
};
export const useServiceProjectTeam = (projectId, isActive) => {
return useQuery({
queryKey: ["serviceProjectTeam", projectId, isActive],
queryFn: async () => {
const response = await ServiceProjectRepository.GetAllocatedEmployees(
projectId,
isActive
);
return response.data;
},
enabled: !!projectId,
});
};
export const useCreateServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (playload) =>
await ServiceProjectRepository.CreateServiceProject(playload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
if (onSuccessCallback) onSuccessCallback();
showToast("Project created successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to delete task",
"error"
);
},
});
};
export const useUpdateServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
return await ServiceProjectRepository.UpdateServiceProject(id, payload);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
queryClient.invalidateQueries({
queryKey: ["serviceProject", variables.id],
});
if (onSuccessCallback) onSuccessCallback();
showToast("Project updated successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
export const useActiveInActiveServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id, status) => {
return await ServiceProjectRepository.DeleteServiceProject(id, status);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
if (onSuccessCallback) onSuccessCallback();
showToast(
`Project ${status ? "restored" : "deleted"} successfully`,
"success"
);
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ payload, isActive }) =>
await ServiceProjectRepository.AllocateEmployee(payload),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectTeam"] });
if (onSuccessCallback) onSuccessCallback();
if (variables.isActive) {
showToast(
`${variables?.payload.length} Employee allocated successfully`,
"success"
);
} else {
showToast(`Employee removed successfully`, "success");
}
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
//#endregion
//#region Service Jobs
export const useServiceProjectJobs = (
pageSize,
pageNumber,
isActive = true,
project,
isArchive
) => {
return useQuery({
queryKey: [
"serviceProjectJobs",
pageSize,
pageNumber,
isActive,
project,
isArchive,
],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList(
pageSize,
pageNumber,
isActive,
project,
isArchive
);
return resp.data;
},
});
};
export const useJobComments = (jobId, pageSize, pageNumber) => {
return useInfiniteQuery({
queryKey: ["jobComments", jobId, pageSize],
queryFn: async ({ pageParam = pageNumber }) => {
const resp = await ServiceProjectRepository.GetJobComment(
jobId,
pageSize,
pageParam
);
return resp.data;
},
enabled: !!jobId,
initialPageParam: pageNumber,
getNextPageParam: (lastPage, allPages) => {
if (!lastPage?.data?.length) return null;
return allPages.length + pageNumber;
},
});
};
export const useJobTags = () => {
return useQuery({
queryKey: ["Job_Tags"],
queryFn: async () => await ServiceProjectRepository.GetJobTags(),
});
};
export const useServiceProjectJobDetails = (job) => {
return useQuery({
queryKey: ["service-job", job],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobDetails(job);
return resp.data;
},
enabled: !!job,
});
};
export const useAddCommentJob = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data) => await ServiceProjectRepository.AddComment(data),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: ["jobComments", variables?.jobTicketId],
});
if (onSuccessCallback) onSuccessCallback();
showToast("Comment added successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
export const useCreateServiceProjectJob = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
return await ServiceProjectRepository.CreateJob(payload);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
if (onSuccessCallback) onSuccessCallback();
showToast("Job Created successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload, isArchiveAction = false }) => {
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
return { resp, isArchiveAction };
},
onSuccess: ({ isArchiveAction }) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
queryClient.invalidateQueries({ queryKey: ["service-job"] });
if (onSuccessCallback) onSuccessCallback();
if (isArchiveAction) {
showToast("Job archived successfully", "success");
} else {
showToast("Job restored successfully", "success");
}
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
//#endregion
//#region Branch
export const useBranches = (
projectId,
isActive,
pageSize,
pageNumber,
searchString
) => {
return useQuery({
queryKey: [
"branches",
projectId,
isActive,
pageSize,
pageNumber,
searchString,
],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchList(
projectId,
isActive,
pageSize,
pageNumber,
searchString
);
return resp.data;
},
enabled: !!projectId,
});
};
export const useBranchTypes = () => {
return useQuery({
queryKey: ["branch_Type"],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchTypeList();
return resp.data;
},
});
};
export const useBranchDetails = (id) => {
return useQuery({
queryKey: ["branch", id],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchDetail(id);
return resp.data;
},
enabled: !!id,
});
};
export const useCreateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
await ServiceProjectRepository.CreateBranch(payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast("Branches Created Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.message || "Something went wrong please try again !",
"error"
);
},
});
};
export const useUpdateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) =>
await ServiceProjectRepository.UpdateBranch(id, payload),
onSuccess: (_, variables) => {
// remove old single-branch cache
queryClient.removeQueries({ queryKey: ["branch", variables.id] });
// refresh list
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast("Branch updated successfully", "success");
onSuccessCallBack?.();
},
onError: () => {
showToast("Something went wrong. Please try again later.", "error");
},
});
};
export const useDeleteBranch = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, isActive }) =>
await ServiceProjectRepository.DeleteBranch(id, isActive),
onSuccess: (_, variable) => {
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast(
`Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
"success"
);
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to delete branch",
"error"
);
},
});
};

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