Compare commits

..

197 Commits

Author SHA1 Message Date
a837eaab72 At Status name is filled. 2025-11-06 17:38:27 +05:30
ee456f5ae1 Changes in Recurring Expnese List at amount. 2025-11-06 17:26:39 +05:30
0729723d5e Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Issues_Expense 2025-11-06 17:22:27 +05:30
72e5cf0bbe display all time lien from darft 2025-11-06 15:48:53 +05:30
1278e32da9 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-06 13:18:00 +05:30
28f15f649f update advance trsnaction table 2025-11-06 13:17:31 +05:30
f18633b3d5 Remove Due to Date to Due Date. 2025-11-06 12:46:08 +05:30
07eedfc9e2 Alphabatically sorting at Manage Roles. 2025-11-06 12:35:06 +05:30
9d35d00009 Adding text-center at status column. 2025-11-06 12:31:14 +05:30
35956cc9bb Implementing Recurring Expense status column. 2025-11-06 12:29:35 +05:30
41377167e6 Correction in UI of Recurring page. 2025-11-06 12:25:32 +05:30
428f22ea8e UI changes in Collection forms and Recurring form. 2025-11-06 12:23:46 +05:30
4d40b8e149 Removing unwanted horizontal line in expense Details popup and remove unwanted console.log. 2025-11-06 10:39:26 +05:30
e4585d6c43 added view for working area 2025-11-06 10:21:33 +05:30
c3e030754f Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-06 10:07:46 +05:30
deda915e74 fixed updated recurring 2025-11-06 10:07:36 +05:30
dfaf90ccac Changes in Advance PaymentList. 2025-11-06 10:05:05 +05:30
a253eea33c fixed action schema or PR 2025-11-06 09:32:05 +05:30
d9e1bfac97 changed inut type for tax , base, tds 2025-11-06 09:15:29 +05:30
06e8046dde Properly updateExpense time line 2025-11-06 09:10:30 +05:30
82f173c0ed changed veriable name expensesCategoryId to expenseCategoryId and passes properly param. for list api 2025-11-06 08:55:55 +05:30
1f784f330d merged recurring into upgrade expense 2025-11-06 01:00:11 +05:30
4ae0b403a6 Merge branch 'Recurring_Expense' into upgrade_Expense 2025-11-06 00:25:26 +05:30
33256981da recover , misssing code during resloving merged 2025-11-06 00:21:57 +05:30
1ca1f15422 use 2025-11-05 23:29:09 +05:30
84ed1984d7 Merge branch 'Recurring_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense 2025-11-05 23:13:07 +05:30
b668faab33 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-05 23:04:34 +05:30
8229a44a74 added advance transaction records 2025-11-05 23:04:27 +05:30
324ad05771 Adding API for EDIT in Recurring Expense. 2025-11-05 21:36:03 +05:30
418979568d Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-05 21:11:30 +05:30
bac9f25df1 added advance payment api 2025-11-05 20:10:19 +05:30
e1e9920b3e added payee at updation time 2025-11-05 18:15:53 +05:30
9b091840d6 added payee default 2025-11-05 18:10:24 +05:30
b897a41f95 Removing unwanted space. 2025-11-05 17:43:24 +05:30
ec2a1706fc added new fields inside Expense action - tds, baseAmount, taxAmount 2025-11-05 17:04:04 +05:30
cb0ed03525 Adding some changes in Recurring Schema. 2025-11-05 17:00:25 +05:30
0372991ac1 Adding auto trim in Schema of Recurring Expense. 2025-11-05 16:57:36 +05:30
ac9eaa1e67 Adding Radio Buttons at Manage Recurring Expense. 2025-11-05 16:19:44 +05:30
daaebf919d added New Expense after Payment Request done 2025-11-05 15:17:07 +05:30
92fa1c9a3d Adding filter in Recurring Expense and search functionality is also working. 2025-11-05 14:45:06 +05:30
c0f30397ef Implementing Payee api for Payee. 2025-11-05 13:02:46 +05:30
00f405069b Adding Some spacing in columns. 2025-11-05 12:40:17 +05:30
2a46ac1349 Adding Spacing in List for Recurring Expense. 2025-11-05 12:33:56 +05:30
f4838441aa removed border 2025-11-05 12:16:27 +05:30
c63b13f200 fetched latest data 2025-11-05 12:13:34 +05:30
9b65c12239 Merge branch 'Recurring_Expense' into upgrade_Expense 2025-11-05 12:09:37 +05:30
10ec11a828 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense 2025-11-05 12:04:14 +05:30
3b318a672c Implementing Get and Correct API in Recurring. 2025-11-05 12:01:05 +05:30
fba19f6ead added currency utils 2025-11-05 11:59:14 +05:30
4069720ed0 changed currency format in list 2025-11-05 11:48:03 +05:30
e8698473db added layout for Expense veiw modal 2025-11-04 23:17:43 +05:30
f688a7169e spearated component for updated logs Expense and RequestedPayment 2025-11-04 22:54:10 +05:30
69e60caf23 initially setup delete request 2025-11-04 19:36:30 +05:30
ecf34b499e fixed Transaction details inside request payment 2025-11-04 19:31:05 +05:30
e868d27d5f Implementing Get and Update API in Recurring Expense. 2025-11-04 18:34:00 +05:30
0dc68eb20d fixed file uploading and payee 2025-11-04 17:15:42 +05:30
7a749b14e9 Impleting Create API for Recurring Expense. 2025-11-04 17:03:27 +05:30
76fe342c1e Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into Recurring_Expense 2025-11-04 15:43:10 +05:30
44a7749cd3 Creating Form for Recurring Expense. 2025-11-04 15:42:00 +05:30
611116b70c chanaged paramter name of payementRequestSchema 2025-11-04 15:34:28 +05:30
e79702661d Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-04 15:30:48 +05:30
10ff3ecd2f fixed permission for update Payement request and expense 2025-11-04 15:30:44 +05:30
bede21aff2 added 2025-11-04 14:11:47 +05:30
bce69783f7 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-04 14:10:29 +05:30
a8f83571ee Added Checkbox at Advance Payment. 2025-11-04 14:10:07 +05:30
616b986deb Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-04 13:02:18 +05:30
57d33ab817 fixed Expense type to Expense Category 2025-11-04 13:01:51 +05:30
5bf5e18c6b Adding some gap in Amout and Currency at PaymentRequestList. 2025-11-04 12:06:59 +05:30
190612feed Set default INR in Currency at popup. 2025-11-04 12:02:33 +05:30
346ef0174b Rearranging the fields of Payment Request Popup. 2025-11-04 11:42:43 +05:30
192c04fb6f fixed category filter field 2025-11-04 10:46:20 +05:30
3aa9934f8b Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-04 09:55:06 +05:30
d02b14272f added acion api t update status 2025-11-03 20:25:41 +05:30
edafc204b8 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-03 17:49:53 +05:30
1e14bfe0b1 added view Paymen request 2025-11-03 17:48:50 +05:30
1c4384a62e Adding Filter Get api and delete modal open. 2025-11-03 17:12:24 +05:30
859eaa747f Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-03 16:56:49 +05:30
871daf6036 added api Get Advance Payment 2025-11-03 14:48:42 +05:30
59c46f7f3b Calling API for Get payment Request. 2025-11-03 14:46:20 +05:30
e80223beba Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-03 12:52:17 +05:30
de2d23ec83 added padding for table header 2025-11-03 12:52:10 +05:30
f3cfb3cf24 adding api for get list for payment request. 2025-11-03 12:50:06 +05:30
0e3c78df11 added advance rpayment list component 2025-11-03 12:11:30 +05:30
e0c0a14777 Adding List view for Payment Request. 2025-11-03 10:06:09 +05:30
17a08304e3 Merge branch 'upgrade_Expense' of https://git.marcoaiot.com/admin/marco.pms.web into upgrade_Expense 2025-11-01 18:11:13 +05:30
c13e0acd3d added advance list inside Advance payments 2025-11-01 18:10:48 +05:30
b0a58740a5 Remove mandatory field from bill attachment. 2025-11-01 17:39:54 +05:30
c123373892 Adding Create API for Payment Request. 2025-11-01 17:10:47 +05:30
9c03303547 added employee search box inside advance payment 2025-11-01 16:55:32 +05:30
d8712c0d04 Merge pull request 'issues_Oct_4W' (#498) from issues_Oct_4W into OnFieldWork_V1
Reviewed-on: #498
Merged
2025-11-01 11:21:09 +00:00
5f0972ee32 addedfixed height for employee list insidemanage bucket 2025-11-01 11:21:09 +00:00
0d1d51d56b fixed check ehcke issue 2025-11-01 11:21:09 +00:00
166b0a1caf Remove fw-bold in View popup at attendance logs. 2025-11-01 11:21:09 +00:00
196069b39a Creating APi for Create Payemnt Request. 2025-11-01 16:00:00 +05:30
10c2e9dfee Added Submit and Cancel button on Payment Request. 2025-11-01 15:08:43 +05:30
494b1b2b77 Added view for Payment Request. 2025-11-01 14:57:47 +05:30
465b67e25c initially added pages for AdvancePayment and Payment Request 2025-11-01 14:43:09 +05:30
ec04426e52 added get icon for file by use uploaded 2025-10-30 15:17:38 +05:30
dc75121d31 fixed date formating inside attenace logs and expense list 2025-10-30 11:29:02 +05:30
d3acf13c0a fixed card size for dashboard cards 2025-10-30 10:50:45 +05:30
cc4cea0f27 fixed date range picker width 2025-10-29 11:46:46 +05:30
02777a8f47 cosmatic and logo changes 2025-10-27 11:46:53 +05:30
7c5dca1665 updated confirm modal, added success option 2025-10-24 11:36:00 +05:30
0d0b7f2cbc changes to green theme 2025-10-23 12:41:58 +05:30
394d28f80d integrated collection mang. completely 2025-10-17 15:17:48 +05:30
95c6e71be8 Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into OnFieldWork_V1 2025-10-17 13:50:16 +05:30
0091b1064e intergrated collection managment 2025-10-17 13:49:44 +05:30
cde6794697 Changes in Index.jtml for hidding some unnessary fields. 2025-10-17 12:51:48 +05:30
78b721dad7 Changes in Expensfilter panel at on apply and clear fitler is closed. 2025-10-17 10:24:35 +05:30
a8ae0fac70 Merge branch 'landing_page_with_new_theme' into OnFieldWork_V1 2025-10-15 16:56:04 +05:30
b318d469f6 Changes in OnField Work. 2025-10-15 16:52:27 +05:30
73534226e3 Changes in Filter panel when on close 2025-10-15 14:38:17 +05:30
9bbe479153 Removing unnessary gap in Documents.jsx file. 2025-10-15 12:45:38 +05:30
ce976a6d5c Changes Directory Notes and Contact filter panel at time of chips remover. 2025-10-15 12:25:33 +05:30
d1af4f402d Changes when we select a Expense Status in ExpenseFilter Panel. 2025-10-15 12:18:27 +05:30
105beb6062 more cosmmatic changes 2025-10-15 11:54:49 +05:30
f61c7a4a35 Removing Unessary gaps. 2025-10-15 11:50:26 +05:30
dacd72f945 handle reposnsive height 2025-10-15 10:52:07 +05:30
a2177dc2af UI changes in attendance, listview Directory, ExpenseList, Project Page at mobile view. 2025-10-14 15:53:37 +05:30
a9d1ba08dd UI updation in Attendance Toady's and Regularization tab. 2025-10-14 15:44:36 +05:30
9526cb0e5e add more cosmatic changes and include contact us section 2025-10-14 10:29:46 +05:30
432cf2122e new design added 2025-10-13 16:12:46 +05:30
765b4356a6 Merge pull request 'Adding Chips in Directory Contact and Notes Page.' (#473) from Kartik_Task#1465 into OnFieldWork_V1
Reviewed-on: #473
Merged
2025-10-11 08:41:19 +00:00
6125130cff Adding Chips in Directory Contact and Notes Page. 2025-10-11 08:41:19 +00:00
006ca21e1d imported PreviwDocument compoent that was missing 2025-10-11 10:52:02 +05:30
bb2d7b8923 added pading in check-in check-out form 2025-10-10 19:54:20 +05:30
c921dbff09 fixed u 2025-10-10 17:49:14 +05:30
e84da2dcb0 Merge pull request 'Kartik_Task_1459 :- Adding Export Excel and PDF in Directory.' (#469) from Kartik_Task_1459 into OnFieldWork_V1
Reviewed-on: #469
Merged
2025-10-10 11:24:30 +00:00
4ba7c72e78 Removing ExportUtils.js file. 2025-10-10 11:24:30 +00:00
9939973d8d Adding Excel and PDF export in Directory. 2025-10-10 11:24:30 +00:00
e17c2064ac Merge pull request 'UI updation in Expense Breakdown' (#471) from Kartik_Bug#1463 into OnFieldWork_V1
Reviewed-on: #471
Merged
2025-10-10 11:24:02 +00:00
9580c9234c UI updation in Expense Breakdown 2025-10-10 11:24:02 +00:00
44b3f842f2 Correction in Date Ranger in Attendance Logs. 2025-10-10 16:36:39 +05:30
9de0b1a0df Project List view UI implementation. 2025-10-10 16:00:52 +05:30
fbb68a4488 Correction in DataRangePicker for date range box and Calender. 2025-10-10 15:24:34 +05:30
c1a8b87a2c Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into OnFieldWork_V1 2025-10-10 12:48:42 +05:30
24501b3c76 persist date 2025-10-10 12:48:37 +05:30
ef00516256 Changes in Dashboard weidget Attendance and Monthly weidget when no data show. 2025-10-10 12:11:12 +05:30
f867a692ed fixed : whenever open modal that not come exiten data 2025-10-09 18:49:14 +05:30
85b4b830d3 Merge pull request 'Kartik_Enhancement#1448 : Document filters chips' (#468) from Kartik_Enhancement#1448 into OnFieldWork_V1
Reviewed-on: #468
Merged
2025-10-09 12:57:36 +00:00
c1c333008f added document filter chips 2025-10-09 12:57:36 +00:00
5370d0eb9d Adding Chips in Document. 2025-10-09 12:57:36 +00:00
5cbd6c427a Adding Chips in Document. 2025-10-09 12:57:36 +00:00
5057738d8b Merge pull request 'Directory tab inside Project Profile page not displayed properly (tab within tab issue)' (#464) from Kartik_Bug#1408 into OnFieldWork_V1
Reviewed-on: #464
2025-10-09 12:56:40 +00:00
063bbee681 Changes directory View. 2025-10-09 12:56:40 +00:00
3dc667db73 Directory tab inside Project Profile page not displayed properly (tab within tab issue) 2025-10-09 12:56:40 +00:00
2f2c31db9f Merge pull request 'Added 3-dots menu in the Employee Module with ‘Show Inactive Employees’ and Export functionalities inside the menu.' (#466) from Kartik_Enhancement#1433 into OnFieldWork_V1
Reviewed-on: #466
merged
2025-10-09 12:55:56 +00:00
0dc540eec2 Added 3-dots menu in the Employee Module with ‘Show Inactive Employees’ and Export functionalities inside the menu. 2025-10-09 12:55:56 +00:00
fc7c33ee54 added right status variable name 2025-10-09 17:51:27 +05:30
eeb886ec36 Removing consol.log in regularize 2025-10-09 14:58:11 +05:30
210b01513e Removing Project name column in Attendance Logs and Regularize. 2025-10-09 14:56:00 +05:30
2fe297c6b6 Adding 2 columns in Regularize column Request at and Requested by 2025-10-09 10:27:13 +05:30
ffd7f15488 Merge pull request 'Attendance Tab Should Display All Records for Each Employee' (#465) from Kartik_Bug#1425 into OnFieldWork_V1
Reviewed-on: #465
Merged
2025-10-08 13:38:09 +00:00
7f33d5e6bd Attendance Tab Should Display All Records for Each Employee 2025-10-08 13:38:09 +00:00
108ca1cc96 Merge pull request 'Adding NA and updation in Excel,PDF and Print functionality in Employee list.' (#463) from Kartik_Bug#1436 into OnFieldWork_V1
Reviewed-on: #463
Merged
2025-10-08 13:37:48 +00:00
0b1a276898 Adding NA and updation in Excel,PDF and Print functionality in Employee list. 2025-10-08 13:37:48 +00:00
6246dbd23f Merge pull request 'Missing title in View popup' (#462) from Kartik#Enhancement#1444 into OnFieldWork_V1
Reviewed-on: #462
Merged
2025-10-08 13:37:18 +00:00
af3bf11beb Missing title in View popup 2025-10-08 13:37:18 +00:00
2e5951d9d1 Merge pull request 'Expense Type label to be renamed to Expense Category.' (#461) from Kartik_Enhancement#1446 into OnFieldWork_V1
Reviewed-on: #461
Merged
2025-10-08 13:37:02 +00:00
cdc5e32554 Expense Type label to be renamed to Expense Category. 2025-10-08 13:37:02 +00:00
bc047a84fc Adding processpending in ExpenseStatus. 2025-10-08 18:49:24 +05:30
0511ed6d82 Changes in Attendance module for all projects. 2025-10-08 13:52:06 +05:30
bc933f4c64 Adding Search box in Expense Paid by dropdown. 2025-10-08 11:29:51 +05:30
bd3645aae6 In Modify project add project.id on UseProjectDetails 2025-10-08 10:58:31 +05:30
2f2215ab8a Merge pull request 'OnField_Issues_Oct_2W' (#455) from OnField_Issues_Oct_2W into OnFieldWork_V1
Reviewed-on: #455
Merged
2025-10-07 04:10:40 +00:00
071f862743 Incorrect navigation when viewing project details 2025-10-07 04:10:40 +00:00
040331e27d Removing the dropdown from the Masters page. 2025-10-07 04:10:40 +00:00
db2730c02b Employee Click Should Navigate to Profile Page from Attendance Page 2025-10-07 04:10:40 +00:00
9df813698d Merge pull request 'Adding_Chips_Expense' (#451) from Adding_Chips_Expense into OnFieldWork_V1
Reviewed-on: #451
Merged
2025-10-06 13:47:36 +00:00
6ba018399c Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into Adding_Chips_Expense 2025-10-06 19:06:33 +05:30
aeb9778216 Add link to login page 2025-10-06 17:50:22 +05:30
81ed65c0c6 add initial version of landig page 2025-10-06 17:49:03 +05:30
1e49cf17a2 addd dropdown at expense table action 2025-10-04 16:25:28 +05:30
ab5e0b2f79 Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into Adding_Chips_Expense 2025-10-04 16:12:07 +05:30
7c9d2ddeb1 Added Chips in Expense filter. 2025-10-04 16:06:26 +05:30
f0f579beae fixed dashbord card size 2025-10-04 15:47:07 +05:30
b53e6284b9 Merge branch 'OnFieldWork_V1' of https://git.marcoaiot.com/admin/marco.pms.web into OnFieldWork_V1 2025-10-04 14:13:43 +05:30
b1c23aab4d added expens group fied Name 2025-10-04 14:13:09 +05:30
4a05e67ff0 Adding dropdown in ExpenseFilter. 2025-10-04 14:12:20 +05:30
87d13d0f77 Changes in Monthly Expense weidget. 2025-10-04 12:20:59 +05:30
997971629f addd Expense By project 2025-10-04 11:08:19 +05:30
18ac8e11bd added expense filte at api url level 2025-10-04 10:50:14 +05:30
78808ecac0 added Expensebstatus widget, fixed small bugs 2025-10-03 19:07:44 +05:30
3553a7b521 change class to className 2025-10-03 10:57:55 +05:30
aca96c60ae removed console and comment 2025-10-02 13:24:36 +05:30
25f4f1e7a7 added expense analysis card inisde dashboard 2025-10-02 13:10:14 +05:30
0c1889e1c1 added Ellipsis Pagination 2025-10-01 22:45:31 +05:30
9884943907 addded imported field added 2025-10-01 18:45:26 +05:30
2531d91209 fixed dashboard graph 2025-10-01 16:21:08 +05:30
49b597c833 fixed expense date, added project name at check in checkout modal 2025-10-01 15:34:19 +05:30
2aae7194b7 implemented singlR 2025-10-01 13:00:38 +05:30
ae772d925a remove project permissions 2025-10-01 10:13:52 +05:30
c1e5ff4043 remove dashboard 2025-09-30 23:39:41 +05:30
2aff3b9e80 changed logins api 2025-09-30 19:35:01 +05:30
4e48478fbc update attendnace and expense 2025-09-30 19:31:24 +05:30
4ea1e06a9c hide project infra, setting, Organization, and show only all employees and inactive employee 2025-09-30 18:16:29 +05:30
4ddb8415cc added gobal create project,change paid by at expense create 2025-09-30 17:27:20 +05:30
344 changed files with 13937 additions and 20214 deletions

View File

@ -5,10 +5,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OnFieldWork.com</title> <title>Marco PMS</title>
<meta name="description" content="" /> <meta name="description" content="" />
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/img/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/img/favicon.ico" />
@ -29,6 +31,8 @@
<link rel="stylesheet" href="/assets/css/default.css" /> <link rel="stylesheet" href="/assets/css/default.css" />
<link rel="stylesheet" href="/assets/css/skeleton.css" /> <link rel="stylesheet" href="/assets/css/skeleton.css" />
<link rel="stylesheet" href="/assets/css/hover-utility.css" /> <link rel="stylesheet" href="/assets/css/hover-utility.css" />
<link rel="stylesheet" href="/assets/css/theme-green.css" />
<link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" /> <link rel="stylesheet" href="/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
@ -46,8 +50,6 @@
<link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" /> <link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" />
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" /> <link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />
<link rel="stylesheet" href="/assets/vendor/libs/spinkit/spinkit.css" /> <link rel="stylesheet" href="/assets/vendor/libs/spinkit/spinkit.css" />
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.css" />
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.js" />
<!-- Helpers --> <!-- Helpers -->
<script src="/assets/vendor/js/helpers.js"></script> <script src="/assets/vendor/js/helpers.js"></script>
@ -96,15 +98,6 @@
<script src="/assets/js/main.js"></script> <script src="/assets/js/main.js"></script>
<!-- Page JS --> <!-- Page JS -->
<script src="/assets/js/form-wizard-icons.js"></script>
<script src="/assets/js/dashboards-analytics.js"></script>
<!-- Bloack Ui -->
<!-- <script src="/assets/js/extended-ui-blockui.js"></script> -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- BlockUI core plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.min.js"></script>
<script src="/assets/js/form-wizard-icons.js"></script> <script src="/assets/js/form-wizard-icons.js"></script>
<script src="/assets/js/dashboards-analytics.js"></script> <script src="/assets/js/dashboards-analytics.js"></script>
@ -116,6 +109,9 @@
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script> <!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script> --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script> -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> -->
<!-- Flatpickr JS --> <!-- Flatpickr JS -->
</body> </body>

View File

@ -3,37 +3,6 @@
--bs-nav-link-font-size: 0.7375rem; --bs-nav-link-font-size: 0.7375rem;
--bg-border-color :#f8f6f6 --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);
border:var(--bs-primary-border-subtle)
}
.bg-light-secondary {
background-color: color-mix(in srgb, var(--bs-secondary) 10.4%, transparent);
}
.bg-light-danger {
background-color: color-mix(in srgb, var(--bs-danger) 10.4%, transparent);
}
.bg-light-success {
background-color: color-mix(in srgb, var(--bs-success) 10.4%, transparent);
}
.bg-light-info {
background-color: color-mix(in srgb, var(--bs-info) 10.4%, transparent);
}
.bg-light-warning {
background-color: color-mix(in srgb, var(--bs-warning) 10.4%, transparent);
}
.card-header { .card-header {
padding: 0.5rem var(--bs-card-cap-padding-x); padding: 0.5rem var(--bs-card-cap-padding-x);
@ -42,116 +11,20 @@
border-bottom:2px solid var(--bs-table-border-color) ; border-bottom:2px solid var(--bs-table-border-color) ;
} }
.text-gary-80 { .text-gary-80 {
color:var(--bs-gray-500) color:var(--bs-gray-500)
} }
.text-royalblue{ .text-royalblue{
color: #1796e3; color: #1796e3;
} }
.text-md { .text-md {
font-size: 2rem; font-size: 2rem;
} }
.text-md-b { .text-md-b {
font-weight: normal; font-weight: normal;
} }
.stepper-container {
position: relative;
}
.timeline-horizontal {
position: relative;
padding: 0;
margin: 0;
}
.timeline-item {
position: relative;
flex: 1;
}
.timeline-point {
width: 20px;
height: 20px;
border-radius: 50%;
background: #dee2e6;
color: #6c757d;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
z-index: 2;
position: relative;
padding: 3px;
transition: all 0.3s ease;
}
.timeline-point.completed {
background-color: var(--bs-success);
color: #fff;
box-shadow: 0 0 5px rgba(25, 135, 84, 0.5);
}
.timeline-point.failed {
background-color: var(--bs-danger);
color: #fff;
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
}
.timeline-point.active {
background-color: var(--bs-info);
color: #fff;
transform: scale(1.15);
box-shadow: 0 0 6px rgba(13, 202, 240, 0.5);
}
.timeline-line-horizontal {
content: "";
position: absolute;
top: 10px;
left: 50%;
width: 100%;
height: 2px;
background-color: #dee2e6;
z-index: 1;
transition: background-color 0.3s ease;
}
/* Make line green for completed sections */
.timeline-item.completed ~ .timeline-line-horizontal {
background-color: var(--bs-success);
}
/* Optional: subtle pulse for active step */
.timeline-point.active::after {
content: "";
position: absolute;
width: 25px;
height: 25px;
border-radius: 50%;
border: 2px solid var(--bs-info);
animation: pulse 1.5s infinite;
opacity: 0.6;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.6;
}
70% {
transform: scale(1.5);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 0;
}
}
.text-xxs { font-size: 0.55rem; } /* 8px */ .text-xxs { font-size: 0.55rem; } /* 8px */
.text-xs { font-size: 0.75rem; } /* 12px */ .text-xs { font-size: 0.75rem; } /* 12px */
.text-sm { font-size: 0.875rem; } /* 14px */ .text-sm { font-size: 0.875rem; } /* 14px */
@ -223,208 +96,6 @@ font-weight: normal;
.h-max { height: max-content; } .h-max { height: max-content; }
/* ==========================
Base Font Sizes (mobile first)
========================== */
.text-xxs { font-size: 0.55rem; } /* 8px */
.text-xs { font-size: 0.75rem; } /* 12px */
.text-sm { font-size: 0.875rem; } /* 14px */
.text-base{ font-size: 1rem; } /* 16px */
.text-lg { font-size: 1.125rem; } /* 18px */
.text-xl { font-size: 1.25rem; } /* 20px */
.text-2xl { font-size: 1.5rem; } /* 24px */
.text-3xl { font-size: 1.875rem; } /* 30px */
.text-4xl { font-size: 2.25rem; } /* 36px */
.text-5xl { font-size: 3rem; } /* 48px */
.text-6xl { font-size: 3.75rem; } /* 60px */
.text-7xl { font-size: 4.5rem; } /* 72px */
.text-8xl { font-size: 6rem; } /* 96px */
.text-9xl { font-size: 8rem; } /* 128px */
/* ==========================
Base Heights
========================== */
.h-0 { height: 0; }
.h-px { height: 1px; }
.h-1 { height: 0.25rem; } /* 4px */
.h-2 { height: 0.5rem; } /* 8px */
.h-3 { height: 0.75rem; } /* 12px */
.h-4 { height: 1rem; } /* 16px */
.h-5 { height: 1.25rem; } /* 20px */
.h-6 { height: 1.5rem; } /* 24px */
.h-8 { height: 2rem; } /* 32px */
.h-10 { height: 2.5rem; } /* 40px */
.h-12 { height: 3rem; } /* 48px */
.h-16 { height: 4rem; } /* 64px */
.h-20 { height: 5rem; } /* 80px */
.h-24 { height: 6rem; } /* 96px */
.h-32 { height: 8rem; } /* 128px */
.h-40 { height: 10rem; } /* 160px */
.h-48 { height: 12rem; } /* 192px */
.h-56 { height: 14rem; } /* 224px */
.h-64 { height: 16rem; } /* 256px */
.h-70 { height: 20rem; } /* 256px */
.h-74 { max-height: 35rem; } /* 256px */
.h-full { height: 100%; }
.h-screen{ height: 100vh; }
/* ==========================
Base Widths
========================== */
.w-0 { width: 0; }
.w-px { width: 1px; }
.w-1 { width: 0.25rem; }
.w-2 { width: 0.5rem; }
.w-3 { width: 0.75rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-8 { width: 2rem; }
.w-10 { width: 2.5rem; }
.w-12 { width: 3rem; }
.w-16 { width: 4rem; }
.w-20 { width: 5rem; }
.w-24 { width: 6rem; }
.w-32 { width: 8rem; }
.w-40 { width: 10rem; }
.w-48 { width: 12rem; }
.w-56 { width: 14rem; }
.w-64 { width: 16rem; }
.w-full { width: 100%; }
.w-screen{ width: 100vw; }
/* ==========================
Responsive Variants
========================== */
@media (min-width: 576px) { /* sm */
/* Font */
.text-xxs-sm { font-size: 0.55rem; }
.text-xs-sm { font-size: 0.75rem; }
.text-sm-sm { font-size: 0.875rem; }
.text-base-sm{ font-size: 1rem; }
.text-lg-sm { font-size: 1.125rem; }
.text-xl-sm { font-size: 1.25rem; }
.text-2xl-sm{ font-size: 1.5rem; }
/* Height */
.h-1-sm{ height: 0.25rem; }
.h-2-sm{ height: 0.5rem; }
.h-3-sm{ height: 0.75rem; }
.h-4-sm{ height: 1rem; }
.h-5-sm{ height: 1.25rem; }
.h-6-sm{ height: 1.5rem; }
.h-8-sm{ height: 2rem; }
.h-10-sm{ height: 2.5rem; }
/* Width */
.w-1-sm{ width: 0.25rem; }
.w-2-sm{ width: 0.5rem; }
.w-3-sm{ width: 0.75rem; }
.w-4-sm{ width: 1rem; }
.w-5-sm{ width: 1.25rem; }
.w-6-sm{ width: 1.5rem; }
.w-8-sm{ width: 2rem; }
.w-10-sm{ width: 2.5rem; }
}
@media (min-width: 768px) { /* md */
/* Font */
.text-xxs-md { font-size: 0.55rem; }
.text-xs-md { font-size: 0.75rem; }
.text-sm-md { font-size: 0.875rem; }
.text-base-md{ font-size: 1rem; }
.text-lg-md { font-size: 1.125rem; }
.text-xl-md { font-size: 1.25rem; }
.text-2xl-md{ font-size: 1.5rem; }
/* Height */
.h-1-md{ height: 0.25rem; }
.h-2-md{ height: 0.5rem; }
.h-3-md{ height: 0.75rem; }
.h-4-md{ height: 1rem; }
.h-5-md{ height: 1.25rem; }
.h-6-md{ height: 1.5rem; }
.h-8-md{ height: 2rem; }
.h-10-md{ height: 2.5rem; }
/* Width */
.w-1-md{ width: 0.25rem; }
.w-2-md{ width: 0.5rem; }
.w-3-md{ width: 0.75rem; }
.w-4-md{ width: 1rem; }
.w-5-md{ width: 1.25rem; }
.w-6-md{ width: 1.5rem; }
.w-8-md{ width: 2rem; }
.w-10-md{ width: 2.5rem; }
}
@media (min-width: 992px) { /* lg */
/* Font */
.text-xxs-lg { font-size: 0.55rem; }
.text-xs-lg { font-size: 0.75rem; }
.text-sm-lg { font-size: 0.875rem; }
.text-base-lg{ font-size: 1rem; }
.text-lg-lg { font-size: 1.125rem; }
.text-xl-lg { font-size: 1.25rem; }
.text-2xl-lg{ font-size: 1.5rem; }
/* Height */
.h-1-lg{ height: 0.25rem; }
.h-2-lg{ height: 0.5rem; }
.h-3-lg{ height: 0.75rem; }
.h-4-lg{ height: 1rem; }
.h-5-lg{ height: 1.25rem; }
.h-6-lg{ height: 1.5rem; }
.h-8-lg{ height: 2rem; }
.h-10-lg{ height: 2.5rem; }
/* Width */
.w-1-lg{ width: 0.25rem; }
.w-2-lg{ width: 0.5rem; }
.w-3-lg{ width: 0.75rem; }
.w-4-lg{ width: 1rem; }
.w-5-lg{ width: 1.25rem; }
.w-6-lg{ width: 1.5rem; }
.w-8-lg{ width: 2rem; }
.w-10-lg{ width: 2.5rem; }
}
@media (min-width: 1200px) { /* xl */
/* Font */
.text-xxs-xl { font-size: 0.55rem; }
.text-xs-xl { font-size: 0.75rem; }
.text-sm-xl { font-size: 0.875rem; }
.text-base-xl{ font-size: 1rem; }
.text-lg-xl { font-size: 1.125rem; }
.text-xl-xl { font-size: 1.25rem; }
.text-2xl-xl{ font-size: 1.5rem; }
/* Height */
.h-1-xl{ height: 0.25rem; }
.h-2-xl{ height: 0.5rem; }
.h-3-xl{ height: 0.75rem; }
.h-4-xl{ height: 1rem; }
.h-5-xl{ height: 1.25rem; }
.h-6-xl{ height: 1.5rem; }
.h-8-xl{ height: 2rem; }
.h-10-xl{ height: 2.5rem; }
/* Width */
.w-1-xl{ width: 0.25rem; }
.w-2-xl{ width: 0.5rem; }
.w-3-xl{ width: 0.75rem; }
.w-4-xl{ width: 1rem; }
.w-5-xl{ width: 1.25rem; }
.w-6-xl{ width: 1.5rem; }
.w-8-xl{ width: 2rem; }
.w-10-xl{ width: 2.5rem; }
}
/* ------------------------Text------------------------- */ /* ------------------------Text------------------------- */
@media (min-width: 576px) { @media (min-width: 576px) {
.fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; } .fs-sm-1 { font-size: calc(1.3rem + 1.6vw) !important; }
@ -456,3 +127,11 @@ font-weight: normal;
.fs-md-xlarge { font-size: 170% !important; } .fs-md-xlarge { font-size: 170% !important; }
.fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; } .fs-md-xxlarge { font-size: calc(1.725rem + 5.7vw) !important; }
} }
/* Tables */
.table th.actions-col,
.table td.actions-col {
width: 1%;
white-space: nowrap;
text-align: center;
}

View File

@ -30,6 +30,11 @@
width: 45px; width: 45px;
} }
.app-brand-logo-border {
border: 1px solid #d5d5d5;
}
.app-brand-text { .app-brand-text {
font-size: 1.75rem; font-size: 1.75rem;
letter-spacing: -0.5px; letter-spacing: -0.5px;
@ -160,9 +165,10 @@ thead tr {
.app-brand-logo-login { .app-brand-logo-login {
max-width: 50px; /* default for mobile */ max-width: 50px; /* default for mobile */
height: auto; /* keep aspect ratio */ height: auto; /* keep aspect ratio */
} }
/* Tablet and up (≥768px) */ /* Tablet and up (≥768px) */
@media (min-width: 768px) { @media (min-width: 768px) {
.app-brand-logo-login { .app-brand-logo-login {
@ -176,3 +182,4 @@ thead tr {
max-width: 80px; max-width: 80px;
} }
} }

View File

@ -0,0 +1,55 @@
.btn-green {
background-color: #49bf3c;
color: #fff;
border-radius: 50px;
padding: 10px 30px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-green-outline {
border-color: #49bf3c;
background-color: transparent;
color: unset;
}
.btn-green-outline:hover {
background-color: #49bf3c;
color: #fff;
}
.btn-square-small {
border-radius: 3px;
padding-bottom: 5.072px;
padding-inline-end: 12px;
padding-inline-start: 12px;
padding-left: 12px;
padding-right: 12px;
padding-top: 5.072px;
}
.btn-green:hover {
background-color: #00a85a;
color: #fff;
}
.text-blue {
color: #696cff !important;
}
.text-green {
color: #49bf3c !important;
}
.btn-outline-green {
border-radius: 50px;
padding: 10px 30px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-outline-green:hover {
background-color: #49bf3c;
color: #fff;
}

View File

@ -76,7 +76,6 @@
--bs-dark-border-subtle: #bfc0c6; --bs-dark-border-subtle: #bfc0c6;
--bs-white-rgb: 255, 255, 255; --bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 34, 48, 62; --bs-black-rgb: 34, 48, 62;
--bs-font-roboto:"Segoe UI", Roboto, "sans-serif",
--bs-font-sans-serif: "Public Sans", -apple-system, blinkmacsystemfont, --bs-font-sans-serif: "Public Sans", -apple-system, blinkmacsystemfont,
"Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif; "Helvetica Neue", sans-serif;
@ -89,7 +88,7 @@
); );
--bs-root-font-size: 16px; --bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif); --bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.85rem; --bs-body-font-size: 0.8375rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.375; --bs-body-line-height: 1.375;
--bs-body-color: #646e78; --bs-body-color: #646e78;
@ -9060,7 +9059,7 @@ img[data-app-light-img][data-app-dark-img] {
} }
.table th { .table th {
color: var(--bs-heading-color); color: var(--bs-heading-color);
font-size: 0.8025rem; font-size: 0.8125rem;
letter-spacing: 0.2px; letter-spacing: 0.2px;
text-transform: uppercase; text-transform: uppercase;
} }
@ -20345,7 +20344,7 @@ li:not(:first-child) .dropdown-item,
} }
.fs-6 { .fs-6 {
font-size: 0.8375rem !important; font-size: 0.9375rem !important;
} }
.fs-tiny { .fs-tiny {
@ -32560,7 +32559,9 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
.bg-blue { .bg-blue {
background-color:var(--bs-blue) background-color:var(--bs-blue)
} }
.text-blue{
color:var(--bs-blue)
}
.bg-indigo { .bg-indigo {
background-color:var(--bs-indigo) background-color:var(--bs-indigo)
} }
@ -32573,9 +32574,6 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
.text-red{ .text-red{
color:var(--bs-red) color:var(--bs-red)
} }
.text-blue{ .bg-gray {
color:var(--bs-blue) background:var(--bs-body-color)
}
.text-green{
color:var(--bs-green)
} }

View File

@ -1,879 +0,0 @@
@charset "UTF-8";
:root {
--tagify-dd-color-primary: rgb(53,149,246);
--tagify-dd-bg-color: white;
--tagify-dd-item-pad: .3em .5em;
--tagify-dd-max-height: 300px;
}
.tagify {
--tags-disabled-bg: #F1F1F1;
--tags-border-color: #DDD;
--tags-hover-border-color: #CCC;
--tags-focus-border-color: #3595f6;
--tag-border-radius: 3px;
--tag-bg: rgba(167, 172, 178, 0.5);
--tag-hover: #D3E2E2;
--tag-text-color: black;
--tag-text-color--edit: black;
--tag-pad: 0.3em 0.5em;
--tag-inset-shadow-size: 2em;
--tag-invalid-color: #ff3e1d;
--tag-invalid-bg: rgba(255, 62, 29, 0.5);
--tag--min-width: 1ch;
--tag--max-width: auto;
--tag-hide-transition: 0.3s;
--tag-remove-bg: rgba(255, 62, 29, 0.3);
--tag-remove-btn-color: #7a838b;
--tag-remove-btn-bg: none;
--tag-remove-btn-bg--hover: #ff2804;
--input-color: inherit;
--placeholder-color: rgba(0, 0, 0, 0.4);
--placeholder-color-focus: rgba(0, 0, 0, 0.25);
--loader-size: .8em;
--readonly-striped: 1;
display: inline-flex;
align-items: flex-start;
flex-wrap: wrap;
border: 1px solid var(--tags-border-color);
padding: 0;
line-height: 0;
cursor: text;
outline: none;
position: relative;
box-sizing: border-box;
transition: 0.1s;
}
@keyframes tags--bump {
30% {
transform: scale(1.2);
}
}
@keyframes rotateLoader {
to {
transform: rotate(1turn);
}
}
.tagify:hover:not(.tagify--focus):not(.tagify--invalid) {
--tags-border-color: var(--tags-hover-border-color);
}
.tagify[disabled] {
background: var(--tags-disabled-bg);
filter: saturate(0);
opacity: 0.5;
pointer-events: none;
}
.tagify[readonly].tagify--select, .tagify[disabled].tagify--select {
pointer-events: none;
}
.tagify[readonly]:not(.tagify--mix):not(.tagify--select), .tagify[disabled]:not(.tagify--mix):not(.tagify--select) {
cursor: default;
}
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) > .tagify__input, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) > .tagify__input {
visibility: hidden;
width: 0;
margin: 5px 0;
}
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div {
padding: var(--tag-pad);
}
.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before, .tagify[disabled]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before {
animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused;
}
@keyframes readonlyStyles {
0% {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(0.95);
}
}
.tagify[readonly] .tagify__tag__removeBtn, .tagify[disabled] .tagify__tag__removeBtn {
display: none;
}
.tagify--loading .tagify__input > br:last-child {
display: none;
}
.tagify--loading .tagify__input::before {
content: none;
}
.tagify--loading .tagify__input::after {
content: "";
vertical-align: middle;
opacity: 1;
width: 0.7em;
height: 0.7em;
width: var(--loader-size);
height: var(--loader-size);
min-width: 0;
border: 3px solid;
border-color: #EEE #BBB #888 transparent;
border-radius: 50%;
animation: rotateLoader 0.4s infinite linear;
content: "" !important;
margin: -2px 0 -2px 0.5em;
}
.tagify--loading .tagify__input:empty::after {
margin-left: 0;
}
.tagify + input,
.tagify + textarea {
position: absolute !important;
left: -9999em !important;
transform: scale(0) !important;
}
.tagify__tag {
display: inline-flex;
align-items: center;
max-width: calc(var(--tag--max-width) - 10px);
margin-inline: 5px 0;
margin-block: 5px;
position: relative;
z-index: 1;
outline: none;
line-height: normal;
cursor: default;
transition: 0.13s ease-out;
}
.tagify__tag > div {
vertical-align: top;
box-sizing: border-box;
max-width: 100%;
padding: var(--tag-pad);
color: var(--tag-text-color);
line-height: inherit;
border-radius: var(--tag-border-radius);
white-space: nowrap;
transition: 0.13s ease-out;
}
.tagify__tag > div > * {
white-space: pre-wrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: top;
min-width: var(--tag--min-width);
max-width: var(--tag--max-width);
transition: 0.8s ease, 0.1s color;
}
.tagify__tag > div > *[contenteditable] {
outline: none;
user-select: text;
cursor: text;
margin: -2px;
padding: 2px;
max-width: 350px;
}
.tagify__tag > div::before {
content: "";
position: absolute;
border-radius: inherit;
inset: var(--tag-bg-inset, 0);
z-index: -1;
pointer-events: none;
transition: 120ms ease;
animation: tags--bump 0.3s ease-out 1;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset;
}
.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before {
--tag-bg-inset: -2.5px;
--tag-bg: var(--tag-hover);
}
.tagify__tag--loading {
pointer-events: none;
}
.tagify__tag--loading .tagify__tag__removeBtn {
display: none;
}
.tagify__tag--loading::after {
--loader-size: .4em;
content: "";
vertical-align: middle;
opacity: 1;
width: 0.7em;
height: 0.7em;
width: var(--loader-size);
height: var(--loader-size);
min-width: 0;
border: 3px solid;
border-color: #EEE #BBB #888 transparent;
border-radius: 50%;
animation: rotateLoader 0.4s infinite linear;
margin: 0 0.5em 0 -0.1em;
}
.tagify__tag--flash div::before {
animation: none;
}
.tagify__tag--hide {
width: 0 !important;
padding-left: 0;
padding-right: 0;
margin-left: 0;
margin-right: 0;
opacity: 0;
transform: scale(0);
transition: var(--tag-hide-transition);
pointer-events: none;
}
.tagify__tag--hide > div > * {
white-space: nowrap;
}
.tagify__tag.tagify--noAnim > div::before {
animation: none;
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div > span {
opacity: 0.5;
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before {
--tag-bg: var(--tag-invalid-bg);
transition: 0.2s;
}
.tagify__tag[readonly] .tagify__tag__removeBtn {
display: none;
}
.tagify__tag[readonly] > div::before {
animation: readonlyStyles 1s calc(-1s * (var(--readonly-striped) - 1)) paused;
}
@keyframes readonlyStyles {
0% {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(0.95);
}
}
.tagify__tag--editable > div {
color: var(--tag-text-color--edit);
}
.tagify__tag--editable > div::before {
box-shadow: 0 0 0 2px var(--tag-hover) inset !important;
}
.tagify__tag--editable > .tagify__tag__removeBtn {
pointer-events: none;
}
.tagify__tag--editable > .tagify__tag__removeBtn::after {
opacity: 0;
transform: translateX(100%) translateX(5px);
}
.tagify__tag--editable.tagify--invalid > div::before {
box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important;
}
.tagify__tag__removeBtn {
order: 5;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50px;
cursor: pointer;
font: 14px/1 Arial;
background: var(--tag-remove-btn-bg);
color: var(--tag-remove-btn-color);
width: 14px;
height: 14px;
margin-inline: auto 4.6666666667px;
overflow: hidden;
transition: 0.2s ease-out;
}
.tagify__tag__removeBtn::after {
content: "×";
transition: 0.3s, color 0s;
}
.tagify__tag__removeBtn:hover {
color: white;
background: var(--tag-remove-btn-bg--hover);
}
.tagify__tag__removeBtn:hover + div > span {
opacity: 0.5;
}
.tagify__tag__removeBtn:hover + div::before {
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg, rgba(255, 62, 29, 0.3)) inset !important;
transition: box-shadow 0.2s;
}
.tagify:not(.tagify--mix) .tagify__input br {
display: none;
}
.tagify:not(.tagify--mix) .tagify__input * {
display: inline;
white-space: nowrap;
}
.tagify__input {
flex-grow: 1;
display: inline-block;
min-width: 110px;
margin: 5px;
padding: var(--tag-pad);
line-height: normal;
position: relative;
white-space: pre-wrap;
color: var(--input-color);
box-sizing: inherit;
/* Seems firefox newer versions don't need this any more
@supports ( -moz-appearance:none ){
&::before{
line-height: inherit;
position:relative;
}
}
*/
}
@-moz-document url-prefix() {}
.tagify__input:empty::before {
position: static;
}
.tagify__input:focus {
outline: none;
}
.tagify__input:focus::before {
transition: 0.2s ease-out;
opacity: 0;
transform: translatex(6px);
/* ALL MS BROWSERS: hide placeholder (on focus) otherwise the caret is placed after it, which is weird */
/* IE Edge 12+ CSS styles go here */
}
@supports (-ms-ime-align: auto) {
.tagify__input:focus::before {
display: none;
}
}
.tagify__input:focus:empty::before {
transition: 0.2s ease-out;
opacity: 1;
transform: none;
color: rgba(0, 0, 0, 0.25);
color: var(--placeholder-color-focus);
}
@-moz-document url-prefix() {
.tagify__input:focus:empty::after {
display: none;
}
}
.tagify__input::before {
content: attr(data-placeholder);
height: 1em;
line-height: 1em;
margin: auto 0;
z-index: 1;
color: var(--placeholder-color);
white-space: nowrap;
pointer-events: none;
opacity: 0;
position: absolute;
}
.tagify__input::after {
content: attr(data-suggest);
display: inline-block;
vertical-align: middle;
position: absolute;
min-width: calc(100% - 1.5em);
text-overflow: ellipsis;
overflow: hidden;
white-space: pre; /* allows spaces at the beginning */
color: var(--tag-text-color);
opacity: 0.3;
pointer-events: none;
max-width: 100px;
}
.tagify__input .tagify__tag {
margin: 0 1px;
}
.tagify--mix {
display: block;
}
.tagify--mix .tagify__input {
padding: 5px;
margin: 0;
width: 100%;
height: 100%;
line-height: 1.5;
display: block;
}
.tagify--mix .tagify__input::before {
height: auto;
display: none;
line-height: inherit;
}
.tagify--mix .tagify__input::after {
content: none;
}
.tagify--select::after {
content: ">";
opacity: 0.5;
position: absolute;
top: 50%;
right: 0;
bottom: 0;
font: 16px monospace;
line-height: 8px;
height: 8px;
pointer-events: none;
transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg);
transition: 0.2s ease-in-out;
}
.tagify--select[aria-expanded=true]::after {
transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2);
}
.tagify--select .tagify__tag {
position: absolute;
top: 0;
right: 1.8em;
bottom: 0;
}
.tagify--select .tagify__tag div {
display: none;
}
.tagify--select .tagify__input {
width: 100%;
}
.tagify--empty .tagify__input::before {
transition: 0.2s ease-out;
opacity: 1;
transform: none;
display: inline-block;
width: auto;
}
.tagify--mix .tagify--empty .tagify__input::before {
display: inline-block;
}
.tagify--focus {
--tags-border-color: var(--tags-focus-border-color);
transition: 0s;
}
.tagify--invalid {
--tags-border-color: #ff3e1d;
}
.tagify__dropdown {
position: absolute;
z-index: 9999;
transform: translateY(-1px);
border-top: 1px solid var(--tagify-dd-color-primary);
overflow: hidden;
}
.tagify__dropdown[dir=rtl] {
transform: translate(-100%, -1px);
}
.tagify__dropdown[placement=top] {
margin-top: 0;
transform: translateY(-100%);
}
.tagify__dropdown[placement=top] .tagify__dropdown__wrapper {
border-top-width: 1.1px;
border-bottom-width: 0;
}
.tagify__dropdown[position=text] {
box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), 0.1);
font-size: 0.9em;
}
.tagify__dropdown[position=text] .tagify__dropdown__wrapper {
border-width: 1px;
}
.tagify__dropdown__wrapper {
max-height: var(--tagify-dd-max-height);
overflow: hidden;
overflow-x: hidden;
background: var(--tagify-dd-bg-color);
border: 1px solid;
border-color: var(--tagify-dd-color-primary);
border-bottom-width: 1.5px;
border-top-width: 0;
box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.2);
transition: 0.3s cubic-bezier(0.5, 0, 0.3, 1), transform 0.15s;
animation: dd-wrapper-show 0s 0.3s forwards;
}
@keyframes dd-wrapper-show {
to {
overflow-y: auto;
}
}
.tagify__dropdown__header:empty {
display: none;
}
.tagify__dropdown__footer {
display: inline-block;
margin-top: 0.5em;
padding: var(--tagify-dd-item-pad);
font-size: 0.7em;
font-style: italic;
opacity: 0.5;
}
.tagify__dropdown__footer:empty {
display: none;
}
.tagify__dropdown--initial .tagify__dropdown__wrapper {
max-height: 20px;
transform: translateY(-1em);
}
.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper {
transform: translateY(2em);
}
.tagify__dropdown__item {
box-sizing: border-box;
padding: var(--tagify-dd-item-pad);
margin: 1px;
white-space: pre-wrap;
cursor: pointer;
border-radius: 2px;
position: relative;
outline: none;
max-height: 60px;
max-width: 100%;
/* custom hidden transition effect is needed for horizontal-layout suggestions */
}
.tagify__dropdown__item--active {
background: var(--tagify-dd-color-primary);
color: white;
}
.tagify__dropdown__item:active {
filter: brightness(105%);
}
.tagify__dropdown__item--hidden {
padding-top: 0;
padding-bottom: 0;
margin: 0 1px;
pointer-events: none;
overflow: hidden;
max-height: 0;
transition: var(--tagify-dd-item--hidden-duration, 0.3s) !important;
}
.tagify__dropdown__item--hidden > * {
transform: translateY(-100%);
opacity: 0;
transition: inherit;
}
/* Suggestions items */
.tagify__dropdown.users-list {
font-size: 1rem;
}
.tagify__dropdown.users-list .addAll {
display: block !important;
}
.tagify__dropdown.users-list .tagify__dropdown__item {
padding: 0.5em 0.7em;
display: grid;
grid-template-columns: auto 1fr;
gap: 0 1em;
grid-template-areas: "avatar name" "avatar email";
}
.tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
grid-area: avatar;
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
transition: 0.1s ease-out;
}
.tagify__dropdown.users-list img {
width: 100%;
vertical-align: top;
}
.tagify__dropdown.users-list strong {
grid-area: name;
width: 100%;
align-self: center;
font-weight: 500;
}
.tagify__dropdown.users-list span {
grid-area: email;
width: 100%;
font-size: 0.9em;
opacity: 0.6;
}
/* Tags items */
.tagify__tag {
white-space: nowrap;
}
.tagify__tag .tagify__tag__avatar-wrap {
width: 22px;
height: 22px;
white-space: normal;
border-radius: 50%;
margin-right: 5px;
transition: 0.12s ease-out;
vertical-align: middle;
}
.tagify__tag img {
width: 100%;
vertical-align: top;
}
[dir=rtl] .tagify__tag .tagify__tag__avatar-wrap {
margin-left: 5px;
margin-right: auto;
}
.light-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
background: #f5f5f9;
}
.light-style .tagify__tag .tagify__tag__avatar-wrap {
background: #f5f5f9;
}
.light-style .tagify__dropdown.users-list .addAll {
border-bottom: 1px solid #e4e6e8;
}
.dark-style .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
background: #232333;
}
.dark-style .tagify__tag .tagify__tag__avatar-wrap {
background: #232333;
}
.dark-style .tagify__dropdown.users-list .addAll {
border-bottom: 1px solid #4e4f6c;
}
.tags-inline .tagify__dropdown__wrapper {
padding: 0 0.4375rem 0.4375rem 0.4375rem;
}
.tags-inline .tagify__dropdown__item {
display: inline-block;
border-radius: 3px;
padding: 0.3em 0.5em;
margin: 0.4375rem 0.4375rem 0 0;
font-size: 0.85em;
transition: 0s;
}
[dir=rtl] .tags-inline .tagify__dropdown__item {
margin: 0.4375rem 0 0 0.4375rem;
}
.light-style .tags-inline .tagify__dropdown__item {
border: 1px solid #e4e6e8;
color: #646e78;
}
.dark-style .tags-inline .tagify__dropdown__item {
border: 1px solid #4e4f6c;
color: #b2b2c4;
}
.tagify-email-list {
display: inline-block;
min-width: 0;
border: none;
/* Do not show the "remove tag" (x) button when only a single tag remains */
}
.tagify-email-list.tagify {
padding: 0 !important;
padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important;
}
.tagify-email-list.tagify {
padding: 0 !important;
padding-bottom: calc(0.4375rem - var(--bs-border-width)) !important;
}
.tagify-email-list.tagify.tagify--focus {
padding-left: 0 !important;
}
.tagify-email-list .tagify__tag {
margin: 0;
margin-inline-start: 0 !important;
margin-inline-end: 0.625rem !important;
margin-bottom: 0.4375rem !important;
}
.tagify-email-list .tagify__tag > div {
padding: 0.21875rem 0.4375rem !important;
padding-inline: 0.875rem !important;
}
.tagify-email-list .tagify__tag:only-of-type > div {
padding-inline: 0.4375rem !important;
}
.tagify-email-list .tagify__tag:only-of-type .tagify__tag__removeBtn {
display: none;
}
.tagify-email-list .tagify__tag__removeBtn {
opacity: 0;
transform: translateX(-6px) scale(0.5);
margin-left: -3ch;
transition: 0.12s;
position: absolute;
inset-inline-end: 0;
}
.tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn {
transform: none;
opacity: 1;
margin-left: -1ch;
}
.tagify-email-list .tagify__input {
display: none;
}
.tagify__tag > div {
border-radius: 50rem;
}
[dir=rtl] .tagify-email-list .tagify__tag {
margin: 0 0.4375rem 0.4375rem 0;
}
[dir=rtl] .tagify-email-list .tagify__tag:hover .tagify__tag__removeBtn {
margin-left: auto;
margin-right: -1ch;
}
[dir=rtl] .tagify-email-list .tagify__tag__removeBtn {
transform: translateX(6px) scale(0.5);
margin-left: auto;
margin-right: -3ch;
}
.light-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before {
box-shadow: 0 0 0 2px #e4e6e8 inset !important;
}
.dark-style .tagify-email-list .tagify__tag--editable:not(.tagify--invalid) > div::before {
box-shadow: 0 0 0 2px #4e4f6c inset !important;
}
.tagify.form-control {
transition: none;
display: flex;
align-items: flex-end;
/* padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.4231rem !important; */
padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.2rem !important;
}
.fv-plugins-bootstrap5-row-invalid .tagify.form-control {
padding: 0 calc(0.4375rem - var(--bs-border-width)) calc(0.4375rem - 2px) !important;
}
.tagify.tagify--focus, .tagify.form-control:focus {
padding: 0 calc(0.4375rem - var(--bs-border-width)) 0.3606rem !important;
border-width: 2px;
}
.tagify__tag, .tagify__input {
margin: 0.1875rem 0.625rem 0 0 !important;
line-height: 1;
}
.tagify__input {
line-height: 1.5rem;
}
.tagify__input:empty::before {
top: 4px;
}
.tagify__tag > div {
line-height: 1.5rem;
padding: 0 0 0 0.4375rem;
}
.tagify__tag__removeBtn {
margin-right: 0.1375rem;
margin-left: 0.21875rem;
font-family: "boxicons";
font-size: 1rem;
opacity: 0.7;
}
.tagify__tag__removeBtn:hover {
background: none;
color: #ff2804 !important;
}
.tagify__tag__removeBtn::after {
content: "\ef06";
}
.tagify__tag:hover:not([readonly]) div::before, .tagify__tag:focus div::before {
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.tagify__dropdown {
transform: translateY(0);
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag > div {
padding: 0 0.4375rem 0 0.4375rem !important;
}
.tagify__input {
padding: 0;
}
.tagify__tag-text {
font-size: 0.8125rem;
font-weight: 500;
}
.tagify.form-control {
padding-top: 0.1412rem !important;
}
.tagify.tagify--focus, .tagify.form-control:focus {
padding-top: calc(0.1412rem - 1px) !important;
}
.tagify__tag__removeBtn {
margin-inline-end: 0.3rem;
}
[dir=rtl] .tagify__tag, [dir=rtl] .tagify__input {
margin: 0.4375rem 0 0 0.4375rem;
}
[dir=rtl] .tagify + input,
[dir=rtl] .tagify + textarea {
left: 0;
right: -9999em !important;
}
[dir=rtl] .tagify__tag > div {
padding: 0 0.6875rem 0 0;
}
[dir=rtl] .tagify__tag__removeBtn {
margin-left: 0.4375rem;
margin-right: 0.21875rem;
}
.light-style .tagify__tag > div::before {
box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.08) inset;
}
.light-style .tagify__tag .tagify__tag-text {
color: #384551;
}
.light-style .tagify__tag:hover:not([readonly]) div::before, .light-style .tagify__tag:focus div::before {
box-shadow: 0 0 0 1.3em rgba(34, 48, 62, 0.12) inset;
}
.light-style .tagify__tag__removeBtn {
color: #7a838b;
}
.light-style .tagify__tag__removeBtn:hover + div::before {
background: rgba(255, 62, 29, 0.3);
}
.light-style .tagify:hover:not([readonly]) {
border-color: #ced1d5;
}
.light-style .tagify__input::before {
color: #a7acb2 !important;
}
.light-style .tagify__dropdown {
box-shadow: 0 0.25rem 0.75rem 0 rgba(34, 48, 62, 0.14);
border-top-color: #e4e6e8;
}
.light-style .tagify__dropdown__wrapper {
background: #fff;
border-color: #e4e6e8;
}
.dark-style .tagify__tag > div::before {
box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.08) inset;
}
.dark-style .tagify__tag > div .tagify__tag-text {
color: #d5d5e2;
}
.dark-style .tagify__tag:hover:not([readonly]) div::before, .dark-style .tagify__tag:focus div::before {
box-shadow: 0 0 0 1.3em rgba(230, 230, 241, 0.12) inset;
}
.dark-style .tagify__tag__removeBtn {
color: #a1a1b5;
}
.dark-style .tagify__tag__removeBtn:hover + div::before {
background: rgba(255, 62, 29, 0.3);
}
.dark-style .tagify:hover:not([readonly]) {
border-color: #5f607b;
}
.dark-style .tagify__input::before {
color: #7e7f96 !important;
}
.dark-style .tagify[readonly]:not(.tagify--mix) .tagify__tag > div::before {
background: linear-gradient(45deg, #5f607b 25%, transparent 25%, transparent 50%, #5f607b 50%, #5f607b 75%, transparent 75%, transparent) 0/5px 5px;
}
.dark-style .tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag > div::before {
animation: none;
box-shadow: none;
}
.dark-style .tagify__dropdown {
box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24);
border-top-color: #4e4f6c;
}
.dark-style .tagify__dropdown__wrapper {
box-shadow: 0 0.25rem 0.75rem 0 rgba(20, 20, 29, 0.24);
background: #2b2c40;
border-color: #4e4f6c;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/img/avatars/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/img/avatars/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/img/avatars/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/img/avatars/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/img/brand/logo-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/img/brand/logo-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/img/brand/logo-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/img/brand/logo-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/img/brand/logo-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/img/brand/logo-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

BIN
public/img/elements/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/img/elements/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/img/elements/12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/img/elements/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/img/elements/17.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/img/elements/18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/img/elements/19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/img/elements/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/img/elements/20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/img/elements/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/img/elements/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/img/elements/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/img/elements/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/img/hero/bg-012.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

BIN
public/img/hero/bg-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
public/img/hero/bg-2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
public/img/hero/bg-3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@ -1,5 +0,0 @@
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" d="M46.5001 10.5288H32.5001L20.2251 26.5288L32.5001 56.5288L60.5001 26.5288L46.5001 10.5288Z" fill="#03C3EC"/>
<path d="M18.5 10.5288H46.5L60.5 26.5288L32.5 56.5288L4.5 26.5288L18.5 10.5288Z" stroke="#03C3EC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2934 9.92012C33.1042 9.67343 32.8109 9.52881 32.5 9.52881C32.1891 9.52881 31.8958 9.67343 31.7066 9.92012L19.7318 25.5288H4.5C3.94772 25.5288 3.5 25.9765 3.5 26.5288C3.5 27.0811 3.94772 27.5288 4.5 27.5288H19.5537L31.5745 56.9075C31.7282 57.2833 32.094 57.5288 32.5 57.5288C32.906 57.5288 33.2718 57.2833 33.4255 56.9075L45.4463 27.5288H60.5C61.0523 27.5288 61.5 27.0811 61.5 26.5288C61.5 25.9765 61.0523 25.5288 60.5 25.5288H45.2682L33.2934 9.92012ZM42.7474 25.5288L32.5 12.1717L22.2526 25.5288H42.7474ZM21.7146 27.5288L32.5 53.8881L43.2854 27.5288H21.7146Z" fill="#03C3EC"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

42
public/img/sneat.svg Normal file
View File

@ -0,0 +1,42 @@
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path
d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z"
id="path-1"></path>
<path
d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z"
id="path-3"></path>
<path
d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z"
id="path-4"></path>
<path
d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z"
id="path-5"></path>
</defs>
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
<g id="Icon" transform="translate(27.000000, 15.000000)">
<g id="Mask" transform="translate(0.000000, 8.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#696cff" xlink:href="#path-1"></use>
<g id="Path-3" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-3"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
</g>
<g id="Path-4" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-4"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
</g>
</g>
<g id="Triangle"
transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
<use fill="#696cff" xlink:href="#path-5"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -6,7 +6,12 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from "./layouts/AuthLayout"; import { queryClient } from "./layouts/AuthLayout";
import ModalProvider from "./ModalProvider"; import ModalProvider from "./ModalProvider";
window.addEventListener("unhandledrejection", (event) => {
if (event.reason?.message?.includes("Failed to fetch")) {
event.preventDefault();
console.debug("Network issue (fetch failed) - suppressed");
}
});
const App = () => { const App = () => {
return ( return (

View File

@ -3,26 +3,20 @@ import { useOrganizationModal } from "./hooks/useOrganization";
import OrganizationModal from "./components/Organization/OrganizationModal"; import OrganizationModal from "./components/Organization/OrganizationModal";
import { useAuthModal, useModal } from "./hooks/useAuth"; import { useAuthModal, useModal } from "./hooks/useAuth";
import SwitchTenant from "./pages/authentication/SwitchTenant"; import SwitchTenant from "./pages/authentication/SwitchTenant";
import ChangePasswordPage from "./pages/authentication/ChangePassword"; import { ProjectModal } from "./components/Project/ManageProjectInfo";
import NewCollection from "./components/collections/ManageCollection";
import ServiceProjectTeamAllocation from "./components/ServiceProject/ServiceProjectTeam/ServiceProjectTeamAllocation";
const ModalProvider = () => { const ModalProvider = () => {
const { isOpen, onClose } = useOrganizationModal(); const { isOpen, onClose } = useOrganizationModal();
const { isOpen: isAuthOpen } = useAuthModal(); const { isOpen: isAuthOpen } = useAuthModal();
const { isOpen: isChangePass } = useModal("ChangePassword"); const {isOpen:isOpenProject} = useModal("ManageProject")
const { isOpen: isCollectionNew } = useModal("newCollection");
const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation");
return ( return (
<> <>
{isOpen && <OrganizationModal />} {isOpen && <OrganizationModal />}
{isAuthOpen && <SwitchTenant />} {isAuthOpen && <SwitchTenant />}
{isChangePass && <ChangePasswordPage />} {isOpenProject && <ProjectModal/>}
{isCollectionNew && <NewCollection />}
{isServiceTeamAllocation && <ServiceProjectTeamAllocation />}
</> </>
); );
}; };
export default ModalProvider; export default ModalProvider;

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -72,7 +72,7 @@
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-root-font-size: 16px; --bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif); --bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.875rem; --bs-body-font-size: 0.9375rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.375; --bs-body-line-height: 1.375;
--bs-body-color: #646e78; --bs-body-color: #646e78;

View File

@ -135,6 +135,7 @@ const AttendLogs = ({ Id }) => {
</p> </p>
)} )}
</div> </div>
{loading && <p>Loading..</p>} {loading && <p>Loading..</p>}
{logs && logs.length > 0 && ( {logs && logs.length > 0 && (
<> <>
@ -168,7 +169,7 @@ const AttendLogs = ({ Id }) => {
</div> </div>
</td> </td>
<td>{convertShortTime(log.activityTime)}</td> <td>{convertShortTime(log.activityTime)}</td>
<td> <td>
{log?.latitude != 0 ? ( {log?.latitude != 0 ? (
<i <i

View File

@ -11,9 +11,18 @@ import { useSelector } from "react-redux";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import Pagination from "../common/Pagination";
import { SpinnerLoader } from "../common/Loader"; import { SpinnerLoader } from "../common/Loader";
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, }) => { const Attendance = ({
getRole,
handleModalData,
searchTerm,
projectId,
organizationId,
includeInactive,
date,
}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
@ -24,12 +33,12 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
attendance, attendance,
loading: attLoading, loading: attLoading,
recall: attrecall, recall: attrecall,
isFetching isFetching,
} = useAttendance(selectedProject, organizationId); } = useAttendance(selectedProject, organizationId, includeInactive, date);
const filteredAttendance = ShowPending const filteredAttendance = ShowPending
? attendance?.filter( ? attendance?.filter(
(att) => att?.checkInTime !== null && att?.checkOutTime === null (att) => att?.checkInTime !== null && att?.checkOutTime === null
) )
: attendance; : attendance;
const attendanceList = Array.isArray(filteredAttendance) const attendanceList = Array.isArray(filteredAttendance)
@ -71,19 +80,19 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
); );
// Reset pagination when the filter or search term changes // Reset pagination when the filter or search term changes
useEffect(() => { useEffect(() => {}, [finalFilteredData]);
}, [finalFilteredData]);
const handler = useCallback( const handler = useCallback(
(msg) => { (msg) => {
if (selectedProject == msg.projectId) { if (selectedProject == msg.projectId) {
queryClient.setQueryData(["attendance", selectedProject], (oldData) => { queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
if (!oldData) { if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendance"] }) queryClient.invalidateQueries({ queryKey: ["attendance"] });
}; }
return oldData.map((record) => return oldData.map((record) =>
record.employeeId === msg.response.employeeId ? { ...record, ...msg.response } : record record.employeeId === msg.response.employeeId
? { ...record, ...msg.response }
: record
); );
}); });
} }
@ -111,190 +120,147 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat
return ( return (
<> <>
<div className="d-flex justify-content-between align-items-center py-2 px-2"> <div>
{/* Left side - Date */} <div className="table-responsive text-nowrap ">
<div className="text-start"> <div className="d-flex justify-content-between align-items-center py-2">
<strong>Date: {formatUTCToLocalTime(todayDate)}</strong> <strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
</div> <div className="form-check form-switch text-start m-0 ms-5">
<input
{/* Right side - Pending Attendance toggle */} type="checkbox"
<div className="form-check form-switch m-0"> className="form-check-input"
<input role="switch"
type="checkbox" id="inactiveEmployeesCheckbox"
className="form-check-input" disabled={isFetching}
role="switch" checked={ShowPending}
id="inactiveEmployeesCheckbox" onChange={(e) => setShowPending(e.target.checked)}
disabled={isFetching} />
checked={ShowPending} <label className="form-check-label ms-0">Show Pending</label>
onChange={(e) => setShowPending(e.target.checked)} </div>
/>
<label className="form-check-label" htmlFor="inactiveEmployeesCheckbox">
Pending Attendance
</label>
</div>
</div>
<div
className="table-responsive modal-min-h text-nowrap h-100"
style={{ minHeight: "200px" }} // Ensures fixed height
>
{attLoading ? (
<div
className="d-flex justify-content-center align-items-center"
style={{ minHeight: "50vh" }}
>
<SpinnerLoader />
</div> </div>
) : currentItems?.length > 0 ? ( {attLoading ? (
<div
<> className="d-flex justify-content-center align-items-center"
<table className="table "> style={{ minHeight: "70vh" }}
<thead> >
<tr className="border-top-1"> <SpinnerLoader />
<th colSpan={2}>Name</th> </div>
<th>Role</th> ) : currentItems?.length > 0 ? (
<th>Organization</th> <>
<th> <table className="table table-hover ">
<i className="bx bxs-down-arrow-alt text-success"></i> <thead>
Check-In <tr className="border-top-1">
</th> <th colSpan={2}>Name</th>
<th> <th className="text-start actions-col text-center">Role</th>
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out {/* <th>Organization</th> */}
</th> <th>
<th>Actions</th> <i className="bx bxs-down-arrow-alt text-success"></i>
</tr> Check-In
</thead> </th>
<tbody className="table-border-bottom-0 "> <th>
{currentItems && <i className="bx bxs-up-arrow-alt text-danger"></i>
currentItems Check-Out
.sort((a, b) => { </th>
const checkInA = a?.checkInTime <th className="actions-col">Actions</th>
? new Date(a.checkInTime)
: new Date(0);
const checkInB = b?.checkInTime
? new Date(b.checkInTime)
: new Date(0);
return checkInB - checkInA;
})
.map((item) => (
<tr key={item.employeeId}>
<td colSpan={2}>
<div className="d-flex justify-content-start align-items-center">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={(e) =>
navigate(
`/employee/${item.employeeId}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.lastName}
</span>
</a>
</div>
</div>
</td>
<td>{item.jobRoleName}</td>
<td>{item.organizationName || "--"}</td>
<td>
{item.checkInTime
? convertShortTime(item.checkInTime)
: "--"}
</td>
<td>
{item.checkOutTime
? convertShortTime(item.checkOutTime)
: "--"}
</td>
<td className="text-center">
<RenderAttendanceStatus
attendanceData={item}
handleModalData={handleModalData}
Tab={1}
currentDate={null}
/>
</td>
</tr>
))}
{!attendance && (
<tr>
<td
colSpan={7}
className="text-center text-secondary"
style={{ height: "200px" }}
>
No employees assigned to the project!
</td>
</tr> </tr>
)} </thead>
</tbody> <tbody className="table-border-bottom-0 ">
</table> {currentItems &&
</> currentItems
) : ( .sort((a, b) => {
<div const checkInA = a?.checkInTime
className="d-flex justify-content-center align-items-center text-muted" ? new Date(a.checkInTime)
style={{ height: "200px" }} : new Date(0);
> const checkInB = b?.checkInTime
{searchTerm ? new Date(b.checkInTime)
? "No results found for your search." : new Date(0);
: attendanceList.length === 0 return checkInB - checkInA;
})
.map((item) => (
<tr key={item.employeeId}>
<td colSpan={2}>
<div className="d-flex justify-content-start align-items-center">
<Avatar
firstName={item.firstName}
lastName={item.lastName}
></Avatar>
<div className="d-flex flex-column">
<a
onClick={(e) =>
navigate(
`/employee/${item.employeeId}?for=attendance`
)
}
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.lastName}
</span>
</a>
</div>
</div>
</td>
<td className="text-start action-col">{item.jobRoleName}</td>
{/* <td>{item.organizationName || "--"}</td> */}
<td>
{item.checkInTime
? convertShortTime(item.checkInTime)
: "--"}
</td>
<td>
{item.checkOutTime
? convertShortTime(item.checkOutTime)
: "--"}
</td>
<td className="text-center actions-col">
<RenderAttendanceStatus
attendanceData={item}
handleModalData={handleModalData}
Tab={1}
currentDate={null}
/>
</td>
</tr>
))}
{!attendance && (
<tr>
<td
colSpan={7}
className="text-center text-secondary"
style={{ height: "200px" }}
>
No employees assigned to the project!
</td>
</tr>
)}
</tbody>
</table>
</>
) : (
<div
className="d-flex justify-content-center align-items-center text-muted"
style={{ height: "200px" }}
>
{searchTerm
? "No results found for your search."
: attendanceList.length === 0
? "No employees assigned to the project." ? "No employees assigned to the project."
: "No pending records available."} : "No pending records available."}
</div> </div>
)}
</div>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={paginate}
/>
)} )}
</div> </div>
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page ">
<ul className="pagination pagination-sm justify-content-end py-1">
<li
className={`page-item ${currentPage === 1 ? "disabled" : ""
}`}
>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link "
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</> </>
); );
}; };
export default Attendance; export default Attendance;

View File

@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository";
import { useAttendancesLogs } from "../../hooks/useAttendance"; import { useAttendancesLogs } from "../../hooks/useAttendance";
import { queryClient } from "../../layouts/AuthLayout"; import { queryClient } from "../../layouts/AuthLayout";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import Pagination from "../common/Pagination";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SpinnerLoader } from "../common/Loader"; import { SpinnerLoader } from "../common/Loader";
@ -45,7 +46,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPending, setShowPending] = useState(false); const [showPending, setShowPending] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [processedData, setProcessedData] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
const today = new Date(); const today = new Date();
@ -69,52 +72,93 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
}; };
const sortByName = (a, b) => { const sortByName = (a, b) => {
const nameA = (a.firstName + a.lastName).toLowerCase(); const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
const nameB = (b.firstName + b.lastName).toLowerCase(); const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
return nameA.localeCompare(nameB); return nameA?.localeCompare(nameB);
}; };
const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs( const {
data = [],
isLoading,
error,
refetch,
isFetching,
} = useAttendancesLogs(
selectedProject, selectedProject,
dateRange.startDate, dateRange.startDate,
dateRange.endDate, dateRange.endDate,
organizationId organizationId
); );
const filtering = useCallback(
(dataToFilter) => {
const filteredData = showPending
? dataToFilter.filter((item) => item.checkOutTime === null)
: dataToFilter;
const processedData = useMemo(() => { const group1 = filteredData
const filteredData = showPending .filter((d) => d.activity === 1 && isSameDay(d.checkInTime))
? data.filter((item) => item.checkOutTime === null) .sort(sortByName);
: data; const group2 = filteredData
.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime))
.sort(sortByName);
const group3 = filteredData
.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime))
.sort(sortByName);
const group4 = filteredData.filter(
(d) => d.activity === 4 && isBeforeToday(d.checkOutTime)
);
const group5 = filteredData
.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime))
.sort(sortByName);
const group6 = filteredData
.filter((d) => d.activity === 5)
.sort(sortByName);
const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName); const sortedList = [
const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName); ...group1,
const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName); ...group2,
const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime)); ...group3,
const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName); ...group4,
const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName); ...group5,
...group6,
];
const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6]; // Group by date
const groupedByDate = sortedList.reduce((acc, item) => {
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0];
if (date) {
acc[date] = acc[date] || [];
acc[date].push(item);
}
return acc;
}, {});
const groupedByDate = sortedList.reduce((acc, item) => { const sortedDates = Object.keys(groupedByDate).sort(
const date = (item.checkInTime || item.checkOutTime)?.split("T")[0]; (a, b) => new Date(b) - new Date(a)
if (date) { );
acc[date] = acc[date] || [];
acc[date].push(item);
}
return acc;
}, {});
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a)); const finalData = sortedDates.flatMap((date) => groupedByDate[date]);
return sortedDates.flatMap((date) => groupedByDate[date]); setProcessedData(finalData);
},
[showPending]
);
useEffect(() => {
if (data?.length) {
filtering(data);
}
}, [data, showPending]); }, [data, showPending]);
// New useEffect to handle search filtering
const filteredSearchData = useMemo(() => { const filteredSearchData = useMemo(() => {
if (!searchTerm) return processedData; if (!searchTerm) {
return processedData;
const lowercased = searchTerm.toLowerCase(); }
return processedData.filter((item) => const lowercasedSearchTerm = searchTerm.toLowerCase();
`${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased) return processedData.filter((item) => {
); const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
return fullName.includes(lowercasedSearchTerm);
});
}, [processedData, searchTerm]); }, [processedData, searchTerm]);
const { const {
@ -127,27 +171,34 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
useEffect(() => { useEffect(() => {
resetPage(); resetPage();
}, [filteredSearchData]); }, [filteredSearchData, resetPage]);
const handler = useCallback( const handler = useCallback(
(msg) => { (msg) => {
const { startDate, endDate } = dateRange; const { startDate, endDate } = dateRange;
const checkIn = msg.response.checkInTime.substring(0, 10); const checkIn = msg.response.checkInTime.substring(0, 10);
if (
if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) { selectedProject === msg.projectId &&
startDate <= checkIn &&
checkIn <= endDate
) {
queryClient.setQueriesData(["attendanceLogs"], (oldData) => { queryClient.setQueriesData(["attendanceLogs"], (oldData) => {
if (!oldData) { if (!oldData) {
queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] }); queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] });
return; return;
} }
return oldData.map((record) => const updatedAttendance = oldData.map((record) =>
record.id === msg.response.id ? { ...record, ...msg.response } : record record.id === msg.response.id
? { ...record, ...msg.response }
: record
); );
filtering(updatedAttendance);
return updatedAttendance;
}); });
resetPage(); resetPage();
} }
}, },
[selectedProject, dateRange, resetPage] [selectedProject, dateRange, filtering, resetPage]
); );
useEffect(() => { useEffect(() => {
@ -159,10 +210,18 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
(msg) => { (msg) => {
const { startDate, endDate } = dateRange; const { startDate, endDate } = dateRange;
if (data.some((item) => item.employeeId == msg.employeeId)) { if (data.some((item) => item.employeeId == msg.employeeId)) {
// dispatch(
// fetchAttendanceData({
// ,
// fromDate: startDate,
// toDate: endDate,
// })
// );
refetch(); refetch();
} }
}, },
[data, refetch] [selectedProject, dateRange, data, refetch]
); );
useEffect(() => { useEffect(() => {
@ -170,64 +229,37 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
return () => eventBus.off("employee", employeeHandler); return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]); }, [employeeHandler]);
return ( return (
<> <>
<div <div
className="dataTables_length text-start py-2 d-flex flex-wrap justify-content-between align-items-center" className="dataTables_length text-start py-2 d-flex justify-content-between "
id="DataTables_Table_0_length" id="DataTables_Table_0_length"
> >
{/* Left Side - Date Picker */} <div className=" col-12">
<div className="d-flex align-items-center">
<DateRangePicker <DateRangePicker
onRangeChange={setDateRange} onRangeChange={setDateRange}
defaultStartDate={yesterday} defaultStartDate={yesterday}
/> />
</div> </div>
{/* Right Side - Pending Attendance Switch */}
<div className="form-check form-switch d-flex align-items-center mb-2">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
disabled={isFetching}
checked={showPending}
onChange={(e) => setShowPending(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
>
Pending Attendance
</label>
</div>
</div> </div>
<div className="table-responsive text-nowrap ">
<div
className="table-responsive modal-min-h text-nowrap"
style={{ minHeight: "200px" }}
>
{isLoading ? ( {isLoading ? (
<div <div
className="d-flex justify-content-center align-items-center" className="d-flex justify-content-center align-items-center"
style={{ minHeight: "50vh" }} style={{ minHeight: "70vh" }}
> >
<SpinnerLoader /> <SpinnerLoader/>
</div> </div>
) : filteredSearchData?.length > 0 ? ( ) : filteredSearchData?.length > 0 ? (
<table className="table mb-0 table-hover">
<table className="table mb-0">
<thead> <thead>
<tr> <tr>
<th className="border-top-1" colSpan={2}> <th className="border-top-1" colSpan={2}>
Name Name
</th> </th>
<th className="border-top-1">Date</th> <th className="border-top-1">Date</th>
<th>Organization</th> {/* <th>Organization</th> */}
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i>{" "} <i className="bx bxs-down-arrow-alt text-success"></i>{" "}
Check-In Check-In
@ -235,7 +267,7 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
<th> <th>
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out <i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
</th> </th>
<th>Action</th> <th className="actions-col">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -246,9 +278,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
const previousAttendance = arr[index - 1]; const previousAttendance = arr[index - 1];
const previousDate = previousAttendance const previousDate = previousAttendance
? moment( ? moment(
previousAttendance.checkInTime || previousAttendance.checkInTime ||
previousAttendance.checkOutTime previousAttendance.checkOutTime
).format("YYYY-MM-DD") ).format("YYYY-MM-DD")
: null; : null;
if (!previousDate || currentDate !== previousDate) { if (!previousDate || currentDate !== previousDate) {
@ -276,7 +308,9 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<a <a
onClick={() => onClick={() =>
navigate(`/employee/${attendance.employeeId}?for=attendance`) navigate(
`/employee/${attendance.employeeId}?for=attendance`
)
} }
className="text-heading text-truncate cursor-pointer" className="text-heading text-truncate cursor-pointer"
> >
@ -292,14 +326,14 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
attendance.checkInTime || attendance.checkOutTime attendance.checkInTime || attendance.checkOutTime
).format("DD-MMM-YYYY")} ).format("DD-MMM-YYYY")}
</td> </td>
<td>{attendance.organizationName || "--"}</td> {/* <td>{attendance.organizationName || "--"}</td> */}
<td>{convertShortTime(attendance.checkInTime)}</td> <td>{convertShortTime(attendance.checkInTime)}</td>
<td> <td>
{attendance.checkOutTime {attendance.checkOutTime
? convertShortTime(attendance.checkOutTime) ? convertShortTime(attendance.checkOutTime)
: "--"} : "--"}
</td> </td>
<td className="text-center"> <td className="text-center actions-col">
<RenderAttendanceStatus <RenderAttendanceStatus
attendanceData={attendance} attendanceData={attendance}
handleModalData={handleModalData} handleModalData={handleModalData}
@ -314,10 +348,13 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
</tbody> </tbody>
</table> </table>
) : ( ) : (
<div className="my-12"> <div
<span className="text-secondary"> className="d-flex justify-content-center align-items-center"
No attendance record found in selected date range. style={{ minHeight: "70vh" }}
</span> >
<p className="text-secondary mb-0">
No data for this date range. Please choose another.
</p>
</div> </div>
)} )}
</div> </div>
@ -330,45 +367,11 @@ const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => {
</div> </div>
)} )}
{filteredSearchData.length > ITEMS_PER_PAGE && ( {filteredSearchData.length > ITEMS_PER_PAGE && (
<nav aria-label="Page "> <Pagination
<ul className="pagination pagination-sm justify-content-end py-1"> currentPage={currentPage}
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}> totalPages={totalPages}
<button onPageChange={paginate}
className="page-link btn-xs" />
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
(pageNumber) => (
<li
key={pageNumber}
className={`page-item ${currentPage === pageNumber ? "active" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(pageNumber)}
>
{pageNumber}
</button>
</li>
)
)}
<li
className={`page-item ${currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)} )}
</> </>
); );

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -9,6 +9,7 @@ import showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils"; import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance"; import { useMarkAttendance } from "../../hooks/useAttendance";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
const createSchema = (modeldata) => { const createSchema = (modeldata) => {
return z return z
@ -19,31 +20,36 @@ const createSchema = (modeldata) => {
.max(200, "Description should be less than 200 characters") .max(200, "Description should be less than 200 characters")
.optional(), .optional(),
}) })
.refine((data) => { .refine(
if (modeldata?.checkInTime && !modeldata?.checkOutTime) { (data) => {
const checkIn = new Date(modeldata.checkInTime); if (modeldata?.checkInTime && !modeldata?.checkOutTime) {
const [time, modifier] = data.markTime.split(" "); const checkIn = new Date(modeldata.checkInTime);
const [hourStr, minuteStr] = time.split(":"); const [time, modifier] = data.markTime.split(" ");
let hour = parseInt(hourStr, 10); const [hourStr, minuteStr] = time.split(":");
const minute = parseInt(minuteStr, 10); let hour = parseInt(hourStr, 10);
const minute = parseInt(minuteStr, 10);
if (modifier === "PM" && hour !== 12) hour += 12; if (modifier === "PM" && hour !== 12) hour += 12;
if (modifier === "AM" && hour === 12) hour = 0; if (modifier === "AM" && hour === 12) hour = 0;
const checkOut = new Date(checkIn); const checkOut = new Date(checkIn);
checkOut.setHours(hour, minute, 0, 0); checkOut.setHours(hour, minute, 0, 0);
return checkOut >= checkIn; return checkOut >= checkIn;
}
return true;
},
{
message: "Checkout time must be later than check-in time",
path: ["markTime"],
} }
return true; );
}, {
message: "Checkout time must be later than check-in time",
path: ["markTime"],
});
}; };
const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => { const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
const [currentProject, setCurrentProject] = useState(null);
const projectId = useSelectedProject(); const projectId = useSelectedProject();
const { projectNames, loading } = useProjectName();
const { mutate: MarkAttendance } = useMarkAttendance(); const { mutate: MarkAttendance } = useMarkAttendance();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const coords = usePositionTracker(); const coords = usePositionTracker();
@ -95,17 +101,24 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
closeModal(); closeModal();
}; };
useEffect(() => {
if (projectId && projectNames) {
setCurrentProject(
projectNames?.find((project) => project.id === projectId)
);
}
}, [projectNames, projectId, loading]);
return ( return (
<form className="row p-2" onSubmit={handleSubmit(onSubmit)}> <form className="row p-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 d-flex justify-content-center mb-4"> <div className="col-12 d-flex justify-content-center mt-2">
<label className="fs-5 tex-semibold text-center"> <label className="fs-5 text-dark text-center">
{modeldata?.checkInTime && !modeldata?.checkOutTime {modeldata?.checkInTime && !modeldata?.checkOutTime
? "Check-Out " ? `Check out for ${currentProject?.name}`
: "Check-In "} : `Check In for ${currentProject?.name}`}
</label> </label>
</div> </div>
<div className="col-6 col-md-6 text-start"> <div className="col-6 col-md-6 text-start">
<label className="form-label" htmlFor="checkInDate"> <label className="form-label" htmlFor="checkInDate">
{modeldata?.checkInTime && !modeldata?.checkOutTime {modeldata?.checkInTime && !modeldata?.checkOutTime
@ -120,7 +133,7 @@ const CheckInCheckOut = ({ modeldata, closeModal, handleSubmitForm }) => {
modeldata?.checkInTime && !modeldata?.checkOutTime modeldata?.checkInTime && !modeldata?.checkOutTime
? formatDate(modeldata?.checkInTime?.split("T")[0]) || "" ? formatDate(modeldata?.checkInTime?.split("T")[0]) || ""
: formatDate(today) : formatDate(today)
} }
disabled disabled
/> />
</div> </div>
@ -207,7 +220,7 @@ export const Regularization = ({ modeldata, closeModal, handleSubmitForm }) => {
}; };
return ( return (
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}> <form className="row " onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12"> <div className="col-12 col-md-12">
<p>Regularize Attendance</p> <p>Regularize Attendance</p>
<label className="form-label" htmlFor="description"> <label className="form-label" htmlFor="description">

View File

@ -24,7 +24,8 @@ import { useProfile } from "../../hooks/useProfile";
import { refreshData, setProjectId } from "../../slices/localVariablesSlice"; import { refreshData, setProjectId } from "../../slices/localVariablesSlice";
import InfraTable from "../Project/Infrastructure/InfraTable"; import InfraTable from "../Project/Infrastructure/InfraTable";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { SpinnerLoader } from "../common/Loader"; import Loader from "../common/Loader";
const InfraPlanning = () => { const InfraPlanning = () => {
const { profile: LoggedUser, refetch: fetchData } = useProfile(); const { profile: LoggedUser, refetch: fetchData } = useProfile();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -32,7 +33,7 @@ const InfraPlanning = () => {
const selectedService = useCurrentService(); const selectedService = useCurrentService();
const { projectInfra, isLoading, isError, error, isFetched } = const { projectInfra, isLoading, isError, error, isFetched } =
useProjectInfra(selectedProject, selectedService || ""); useProjectInfra(selectedProject, selectedService || "" );
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA); const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
const canApproveTask = useHasUserPermission(APPROVE_TASK); const canApproveTask = useHasUserPermission(APPROVE_TASK);
@ -56,25 +57,14 @@ const InfraPlanning = () => {
} }
if (isLoading) { if (isLoading) {
return ( return <Loader />;
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "60vh" }}
>
<SpinnerLoader />
</div>
);
} }
if (isFetched && (!projectInfra || projectInfra.length === 0)) { if (isFetched && (!projectInfra || projectInfra.length === 0)) {
return ( return (
<div <div className="text-center">
className="text-center d-flex justify-content-center align-items-center text-muted" <p className="my-3">No Result Found</p>
style={{ minHeight: "40vh", fontSize: "0.9rem" }}
>
<p className="my-3 m-0">No Result Found</p>
</div> </div>
); );
} }

View File

@ -1,48 +1,42 @@
import React, { useCallback, useEffect, useState, useMemo } from "react"; import React, { useCallback, useEffect, useState, useMemo } from "react";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { convertShortTime, formatUTCToLocalTime } from "../../utils/dateUtils"; import { convertShortTime } from "../../utils/dateUtils";
import RegularizationActions from "./RegularizationActions"; import RegularizationActions from "./RegularizationActions";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useRegularizationRequests } from "../../hooks/useAttendance"; import { useRegularizationRequests } from "../../hooks/useAttendance";
import moment from "moment"; import moment from "moment";
import usePagination from "../../hooks/usePagination"; import usePagination from "../../hooks/usePagination";
import eventBus from "../../services/eventBus"; import eventBus from "../../services/eventBus";
import { import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
cacheData,
clearCacheKey,
useSelectedProject,
} from "../../slices/apiDataManager";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import Pagination from "../../components/common/Pagination"; import Pagination from "../common/Pagination";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { employee } from "../../data/masters";
import { SpinnerLoader } from "../common/Loader"; import { SpinnerLoader } from "../common/Loader";
const Regularization = ({ const Regularization = ({ handleRequest, searchTerm, projectId, organizationId, IncludeInActive }) => {
handleRequest,
searchTerm,
projectId,
organizationId,
IncludeInActive,
}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// var selectedProject = useSelector((store) => store.localVariables.projectId); // var selectedProject = useSelector((store) => store.localVariables.projectId);
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const [regularizesList, setregularizedList] = useState([]); const [regularizesList, setregularizedList] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
const { regularizes, loading, error, refetch } = useRegularizationRequests( const { regularizes, loading, error, refetch } =
selectedProject, useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
organizationId,
IncludeInActive
);
useEffect(() => { useEffect(() => {
if (!regularizes) return if (regularizes && regularizes.length) {
if (regularizes?.length) { setregularizedList((prev) => {
setregularizedList(regularizes); const prevIds = prev.map((i) => i.id).join(",");
const newIds = regularizes.map((i) => i.id).join(",");
if (prevIds !== newIds) {
return regularizes;
}
return prev;
});
} }
}, [regularizes]); }, [regularizes]);
const sortByName = (a, b) => { const sortByName = (a, b) => {
const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase(); const nameA = a.firstName.toLowerCase() + a.lastName.toLowerCase();
const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase(); const nameB = b.firstName.toLowerCase() + b.lastName.toLowerCase();
@ -75,15 +69,48 @@ const Regularization = ({
} }
const lowercasedSearchTerm = searchTerm.toLowerCase(); const lowercasedSearchTerm = searchTerm.toLowerCase();
return sortedList.filter((item) => { return sortedList.filter((item) => {
const fullName = `${item?.firstName} ${item?.lastName}`.toLowerCase(); const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
return fullName.includes(lowercasedSearchTerm); return fullName.includes(lowercasedSearchTerm);
}); });
}, [regularizesList, searchTerm]); }, [regularizesList, searchTerm]);
const { currentPage, totalPages, currentItems, paginate } = usePagination( // const filteredSearchData = useMemo(() => {
filteredSearchData, // let sortedList = [...regularizesList].sort(sortByName);
20
); // // Search filter
// if (searchTerm) {
// const lowercasedSearchTerm = searchTerm.toLowerCase();
// sortedList = sortedList.filter((item) => {
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
// return fullName.includes(lowercasedSearchTerm);
// });
// }
// // Organization filter
// if (filters?.selectedOrganization) {
// sortedList = sortedList.filter(
// (item) => item.organization?.name === filters.selectedOrganization
// );
// }
// // Services filter
// if (filters?.selectedServices?.length > 0) {
// sortedList = sortedList.filter((item) =>
// filters.selectedServices.includes(item.service?.name)
// );
// }
// return sortedList;
// }, [regularizesList, searchTerm, filters]);
const { currentPage, totalPages, currentItems, paginate } =
usePagination(filteredSearchData, 20);
// Reset pagination when the search term or data changes
useEffect(() => {
}, [filteredSearchData]);
useEffect(() => { useEffect(() => {
eventBus.on("regularization", handler); eventBus.on("regularization", handler);
@ -104,39 +131,34 @@ const Regularization = ({
return () => eventBus.off("employee", employeeHandler); return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]); }, [employeeHandler]);
return ( return (
<div> <div>
<div <div className="table-responsive text-nowrap pb-4" style={{ minHeight: "200px" }}>
className="table-responsive modal-min-h pt-3 text-nowrap pb-4"
style={{ minHeight: "200px" }}
>
{loading ? ( {loading ? (
<div <div
className="d-flex justify-content-center align-items-center" className="d-flex justify-content-center align-items-center"
style={{ minHeight: "50vh" }} style={{ minHeight: "70vh" }}
> >
<SpinnerLoader /> <SpinnerLoader/>
</div> </div>
) : currentItems?.length > 0 ? ( ) : currentItems?.length > 0 ? (
<table className="table mb-0"> <table className="table mb-0 table-hover">
<thead> <thead>
<tr> <tr>
<th colSpan={2}>Name</th> <th colSpan={2}>Name</th>
<th>Date</th> <th>Date</th>
<th>Organization</th> {/* <th>Organization</th> */}
<th> <th>
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In <i className="bx bxs-down-arrow-alt text-success"></i>Check-In
</th> </th>
<th> <th>
<i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out <i className="bx bxs-up-arrow-alt text-danger"></i>Check-Out
</th> </th>
<th colSpan={2}>
Requested By <th>Request By</th>
</th> <th>Requested At</th>
<th > <th className="actions-col">Action</th>
Requested At
</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -144,42 +166,47 @@ const Regularization = ({
<tr key={index}> <tr key={index}>
<td colSpan={2}> <td colSpan={2}>
<div className="d-flex justify-content-start align-items-center"> <div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att.firstName} lastName={att.lastName} /> <Avatar
<div className="d-flex flex-column"> <a firstName={att.firstName}
onClick={() => lastName={att.lastName}
navigate(`/employee/${att.employeeId}?for=attendance`) />
} <div className="d-flex flex-column">
className="text-heading text-truncate cursor-pointer" > {/* <a href="#" className="text-heading text-truncate">
<span className="fw-normal"> <span className="fw-normal">
{att.firstName} {att.lastName} {att.firstName} {att.lastName}
</span> </span>
</a> </a> */}
</div> <a
</div> onClick={() =>
</td> navigate(`/employee/${att.employeeId}?for=attendance`)
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td> }
className="text-heading text-truncate cursor-pointer"
<td>{att.organizationName || "--"}</td> >
<td>{convertShortTime(att.checkInTime)}</td>
<td>
{att.requestedAt ? convertShortTime(att.checkOutTime) : "--"}
</td>
<td colSpan={2}>
{att.requestedBy ? (<div className="d-flex justify-content-start align-items-center">
<Avatar firstName={att?.requestedBy?.firstName} lastName={att?.requestedBy?.lastName} />
<div className="d-flex flex-column">
<a href="#" className="text-heading text-truncate">
<span className="fw-normal"> <span className="fw-normal">
{att?.requestedBy?.firstName} {att?.requestedBy?.lastName} {att.firstName} {att.lastName}
</span> </span>
</a> </a>
</div> </div>
</div>) : (<small>--</small>)} </div>
</td>
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
{/* <td>{att.organizationName || "--"}</td> */}
<td>{convertShortTime(att.checkInTime)}</td>
<td>
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
</td> </td>
<td> <td>
{att?.requestedAt ? formatUTCToLocalTime(att.requestedAt, true) : "--"} {att.requestedBy
? `${att.requestedBy?.firstName} ${att.requestedBy?.lastName}`
: "--"}
</td>
<td>
{att.requestedAt
? moment(att.requestedAt).format("DD-MMM-YYYY")
: "--"}
</td> </td>
<td className="text-center "> <td className="text-center ">
<RegularizationActions <RegularizationActions
@ -192,6 +219,7 @@ const Regularization = ({
))} ))}
</tbody> </tbody>
</table> </table>
) : ( ) : (
<div <div
className="d-flex justify-content-center align-items-center" className="d-flex justify-content-center align-items-center"
@ -205,14 +233,13 @@ const Regularization = ({
</div> </div>
)} )}
</div> </div>
{totalPages > 0 && ( {!loading && totalPages > 1 && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={paginate} onPageChange={paginate}
/> />
)} )}
</div> </div>
); );
}; };

View File

@ -1,6 +1,5 @@
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from "react";
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense"; import { useExpenseTransactions } from "../../hooks/useExpense";
import Error from "../common/Error"; import Error from "../common/Error";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader, { SpinnerLoader } from "../common/Loader"; import Loader, { SpinnerLoader } from "../common/Loader";
@ -11,10 +10,11 @@ import { employee } from "../../data/masters";
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage"; import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
import { formatFigure } from "../../utils/appUtils"; import { formatFigure } from "../../utils/appUtils";
const AdvancePaymentList = ({ employeeId, searchString }) => { const AdvancePaymentList = ({ employeeId }) => {
const { setBalance } = useAdvancePaymentContext(); const { setBalance } = useAdvancePaymentContext();
const { data, isError, isLoading, error, isFetching } = const { data, isError, isLoading, error, isFetching } =
useExpenseTransactions(employeeId, { enabled: !!employeeId }); useExpenseTransactions(employeeId, { enabled: !!employeeId });
const records = Array.isArray(data) ? data : []; const records = Array.isArray(data) ? data : [];
let currentBalance = 0; let currentBalance = 0;
@ -84,7 +84,7 @@ const AdvancePaymentList = ({ employeeId, searchString }) => {
key: "date", key: "date",
label: ( label: (
<> <>
Date Date
</> </>
), ),
align: "text-start", align: "text-start",
@ -174,7 +174,7 @@ const AdvancePaymentList = ({ employeeId, searchString }) => {
) )
) : col.key === "balance" ? ( ) : col.key === "balance" ? (
<div className="d-flex align-items-center justify-content-end"> <div className="d-flex align-items-center justify-content-end">
{/* <DecideCreditOrDebit financeUId={row?.financeUId} /> */} <DecideCreditOrDebit financeUId={row?.financeUId} />
<span className="mx-2"> <span className="mx-2">
{formatFigure(row.currentBalance)} {formatFigure(row.currentBalance)}
</span> </span>

View File

@ -1,100 +0,0 @@
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

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import ReactApexChart from "react-apexcharts"; import ReactApexChart from "react-apexcharts";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { SpinnerLoader } from "../common/Loader";
const HorizontalBarChart = ({ const HorizontalBarChart = ({
seriesData = [], seriesData = [],
@ -24,12 +23,8 @@ const HorizontalBarChart = ({
if (loading) { if (loading) {
return ( return (
<div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl"> <div className="w-full h-[380px] flex items-center justify-center bg-gray-100 rounded-xl">
<div <span className="text-gray-500 text-sm">Loading chart...</span>
className="d-flex justify-content-center align-items-center" {/* Replace this with a skeleton or spinner if you prefer */}
style={{ minHeight: "50vh" }}
>
<SpinnerLoader />
</div>
</div> </div>
); );
} }

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import ReactApexChart from "react-apexcharts"; import ReactApexChart from "react-apexcharts";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { SpinnerLoader } from "../common/Loader";
const LineChart = ({ const LineChart = ({
seriesData = [], seriesData = [],
@ -10,28 +9,24 @@ const LineChart = ({
loading = false, loading = false,
lineChartCategoriesDates = [], lineChartCategoriesDates = [],
}) => { }) => {
const hasValidData = const hasValidData =
Array.isArray(seriesData) && Array.isArray(seriesData) &&
seriesData.length > 0 && seriesData.length > 0 &&
Array.isArray(categories) && Array.isArray(categories) &&
categories.length > 0; categories.length > 0;
if (loading) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-[350px] text-gray-500"> <div className="flex justify-center items-center h-[350px] text-gray-500">
<div <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mr-2" />
className="d-flex justify-content-center align-items-center" Loading chart...
style={{ minHeight: "50vh" }} </div>
> );
<SpinnerLoader /> }
</div>
</div>
);
}
if (!hasValidData) { if (!hasValidData) {
return <div className="text-center text-gray-500">No data to display</div>; return <div className="text-center text-gray-500">No data to display</div>;
} }
const chartOptions = { const chartOptions = {
chart: { chart: {
@ -134,16 +129,16 @@ const LineChart = ({
}; };
LineChart.propTypes = { LineChart.propTypes = {
seriesData: PropTypes.arrayOf( seriesData: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
data: PropTypes.arrayOf(PropTypes.number).isRequired data: PropTypes.arrayOf(PropTypes.number).isRequired
}) })
), ),
categories: PropTypes.arrayOf(PropTypes.string), categories: PropTypes.arrayOf(PropTypes.string),
colors: PropTypes.arrayOf(PropTypes.string), colors: PropTypes.arrayOf(PropTypes.string),
title: PropTypes.string, title: PropTypes.string,
loading: PropTypes.bool loading: PropTypes.bool
}; };
export default LineChart; export default LineChart;

View File

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

View File

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useCurrentService } from "../../hooks/useProjects"; import { useCurrentService, useProjectInfra } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -10,14 +10,15 @@ import {
import { DateRangePicker1 } from "../common/DateRangePicker"; import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple"; import SelectMultiple from "../common/SelectMultiple";
import { localToUtc } from "../../utils/appUtils"; import { localToUtc } from "../../utils/appUtils";
import { useTaskFilter } from "../../hooks/useTasks";
const TaskReportFilterPanel = ({ handleFilter }) => { const TaskReportFilterPanel = ({ handleFilter }) => {
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const selectedProject = useSelectedProject(); const selectedProjec = useSelectedProject();
const selectedService = useCurrentService(); const selectedService = useCurrentService();
const { data } = useTaskFilter(selectedProject); const { projectInfra, isLoading, error, isFetched } = useProjectInfra(
selectedProjec,
selectedService
);
const methods = useForm({ const methods = useForm({
resolver: zodResolver(TaskReportFilterSchema), resolver: zodResolver(TaskReportFilterSchema),
defaultValues: TaskReportDefaultValue, defaultValues: TaskReportDefaultValue,
@ -29,75 +30,58 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = methods; } = methods;
const closePanel = () => {
document.querySelector(".offcanvas.show .btn-close")?.click();
};
const onSubmit = (formData) => { const onSubmit = (formData) => {
const filterPayload = { console.log(formData)
...formData, const filterPayload = {
dateFrom: localToUtc(formData.dateFrom), startDate:localToUtc(formData.startDate),
dateTo: localToUtc(formData.dateTo), endDate:localToUtc(formData.endDate)
};
}
handleFilter(filterPayload); handleFilter(filterPayload);
closePanel();
}; };
const onClear = () => { const onClear =()=>{
setResetKey((prev) => prev + 1); setResetKey((prev) => prev + 1);
handleFilter(TaskReportDefaultValue); handleFilter(TaskReportDefaultValue)
reset(TaskReportDefaultValue); reset(TaskReportDefaultValue)
closePanel(); }
};
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start"> <form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
<div className="mb-3 w-100"> <div className="mb-3 w-100">
<label className="fw-semibold">Choose Date Range:</label> <label className="fw-semibold">Choose Date Range:</label>
<DateRangePicker1 <DateRangePicker1
className="w-100"
placeholder="DD-MM-YYYY To DD-MM-YYYY" placeholder="DD-MM-YYYY To DD-MM-YYYY"
startField="dateFrom" startField="startDate"
endField="dateTo" endField="endDate"
resetSignal={resetKey} resetSignal={resetKey}
defaultRange={false} defaultRange={true}
maxDate={new Date()}
/> />
</div> </div>
<div className="row mb-2"> {/* <div className="row g-2">
<SelectMultiple <SelectMultiple
name="buildingIds" name="buildingIds"
label="Building" label="Projects"
options={data?.buildings} options={projectInfra}
labelKey="name" labelKey="buildingName"
valueKey="id" valueKey="id"
/> />
</div> </div>
<div className="row mb-2"> <div className="row g-2">
<SelectMultiple <SelectMultiple
name="floorIds" name="floorIds"
label="Floor" label="Floor"
options={data?.floors} options={projectInfra}
labelKey="name" labelKey="floorName"
valueKey="id" valueKey="id"
/> />
</div> </div> */}
<div className="row mb-2">
<SelectMultiple
name="activityIds"
label="Activities"
options={data?.activities}
labelKey="name"
valueKey="id"
/>
</div>
<div className="d-flex justify-content-end py-3 gap-2"> <div className="d-flex justify-content-end py-3 gap-2">
<button <button type="button" className="btn btn-label-secondary btn-sm" onClick={onClear}>
type="button"
className="btn btn-label-secondary btn-sm"
onClick={onClear}
>
Clear Clear
</button> </button>
<button type="submit" className="btn btn-primary btn-sm"> <button type="submit" className="btn btn-primary btn-sm">

View File

@ -29,7 +29,7 @@ const TaskReportList = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK); const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK); const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { service, openModal, closeModal, filter } = useDailyProgrssContext(); const { service, openModal, closeModal,filter } = useDailyProgrssContext();
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const { projectNames } = useProjectName(); const { projectNames } = useProjectName();
@ -37,7 +37,7 @@ const TaskReportList = () => {
selectedProject, selectedProject,
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
currentPage, currentPage,
service, filter service,filter
); );
const ProgrssReportColumn = [ const ProgrssReportColumn = [
@ -192,116 +192,109 @@ const TaskReportList = () => {
if (isLoading) return <TaskReportListSkeleton />; if (isLoading) return <TaskReportListSkeleton />;
if (isError) return <div>Loading....</div>; if (isError) return <div>Loading....</div>;
return ( return (
<div> <div className="mt-2">
<div className="mt-2 table-responsive text-nowrap"> <table className="table">
<table className="table"> <thead>
<thead> <tr>
<th className="text-start">Activity</th>
<th>
<span>
Total Pending{" "}
<HoverPopup
title="Total Pending Task"
content={<p>This shows the total pending tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>
<span>
Reported/Planned{" "}
<HoverPopup
title="Reported and Planned Task"
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{groupedTasks.length === 0 && (
<tr> <tr>
<th className="text-start">Activity</th> <td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
<th> No reports available
<span> </td>
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>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>
<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>}
>
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
</HoverPopup>
</span>
</th>
<th>Assign Date</th>
<th>Team</th>
<th className="text-center">Actions</th>
</tr> </tr>
</thead> )}
<tbody>
{groupedTasks.length === 0 && ( {groupedTasks.map(({ date, tasks }) => (
<tr> <React.Fragment key={date}>
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}> <tr className="table-row-header text-start">
No reports available <td colSpan={6}>
<strong>{formatUTCToLocalTime(date)}</strong>
</td> </td>
</tr> </tr>
)} {tasks.map((task, idx) => (
<tr key={task.id || idx}>
{groupedTasks.map(({ date, tasks }) => ( <td className="flex-wrap text-start">
<React.Fragment key={date}> <div>
<tr className="table-row-header text-start"> {task.workItem.activityMaster?.activityName || "No Activity Name"}
<td colSpan={6}> </div>
<strong>{formatUTCToLocalTime(date)}</strong> <div className="text-sm py-2">
{task.workItem.workArea?.floor?.building?.name} {" "}
{task.workItem.workArea?.floor?.floorName} {" "}
{task.workItem.workArea?.areaName}
</div>
</td>
<td>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td> </td>
</tr> </tr>
{tasks.map((task, idx) => ( ))}
<tr key={task.id || idx}> </React.Fragment>
<td className="flex-wrap text-start"> ))}
<div> </tbody>
{task.workItem.activityMaster?.activityName || "No Activity Name"} </table>
</div> {data?.data?.length > 0 && (
<div className="text-sm py-2"> <Pagination
{task.workItem.workArea?.floor?.building?.name} {" "} currentPage={currentPage}
{task.workItem.workArea?.floor?.floorName} {" "} totalPages={data.totalPages}
{task.workItem.workArea?.areaName} onPageChange={paginate}
</div> />
</td> )}
<td> </div>
{formatNumber(task.workItem.plannedWork)}
</td>
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
<td className="text-center">{renderTeamMembers(task, idx)}</td>
<td className="text-center">
<div className="d-flex justify-content-end gap-2">
{ReportTaskRights && !task.reportedDate && (
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
Report
</button>
)}
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
<button
className="btn btn-xs btn-warning"
onClick={() => openModal("comments", { task, isActionAllow: true })}
>
QC
</button>
)}
<button
className="btn btn-xs btn-primary"
onClick={() => openModal("comments", { task, isActionAllow: false })}
>
Comment
</button>
</div>
</td>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
{
data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)
}
</div >
); );
}; };

View File

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

View File

@ -2,14 +2,13 @@ import React, { useState, useMemo } from "react";
import ApexChart from "../Charts/Circle"; import ApexChart from "../Charts/Circle";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data"; import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
import { useSelectedProject } from "../../hooks/useSelectedProject"; // your custom hook import { useSelectedProject } from "../../hooks/useSelectedProject";
const Attendance = () => { const Attendance = () => {
const { projects } = useProjects(); const { projects } = useProjects();
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD const today = new Date().toISOString().split("T")[0];
const [selectedDate, setSelectedDate] = useState(today); const [selectedDate, setSelectedDate] = useState(today);
// central project selection hook
const selectedProjectId = useSelectedProject() const selectedProjectId = useSelectedProject()
const { const {
@ -31,7 +30,7 @@ const selectedProjectId = useSelectedProject()
<div className="card-header mb-1 pb-0"> <div className="card-header mb-1 pb-0">
<div className="d-flex flex-wrap justify-content-between align-items-center"> <div className="d-flex flex-wrap justify-content-between align-items-center">
<div className="card-title mb-0 text-start"> <div className="card-title mb-0 text-start">
<h5 className="mb-1">Attendance</h5> <h5 className="mb-1 card-title">Attendance</h5>
<p className="card-subtitle">Daily Attendance Data</p> <p className="card-subtitle">Daily Attendance Data</p>
</div> </div>
@ -136,7 +135,6 @@ const selectedProjectId = useSelectedProject()
</div> </div>
)} )}
{/* Details */}
{AttendanceData?.activeTab === "Details" && ( {AttendanceData?.activeTab === "Details" && (
<div className="table-responsive" style={{ maxHeight: "300px" }}> <div className="table-responsive" style={{ maxHeight: "300px" }}>
<table className="table table-hover mb-0 text-start"> <table className="table table-hover mb-0 text-start">

View File

@ -4,40 +4,41 @@ import ReactApexChart from "react-apexcharts";
import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data"; import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data";
import flatColors from "../Charts/flatColor"; import flatColors from "../Charts/flatColor";
import ChartSkeleton from "../Charts/Skelton"; import ChartSkeleton from "../Charts/Skelton";
import { SpinnerLoader } from "../common/Loader"; import { useSelectedProject } from "../../slices/apiDataManager";
import { formatDate_DayMonth } from "../../utils/dateUtils";
const formatDate = (dateStr) => {
const date = new Date(dateStr);
return date.toLocaleDateString("en-GB", {
day: "2-digit",
month: "long",
});
};
const AttendanceOverview = () => { const AttendanceOverview = () => {
const [dayRange, setDayRange] = useState(7); const [dayRange, setDayRange] = useState(7);
const [view, setView] = useState("chart"); const [view, setView] = useState("chart");
const selectedProject = useSelectedProject();
const projectId = useSelector((store) => store.localVariables.projectId); const {
const { attendanceOverviewData, loading, error } = useAttendanceOverviewData( data: attendanceOverviewData,
projectId, isLoading,
dayRange isError,
); error,
} = useAttendanceOverviewData(selectedProject, dayRange);
// Use empty array while loading
const attendanceData = attendanceOverviewData || [];
const { tableData, roles, dates } = useMemo(() => { const { tableData, roles, dates } = useMemo(() => {
if (!attendanceData || attendanceData.length === 0) {
return { tableData: [], roles: [], dates: [] };
}
const map = new Map(); const map = new Map();
attendanceOverviewData.forEach((entry) => { attendanceData.forEach((entry) => {
const date = formatDate(entry.date); const date = formatDate_DayMonth(entry.date);
if (!map.has(date)) map.set(date, {}); if (!map.has(date)) map.set(date, {});
map.get(date)[entry.role.trim()] = entry.present; map.get(date)[entry.role.trim()] = entry.present;
}); });
const uniqueRoles = [ const uniqueRoles = [...new Set(attendanceData.map((e) => e.role.trim()))];
...new Set(attendanceOverviewData.map((e) => e.role.trim())),
];
const sortedDates = [...map.keys()]; const sortedDates = [...map.keys()];
const data = sortedDates.map((date) => {
const tableData = sortedDates.map((date) => {
const row = { date }; const row = { date };
uniqueRoles.forEach((role) => { uniqueRoles.forEach((role) => {
row[role] = map.get(date)?.[role] ?? 0; row[role] = map.get(date)?.[role] ?? 0;
@ -45,12 +46,8 @@ const AttendanceOverview = () => {
return row; return row;
}); });
return { return { tableData, roles: uniqueRoles, dates: sortedDates };
tableData: data, }, [attendanceData]);
roles: uniqueRoles,
dates: sortedDates,
};
}, [attendanceOverviewData]);
const chartSeries = roles.map((role) => ({ const chartSeries = roles.map((role) => ({
name: role, name: role,
@ -64,52 +61,31 @@ const AttendanceOverview = () => {
height: 400, height: 400,
toolbar: { show: false }, toolbar: { show: false },
}, },
plotOptions: { plotOptions: { bar: { borderRadius: 2, columnWidth: "60%" } },
bar: { xaxis: { categories: tableData.map((row) => row.date) },
borderRadius: 2,
columnWidth: "60%",
},
},
xaxis: {
categories: tableData.map((row) => row.date),
},
yaxis: { yaxis: {
show: true, show: true,
axisBorder: { axisBorder: { show: true, color: "#78909C" },
show: true, axisTicks: { show: true, color: "#78909C", width: 6 },
color: "#78909C",
offsetX: 0,
offsetY: 0,
},
axisTicks: {
show: true,
borderType: "solid",
color: "#78909C",
width: 6,
offsetX: 0,
offsetY: 0,
},
},
legend: {
position: "bottom",
},
fill: {
opacity: 1,
}, },
legend: { position: "bottom" },
fill: { opacity: 1 },
colors: roles.map((_, i) => flatColors[i % flatColors.length]), colors: roles.map((_, i) => flatColors[i % flatColors.length]),
}; };
return ( return (
<div className="bg-white px-4 rounded shadow d-flex flex-column h-100"> <div className="bg-white p-4 rounded shadow d-flex flex-column position-relative">
{/* Header */} <div className="row mb-3 align-items-center">
<div className="d-flex mt-2 justify-content-between align-items-center mb-3"> <div className="col-md-6 text-start">
<div className="card-title mb-0 text-start"> <p className="mb-1 fs-6 fs-md-5 fw-medium">Attendance Overview</p>
<h5 className="mb-1 fw-bold">Attendance Overview</h5> <p className="card-subtitle text-muted mb-0">
<p className="card-subtitle">Role-wise present count</p> Role-wise present count
</p>
</div> </div>
<div className="d-flex gap-2">
<div className="col-md-6 d-flex flex-column align-items-end gap-2">
<select <select
className="form-select form-select-sm" className="form-select form-select-sm w-auto"
value={dayRange} value={dayRange}
onChange={(e) => setDayRange(Number(e.target.value))} onChange={(e) => setDayRange(Number(e.target.value))}
> >
@ -117,41 +93,53 @@ const AttendanceOverview = () => {
<option value={15}>Last 15 Days</option> <option value={15}>Last 15 Days</option>
<option value={30}>Last 30 Days</option> <option value={30}>Last 30 Days</option>
</select> </select>
<button
className={`btn btn-sm p-1 ${view === "chart" ? "btn-primary" : "btn-outline-primary" <div className="d-flex gap-2 justify-content-end">
<button
className={`btn btn-sm p-1 ${
view === "chart" ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setView("chart")} onClick={() => setView("chart")}
title="Chart View" title="Chart View"
> >
<i className="bx bx-bar-chart-alt-2"></i> <i className="bx bx-bar-chart-alt-2 fs-5"></i>
</button> </button>
<button
className={`btn btn-sm p-1 ${view === "table" ? "btn-primary" : "btn-outline-primary" <button
className={`btn btn-sm p-1 ${
view === "table" ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setView("table")} onClick={() => setView("table")}
title="Table View" title="Table View"
> >
<i className="bx bx-list-ul fs-5"></i> <i className="bx bx-list-ul fs-5"></i>
</button> </button>
</div>
</div> </div>
</div> </div>
{/* Content */} {/* Content Section */}
<div className="flex-grow-1 d-flex align-items-center justify-content-center"> <div className="flex-grow-1 d-flex align-items-center justify-content-center position-relative">
{loading ? ( {isLoading && (
<SpinnerLoader /> <div className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-50">
) : error ? ( <span>Loading...</span>
<p className="text-danger">{error}</p> </div>
) : attendanceOverviewData.length === 0 || )}
attendanceOverviewData.every((item) => item.present === 0) ? (
<div className="text-center text-dark">No data found</div> {!isLoading && (!attendanceData || attendanceData.length === 0) ? (
<div
className="text-muted fw-semibold d-flex align-items-center justify-content-center"
style={{ minHeight: "250px" }}
>
No data found
</div>
) : view === "chart" ? ( ) : view === "chart" ? (
<div className="w-100"> <div className="w-100">
<ReactApexChart <ReactApexChart
options={chartOptions} options={chartOptions}
series={chartSeries} series={chartSeries}
type="bar" type="bar"
height={400} height={300}
/> />
</div> </div>
) : ( ) : (
@ -159,32 +147,34 @@ const AttendanceOverview = () => {
className="table-responsive w-100" className="table-responsive w-100"
style={{ maxHeight: "350px", overflowY: "auto" }} style={{ maxHeight: "350px", overflowY: "auto" }}
> >
<table className="table table-bordered table-sm text-start align-middle mb-0"> <table className="table table-bordered table-sm align-middle mb-0">
<thead <thead
className="table-light" className="table-light"
style={{ position: "sticky", top: 0, zIndex: 1 }} style={{ position: "sticky", top: 0, zIndex: 1 }}
> >
<tr> <tr>
<th style={{ background: "#f8f9fa", textTransform: "none" }}>Role</th> <th style={{ background: "#f8f9fa" }}>Role</th>
{dates.map((date, idx) => ( {dates.map((date, idx) => (
<th <th key={idx} style={{ background: "#f8f9fa" }}>
key={idx}
style={{ background: "#f8f9fa", textTransform: "none" }}
>
{date} {date}
</th> </th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{roles.map((role) => ( {roles.map((role) => (
<tr key={role}> <tr key={role}>
<td>{role}</td> <td className="fw-medium text-start table-cell">{role}</td>
{tableData.map((row, idx) => { {tableData.map((row, idx) => {
const value = row[role]; const value = row[role];
const cellStyle = value > 0 ? { backgroundColor: "#d5d5d5" } : {};
return ( return (
<td key={idx} style={cellStyle}> <td
key={idx}
style={
value > 0 ? { backgroundColor: "#e9ecef" } : {}
}
>
{value} {value}
</td> </td>
); );

View File

@ -1,82 +1,76 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { // import {
useDashboardProjectsCardData, // useDashboardProjectsCardData,
useDashboardTeamsCardData, // useDashboardTeamsCardData,
useDashboardTasksCardData, // useDashboardTasksCardData,
useAttendanceOverviewData // useAttendanceOverviewData
} from "../../hooks/useDashboard_Data"; // } from "../../hooks/useDashboard_Data";
import Projects from "./Projects"; // import Projects from "./Projects";
import Teams from "./Teams"; // import Teams from "./Teams";
import TasksCard from "./Tasks"; // import TasksCard from "./Tasks";
import ProjectCompletionChart from "./ProjectCompletionChart"; // import ProjectCompletionChart from "./ProjectCompletionChart";
import ProjectProgressChart from "./ProjectProgressChart"; // import ProjectProgressChart from "./ProjectProgressChart";
// import ProjectOverview from "../Project/ProjectOverview";
import AttendanceOverview from "./AttendanceOverview"; import AttendanceOverview from "./AttendanceOverview";
import { useSelectedProject } from "../../slices/apiDataManager";
import { useProjectName } from "../../hooks/useProjects";
import ExpenseAnalysis from "./ExpenseAnalysis"; import ExpenseAnalysis from "./ExpenseAnalysis";
import ExpenseStatus from "./ExpenseStatus"; import ExpenseStatus from "./ExpenseStatus";
import ExpenseByProject from "./ExpenseByProject"; import ExpenseByProject from "./ExpenseByProject";
import ProjectStatistics from "../Project/ProjectStatistics"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import {
APPROVE_EXPENSE,
EXPENSE_MANAGE,
VIEW_ALL_EXPNESE,
} from "../../utils/constants";
import { useHasAnyPermission } from "../../hooks/useExpense";
const Dashboard = () => { const Dashboard = () => {
// const { projectsCardData } = useDashboardProjectsCardData();
// const { teamsCardData } = useDashboardTeamsCardData();
// const { tasksCardData } = useDashboardTasksCardData();
// Get the selected project ID from Redux store // Get the selected project ID from Redux store
const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelector((store) => store.localVariables.projectId);
const isAllProjectsSelected = projectId === null; const isAllProjectsSelected = projectId === null;
return ( const isViewExpense = useHasAnyPermission(
<div className="container-fluid mt-5"> VIEW_ALL_EXPNESE,
<div className="row gy-4"> APPROVE_EXPENSE,
{isAllProjectsSelected && ( EXPENSE_MANAGE
<div className="col-sm-6 col-lg-4"> );
<Projects /> return (
</div> <div className="container-fluid py-5">
)} {isViewExpense && (
<div className="row mb-6 g-6">
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}> <div className="col-12 col-xl-8">
<Teams /> <div className="card h-100">
</div> <ExpenseAnalysis />
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
<TasksCard />
</div>
{isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<ProjectCompletionChart />
</div>
)}
<div className="col-xxl-6 col-lg-6">
<ProjectProgressChart />
</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>
</div> </div>
</div>
<div className="col-12 col-xl-4 col-md-6">
<div className="card h-100">
<ExpenseStatus />
</div>
</div>
</div> </div>
); )}
<div className="row vh-100">
{!isAllProjectsSelected && (
<div className="col-12 col-md-6 mb-sm-0 mb-4 ">
<AttendanceOverview />
</div>
)}
<div className="col-12 col-md-6">
<ExpenseByProject />
</div>
</div>
</div>
);
}; };
export default Dashboard; export default Dashboard;

View File

@ -1,110 +0,0 @@
import React from "react";
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
<div
className={`skeleton ${className}`}
style={{
height,
width,
borderRadius: "4px",
background: "linear-gradient(90deg, #eee, #f5f5f5, #eee)",
backgroundSize: "200% 100%",
animation: "skeleton-loading 1.5s infinite",
}}
></div>
);
const skeletonStyle = `
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`;
export const ProjectCardSkeleton = () => {
return (
<>
{/* Inject animation CSS once */}
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="rounded-circle bx bx-building-house text-primary"></i>{" "}
Projects
</h5>
</div>
{/* Skeleton body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="40px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="40px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};
export const TeamsSkeleton = () => {
return (
<>
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="bx bx-group text-warning"></i> Teams
</h5>
</div>
{/* Skeleton Body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="90px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="70px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};
export const TasksSkeleton = () => {
return (
<>
<style>{skeletonStyle}</style>
<div className="card p-3 h-100 text-center d-flex justify-content-between">
{/* Header */}
<div className="d-flex justify-content-start align-items-center mb-3">
<h5 className="fw-bold mb-0 ms-2">
<i className="bx bx-task text-success"></i> Tasks
</h5>
</div>
{/* Skeleton Body */}
<div className="d-flex justify-content-around align-items-start mt-n2 w-100">
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="70px" className="mx-auto" />
</div>
<div className="text-center">
<SkeletonLine height={28} width="60px" className="mx-auto mb-2" />
<SkeletonLine height={14} width="90px" className="mx-auto" />
</div>
</div>
</div>
</>
);
};

View File

@ -1,32 +1,18 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo } from "react";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import { useExpenseAnalysis } from "../../hooks/useDashboard_Data"; import { useExpenseAnalysis } from "../../hooks/useDashboard_Data";
import { useSelectedProject } from "../../slices/apiDataManager"; import { useSelectedProject } from "../../slices/apiDataManager";
import { DateRangePicker1 } from "../common/DateRangePicker"; import { DateRangePicker1 } from "../common/DateRangePicker";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { formatCurrency, localToUtc } from "../../utils/appUtils"; import { formatCurrency, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects";
import { SpinnerLoader } from "../common/Loader";
import flatColors from "../Charts/flatColor";
const ExpenseAnalysis = () => { const ExpenseAnalysis = () => {
const projectId = useSelectedProject(); const projectId = useSelectedProject();
const [projectName, setProjectName] = useState("All Project");
const { projectNames } = useProjectName();
const methods = useForm({ const methods = useForm({
defaultValues: { startDate: "", endDate: "" }, defaultValues: { startDate: "", endDate: "" },
}); });
useEffect(() => {
if (projectId && projectNames?.length) {
const project = projectNames.find((p) => p.id === projectId);
setProjectName(project?.name || "All Project");
} else {
setProjectName("All Project");
}
}, [projectNames, projectId]);
const { watch } = methods; const { watch } = methods;
const [startDate, endDate] = watch(["startDate", "endDate"]); const [startDate, endDate] = watch(["startDate", "endDate"]);
@ -51,7 +37,7 @@ const ExpenseAnalysis = () => {
labels, labels,
legend: { show: false }, legend: { show: false },
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` }, dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
colors: flatColors, colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
plotOptions: { plotOptions: {
pie: { pie: {
donut: { donut: {
@ -68,48 +54,63 @@ const ExpenseAnalysis = () => {
}, },
}, },
}, },
responsive: [ responsive: [
{ {
breakpoint: 576, // mobile breakpoint breakpoint: 1200,
options: { options: {
chart: { width: "100%" }, chart: { width: "100%", height: 350 },
legend: { position: "bottom" },
},
},
{
breakpoint: 992,
options: {
chart: { width: "100%", height: 300 },
dataLabels: { style: { fontSize: "11px" } },
},
},
{
breakpoint: 576,
options: {
chart: { width: "100%", height: 250 },
legend: { fontSize: "10px" },
plotOptions: {
pie: { donut: { size: "65%" } },
}, },
}, },
], },
],
}; };
return ( return (
<> <>
{/* Header */}
<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="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 w-100">
<h5 className="mb-1 card-title">Expense Breakdown</h5> <p className="mb-1 fw-medium fs-6 fs-md-5">Expense Breakdown</p>
<p className="card-subtitle m-0">{projectName}</p> <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p>
</div> </div>
<div className="text-end text-sm-end"> <div className="d-flex justify-content-start justify-content-md-end te w-75">
<FormProvider {...methods}> <FormProvider {...methods}>
<DateRangePicker1 /> <DateRangePicker1 />
</FormProvider> </FormProvider>
</div> </div>
</div> </div>
<div className="card-body position-relative"> <div className="card-body position-relative">
{isLoading && ( {isLoading && (
<div <div
className="d-flex justify-content-center align-items-center" className="d-flex justify-content-center align-items-center"
style={{ minHeight: "50vh" }} style={{ height: "200px" }}
> >
<SpinnerLoader /> <span>Loading...</span>
</div> </div>
)} )}
{!isLoading && report.length === 0 && ( {!isLoading && report.length === 0 && (
<div <div className="d-flex justify-content-center align-items-center">No data found</div>
className="d-flex justify-content-center align-items-center text-muted"
style={{ height: "300px" }}
>
No data found
</div>
)} )}
{!isLoading && report.length > 0 && ( {!isLoading && report.length > 0 && (
@ -120,54 +121,42 @@ const ExpenseAnalysis = () => {
</div> </div>
)} )}
<div className="row"> <div className="d-flex justify-content-center mb-3">
{/* Chart Column */} <Chart
<div className="col-12 col-lg-6 d-flex justify-content-center mt-5 mb-3 mb-lg-0"> options={donutOptions}
<Chart series={series}
options={donutOptions} type="donut"
series={series} width="100%"
type="donut" height={320}
width="70%" />
height={320} </div>
/>
</div>
{/* Data/Legend Column */} <div className="mb-2 w-100">
<div className="col-12 mt-6 col-lg-6"> <div className="row g-2">
<div className="row g-4"> {report.map((item, idx) => (
{report.map((item, idx) => ( <div
<div className="col-12 col-sm-6 d-flex align-items-start"
className="col-6" key={idx}
key={idx} >
style={{ <div className="avatar me-2">
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`, <span
}} className="avatar-initial rounded-2"
> style={{
<div className="d-flex flex-column text-start"> backgroundColor:
<small donutOptions.colors[idx % donutOptions.colors.length],
className="fw-semibold text-wrap text-dark" }}
style={{ >
fontSize: "0.8rem", <i className="bx bx-receipt fs-4"></i>
whiteSpace: "normal", </span>
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>
<div className="d-flex flex-column gap-1 text-start">
))} <small className="fw-semibold">{item.projectName}</small>
</div> <span className="fw-semibold text-muted ms-1">
{formatCurrency(item.totalApprovedAmount)}
</span>
</div>
</div>
))}
</div> </div>
</div> </div>
</> </>

View File

@ -1,25 +1,19 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useExpenseDataByProject } from "../../hooks/useDashboard_Data"; import { useExpenseDataByProject } from "../../hooks/useDashboard_Data";
import { formatCurrency } from "../../utils/appUtils"; import { formatCurrency } from "../../utils/appUtils";
import { formatDate_DayMonth } from "../../utils/dateUtils"; import { formatDate_DayMonth } from "../../utils/dateUtils";
import { useProjectName } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import { SpinnerLoader } from "../common/Loader";
import { useExpenseCategory } from "../../hooks/masterHook/useMaster";
const ExpenseByProject = () => { const ExpenseByProject = () => {
const projectId = useSelector((store) => store.localVariables.projectId); const projectId = useSelector((store) => store.localVariables.projectId);
const [projectName, setProjectName] = useState("All Project");
const [range, setRange] = useState("12M"); const [range, setRange] = useState("12M");
const { projectNames, loading } = useProjectName();
const [selectedType, setSelectedType] = useState(""); const [selectedType, setSelectedType] = useState("");
const [viewMode, setViewMode] = useState("Category"); const [viewMode, setViewMode] = useState("Category");
const [chartData, setChartData] = useState({ categories: [], data: [] }); const [chartData, setChartData] = useState({ categories: [], data: [] });
const selectedProject = useSelectedProject();
const {expenseCategories , loading: typeLoading } = useExpenseCategory(); const { ExpenseCategories, loading: typeLoading } = useExpenseCategory();
const { data: expenseApiData, isLoading } = useExpenseDataByProject( const { data: expenseApiData, isLoading } = useExpenseDataByProject(
projectId, projectId,
@ -27,15 +21,6 @@ const ExpenseByProject = () => {
range === "All" ? null : parseInt(range) range === "All" ? null : parseInt(range)
); );
useEffect(() => {
if (selectedProject && projectNames?.length) {
const project = projectNames.find((p) => p.id === selectedProject);
setProjectName(project?.name || "All Project");
} else {
setProjectName("All Project");
}
}, [projectNames, selectedProject]);
useEffect(() => { useEffect(() => {
if (expenseApiData) { if (expenseApiData) {
const categories = expenseApiData.map((item) => const categories = expenseApiData.map((item) =>
@ -50,7 +35,7 @@ const ExpenseByProject = () => {
const getSelectedTypeName = () => { const getSelectedTypeName = () => {
if (!selectedType) return "All Types"; if (!selectedType) return "All Types";
const found = expenseCategories?.find((t) => t.id === selectedType); const found = ExpenseTypes.find((t) => t.id === selectedType);
return found ? found.name : "All Types"; return found ? found.name : "All Types";
}; };
@ -64,12 +49,6 @@ const ExpenseByProject = () => {
categories: chartData.categories, categories: chartData.categories,
labels: { style: { fontSize: "12px" }, rotate: -45 }, labels: { style: { fontSize: "12px" }, rotate: -45 },
}, },
yaxis: {
labels: {
formatter: (val) => formatCurrency(val),
style: { fontSize: "12px", colors: "#555" },
},
},
tooltip: { tooltip: {
y: { y: {
formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`, formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`,
@ -88,16 +67,22 @@ const ExpenseByProject = () => {
}, },
]; ];
const ExpenseCategoryType = [
{id:1,category:"Category",label:"Category"},
{id:2,category:"Project",label:"Project"}
]
return ( return (
<div className="card shadow-sm h-100 rounded "> <div className="card shadow-sm rounded ">
{/* Header */} {/* Header */}
<div className="card-header"> <div className="card-header">
<div className="d-flex justify-content-between align-items-center mb-1 mt-1"> <div className="d-flex justify-content-between align-items-center mb-3 mt-3">
<div className="text-start"> <div className="text-start">
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5> <p className="mb-1 fw-medium fs-6 fs-md-5 ">Monthly Expense -</p>
<p className="card-subtitle m-0">{projectName}</p> <p className="card-subtitle me-5 mb-0">Detailed project expenses</p>
</div> </div>
<div className="btn-group mb-5 ms-n8"> <div className="btn-group mb-4 ms-n8">
<button <button
className="btn btn-sm dropdown-toggle fs-5" className="btn btn-sm dropdown-toggle fs-5"
type="button" type="button"
@ -106,29 +91,20 @@ const ExpenseByProject = () => {
> >
{viewMode} {viewMode}
</button> </button>
<ul className="dropdown-menu dropdown-menu-end "> <ul className="dropdown-menu dropdown-menu-end ">
<li> {ExpenseCategoryType.map((cat)=>(
<li>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {
setViewMode("Category"); setViewMode(cat.category);
setSelectedType(""); setSelectedType("");
}} }}
> >
Category {cat.label}
</button>
</li>
<li>
<button
className="dropdown-item"
onClick={() => {
setViewMode("Project");
setSelectedType("");
}}
>
Project
</button> </button>
</li> </li>
))}
</ul> </ul>
</div> </div>
</div> </div>
@ -139,8 +115,8 @@ const ExpenseByProject = () => {
<button <button
key={item} key={item}
className={`border-0 px-2 py-1 text-sm rounded ${range === item className={`border-0 px-2 py-1 text-sm rounded ${range === item
? "text-white bg-primary" ? "text-white bg-primary"
: "text-body bg-transparent" : "text-body bg-transparent"
}`} }`}
style={{ cursor: "pointer", transition: "all 0.2s ease" }} style={{ cursor: "pointer", transition: "all 0.2s ease" }}
onClick={() => setRange(item)} onClick={() => setRange(item)}
@ -157,7 +133,7 @@ const ExpenseByProject = () => {
style={{ maxWidth: "200px" }} style={{ maxWidth: "200px" }}
> >
<option value="">All Types</option> <option value="">All Types</option>
{expenseCategories?.map((type) => ( {ExpenseCategories?.map((type) => (
<option key={type.id} value={type.id}> <option key={type.id} value={type.id}>
{type.name} {type.name}
</option> </option>
@ -170,15 +146,12 @@ const ExpenseByProject = () => {
{/* Chart */} {/* Chart */}
<div className="card-body bg-white text-dark p-3 rounded" style={{ minHeight: "210px" }}> <div className="card-body bg-white text-dark p-3 rounded" style={{ minHeight: "210px" }}>
{isLoading ? ( {isLoading ? (
<div className="d-flex justify-content-center align-items-center py-5"> <p>Loading chart...</p>
<SpinnerLoader />
</div>
) : !expenseApiData || expenseApiData.length === 0 ? ( ) : !expenseApiData || expenseApiData.length === 0 ? (
<div className="text-center text-muted py-12">No data found</div> <div className="text-center text-muted py-5">No data found</div>
) : ( ) : (
<Chart options={options} series={series} type="bar" height={235} /> <Chart options={options} series={series} type="bar" height={235} />
)} )}
</div> </div>
</div> </div>

View File

@ -8,7 +8,6 @@ import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useHasUserPermission } from "../../hooks/useHasUserPermission";
const ExpenseStatus = () => { const ExpenseStatus = () => {
const [projectName, setProjectName] = useState("All Project"); const [projectName, setProjectName] = useState("All Project");
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
@ -37,14 +36,14 @@ const ExpenseStatus = () => {
<> <>
<div className="card-header d-flex justify-content-between text-start "> <div className="card-header d-flex justify-content-between text-start ">
<div className="m-0"> <div className="m-0">
<h5 className="card-title mb-1">Expense - By Status</h5> <p className="fs-6 fw-medium fs-md-5 mb-1">Expense - By Status</p>
<p className="card-subtitle m-0 ">{projectName}</p> <p className="card-subtitle m-0 ">{projectName}</p>
</div> </div>
</div> </div>
<div className="card-body "> <div className="card-body ">
<div className="report-list text-start"> <div className="report-list text-start h-max">
{[ {[
{ {
title: "Pending Payment", title: "Pending Payment",
@ -103,7 +102,7 @@ const ExpenseStatus = () => {
</div> </div>
<div> <div>
<small <small
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-xl" className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
} text-gray-500`} } text-gray-500`}
> >
{item?.count || 0} {item?.count || 0}
@ -122,7 +121,7 @@ const ExpenseStatus = () => {
{isManageExpense && ( {isManageExpense && (
<div <div
className="d-flex justify-content-between align-items-center cursor-pointer" className="d-flex justify-content-between align-items-center cursor-pointer"
onClick={() => handleNavigate(EXPENSE_STATUS.payment_processed)} onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
> >
<div className="d-block"> <div className="d-block">
<span <span
@ -137,7 +136,7 @@ const ExpenseStatus = () => {
</div> </div>
<div className="d-flex align-items-center gap-2"> <div className="d-flex align-items-center gap-2">
<span <span
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-xl" : "text-3xl" className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
} text-md`} } text-md`}
> >
{formatCurrency(data?.totalAmount || 0)} {formatCurrency(data?.totalAmount || 0)}

View File

@ -1,23 +1,17 @@
import React, { useState } from "react"; import React from "react";
import HorizontalBarChart from "../Charts/HorizontalBarChart"; import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
const ProjectCompletionChart = () => { const ProjectCompletionChart = () => {
const [currentPage, setCurrentPage] = useState(1); const { projects, loading } = useProjects();
const {
data: projects, // Bar chart logic
isLoading: loading,
isError,
error,
} = useProjectCompletionStatus();
const projectNames = projects?.map((p) => p.name) || []; const projectNames = projects?.map((p) => p.name) || [];
const projectProgress = const projectProgress =
projects?.map((p) => { projects?.map((p) => {
const completed = p.completedWork || 0; const completed = p.completedWork || 0;
const planned = p.plannedWork || 1; const planned = p.plannedWork || 1;
const percent = planned ? (completed / planned) * 100 : 0; const percent = (completed / planned) * 100;
return Math.min(Math.round(percent), 100); return Math.min(Math.round(percent), 100);
}) || []; }) || [];
@ -29,7 +23,7 @@ const ProjectCompletionChart = () => {
<p className="card-subtitle">Projects Completion Status</p> <p className="card-subtitle">Projects Completion Status</p>
</div> </div>
</div> </div>
<div className="card-body ms-n7"> <div className="card-body">
<HorizontalBarChart <HorizontalBarChart
categories={projectNames} categories={projectNames}
seriesData={projectProgress} seriesData={projectProgress}

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