Compare commits
No commits in common. "3d1e83cd1e186cf3b8b44a10a677259ef772b2c8" and "190064a353244ef5a601485105333ad78d55886a" have entirely different histories.
3d1e83cd1e
...
190064a353
26
public/mockApi/approvals/director_pending_leaves.json
Normal file
26
public/mockApi/approvals/director_pending_leaves.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "SA001",
|
||||
"employeeId": "ADM001",
|
||||
"employeeName": "Alice Williams",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-10",
|
||||
"fromDate": "2026-08-01",
|
||||
"toDate": "2026-08-01",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Festival observance."
|
||||
},
|
||||
{
|
||||
"id": "SA002",
|
||||
"employeeId": "ADM002",
|
||||
"employeeName": "Bob Smith",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-12",
|
||||
"fromDate": "2026-08-05",
|
||||
"toDate": "2026-08-06",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Personal work."
|
||||
}
|
||||
]
|
||||
26
public/mockApi/approvals/hrmanager_pending_leaves.json
Normal file
26
public/mockApi/approvals/hrmanager_pending_leaves.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "AA001",
|
||||
"employeeId": "MGR001",
|
||||
"employeeName": "Michael Scott",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-15",
|
||||
"fromDate": "2026-07-25",
|
||||
"toDate": "2026-07-26",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Family trip."
|
||||
},
|
||||
{
|
||||
"id": "AA002",
|
||||
"employeeId": "MGR002",
|
||||
"employeeName": "Jane Doe",
|
||||
"leaveType": "Sick Leave",
|
||||
"applicationDate": "2026-07-16",
|
||||
"fromDate": "2026-07-22",
|
||||
"toDate": "2026-07-22",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Fever."
|
||||
}
|
||||
]
|
||||
242
public/mockApi/approvals/manager_pending_leaves.json
Normal file
242
public/mockApi/approvals/manager_pending_leaves.json
Normal file
@ -0,0 +1,242 @@
|
||||
[
|
||||
{
|
||||
"id": "MA005",
|
||||
"employeeId": "EMP005",
|
||||
"employeeName": "Michael Brown",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-16",
|
||||
"fromDate": "2026-07-28",
|
||||
"toDate": "2026-07-29",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Attending a family wedding."
|
||||
},
|
||||
{
|
||||
"id": "MA006",
|
||||
"employeeId": "EMP006",
|
||||
"employeeName": "Emily Davis",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-17",
|
||||
"fromDate": "2026-08-01",
|
||||
"toDate": "2026-08-01",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Festival celebration."
|
||||
},
|
||||
{
|
||||
"id": "MA007",
|
||||
"employeeId": "EMP007",
|
||||
"employeeName": "Chris Wilson",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-18",
|
||||
"fromDate": "2026-08-03",
|
||||
"toDate": "2026-08-04",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Personal commitments."
|
||||
},
|
||||
{
|
||||
"id": "MA008",
|
||||
"employeeId": "EMP008",
|
||||
"employeeName": "Sophia Johnson",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-19",
|
||||
"fromDate": "2026-08-05",
|
||||
"toDate": "2026-08-05",
|
||||
"leaveDays": 1,
|
||||
"status": "Rejected",
|
||||
"reason": "Festival observance."
|
||||
},
|
||||
{
|
||||
"id": "MA009",
|
||||
"employeeId": "EMP009",
|
||||
"employeeName": "Daniel Lee",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-20",
|
||||
"fromDate": "2026-08-06",
|
||||
"toDate": "2026-08-08",
|
||||
"leaveDays": 3,
|
||||
"status": "Pending",
|
||||
"reason": "Family emergency."
|
||||
},
|
||||
{
|
||||
"id": "MA010",
|
||||
"employeeId": "EMP010",
|
||||
"employeeName": "Olivia Martin",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-21",
|
||||
"fromDate": "2026-08-10",
|
||||
"toDate": "2026-08-10",
|
||||
"leaveDays": 1,
|
||||
"status": "Approved",
|
||||
"reason": "Cultural celebration."
|
||||
},
|
||||
{
|
||||
"id": "MA011",
|
||||
"employeeId": "EMP011",
|
||||
"employeeName": "James Taylor",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-22",
|
||||
"fromDate": "2026-08-12",
|
||||
"toDate": "2026-08-13",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Home relocation."
|
||||
},
|
||||
{
|
||||
"id": "MA012",
|
||||
"employeeId": "EMP012",
|
||||
"employeeName": "Emma Garcia",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-23",
|
||||
"fromDate": "2026-08-14",
|
||||
"toDate": "2026-08-14",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Religious observance."
|
||||
},
|
||||
{
|
||||
"id": "MA013",
|
||||
"employeeId": "EMP013",
|
||||
"employeeName": "William Harris",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-24",
|
||||
"fromDate": "2026-08-17",
|
||||
"toDate": "2026-08-18",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Health checkup."
|
||||
},
|
||||
{
|
||||
"id": "MA014",
|
||||
"employeeId": "EMP014",
|
||||
"employeeName": "Ava Clark",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-25",
|
||||
"fromDate": "2026-08-19",
|
||||
"toDate": "2026-08-19",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Community event."
|
||||
},
|
||||
{
|
||||
"id": "MA015",
|
||||
"employeeId": "EMP015",
|
||||
"employeeName": "Benjamin Lewis",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-26",
|
||||
"fromDate": "2026-08-20",
|
||||
"toDate": "2026-08-22",
|
||||
"leaveDays": 3,
|
||||
"status": "Rejected",
|
||||
"reason": "Vacation with family."
|
||||
},
|
||||
{
|
||||
"id": "MA016",
|
||||
"employeeId": "EMP016",
|
||||
"employeeName": "Mia Walker",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-27",
|
||||
"fromDate": "2026-08-24",
|
||||
"toDate": "2026-08-24",
|
||||
"leaveDays": 1,
|
||||
"status": "Approved",
|
||||
"reason": "Festival celebration."
|
||||
},
|
||||
{
|
||||
"id": "MA017",
|
||||
"employeeId": "EMP017",
|
||||
"employeeName": "Ethan Hall",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-28",
|
||||
"fromDate": "2026-08-25",
|
||||
"toDate": "2026-08-26",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Personal travel."
|
||||
},
|
||||
{
|
||||
"id": "MA018",
|
||||
"employeeId": "EMP018",
|
||||
"employeeName": "Charlotte Allen",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-29",
|
||||
"fromDate": "2026-08-27",
|
||||
"toDate": "2026-08-27",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Special occasion."
|
||||
},
|
||||
{
|
||||
"id": "MA019",
|
||||
"employeeId": "EMP019",
|
||||
"employeeName": "Alexander Young",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-30",
|
||||
"fromDate": "2026-08-28",
|
||||
"toDate": "2026-08-30",
|
||||
"leaveDays": 3,
|
||||
"status": "Approved",
|
||||
"reason": "Family gathering."
|
||||
},
|
||||
{
|
||||
"id": "MA020",
|
||||
"employeeId": "EMP020",
|
||||
"employeeName": "Amelia King",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-31",
|
||||
"fromDate": "2026-09-01",
|
||||
"toDate": "2026-09-01",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Festival leave."
|
||||
},
|
||||
{
|
||||
"id": "MA021",
|
||||
"employeeId": "EMP021",
|
||||
"employeeName": "Matthew Scott",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-08-01",
|
||||
"fromDate": "2026-09-02",
|
||||
"toDate": "2026-09-03",
|
||||
"leaveDays": 2,
|
||||
"status": "Rejected",
|
||||
"reason": "Personal errands."
|
||||
},
|
||||
{
|
||||
"id": "MA022",
|
||||
"employeeId": "EMP022",
|
||||
"employeeName": "Harper Green",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-08-02",
|
||||
"fromDate": "2026-09-04",
|
||||
"toDate": "2026-09-04",
|
||||
"leaveDays": 1,
|
||||
"status": "Approved",
|
||||
"reason": "Religious holiday."
|
||||
},
|
||||
{
|
||||
"id": "MA023",
|
||||
"employeeId": "EMP023",
|
||||
"employeeName": "David Baker",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-08-03",
|
||||
"fromDate": "2026-09-07",
|
||||
"toDate": "2026-09-08",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Attending a seminar."
|
||||
},
|
||||
{
|
||||
"id": "MA024",
|
||||
"employeeId": "EMP024",
|
||||
"employeeName": "Ella Adams",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-08-04",
|
||||
"fromDate": "2026-09-09",
|
||||
"toDate": "2026-09-09",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Festival observance."
|
||||
}
|
||||
]
|
||||
12
public/mockApi/leave/director_leave_history.json
Normal file
12
public/mockApi/leave/director_leave_history.json
Normal file
@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"id": "S001",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-06-22",
|
||||
"fromDate": "2026-07-21",
|
||||
"toDate": "2026-07-21",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Festival."
|
||||
}
|
||||
]
|
||||
16
public/mockApi/leave/director_leave_summary.json
Normal file
16
public/mockApi/leave/director_leave_summary.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"leaveType": "Casual Leave",
|
||||
"credited": 20,
|
||||
"utilized": 2,
|
||||
"lapsed": 0,
|
||||
"balance": 18
|
||||
},
|
||||
{
|
||||
"leaveType": "Optional Holiday",
|
||||
"credited": 4,
|
||||
"utilized": 2,
|
||||
"lapsed": 0,
|
||||
"balance": 2
|
||||
}
|
||||
]
|
||||
82
public/mockApi/leave/employee_leave_history.json
Normal file
82
public/mockApi/leave/employee_leave_history.json
Normal file
@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"id": "L001",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-10",
|
||||
"fromDate": "2026-07-04",
|
||||
"toDate": "2026-07-05",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Family function out of town."
|
||||
},
|
||||
{
|
||||
"id": "L002",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-15",
|
||||
"fromDate": "2026-07-14",
|
||||
"toDate": "2026-07-14",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Personal work."
|
||||
},
|
||||
{
|
||||
"id": "L003",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-20",
|
||||
"fromDate": "2026-06-10",
|
||||
"toDate": "2026-06-11",
|
||||
"leaveDays": 2,
|
||||
"status": "Withdrawn",
|
||||
"reason": "Changed plans."
|
||||
},
|
||||
{
|
||||
"id": "L004",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-22",
|
||||
"fromDate": "2026-06-20",
|
||||
"toDate": "2026-06-20",
|
||||
"leaveDays": 1,
|
||||
"status": "Approved",
|
||||
"reason": "Medical appointment."
|
||||
},
|
||||
{
|
||||
"id": "L005",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-18",
|
||||
"fromDate": "2026-07-01",
|
||||
"toDate": "2026-07-02",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending",
|
||||
"reason": "Religious festival."
|
||||
},
|
||||
{
|
||||
"id": "L006",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-02",
|
||||
"fromDate": "2026-05-15",
|
||||
"toDate": "2026-05-16",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Friend's wedding."
|
||||
},
|
||||
{
|
||||
"id": "L007",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-07-10",
|
||||
"fromDate": "2026-04-25",
|
||||
"toDate": "2026-04-26",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "House shifting."
|
||||
},
|
||||
{
|
||||
"id": "L008",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-07-22",
|
||||
"fromDate": "2026-07-21",
|
||||
"toDate": "2026-07-21",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Personal work."
|
||||
}
|
||||
]
|
||||
16
public/mockApi/leave/employee_leave_summary.json
Normal file
16
public/mockApi/leave/employee_leave_summary.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"leaveType": "Casual Leave",
|
||||
"credited": 10,
|
||||
"utilized": 8,
|
||||
"lapsed": 0,
|
||||
"balance": 2
|
||||
},
|
||||
{
|
||||
"leaveType": "Optional Holiday",
|
||||
"credited": 4,
|
||||
"utilized": 3,
|
||||
"lapsed": 0,
|
||||
"balance": 1
|
||||
}
|
||||
]
|
||||
22
public/mockApi/leave/hrmanager_leave_history.json
Normal file
22
public/mockApi/leave/hrmanager_leave_history.json
Normal file
@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"id": "A001",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-05-10",
|
||||
"fromDate": "2026-06-10",
|
||||
"toDate": "2026-06-11",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Medical appointment."
|
||||
},
|
||||
{
|
||||
"id": "A002",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-06-20",
|
||||
"fromDate": "2026-07-01",
|
||||
"toDate": "2026-07-02",
|
||||
"leaveDays": 2,
|
||||
"status": "Withdrawn",
|
||||
"reason": "Changed plans."
|
||||
}
|
||||
]
|
||||
16
public/mockApi/leave/hrmanager_leave_summary.json
Normal file
16
public/mockApi/leave/hrmanager_leave_summary.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"leaveType": "Casual Leave",
|
||||
"credited": 15,
|
||||
"utilized": 5,
|
||||
"lapsed": 0,
|
||||
"balance": 10
|
||||
},
|
||||
{
|
||||
"leaveType": "Optional Holiday",
|
||||
"credited": 4,
|
||||
"utilized": 0,
|
||||
"lapsed": 0,
|
||||
"balance": 4
|
||||
}
|
||||
]
|
||||
134
public/mockApi/leave/leave_applications.json
Normal file
134
public/mockApi/leave/leave_applications.json
Normal file
@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"id": "LA001",
|
||||
"employeeId": "EMP001",
|
||||
"employeeName": "Alice Williams",
|
||||
"designation": "Software Engineer",
|
||||
"department": "IT",
|
||||
"managerName": "Bob Smith",
|
||||
"leaveType": "Casual Leave",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved"
|
||||
},
|
||||
{
|
||||
"id": "LA002",
|
||||
"employeeId": "EMP002",
|
||||
"employeeName": "Bob Smith",
|
||||
"designation": "Senior Software Engineer",
|
||||
"department": "IT",
|
||||
"managerName": "Charlie Brown",
|
||||
"leaveType": "Sick Leave",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "LA003",
|
||||
"employeeId": "EMP003",
|
||||
"employeeName": "Charlie Brown",
|
||||
"designation": "Trainer",
|
||||
"department": "Training",
|
||||
"managerName": "Diana Prince",
|
||||
"leaveType": "Optional Holiday",
|
||||
"leaveDays": 1,
|
||||
"status": "Rejected"
|
||||
},
|
||||
{
|
||||
"id": "LA004",
|
||||
"employeeId": "EMP004",
|
||||
"employeeName": "Diana Prince",
|
||||
"designation": "Data Analyst",
|
||||
"department": "Analytics",
|
||||
"managerName": "Evan Wright",
|
||||
"leaveType": "Casual Leave",
|
||||
"leaveDays": 3,
|
||||
"status": "Approved"
|
||||
},
|
||||
{
|
||||
"id": "LA005",
|
||||
"employeeId": "EMP005",
|
||||
"employeeName": "Evan Wright",
|
||||
"designation": "QA Tester",
|
||||
"department": "Software Dev",
|
||||
"managerName": "Fiona Gallagher",
|
||||
"leaveType": "Sick Leave",
|
||||
"leaveDays": 2,
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "LA006",
|
||||
"employeeId": "EMP006",
|
||||
"employeeName": "Fiona Gallagher",
|
||||
"designation": "HR Executive",
|
||||
"department": "HR",
|
||||
"managerName": "George Costanza",
|
||||
"leaveType": "Casual Leave",
|
||||
"leaveDays": 1,
|
||||
"status": "Withdrawn"
|
||||
},
|
||||
{
|
||||
"id": "LA007",
|
||||
"employeeId": "EMP007",
|
||||
"employeeName": "George Costanza",
|
||||
"designation": "Placement Officer",
|
||||
"department": "Placement",
|
||||
"managerName": "Hannah Abbott",
|
||||
"leaveType": "Optional Holiday",
|
||||
"leaveDays": 1,
|
||||
"status": "Approved"
|
||||
},
|
||||
{
|
||||
"id": "LA008",
|
||||
"employeeId": "EMP008",
|
||||
"employeeName": "Hannah Abbott",
|
||||
"designation": "Support Engineer",
|
||||
"department": "Support",
|
||||
"managerName": "Ian Malcolm",
|
||||
"leaveType": "Sick Leave",
|
||||
"leaveDays": 4,
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "LA009",
|
||||
"employeeId": "EMP009",
|
||||
"employeeName": "Ian Malcolm",
|
||||
"designation": "Lead Trainer",
|
||||
"department": "Training",
|
||||
"managerName": "Jane Porter",
|
||||
"leaveType": "Casual Leave",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved"
|
||||
},
|
||||
{
|
||||
"id": "LA010",
|
||||
"employeeId": "EMP010",
|
||||
"employeeName": "Jane Porter",
|
||||
"designation": "Project Manager",
|
||||
"department": "Software Dev",
|
||||
"managerName": "Kevin Hart",
|
||||
"leaveType": "Optional Holiday",
|
||||
"leaveDays": 1,
|
||||
"status": "Rejected"
|
||||
},
|
||||
{
|
||||
"id": "LA011",
|
||||
"employeeId": "EMP011",
|
||||
"employeeName": "Kevin Hart",
|
||||
"designation": "Software Engineer",
|
||||
"department": "IT",
|
||||
"managerName": "Laura Croft",
|
||||
"leaveType": "Sick Leave",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending"
|
||||
},
|
||||
{
|
||||
"id": "LA012",
|
||||
"employeeId": "EMP012",
|
||||
"employeeName": "Laura Croft",
|
||||
"designation": "Data Analyst",
|
||||
"department": "Analytics",
|
||||
"managerName": "Alice Williams",
|
||||
"leaveType": "Casual Leave",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved"
|
||||
}
|
||||
]
|
||||
52
public/mockApi/leave/leave_balance.json
Normal file
52
public/mockApi/leave/leave_balance.json
Normal file
@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"employeeId": "EMP001",
|
||||
"employeeName": "Alice Williams",
|
||||
"month": 7,
|
||||
"year": 2026,
|
||||
"openingBalance": 12,
|
||||
"closingBalance": 10,
|
||||
"casualLeave": 8,
|
||||
"optionalLeave": 2
|
||||
},
|
||||
{
|
||||
"employeeId": "EMP002",
|
||||
"employeeName": "Bob Smith",
|
||||
"month": 7,
|
||||
"year": 2026,
|
||||
"openingBalance": 15,
|
||||
"closingBalance": 14,
|
||||
"casualLeave": 10,
|
||||
"optionalLeave": 4
|
||||
},
|
||||
{
|
||||
"employeeId": "EMP001",
|
||||
"employeeName": "Alice Williams",
|
||||
"month": 6,
|
||||
"year": 2026,
|
||||
"openingBalance": 15,
|
||||
"closingBalance": 12,
|
||||
"casualLeave": 10,
|
||||
"optionalLeave": 2
|
||||
},
|
||||
{
|
||||
"employeeId": "EMP003",
|
||||
"employeeName": "Charlie Brown",
|
||||
"month": 7,
|
||||
"year": 2026,
|
||||
"openingBalance": 10,
|
||||
"closingBalance": 8,
|
||||
"casualLeave": 6,
|
||||
"optionalLeave": 2
|
||||
},
|
||||
{
|
||||
"employeeId": "EMP004",
|
||||
"employeeName": "Diana Prince",
|
||||
"month": 7,
|
||||
"year": 2026,
|
||||
"openingBalance": 12,
|
||||
"closingBalance": 12,
|
||||
"casualLeave": 12,
|
||||
"optionalLeave": 0
|
||||
}
|
||||
]
|
||||
22
public/mockApi/leave/manager_leave_history.json
Normal file
22
public/mockApi/leave/manager_leave_history.json
Normal file
@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"id": "M001",
|
||||
"leaveType": "Casual Leave",
|
||||
"applicationDate": "2026-06-12",
|
||||
"fromDate": "2026-07-04",
|
||||
"toDate": "2026-07-05",
|
||||
"leaveDays": 2,
|
||||
"status": "Approved",
|
||||
"reason": "Family function."
|
||||
},
|
||||
{
|
||||
"id": "M002",
|
||||
"leaveType": "Optional Holiday",
|
||||
"applicationDate": "2026-06-18",
|
||||
"fromDate": "2026-07-14",
|
||||
"toDate": "2026-07-14",
|
||||
"leaveDays": 1,
|
||||
"status": "Pending",
|
||||
"reason": "Personal work."
|
||||
}
|
||||
]
|
||||
16
public/mockApi/leave/manager_leave_summary.json
Normal file
16
public/mockApi/leave/manager_leave_summary.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"leaveType": "Casual Leave",
|
||||
"credited": 12,
|
||||
"utilized": 4,
|
||||
"lapsed": 0,
|
||||
"balance": 8
|
||||
},
|
||||
{
|
||||
"leaveType": "Optional Holiday",
|
||||
"credited": 4,
|
||||
"utilized": 1,
|
||||
"lapsed": 0,
|
||||
"balance": 3
|
||||
}
|
||||
]
|
||||
134
public/mockApi/master-data/hrmanagers.json
Normal file
134
public/mockApi/master-data/hrmanagers.json
Normal file
@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"id": "EMP001",
|
||||
"name": "Alice Williams",
|
||||
"email": "alice@example.com",
|
||||
"designationName": "Software Engineer",
|
||||
"deptName": "IT",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": true
|
||||
},
|
||||
{
|
||||
"id": "EMP002",
|
||||
"name": "Bob Smith",
|
||||
"email": "bob@example.com",
|
||||
"designationName": "Senior Software Engineer",
|
||||
"deptName": "IT",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP003",
|
||||
"name": "Charlie Brown",
|
||||
"email": "charlie@example.com",
|
||||
"designationName": "Trainer",
|
||||
"deptName": "Training",
|
||||
"branchName": "Pune",
|
||||
"companyName": "CLRI",
|
||||
"status": "Disabled",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP004",
|
||||
"name": "Diana Prince",
|
||||
"email": "diana@example.com",
|
||||
"designationName": "Data Analyst",
|
||||
"deptName": "Analytics",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": true
|
||||
},
|
||||
{
|
||||
"id": "EMP005",
|
||||
"name": "Evan Wright",
|
||||
"email": "evan@example.com",
|
||||
"designationName": "QA Tester",
|
||||
"deptName": "Software Dev",
|
||||
"branchName": "Mumbai",
|
||||
"companyName": "TechNova Solutions",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP006",
|
||||
"name": "Fiona Gallagher",
|
||||
"email": "fiona@example.com",
|
||||
"designationName": "HR Executive",
|
||||
"deptName": "HR",
|
||||
"branchName": "Chennai",
|
||||
"companyName": "Infotech Ltd",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP007",
|
||||
"name": "George Costanza",
|
||||
"email": "george@example.com",
|
||||
"designationName": "Placement Officer",
|
||||
"deptName": "Placement",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": true
|
||||
},
|
||||
{
|
||||
"id": "EMP008",
|
||||
"name": "Hannah Abbott",
|
||||
"email": "hannah@example.com",
|
||||
"designationName": "Support Engineer",
|
||||
"deptName": "Support",
|
||||
"branchName": "Ahmedabad",
|
||||
"companyName": "Global Systems",
|
||||
"status": "Disabled",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP009",
|
||||
"name": "Ian Malcolm",
|
||||
"email": "ian@example.com",
|
||||
"designationName": "Lead Trainer",
|
||||
"deptName": "Training",
|
||||
"branchName": "Pune",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP010",
|
||||
"name": "Jane Porter",
|
||||
"email": "jane@example.com",
|
||||
"designationName": "Project Manager",
|
||||
"deptName": "Software Dev",
|
||||
"branchName": "Mumbai",
|
||||
"companyName": "TechNova Solutions",
|
||||
"status": "Active",
|
||||
"isHRManager": true
|
||||
},
|
||||
{
|
||||
"id": "EMP011",
|
||||
"name": "Kevin Hart",
|
||||
"email": "kevin@example.com",
|
||||
"designationName": "Software Engineer",
|
||||
"deptName": "IT",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
},
|
||||
{
|
||||
"id": "EMP012",
|
||||
"name": "Laura Croft",
|
||||
"email": "laura@example.com",
|
||||
"designationName": "Data Analyst",
|
||||
"deptName": "Analytics",
|
||||
"branchName": "Bengaluru",
|
||||
"companyName": "CLRI",
|
||||
"status": "Active",
|
||||
"isHRManager": false
|
||||
}
|
||||
]
|
||||
@ -22,6 +22,7 @@ const routeNameMap: Record<string, string> = {
|
||||
departments: 'Manage Departments',
|
||||
'job-roles': 'Manage Job Roles',
|
||||
employees: 'Manage Employees',
|
||||
'manage-hrmanagers': 'Manage HR Managers',
|
||||
policies: 'Policy and Rules',
|
||||
applications: 'Leave Applications',
|
||||
balance: 'Leave Balance Report'
|
||||
|
||||
@ -12,6 +12,7 @@ import AttendanceSummaryRouter from '../../features/attendance/routes/Attendance
|
||||
import LeaveSummaryRouter from '../../features/leave/routes/LeaveSummaryRouter';
|
||||
import MasterDataRouter from '../../features/masterData/routes/MasterDataRouter';
|
||||
import PolicyRouter from '../../features/masterData/routes/PolicyRouter';
|
||||
import ManageHRManagersRouter from '../../features/masterData/routes/ManageHRManagersRouter';
|
||||
|
||||
export default function MainLayout() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
@ -39,6 +40,7 @@ export default function MainLayout() {
|
||||
<Route path="/attendance-summary/*" element={<AttendanceSummaryRouter />} />
|
||||
<Route path="/leave-summary/*" element={<LeaveSummaryRouter />} />
|
||||
<Route path="/master-data/*" element={<MasterDataRouter />} />
|
||||
<Route path="/manage-hrmanagers" element={<ManageHRManagersRouter />} />
|
||||
<Route path="/policies" element={<PolicyRouter />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
|
||||
@ -6,15 +6,14 @@ import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||
Menu, Users, ShieldUser, ScrollText, ChevronDown, FileText, CalendarDays, ClipboardCheck,
|
||||
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus, History
|
||||
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus
|
||||
} from 'lucide-react';
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const iconMap: Record<string, any> = {
|
||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
|
||||
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus, History // ADDED History
|
||||
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus
|
||||
};
|
||||
|
||||
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
||||
|
||||
@ -41,7 +41,6 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' },
|
||||
{ name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' },
|
||||
{ name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' },
|
||||
{ name: 'Regularization History', icon: 'History', path: '/attendance-summary/regularization-history' } ,
|
||||
...(role === 'hrmanager' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : [])
|
||||
]
|
||||
};
|
||||
@ -86,6 +85,7 @@ export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
attendanceSummaryItems,
|
||||
leaveSummaryItems,
|
||||
masterDataItems,
|
||||
{ name: 'Manage HR Managers', icon: 'ShieldUser', path: '/manage-hrmanagers' },
|
||||
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../../../api/client';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { apiClient, fetchMockData } from '../../../api/client';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import type { UserRole } from '../../../store/roleSlice';
|
||||
import type { AttendanceRecord, AttendanceStatus, AttendanceSummary, DashboardMetrics, DailyReportRecord, AdminReportRecord } from '../types/attendance';
|
||||
|
||||
// Helper to map backend statuses to frontend statuses
|
||||
@ -7,7 +10,9 @@ const mapStatus = (backendStatus: string): AttendanceStatus => {
|
||||
switch (backendStatus) {
|
||||
case 'FULL_DAY': return 'Present';
|
||||
case 'MISPUNCH': return 'Mispunch';
|
||||
case 'LATE': return 'Late';
|
||||
case 'HALF_DAY': return 'Half Day';
|
||||
case 'WFH': return 'WFH';
|
||||
case 'ABSENT': return 'Absent';
|
||||
case 'LEAVE': return 'Leave';
|
||||
case 'HOLIDAY': return 'Holiday';
|
||||
@ -26,9 +31,9 @@ export const useEmployeeRangeReport = (employeeId: number | null, fromDate: stri
|
||||
const history = response.data.history || [];
|
||||
return history.map((h: any) => ({
|
||||
date: h.workDate,
|
||||
checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||
checkOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||
totalHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||
timeIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||
timeOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||
loggedHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||
status: mapStatus(h.finalStatus),
|
||||
attendanceId: h.attendanceId || 0
|
||||
}));
|
||||
@ -37,7 +42,7 @@ export const useEmployeeRangeReport = (employeeId: number | null, fromDate: stri
|
||||
});
|
||||
};
|
||||
|
||||
// 2. Fetch Employee Summary
|
||||
// 2.Fetch Employee Summary
|
||||
export const useAttendanceSummary = (employeeId: number, startDate: string, endDate: string) => {
|
||||
return useQuery<AttendanceSummary>({
|
||||
queryKey: ['attendanceSummary', employeeId, startDate, endDate],
|
||||
@ -48,9 +53,9 @@ export const useAttendanceSummary = (employeeId: number, startDate: string, endD
|
||||
const data = response.data;
|
||||
const history: AttendanceRecord[] = data.history.map((h: any) => ({
|
||||
date: h.workDate,
|
||||
checkIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||
checkOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||
totalHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||
timeIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||
timeOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||
loggedHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||
status: mapStatus(h.finalStatus),
|
||||
attendanceId: h.attendanceId || 0
|
||||
}));
|
||||
@ -60,17 +65,17 @@ export const useAttendanceSummary = (employeeId: number, startDate: string, endD
|
||||
});
|
||||
};
|
||||
|
||||
// 3. Fetch Dashboard Metrics
|
||||
// 3. Fetch Dashboard Metrics
|
||||
export const useDashboardMetrics = (date: string, companyId: string, branchId: string, departmentId: string) => {
|
||||
return useQuery<DashboardMetrics>({
|
||||
queryKey: ['dashboardMetrics', date, companyId, branchId, departmentId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/ams/attendance/dashboard/metrics', {
|
||||
params: {
|
||||
date,
|
||||
company_id: companyId || undefined,
|
||||
branch_id: branchId || undefined,
|
||||
department_id: departmentId || undefined
|
||||
params: {
|
||||
date,
|
||||
company_id: companyId || undefined,
|
||||
branch_id: branchId || undefined,
|
||||
department_id: departmentId || undefined
|
||||
}
|
||||
});
|
||||
const rawData = response.data?.data || response.data;
|
||||
@ -84,7 +89,7 @@ export const useDashboardMetrics = (date: string, companyId: string, branchId: s
|
||||
});
|
||||
};
|
||||
|
||||
// 4. Fetch Daily Report
|
||||
// 4. Fetch Daily Report
|
||||
export interface DailyReportFilters {
|
||||
date: string;
|
||||
companyId: string;
|
||||
@ -116,7 +121,7 @@ export const useDailyReport = (filters: DailyReportFilters) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 5. Fetch Admin Range Report
|
||||
// 5. Fetch Admin Range Report
|
||||
export interface AdminReportFilters {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
@ -150,7 +155,7 @@ export const useAdminReport = (filters: AdminReportFilters) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 6. Create Regularization Request
|
||||
// 6. Create Regularization Request
|
||||
export interface RegularizationPayload {
|
||||
attendance_id: number;
|
||||
employee_id: number;
|
||||
@ -176,19 +181,32 @@ export const useCreateRegularization = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 7. Fetch HR Manager Regularizations
|
||||
// 7. MOCK DATA: Fetch attendance records for other roles (Manager, HR, Director)
|
||||
const MOCK_ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
||||
employee: 'attendance/employee_attendance.json',
|
||||
manager: 'attendance/manager_attendance.json',
|
||||
hrmanager: 'attendance/hrmanager_attendance.json',
|
||||
director: 'attendance/director_attendance.json',
|
||||
};
|
||||
|
||||
export const useAttendanceData = () => {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
return useQuery<AttendanceRecord[]>({
|
||||
queryKey: ['attendance', role],
|
||||
queryFn: () => fetchMockData<AttendanceRecord[]>(MOCK_ATTENDANCE_ENDPOINTS[role]),
|
||||
});
|
||||
};
|
||||
|
||||
// 8. Fetch HR Manager Regularizations
|
||||
export interface RegularizationRecord {
|
||||
id: string;
|
||||
empId: string;
|
||||
empName: string;
|
||||
date: string;
|
||||
regularizationType: string;
|
||||
reqCheckIn: string;
|
||||
reqCheckOut: string;
|
||||
date: string;
|
||||
reason: string;
|
||||
status: 'Pending' | 'Approved' | 'Rejected';
|
||||
branchName: string;
|
||||
reviewerName?: string;
|
||||
}
|
||||
|
||||
export const useHRManagerRegularizations = () => {
|
||||
@ -197,12 +215,12 @@ export const useHRManagerRegularizations = () => {
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/ams/attendance/regularize/pending');
|
||||
const rawData = response.data.data || [];
|
||||
|
||||
|
||||
return rawData.map((r: any) => {
|
||||
const targetDate = r.targetDate ? new Date(r.targetDate) : new Date();
|
||||
const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date();
|
||||
const reqCheckOutDate = r.requestedCheckOut ? new Date(r.requestedCheckOut) : new Date();
|
||||
|
||||
|
||||
const formattedDate = targetDate.toISOString().split('T')[0];
|
||||
const formatTime = (date: Date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const capitalizeStatus = (status: string) => status ? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() : 'Pending';
|
||||
@ -211,75 +229,13 @@ export const useHRManagerRegularizations = () => {
|
||||
id: String(r.regularizationId),
|
||||
empId: r.employeeCode || String(r.employeeId),
|
||||
empName: `${r.firstName || ''} ${r.lastName || ''}`.trim(),
|
||||
date: formattedDate,
|
||||
regularizationType: r.regularizationType ? r.regularizationType.replace('_', ' ') : '-',
|
||||
reqCheckIn: formatTime(reqCheckInDate),
|
||||
reqCheckOut: formatTime(reqCheckOutDate),
|
||||
status: capitalizeStatus(r.status) as 'Pending' | 'Approved' | 'Rejected',
|
||||
branchName: r.branchName || 'N/A',
|
||||
date: formattedDate,
|
||||
reason: r.reason,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 8. Review Regularization Request (Approve/Reject)
|
||||
export const useReviewRegularization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, status }: { id: string; status: 'Approved' | 'Rejected' }) => {
|
||||
const action = status.toUpperCase();
|
||||
const response = await apiClient.post('/api/ams/attendance/regularize/review', {
|
||||
regularization_id: Number(id),
|
||||
action: action
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['HRManagerRegularizations'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useRegularizationHistory = () => {
|
||||
return useQuery<RegularizationRecord[]>({
|
||||
queryKey: ['regularizationHistory'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/ams/attendance/regularize/history');
|
||||
const rawData = response.data.data || [];
|
||||
|
||||
return rawData.map((r: any) => {
|
||||
const targetDate = r.targetDate ? new Date(r.targetDate) : new Date();
|
||||
const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date();
|
||||
const reqCheckOutDate = r.requestedCheckOut ? new Date(r.requestedCheckOut) : new Date();
|
||||
|
||||
const formattedDate = targetDate.toISOString().split('T')[0];
|
||||
const formatTime = (date: Date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const capitalizeStatus = (status: string) => status ? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() : 'Pending';
|
||||
|
||||
return {
|
||||
id: String(r.regularizationId),
|
||||
empId: r.employeeCode || String(r.employeeId),
|
||||
empName: `${r.firstName || ''} ${r.lastName || ''}`.trim(),
|
||||
date: formattedDate,
|
||||
regularizationType: r.regularizationType ? r.regularizationType.replace('_', ' ') : '-', // Format HALF_DAY to HALF DAY
|
||||
reqCheckIn: formatTime(reqCheckInDate),
|
||||
reqCheckOut: formatTime(reqCheckOutDate),
|
||||
status: capitalizeStatus(r.status) as 'Pending' | 'Approved' | 'Rejected',
|
||||
branchName: r.branchName || 'N/A',
|
||||
reviewerName: r.reviewerName || '-',
|
||||
reason: r.reason || '',
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Update the interface
|
||||
|
||||
|
||||
// Update the useHRManagerRegularizations hook mapping
|
||||
};
|
||||
@ -138,7 +138,7 @@ export default function AttendanceRecordsTable({
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
{(['date', 'checkIn', 'checkOut', 'totalHours', 'status'] as SortKey[]).map((key) => (
|
||||
{(['date', 'timeIn', 'timeOut', 'loggedHours', 'status'] as SortKey[]).map((key) => (
|
||||
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
|
||||
<div className="flex items-center">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} {getSortIcon(key)}
|
||||
@ -163,9 +163,9 @@ export default function AttendanceRecordsTable({
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
|
||||
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.checkIn}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.checkOut}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.totalHours}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeIn}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeOut}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.loggedHours}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</td>
|
||||
|
||||
@ -48,7 +48,9 @@ export default function Calendar({ data, selectedDate, currentMonth, currentYear
|
||||
const dayData = data[dateStr];
|
||||
|
||||
if (dayData?.status === 'Present') return 'bg-present-100 text-present-800 border-present-300 hover:bg-present-200';
|
||||
if (dayData?.status === 'Late') return 'bg-late-100 text-late-800 border-late-300 hover:bg-late-200';
|
||||
if (dayData?.status === 'Half Day') return 'bg-halfday-100 text-halfday-800 border-halfday-300 hover:bg-halfday-200';
|
||||
if (dayData?.status === 'WFH') return 'bg-wfh-100 text-wfh-800 border-wfh-300 hover:bg-wfh-200';
|
||||
if (dayData?.status === 'Absent') return 'bg-absent-100 text-absent-800 border-absent-300 hover:bg-absent-200';
|
||||
if (dayData?.status === 'Leave') return 'bg-leave-100 text-leave-800 border-leave-300 hover:bg-leave-200';
|
||||
if (dayData?.status === 'Mispunch') return 'bg-mispunch-100 text-mispunch-800 border-mispunch-300 hover:bg-mispunch-200';
|
||||
@ -100,7 +102,9 @@ export default function Calendar({ data, selectedDate, currentMonth, currentYear
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-app-border flex flex-wrap gap-4">
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-present-100 border border-present-300"></div><span className="text-xs text-text-secondary">Present</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-late-100 border border-late-300"></div><span className="text-xs text-text-secondary">Late Arrival</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-halfday-100 border border-halfday-300"></div><span className="text-xs text-text-secondary">Half Day</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-wfh-100 border border-wfh-300"></div><span className="text-xs text-text-secondary">Work From Home</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-absent-100 border border-absent-300"></div><span className="text-xs text-text-secondary">Absent</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-leave-100 border border-leave-300"></div><span className="text-xs text-text-secondary">Leave</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-mispunch-100 border border-mispunch-300"></div><span className="text-xs text-text-secondary">Mispunch</span></div>
|
||||
|
||||
@ -11,16 +11,14 @@ interface DailySummaryProps {
|
||||
login: string;
|
||||
logout: string;
|
||||
hours: string;
|
||||
attendanceId: number;
|
||||
attendanceId: number;
|
||||
} | null;
|
||||
employeeId: number;
|
||||
employeeId: number;
|
||||
}
|
||||
|
||||
export default function DailySummary({ selectedDate, data, employeeId }: DailySummaryProps) {
|
||||
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-', attendanceId: 0 };
|
||||
|
||||
// Updated condition to include Half Day
|
||||
const canRegularize = selectedData.status === 'Mispunch' || selectedData.status === 'Half Day';
|
||||
const isMispunch = selectedData.status === 'Mispunch';
|
||||
|
||||
const [isApplied, setIsApplied] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@ -90,7 +88,7 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
|
||||
</div>
|
||||
|
||||
{/* Conditional Regularization Button */}
|
||||
{canRegularize && (
|
||||
{isMispunch && (
|
||||
<div className="mt-6">
|
||||
{isApplied ? (
|
||||
<button
|
||||
@ -117,7 +115,6 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
|
||||
selectedDate={selectedDate}
|
||||
attendanceId={selectedData.attendanceId}
|
||||
employeeId={employeeId}
|
||||
status={selectedData.status} // Pass the status to the modal
|
||||
onClose={() => setShowModal(false)}
|
||||
onApply={handleApplyRegularization}
|
||||
/>
|
||||
|
||||
@ -5,12 +5,11 @@ interface RegularizationModalProps {
|
||||
selectedDate: string;
|
||||
attendanceId: number;
|
||||
employeeId: number;
|
||||
status: string; // Added status prop
|
||||
onClose: () => void;
|
||||
onApply: (payload: any) => void;
|
||||
onApply: (payload: any) => void;
|
||||
}
|
||||
|
||||
export default function RegularizationModal({ selectedDate, attendanceId, employeeId, status, onClose, onApply }: RegularizationModalProps) {
|
||||
export default function RegularizationModal({ selectedDate, attendanceId, employeeId, onClose, onApply }: RegularizationModalProps) {
|
||||
const [regData, setRegData] = useState({ checkIn: '', checkOut: '', reason: '' });
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
@ -19,21 +18,18 @@ export default function RegularizationModal({ selectedDate, attendanceId, employ
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Map frontend status to backend enum
|
||||
const backendType = status === 'Half Day' ? 'HALF_DAY' : 'MISPUNCH';
|
||||
|
||||
|
||||
// Format dates to "YYYY-MM-DD HH:mm:ss" for backend
|
||||
const payload = {
|
||||
attendance_id: attendanceId,
|
||||
employee_id: employeeId,
|
||||
regularization_type: backendType, // Dynamic type
|
||||
regularization_type: "MISPUNCH",
|
||||
target_date: selectedDate,
|
||||
requested_check_in: `${selectedDate} ${regData.checkIn}:00`,
|
||||
requested_check_out: `${selectedDate} ${regData.checkOut}:00`,
|
||||
reason: regData.reason
|
||||
};
|
||||
|
||||
|
||||
onApply(payload);
|
||||
};
|
||||
|
||||
@ -82,7 +78,7 @@ export default function RegularizationModal({ selectedDate, attendanceId, employ
|
||||
required
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card resize-none"
|
||||
placeholder={`Brief reason for ${status.toLowerCase()}...`}
|
||||
placeholder="Brief reason for mispunch..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
|
||||
@ -34,11 +34,11 @@ export default function DailyReportFilters({ filters, onFilterChange }: DailyRep
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newFilters = { ...filters, [name]: value };
|
||||
|
||||
|
||||
// Reset child dropdowns if parent changes
|
||||
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }
|
||||
if (name === 'branchId') { newFilters.departmentId = ''; }
|
||||
|
||||
|
||||
onFilterChange(newFilters);
|
||||
};
|
||||
|
||||
@ -73,10 +73,10 @@ export default function DailyReportFilters({ filters, onFilterChange }: DailyRep
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className={labelClass}>Job Role</label>
|
||||
<label className={labelClass}>Designation</label>
|
||||
<select name="designationId" value={filters.designationId} onChange={handleChange} className={inputClass}>
|
||||
<option value="">All Job Roles</option>
|
||||
{designations?.map(d => <option key={d.id} value={d.name}>{d.name}</option>)}
|
||||
<option value="">All Designations</option>
|
||||
{designations?.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -77,10 +77,10 @@ export default function MonthlyReportFilters({ filters, onFilterChange }: Monthl
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className={labelClass}>Job Role</label>
|
||||
<label className={labelClass}>Designation</label>
|
||||
<select name="designation" value={filters.designation} onChange={handleChange} className={inputClass}>
|
||||
<option value="">All Job Roles</option>
|
||||
{designations?.map(d => <option key={d.id} value={d.name}>{d.name}</option>)}
|
||||
<option value="">All Designations</option>
|
||||
{designations?.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { RegularizationRecord } from '../../api/useAttendanceData';
|
||||
|
||||
interface RegularizationHistoryModalProps {
|
||||
record: RegularizationRecord;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function RegularizationHistoryModal({ record, onClose }: RegularizationHistoryModalProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto">
|
||||
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
||||
<X size={24} />
|
||||
</button>
|
||||
<h3 className="text-xl font-bold text-text-primary mb-1">Regularization Details</h3>
|
||||
<p className="text-sm text-text-muted mb-6">Request ID: {record.id}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Emp Id</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.empId}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Emp Name</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.empName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Branch</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.branchName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Date</span>
|
||||
<span className="text-sm font-medium text-text-primary">{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Type</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.regularizationType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Req Check In</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.reqCheckIn}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Req Check Out</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.reqCheckOut}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Status</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Reviewed By</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.reviewerName || 'Pending Review'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-text-muted block mb-1">Reason</span>
|
||||
<p className="text-sm text-text-secondary bg-app-muted p-3 rounded-lg">{record.reason}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<button onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -28,11 +28,11 @@ export default function RegularizationModal({ record, onClose, onAction }: Regul
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Emp Id</span>
|
||||
<span className="text-sm text-text-muted">Employee ID</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.empId}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Emp Name</span>
|
||||
<span className="text-sm text-text-muted">Employee Name</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.empName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
@ -40,15 +40,15 @@ export default function RegularizationModal({ record, onClose, onAction }: Regul
|
||||
<span className="text-sm font-medium text-text-primary">{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Req Check In</span>
|
||||
<span className="text-sm text-text-muted">Requested Check-In</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.reqCheckIn}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Req Check Out</span>
|
||||
<span className="text-sm text-text-muted">Requested Check-Out</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.reqCheckOut}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Status</span>
|
||||
<span className="text-sm text-text-muted">Current Status</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -8,8 +8,7 @@ interface RegularizationTableProps {
|
||||
onAction: (id: string, status: 'Approved' | 'Rejected') => void;
|
||||
}
|
||||
|
||||
// Updated SortKey to include branchName and regularizationType
|
||||
type SortKey = 'empId' | 'empName' | 'branchName' | 'date' | 'regularizationType' | 'reqCheckIn' | 'reqCheckOut' | 'reason' | 'status';
|
||||
type SortKey = 'empId' | 'empName' | 'date' | 'reqCheckIn' | 'reqCheckOut' | 'reason' | 'status';
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
@ -27,11 +26,13 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// Default sort: date descending
|
||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||
key: 'date',
|
||||
direction: 'descending'
|
||||
});
|
||||
|
||||
// Filter data based on search query
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchQuery) return records;
|
||||
return records.filter(record =>
|
||||
@ -41,6 +42,7 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
);
|
||||
}, [records, searchQuery]);
|
||||
|
||||
// Sort filtered data
|
||||
const sortedData = useMemo(() => {
|
||||
const sortableData = [...filteredData];
|
||||
sortableData.sort((a, b) => {
|
||||
@ -51,6 +53,7 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
return sortableData;
|
||||
}, [filteredData, sortConfig]);
|
||||
|
||||
// Pagination Logic
|
||||
const totalPages = Math.ceil(sortedData.length / pageSize);
|
||||
const isPaginationEnabled = sortedData.length > pageSize;
|
||||
|
||||
@ -90,16 +93,6 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
return <ArrowDown size={14} className="ml-1 text-primary" />;
|
||||
};
|
||||
|
||||
const formatHeader = (key: string) => {
|
||||
if (key === 'empId') return 'Emp Id';
|
||||
if (key === 'empName') return 'Emp Name';
|
||||
if (key === 'branchName') return 'Branch';
|
||||
if (key === 'regularizationType') return 'Type';
|
||||
if (key === 'reqCheckIn') return 'Req Check In';
|
||||
if (key === 'reqCheckOut') return 'Req Check Out';
|
||||
return key.charAt(0).toUpperCase() + key.slice(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||
@ -119,14 +112,14 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
{(['empId', 'empName', 'branchName', 'date', 'regularizationType', 'reqCheckIn', 'reqCheckOut', 'reason', 'status'] as SortKey[]).map((key) => (
|
||||
{(['empId', 'empName', 'date', 'reqCheckIn', 'reqCheckOut', 'reason', 'status'] as SortKey[]).map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
onClick={() => requestSort(key)}
|
||||
className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{formatHeader(key)} {getSortIcon(key)}
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} {getSortIcon(key)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
@ -139,9 +132,7 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
<tr key={record.id} className="hover:bg-app-muted transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{record.empId}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{record.empName}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.regularizationType}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.reqCheckIn}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.reqCheckOut}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary max-w-xs truncate">{record.reason}</td>
|
||||
@ -157,12 +148,13 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">No matching regularization requests found.</td>
|
||||
<td colSpan={8} className="px-4 py-8 text-center text-sm text-text-muted">No matching regularization requests found.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Footer: Pagination & Page Size */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-text-muted">Show</span>
|
||||
|
||||
@ -3,8 +3,6 @@ import { FileText, Download, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import DailyReportFilters from '../components/summary/DailyReportFilters';
|
||||
import { useDailyReport, type DailyReportFilters as FilterValues } from '../api/useAttendanceData';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../../store/store';
|
||||
|
||||
const getTodayDate = () => {
|
||||
const today = new Date();
|
||||
@ -22,52 +20,10 @@ export default function DailyReport() {
|
||||
employeeCode: '',
|
||||
});
|
||||
|
||||
const [downloadFormat, setDownloadFormat] = useState<'excel' | 'pdf'>('excel');
|
||||
const roleState = useSelector((state: RootState) => state.role);
|
||||
|
||||
const { data: reportData, isLoading, isFetching } = useDailyReport(filters);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
date: filters.date,
|
||||
company_id: filters.companyId,
|
||||
branch_id: filters.branchId,
|
||||
department_id: filters.departmentId,
|
||||
designation: filters.designationId,
|
||||
employee_name: filters.employeeName,
|
||||
employee_code: filters.employeeCode,
|
||||
file_type: downloadFormat, // Pass format to backend
|
||||
}).toString();
|
||||
|
||||
const response = await fetch(`/api/ams/attendance/daily-report/export?${queryParams}`, {
|
||||
headers: {
|
||||
'X-Mock-User-Id': roleState.mockUserId,
|
||||
'X-Mock-User-Role': roleState.mockUserRole,
|
||||
'X-Mock-Branches': roleState.mockBranches
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to download report');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Daily_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`;
|
||||
a.download = fileName;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('Report downloaded successfully.');
|
||||
} catch (error) {
|
||||
toast.error('Failed to download report.');
|
||||
}
|
||||
const handleDownload = () => {
|
||||
toast.success(`Preparing daily report for ${filters.date}...`);
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
@ -93,26 +49,14 @@ export default function DailyReport() {
|
||||
<p className="text-sm text-text-muted">View daily attendance logs based on filters.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Controls */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
value={downloadFormat}
|
||||
onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')}
|
||||
className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card h-[38px]"
|
||||
>
|
||||
<option value="excel">Excel (.xlsx)</option>
|
||||
<option value="pdf">PDF (.pdf)</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isLoading || isFetching}
|
||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed h-[38px]"
|
||||
>
|
||||
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isLoading || isFetching}
|
||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||
Download Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DailyReportFilters filters={filters} onFilterChange={setFilters} />
|
||||
@ -134,7 +78,7 @@ export default function DailyReport() {
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Job Role</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Designation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-app-border">
|
||||
@ -173,6 +117,7 @@ export default function DailyReport() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,27 +1,33 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Check } from 'lucide-react';
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import RegularizationTable from '../components/summary/RegularizationTable';
|
||||
import { toast } from 'sonner';
|
||||
import { useHRManagerRegularizations, useReviewRegularization } from '../api/useAttendanceData';
|
||||
import { useHRManagerRegularizations, type RegularizationRecord } from '../api/useAttendanceData';
|
||||
|
||||
const mockReviewAction = (records: RegularizationRecord[], id: string, newStatus: 'Approved' | 'Rejected') => {
|
||||
return records.map(r => r.id === id ? { ...r, status: newStatus } : r);
|
||||
};
|
||||
|
||||
export default function HRManagerRegularization() {
|
||||
const { data: records, isLoading, isError } = useHRManagerRegularizations();
|
||||
const { mutate } = useReviewRegularization();
|
||||
const { data: initialRecords, isLoading, isError } = useHRManagerRegularizations();
|
||||
const [localRecords, setLocalRecords] = useState<RegularizationRecord[]>([]);
|
||||
|
||||
// Once data is loaded, sync it to local state so we can update statuses without refetching immediately
|
||||
useMemo(() => {
|
||||
if (initialRecords) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-render
|
||||
setLocalRecords(initialRecords);
|
||||
}
|
||||
}, [initialRecords]);
|
||||
|
||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||
mutate({ id, status }, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Regularization request ${status.toLowerCase()} successfully.`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.response?.data?.error || "Failed to update regularization status.");
|
||||
}
|
||||
});
|
||||
// TODO: Replace with real API call to POST /api/ams/attendance/regularize/review
|
||||
setLocalRecords(prev => mockReviewAction(prev, id, status));
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader message="Loading Regularizations..." />;
|
||||
if (isError) return <ErrorState message="Failed to load regularizations data." />;
|
||||
if (isError) return <ErrorState message="Failed to load regularizations data." />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@ -35,7 +41,7 @@ export default function HRManagerRegularization() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RegularizationTable records={records || []} onAction={handleAction} />
|
||||
<RegularizationTable records={localRecords} onAction={handleAction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,8 +3,6 @@ import { Download, FileText, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import MonthlyReportFilters from '../components/summary/MonthlyReportFilters';
|
||||
import { useAdminReport, type AdminReportFilters } from '../api/useAttendanceData';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../../store/store';
|
||||
|
||||
const getTodayDate = () => {
|
||||
const today = new Date();
|
||||
@ -28,53 +26,10 @@ export default function MonthlyReport() {
|
||||
employeeCode: '',
|
||||
});
|
||||
|
||||
const [downloadFormat, setDownloadFormat] = useState<'excel' | 'pdf'>('excel');
|
||||
const roleState = useSelector((state: RootState) => state.role);
|
||||
|
||||
const { data: reportData, isLoading, isFetching } = useAdminReport(filters);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
from_date: filters.fromDate,
|
||||
to_date: filters.toDate,
|
||||
company_id: filters.companyId,
|
||||
branch_id: filters.branchId,
|
||||
department_id: filters.departmentId,
|
||||
designation: filters.designation,
|
||||
employee_name: filters.employeeName,
|
||||
employee_code: filters.employeeCode,
|
||||
file_type: downloadFormat,
|
||||
}).toString();
|
||||
|
||||
const response = await fetch(`/api/ams/attendance/admin-report/export?${queryParams}`, {
|
||||
headers: {
|
||||
'X-Mock-User-Id': roleState.mockUserId,
|
||||
'X-Mock-User-Role': roleState.mockUserRole,
|
||||
'X-Mock-Branches': roleState.mockBranches
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to download report');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const fileName = contentDisposition ? contentDisposition.split('filename=')[1].replace(/"/g, '') : `Monthly_Report.${downloadFormat === 'excel' ? 'xlsx' : 'pdf'}`;
|
||||
a.download = fileName;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('Report downloaded successfully.');
|
||||
} catch (error) {
|
||||
toast.error('Failed to download report.');
|
||||
}
|
||||
const handleDownload = () => {
|
||||
toast.success("Preparing report for download...");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -89,25 +44,14 @@ export default function MonthlyReport() {
|
||||
<p className="text-sm text-text-muted">View monthly attendance metrics based on filters.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
value={downloadFormat}
|
||||
onChange={(e) => setDownloadFormat(e.target.value as 'excel' | 'pdf')}
|
||||
className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card h-[38px]"
|
||||
>
|
||||
<option value="excel">Excel (.xlsx)</option>
|
||||
<option value="pdf">PDF (.pdf)</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isLoading || isFetching}
|
||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed h-[38px]"
|
||||
>
|
||||
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={isLoading || isFetching}
|
||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||
Download Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MonthlyReportFilters filters={filters} onFilterChange={setFilters} />
|
||||
@ -159,6 +103,7 @@ export default function MonthlyReport() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -44,9 +44,9 @@ export default function MyAttendance() {
|
||||
calendarRecords.forEach(record => {
|
||||
map[record.date] = {
|
||||
status: record.status,
|
||||
login: record.checkIn,
|
||||
logout: record.checkOut,
|
||||
hours: record.totalHours,
|
||||
login: record.timeIn,
|
||||
logout: record.timeOut,
|
||||
hours: record.loggedHours,
|
||||
attendanceId: record.attendanceId
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { History } from 'lucide-react';
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import RegularizationHistoryTable from '../components/summary/RegularizationHistoryTable';
|
||||
import { useRegularizationHistory } from '../api/useAttendanceData';
|
||||
|
||||
export default function RegularizationHistory() {
|
||||
const { data: records, isLoading, isError } = useRegularizationHistory();
|
||||
|
||||
if (isLoading) return <Loader message="Loading Regularization History..." />;
|
||||
if (isError) return <ErrorState message="Failed to load regularization history." />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex items-center space-x-3">
|
||||
<div className="p-3 bg-primary/10 rounded-full">
|
||||
<History className="text-primary" size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-text-primary">Regularization History</h2>
|
||||
<p className="text-sm text-text-muted">View all past and pending regularization requests.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RegularizationHistoryTable records={records || []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,7 +3,6 @@ import AttendanceDashboard from '../pages/AttendanceDashboard';
|
||||
import MonthlyReport from '../pages/MonthlyReport';
|
||||
import DailyReport from '../pages/DailyReport';
|
||||
import HRManagerRegularization from '../pages/HRManagerRegularization';
|
||||
import RegularizationHistory from '../pages/RegularizationHistory';
|
||||
|
||||
export default function AttendanceSummaryRouter() {
|
||||
return (
|
||||
@ -13,7 +12,6 @@ export default function AttendanceSummaryRouter() {
|
||||
<Route path="/monthly" element={<MonthlyReport />} />
|
||||
<Route path="/daily" element={<DailyReport />} />
|
||||
<Route path="/regularization" element={<HRManagerRegularization />} />
|
||||
<Route path="/regularization-history" element={<RegularizationHistory />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
export type AttendanceStatus = 'Present' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Half Day';
|
||||
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
||||
|
||||
export interface AttendanceRecord {
|
||||
date: string;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
totalHours: string;
|
||||
timeIn: string;
|
||||
timeOut: string;
|
||||
loggedHours: string;
|
||||
status: AttendanceStatus;
|
||||
attendanceId: number;
|
||||
attendanceId: number;
|
||||
}
|
||||
|
||||
export type SortKey = keyof AttendanceRecord;
|
||||
|
||||
@ -1,308 +1,108 @@
|
||||
// src/features/leave/api/useLeaveData.ts
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { apiClient, fetchMockData } from '../../../api/client';
|
||||
import { fetchMockData, postMockData } from '../../../api/client';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import type {
|
||||
LeaveSummary,
|
||||
LeaveRequestPayload,
|
||||
LeaveHistoryRecord,
|
||||
ManagerLeaveApproval,
|
||||
ApprovalStatus,
|
||||
LeaveApplicationSummary,
|
||||
LeaveBalanceRecord
|
||||
} from '../types/leave';
|
||||
// Add this import at the top of the file
|
||||
import { useEmployeeById } from '../../masterData/api/useEmployeeData';
|
||||
import type { UserRole } from '../../../store/roleSlice';
|
||||
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
||||
import type { LeaveApplicationSummary } from '../types/leave';
|
||||
import type { LeaveBalanceRecord } from '../types/leave';
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 1. ADMIN SETUP & CONFIGURATION
|
||||
// ==========================================
|
||||
|
||||
// Create Leave Type
|
||||
export const useCreateLeaveType = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (data: { company_id: number; name: string; requires_allocation: boolean; carry_over_allowed: boolean; max_carry_over_days: number }) => {
|
||||
const response = await apiClient.post('/api/lms/config/types', data);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['leaveTypes'] });
|
||||
},
|
||||
});
|
||||
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
||||
employee: {
|
||||
summary: 'leave/employee_leave_summary.json',
|
||||
history: 'leave/employee_leave_history.json',
|
||||
},
|
||||
manager: {
|
||||
summary: 'leave/manager_leave_summary.json',
|
||||
history: 'leave/manager_leave_history.json',
|
||||
},
|
||||
hrmanager: {
|
||||
summary: 'leave/hrmanager_leave_summary.json',
|
||||
history: 'leave/hrmanager_leave_history.json',
|
||||
},
|
||||
director: {
|
||||
summary: 'leave/director_leave_summary.json',
|
||||
history: 'leave/director_leave_history.json',
|
||||
},
|
||||
};
|
||||
|
||||
// Get Leave Types
|
||||
export const useLeaveTypes = (companyId: number = 1) => {
|
||||
return useQuery<any[]>({
|
||||
queryKey: ['leaveTypes', companyId],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get(`/api/lms/config/types?company_id=${companyId}`);
|
||||
return response.data.data || [];
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Create Policy Rule
|
||||
export const useCreatePolicyRule = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
const response = await apiClient.post('/api/lms/config/rules', data);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['policyRules'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Create Company Holiday (Bulk)
|
||||
export const useCreateCompanyHoliday = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (holidays: any[]) => {
|
||||
const response = await apiClient.post('/api/lms/config/holidays', holidays);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['holidays'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Update Work Settings
|
||||
export const useUpdateWorkSettings = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (data: { company_id: number; branch_id: number; weekly_off_days: number[]; effective_from: string }) => {
|
||||
const response = await apiClient.put('/api/lms/config/work-settings', data);
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 2. EMPLOYEE ACTIONS
|
||||
// ==========================================
|
||||
|
||||
// Get Leave Balances (Summary)
|
||||
export const useLeaveSummary = () => {
|
||||
return useQuery<LeaveSummary[]>({
|
||||
queryKey: ['leaveSummary'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/leaves/balances', {
|
||||
params: { year: new Date().getFullYear() }
|
||||
});
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
// Map backend response to frontend LeaveSummary interface
|
||||
// Inside useLeaveSummary
|
||||
return (response.data.balances || []).map((b: any) => ({
|
||||
leaveTypeId: b.leaveTypeId, // Ensure this is mapped!
|
||||
leaveType: b.leaveTypeName,
|
||||
credited: b.grantedDays,
|
||||
utilized: b.usedDays,
|
||||
lapsed: 0,
|
||||
balance: b.availableBalance
|
||||
}));
|
||||
},
|
||||
return useQuery<LeaveSummary[]>({
|
||||
queryKey: ['leaveSummary', role],
|
||||
queryFn: () => fetchMockData<LeaveSummary[]>(LEAVE_ENDPOINTS[role].summary),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ... (keep other hooks)
|
||||
|
||||
// Apply for Leave
|
||||
export const useApplyLeave = () => {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
const queryClient = useQueryClient();
|
||||
const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId));
|
||||
|
||||
// Dynamically fetch the logged-in user's profile to get companyId and branchId
|
||||
const { data: empDetail } = useEmployeeById(employeeId);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: LeaveRequestPayload) => {
|
||||
if (!empDetail) {
|
||||
throw new Error("User profile is still loading. Please try again in a moment.");
|
||||
}
|
||||
|
||||
// All values are now fully dynamic based on the user's real data
|
||||
const reqPayload = {
|
||||
employee_id: employeeId,
|
||||
leave_type_id: payload.leaveTypeId,
|
||||
company_id: empDetail.companyId, // Dynamic from EMS
|
||||
branch_id: empDetail.branchId, // Dynamic from EMS
|
||||
date_from: payload.fromDate,
|
||||
date_to: payload.toDate,
|
||||
is_half_day: payload.leaveDays === 0.5,
|
||||
reason: payload.reason,
|
||||
};
|
||||
const response = await apiClient.post('/api/lms/leaves/apply', reqPayload);
|
||||
return response.data;
|
||||
},
|
||||
mutationFn: (payload: LeaveRequestPayload) => postMockData(`leave/${role}_leave_requests`, payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['leaveSummary'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['leaveHistory'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['leaveSummary', role] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ... (keep the rest of the file)
|
||||
|
||||
// Get Leave History
|
||||
export const useLeaveHistory = () => {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
return useQuery<LeaveHistoryRecord[]>({
|
||||
queryKey: ['leaveHistory'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/leaves/history', {
|
||||
params: { year: new Date().getFullYear() }
|
||||
});
|
||||
|
||||
// Map backend response to frontend LeaveHistoryRecord interface
|
||||
return (response.data.history || []).map((h: any) => ({
|
||||
id: String(h.applicationId),
|
||||
leaveType: h.leaveTypeName,
|
||||
applicationDate: new Date().toISOString().split('T')[0], // Backend doesn't return applicationDate yet
|
||||
fromDate: h.dateFrom,
|
||||
toDate: h.dateTo,
|
||||
leaveDays: h.numberOfDays,
|
||||
status: h.status,
|
||||
reason: h.reason
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Get Valid Optional Holidays
|
||||
export const useValidOptionalHolidays = () => {
|
||||
return useQuery<any[]>({
|
||||
queryKey: ['optionalHolidays'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/holidays/valid-optional');
|
||||
return response.data.holidays || [];
|
||||
},
|
||||
queryKey: ['leaveHistory', role],
|
||||
queryFn: () => fetchMockData<LeaveHistoryRecord[]>(LEAVE_ENDPOINTS[role].history),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 3. MANAGER ACTIONS
|
||||
// ==========================================
|
||||
|
||||
// Get Pending Leaves for Team (Generic for Manager, HR, Director)
|
||||
export const usePendingApprovals = () => {
|
||||
export const useManagerApprovals = () => {
|
||||
return useQuery<ManagerLeaveApproval[]>({
|
||||
queryKey: ['pendingApprovals'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/manager/pending');
|
||||
|
||||
// Map backend response to frontend ManagerLeaveApproval interface
|
||||
return (response.data.data || []).map((p: any) => ({
|
||||
id: String(p.applicationId),
|
||||
employeeId: p.employeeCode || String(p.employeeId),
|
||||
employeeName: `${p.firstName} ${p.lastName}`.trim(),
|
||||
leaveType: p.leaveType,
|
||||
applicationDate: new Date().toISOString().split('T')[0],
|
||||
fromDate: p.dateFrom,
|
||||
toDate: p.dateTo,
|
||||
leaveDays: p.numberOfDays,
|
||||
status: p.status,
|
||||
reason: p.reason
|
||||
}));
|
||||
},
|
||||
queryKey: ['leaveApprovals', 'manager'],
|
||||
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/manager_pending_leaves.json'),
|
||||
});
|
||||
};
|
||||
|
||||
// Aliases for role-specific hooks to keep existing components working
|
||||
export const useManagerApprovals = usePendingApprovals;
|
||||
export const useHRManagerApprovals = usePendingApprovals;
|
||||
export const useDirectorApprovals = usePendingApprovals;
|
||||
export const useLeaveApplications = () => {
|
||||
return useQuery<LeaveApplicationSummary[]>({
|
||||
queryKey: ['leaveApplications'],
|
||||
queryFn: () => fetchMockData<LeaveApplicationSummary[]>('leave/leave_applications.json'),
|
||||
});
|
||||
};
|
||||
|
||||
export const useLeaveBalance = () => {
|
||||
return useQuery<LeaveBalanceRecord[]>({
|
||||
queryKey: ['leaveBalance'],
|
||||
queryFn: () => fetchMockData<LeaveBalanceRecord[]>('leave/leave_balance.json'),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useHRManagerApprovals = () => {
|
||||
return useQuery<ManagerLeaveApproval[]>({
|
||||
queryKey: ['leaveApprovals', 'hrmanager'],
|
||||
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/hrmanager_pending_leaves.json'),
|
||||
});
|
||||
};
|
||||
|
||||
export const useDirectorApprovals = () => {
|
||||
return useQuery<ManagerLeaveApproval[]>({
|
||||
queryKey: ['leaveApprovals', 'director'],
|
||||
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/director_pending_leaves.json'),
|
||||
});
|
||||
};
|
||||
|
||||
// Approve / Reject Leave
|
||||
export const useUpdateLeaveApproval = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, status }: { id: string; status: ApprovalStatus }) => {
|
||||
const endpoint = status === 'Approved' ? 'approve' : 'reject';
|
||||
const payload = status === 'Rejected' ? { reason: "Rejected by approver" } : {};
|
||||
const response = await apiClient.post(`/api/lms/manager/leaves/${id}/${endpoint}`, payload);
|
||||
return response.data;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.log(`[Mock PUT] Updated leave ${id} to ${status}`);
|
||||
return { id, status };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pendingApprovals'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 4. REPORTS & MOCK FALLBACKS
|
||||
// ==========================================
|
||||
|
||||
// Get Ledger Report (OB-CB)
|
||||
export const useLeaveBalance = (params: { month: number; year: number; companyId: number }, enabled: boolean) => {
|
||||
return useQuery<LeaveBalanceRecord[]>({
|
||||
queryKey: ['leaveBalance', params],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/admin/reports/ob-cb', {
|
||||
params: { month: params.month, year: params.year, company_id: params.companyId }
|
||||
});
|
||||
|
||||
const rawData = response.data.data || [];
|
||||
const employeeMap: Record<string, LeaveBalanceRecord> = {};
|
||||
|
||||
// Group raw data by employeeId
|
||||
rawData.forEach((item: any) => {
|
||||
if (!employeeMap[item.employeeId]) {
|
||||
employeeMap[item.employeeId] = {
|
||||
employeeId: item.employeeCode || String(item.employeeId),
|
||||
employeeName: item.employeeName,
|
||||
casualOpening: 0,
|
||||
casualClosing: 0,
|
||||
optionalOpening: 0,
|
||||
optionalClosing: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Assign values based on leave type
|
||||
if (item.leaveType === 'Casual Leave') {
|
||||
employeeMap[item.employeeId].casualOpening = item.openingBalance;
|
||||
employeeMap[item.employeeId].casualClosing = item.closingBalance;
|
||||
} else if (item.leaveType === 'Optional Leave') {
|
||||
employeeMap[item.employeeId].optionalOpening = item.openingBalance;
|
||||
employeeMap[item.employeeId].optionalClosing = item.closingBalance;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(employeeMap);
|
||||
},
|
||||
enabled: enabled, // Only fetch when the user clicks search
|
||||
});
|
||||
};
|
||||
|
||||
// Get Leave Applications (Real API for HR/Director)
|
||||
export const useLeaveApplications = () => {
|
||||
return useQuery<LeaveApplicationSummary[]>({
|
||||
queryKey: ['leaveApplications'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get('/api/lms/hr/applications');
|
||||
|
||||
return (response.data.data || []).map((p: any) => ({
|
||||
id: String(p.applicationId),
|
||||
employeeId: p.employeeCode,
|
||||
employeeName: `${p.firstName} ${p.lastName}`.trim(),
|
||||
jobRole: p.jobTitle, // Mapped from jobTitle
|
||||
department: p.departmentName,
|
||||
managerName: p.managerName, // Mapped from backend
|
||||
leaveType: p.leaveType,
|
||||
fromDate: p.dateFrom,
|
||||
toDate: p.dateTo,
|
||||
leaveDays: p.numberOfDays,
|
||||
status: p.status,
|
||||
reason: p.reason, // Mapped from backend
|
||||
}));
|
||||
queryClient.invalidateQueries({ queryKey: ['leaveApprovals'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -7,13 +7,13 @@ interface ApprovalModalProps {
|
||||
onAction: (id: string, status: ApprovalStatus) => void;
|
||||
}
|
||||
|
||||
// Updated to handle backend uppercase statuses
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function ApprovalModal({ record, onClose, onAction }: ApprovalModalProps) {
|
||||
@ -21,9 +21,6 @@ export default function ApprovalModal({ record, onClose, onAction }: ApprovalMod
|
||||
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
// Check if the status is pending (case-insensitive)
|
||||
const isPending = record.status.toUpperCase().includes('PENDING');
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
||||
@ -70,8 +67,7 @@ export default function ApprovalModal({ record, onClose, onAction }: ApprovalMod
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
{/* Render buttons if the status is PENDING */}
|
||||
{isPending ? (
|
||||
{record.status === 'Pending' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onAction(record.id, 'Rejected')}
|
||||
|
||||
@ -11,13 +11,13 @@ interface ApprovalTableProps {
|
||||
type SortKey = 'employeeId' | 'employeeName' | 'leaveType' | 'applicationDate' | 'leaveDays' | 'status';
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
// Updated to handle backend uppercase statuses
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function ApprovalTable({ records, onAction }: ApprovalTableProps) {
|
||||
@ -26,11 +26,13 @@ export default function ApprovalTable({ records, onAction }: ApprovalTableProps)
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// Default sort: applicationDate descending (last applied first)
|
||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||
key: 'applicationDate',
|
||||
direction: 'descending'
|
||||
});
|
||||
|
||||
// Filter data based on search query
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchQuery) return records;
|
||||
return records.filter(record =>
|
||||
@ -41,6 +43,7 @@ export default function ApprovalTable({ records, onAction }: ApprovalTableProps)
|
||||
);
|
||||
}, [records, searchQuery]);
|
||||
|
||||
// Sort filtered data
|
||||
const sortedData = useMemo(() => {
|
||||
const sortableData = [...filteredData];
|
||||
sortableData.sort((a, b) => {
|
||||
@ -51,6 +54,7 @@ export default function ApprovalTable({ records, onAction }: ApprovalTableProps)
|
||||
return sortableData;
|
||||
}, [filteredData, sortConfig]);
|
||||
|
||||
// Pagination Logic
|
||||
const totalPages = Math.ceil(sortedData.length / pageSize);
|
||||
const isPaginationEnabled = sortedData.length > pageSize;
|
||||
|
||||
@ -94,6 +98,8 @@ export default function ApprovalTable({ records, onAction }: ApprovalTableProps)
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||
<h2 className="text-lg font-semibold text-text-primary">Pending Leave Approvals</h2>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative w-full md:w-64">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||
<input
|
||||
@ -152,6 +158,7 @@ export default function ApprovalTable({ records, onAction }: ApprovalTableProps)
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer: Pagination & Page Size */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-text-muted">Show</span>
|
||||
|
||||
@ -19,11 +19,17 @@ const calculateLeaveDays = (from: string, fromMode: DayMode, to: string, toMode:
|
||||
return days;
|
||||
};
|
||||
|
||||
// logic: +1 day, if Sunday -> Monday, with safe local formatting
|
||||
const calculateResumptionDate = (to: string) => {
|
||||
if (!to) return '';
|
||||
const date = new Date(to + 'T00:00:00');
|
||||
date.setDate(date.getDate() + 1);
|
||||
if (date.getDay() === 0) date.setDate(date.getDate() + 1);
|
||||
date.setDate(date.getDate() + 1); // Add 1 day
|
||||
|
||||
if (date.getDay() === 0) { // 0 is Sunday
|
||||
date.setDate(date.getDate() + 1); // Make it Monday
|
||||
}
|
||||
|
||||
// Format safely in local time
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
@ -33,22 +39,18 @@ const calculateResumptionDate = (to: string) => {
|
||||
export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
|
||||
const { mutate, isPending } = useApplyLeave();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
// Track both ID (for API) and Name (for UI display)
|
||||
const [selectedLeaveTypeId, setSelectedLeaveTypeId] = useState<number | ''>('');
|
||||
const [selectedLeaveTypeName, setSelectedLeaveTypeName] = useState('');
|
||||
|
||||
const [selectedLeaveType, setSelectedLeaveType] = useState('');
|
||||
const [formData, setFormData] = useState<LeaveRequestPayload>({
|
||||
leaveTypeId: 0, leaveType: '', fromDate: '', fromDayMode: 'Full Day', toDate: '', toDayMode: 'Full Day', leaveDays: 0, resumptionDate: '', reason: '',
|
||||
leaveType: '', fromDate: '', fromDayMode: 'Full Day', toDate: '', toDayMode: 'Full Day', leaveDays: 0, resumptionDate: '', reason: '',
|
||||
});
|
||||
|
||||
const calculatedDays = useMemo(() => calculateLeaveDays(formData.fromDate, formData.fromDayMode, formData.toDate, formData.toDayMode), [formData.fromDate, formData.fromDayMode, formData.toDate, formData.toDayMode]);
|
||||
const calculatedResumption = useMemo(() => calculateResumptionDate(formData.toDate), [formData.toDate]);
|
||||
|
||||
const handleProceed = () => {
|
||||
if (!selectedLeaveTypeId) { toast.error("Please select a leave type first."); return; }
|
||||
if (!selectedLeaveType) { toast.error("Please select a leave type first."); return; }
|
||||
setShowForm(true);
|
||||
setFormData(prev => ({ ...prev, leaveTypeId: Number(selectedLeaveTypeId), leaveType: selectedLeaveTypeName }));
|
||||
setFormData(prev => ({ ...prev, leaveType: selectedLeaveType }));
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
@ -62,9 +64,8 @@ export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
|
||||
onSuccess: () => {
|
||||
toast.success("Leave applied successfully!");
|
||||
setShowForm(false);
|
||||
setSelectedLeaveTypeId('');
|
||||
setSelectedLeaveTypeName('');
|
||||
setFormData({ leaveTypeId: 0, leaveType: '', fromDate: '', fromDayMode: 'Full Day', toDate: '', toDayMode: 'Full Day', leaveDays: 0, resumptionDate: '', reason: '' });
|
||||
setSelectedLeaveType('');
|
||||
setFormData({ leaveType: '', fromDate: '', fromDayMode: 'Full Day', toDate: '', toDayMode: 'Full Day', leaveDays: 0, resumptionDate: '', reason: '' });
|
||||
},
|
||||
onError: () => toast.error("Failed to apply leave."),
|
||||
});
|
||||
@ -77,20 +78,9 @@ export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">Leave Type</label>
|
||||
<select
|
||||
value={selectedLeaveTypeId}
|
||||
onChange={(e) => {
|
||||
const id = Number(e.target.value);
|
||||
setSelectedLeaveTypeId(id);
|
||||
const selected = summaryData.find(l => l.leaveTypeId === id);
|
||||
setSelectedLeaveTypeName(selected?.leaveType || '');
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
||||
>
|
||||
<select value={selectedLeaveType} onChange={(e) => setSelectedLeaveType(e.target.value)} className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||
<option value="">Select Leave Type</option>
|
||||
{summaryData.map((leave) => (
|
||||
<option key={leave.leaveTypeId} value={leave.leaveTypeId}>{leave.leaveType}</option>
|
||||
))}
|
||||
{summaryData.map((leave, idx) => (<option key={idx} value={leave.leaveType}>{leave.leaveType}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={handleProceed} className="w-full px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium">Proceed to Apply Leave</button>
|
||||
@ -98,7 +88,7 @@ export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-primary bg-primary/10 px-3 py-1 rounded-full">{selectedLeaveTypeName}</span>
|
||||
<span className="text-sm font-medium text-primary bg-primary/10 px-3 py-1 rounded-full">{selectedLeaveType}</span>
|
||||
<button type="button" onClick={() => setShowForm(false)} className="flex items-center px-4 py-1 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm ">Cancel</button>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { LeaveApplicationSummary } from '../../../types/leave';
|
||||
import type { LeaveApplicationSummary } from '../../types/leave';
|
||||
|
||||
interface LeaveApplicationModalProps {
|
||||
record: LeaveApplicationSummary;
|
||||
@ -7,17 +7,13 @@ interface LeaveApplicationModalProps {
|
||||
}
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
if (normalizedStatus.includes('WITHDRAWN') || normalizedStatus.includes('CANCELLED')) return 'bg-app-muted text-text-secondary';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
};
|
||||
|
||||
const formatReadableDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
case 'Withdrawn': return 'bg-app-muted text-text-secondary';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function LeaveApplicationModal({ record, onClose }: LeaveApplicationModalProps) {
|
||||
@ -43,8 +39,8 @@ export default function LeaveApplicationModal({ record, onClose }: LeaveApplicat
|
||||
<span className="text-sm font-medium text-text-primary">{record.employeeName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Job Role</span> {/* Changed */}
|
||||
<span className="text-sm font-medium text-text-primary">{record.jobRole}</span> {/* Changed */}
|
||||
<span className="text-sm text-text-muted">Designation</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.designation}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Department</span>
|
||||
@ -59,21 +55,13 @@ export default function LeaveApplicationModal({ record, onClose }: LeaveApplicat
|
||||
<span className="text-sm font-medium text-text-primary">{record.leaveType}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Duration</span>
|
||||
<span className="text-sm font-medium text-text-primary text-right">
|
||||
{formatReadableDate(record.fromDate)} to {formatReadableDate(record.toDate)}<br />
|
||||
<span className="text-text-muted">({record.leaveDays} Days)</span>
|
||||
</span>
|
||||
<span className="text-sm text-text-muted">Leave Days</span>
|
||||
<span className="text-sm font-medium text-text-primary">{record.leaveDays}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted">Status</span>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</div>
|
||||
{/* Added Reason Field */}
|
||||
<div className="border-b border-app-border pb-2">
|
||||
<span className="text-sm text-text-muted block mb-1">Reason</span>
|
||||
<p className="text-sm text-text-secondary bg-app-muted p-3 rounded-lg">{record.reason || 'No reason provided.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
@ -1,23 +1,23 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import type { LeaveApplicationSummary } from '../../../types/leave';
|
||||
import LeaveApplicationModal from '../modal/LeaveApplicationModal';
|
||||
import type { LeaveApplicationSummary } from '../../types/leave';
|
||||
import LeaveApplicationModal from './LeaveApplicationModal';
|
||||
|
||||
interface LeaveApplicationsTableProps {
|
||||
records: LeaveApplicationSummary[];
|
||||
}
|
||||
|
||||
// Updated to use jobRole instead of designation
|
||||
type SortKey = 'id' | 'employeeId' | 'employeeName' | 'jobRole' | 'department' | 'managerName' | 'leaveType' | 'leaveDays' | 'status';
|
||||
type SortKey = 'id' | 'employeeId' | 'employeeName' | 'designation' | 'department' | 'managerName' | 'leaveType' | 'leaveDays' | 'status';
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
if (normalizedStatus.includes('WITHDRAWN') || normalizedStatus.includes('CANCELLED')) return 'bg-app-muted text-text-secondary';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
case 'Withdrawn': return 'bg-app-muted text-text-secondary';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function LeaveApplicationsTable({ records }: LeaveApplicationsTableProps) {
|
||||
@ -36,7 +36,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
|
||||
record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.employeeId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.employeeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.jobRole.toLowerCase().includes(searchQuery.toLowerCase()) || // Changed
|
||||
record.designation.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.department.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.managerName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.leaveType.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
@ -97,7 +97,6 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
|
||||
if (key === 'id') return 'App ID';
|
||||
if (key === 'employeeId') return 'Emp CODE';
|
||||
if (key === 'employeeName') return 'Employee Name';
|
||||
if (key === 'jobRole') return 'Job Role'; // Changed
|
||||
if (key === 'managerName') return 'Manager Name';
|
||||
if (key === 'leaveType') return 'Leave Type';
|
||||
if (key === 'leaveDays') return 'Leave Days';
|
||||
@ -124,7 +123,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
{(['id', 'employeeId', 'employeeName', 'jobRole', 'department', 'managerName', 'leaveType', 'leaveDays', 'status'] as SortKey[]).map((key) => (
|
||||
{(['id', 'employeeId', 'employeeName', 'designation', 'department', 'managerName', 'leaveType', 'leaveDays', 'status'] as SortKey[]).map((key) => (
|
||||
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
|
||||
<div className="flex items-center">
|
||||
{formatHeader(key)} {getSortIcon(key)}
|
||||
@ -141,7 +140,7 @@ export default function LeaveApplicationsTable({ records }: LeaveApplicationsTab
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeId}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.employeeName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.jobRole}</td> {/* Changed */}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.designation}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.department}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.managerName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.leaveType}</td>
|
||||
@ -1,9 +1,9 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Ban } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { LeaveHistoryRecord, LeaveSortKey } from '../../../types/leave';
|
||||
import ViewLeaveModal from '../modal/ViewLeaveModal';
|
||||
import WithdrawLeaveModal from '../modal/WithdrawLeaveModal';
|
||||
import type { LeaveHistoryRecord, LeaveSortKey } from '../../types/leave';
|
||||
import ViewLeaveModal from './ViewLeaveModal';
|
||||
import WithdrawLeaveModal from './WithdrawLeaveModal';
|
||||
|
||||
interface LeaveHistoryTableProps {
|
||||
records: LeaveHistoryRecord[];
|
||||
@ -40,34 +40,17 @@ export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return combinedRecords.filter((record) => {
|
||||
// Filter based on the actual Leave Dates (fromDate and toDate)
|
||||
// If a filter date is selected, check if the leave period overlaps with the selected range
|
||||
const matchesFrom = fromDate ? record.toDate >= fromDate : true;
|
||||
const matchesTo = toDate ? record.fromDate <= toDate : true;
|
||||
|
||||
return matchesFrom && matchesTo;
|
||||
const isAfterFrom = fromDate ? record.applicationDate >= fromDate : true;
|
||||
const isBeforeTo = toDate ? record.applicationDate <= toDate : true;
|
||||
return isAfterFrom && isBeforeTo;
|
||||
});
|
||||
}, [combinedRecords, fromDate, toDate]);
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
const sortableData = [...filteredData];
|
||||
// Handle sorting for dates properly
|
||||
sortableData.sort((a, b) => {
|
||||
const valA = a[sortConfig.key];
|
||||
const valB = b[sortConfig.key];
|
||||
|
||||
// If sorting by dates, convert to Date objects for accurate comparison
|
||||
if (['applicationDate', 'fromDate', 'toDate'].includes(sortConfig.key)) {
|
||||
const dateA = new Date(valA + 'T00:00:00').getTime();
|
||||
const dateB = new Date(valB + 'T00:00:00').getTime();
|
||||
if (dateA < dateB) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
if (dateA > dateB) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Default string/number comparison
|
||||
if (valA < valB) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
if (valA > valB) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
return sortableData;
|
||||
@ -139,12 +122,12 @@ export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
if (normalizedStatus.includes('WITHDRAWN') || normalizedStatus.includes('CANCELLED')) return 'bg-app-muted text-text-secondary';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Withdrawn': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -195,7 +178,7 @@ export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
|
||||
<button onClick={() => setViewRecord(record)} className="text-primary hover:text-primary-hover flex items-center">
|
||||
<Eye size={16} className="mr-1" /> View
|
||||
</button>
|
||||
{record.status.toUpperCase() === 'PENDING' && (
|
||||
{record.status === 'Pending' && (
|
||||
<button onClick={() => setWithdrawRecord(record)} className="text-absent-500 hover:text-absent-700 flex items-center">
|
||||
<Ban size={16} className="mr-1" /> Withdraw
|
||||
</button>
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LeaveSummary } from '../../../types/leave';
|
||||
import type { LeaveSummary } from '../../types/leave';
|
||||
|
||||
interface LeaveSummaryTableProps {
|
||||
data: LeaveSummary[];
|
||||
@ -1,5 +1,5 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { LeaveHistoryRecord } from '../../../types/leave';
|
||||
import type { LeaveHistoryRecord } from '../../types/leave';
|
||||
|
||||
interface ViewLeaveModalProps {
|
||||
record: LeaveHistoryRecord;
|
||||
@ -7,15 +7,13 @@ interface ViewLeaveModalProps {
|
||||
onWithdraw: (record: LeaveHistoryRecord) => void;
|
||||
}
|
||||
|
||||
// Inside ViewLeaveModal.tsx
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
const normalizedStatus = status.toUpperCase();
|
||||
if (normalizedStatus.includes('APPROVED')) return 'bg-present-100 text-present-700';
|
||||
if (normalizedStatus.includes('PENDING')) return 'bg-late-100 text-late-700';
|
||||
if (normalizedStatus.includes('REJECTED')) return 'bg-absent-100 text-absent-700';
|
||||
if (normalizedStatus.includes('WITHDRAWN') || normalizedStatus.includes('CANCELLED')) return 'bg-app-muted text-text-secondary';
|
||||
return 'bg-app-muted text-text-secondary';
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Withdrawn': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeaveModalProps) {
|
||||
@ -52,8 +50,7 @@ export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeav
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
{/* Update this condition to check uppercase PENDING */}
|
||||
{record.status.toUpperCase() === 'PENDING' && (
|
||||
{record.status === 'Pending' && (
|
||||
<button onClick={() => onWithdraw(record)} className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium">
|
||||
Withdraw Application
|
||||
</button>
|
||||
@ -62,7 +59,6 @@ export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeav
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -1,5 +1,5 @@
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import type { LeaveHistoryRecord } from '../../../types/leave';
|
||||
import type { LeaveHistoryRecord } from '../../types/leave';
|
||||
|
||||
interface WithdrawLeaveModalProps {
|
||||
record: LeaveHistoryRecord;
|
||||
@ -1,14 +1,14 @@
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export interface BalanceFilterValues {
|
||||
companyId: string;
|
||||
empId: string;
|
||||
empName: string;
|
||||
month: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
interface BalanceFiltersProps {
|
||||
filters: BalanceFilterValues;
|
||||
companies: { id: number; name: string }[];
|
||||
onFilterChange: (newFilters: BalanceFilterValues) => void;
|
||||
onSearch: (e: React.FormEvent) => void;
|
||||
}
|
||||
@ -23,23 +23,34 @@ const monthsArray = [
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearsArray = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
||||
|
||||
export default function BalanceFilters({ filters, companies, onFilterChange, onSearch }: BalanceFiltersProps) {
|
||||
export default function BalanceFilters({ filters, onFilterChange, onSearch }: BalanceFiltersProps) {
|
||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
||||
const labelClass = "text-xs text-text-muted mb-1";
|
||||
|
||||
return (
|
||||
<form onSubmit={onSearch} className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-end">
|
||||
<form onSubmit={onSearch} className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 items-end">
|
||||
<div className="flex flex-col">
|
||||
<label className={labelClass}>Company Name</label>
|
||||
<select
|
||||
value={filters.companyId}
|
||||
onChange={(e) => onFilterChange({ ...filters, companyId: e.target.value })}
|
||||
<label className={labelClass}>Employee ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="empId"
|
||||
value={filters.empId}
|
||||
onChange={(e) => onFilterChange({ ...filters, empId: e.target.value })}
|
||||
className={inputClass}
|
||||
required
|
||||
>
|
||||
<option value="">Select Company</option>
|
||||
{companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
placeholder="e.g. EMP001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label className={labelClass}>Employee Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="empName"
|
||||
value={filters.empName}
|
||||
onChange={(e) => onFilterChange({ ...filters, empName: e.target.value })}
|
||||
className={inputClass}
|
||||
placeholder="e.g. Alice"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -13,6 +13,7 @@ export default function BalanceTable({ records, isSearched, month, year }: Balan
|
||||
return `${monthName} ${year}`;
|
||||
};
|
||||
|
||||
// Only render if the search button has been clicked
|
||||
if (!isSearched) return null;
|
||||
|
||||
return (
|
||||
@ -26,17 +27,13 @@ export default function BalanceTable({ records, isSearched, month, year }: Balan
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Employee Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||
Casual Opening ({formatMonthYear()})
|
||||
Opening Balance ({formatMonthYear()})
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||
Casual Closing ({formatMonthYear()})
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||
Optional Opening ({formatMonthYear()})
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||
Optional Closing ({formatMonthYear()})
|
||||
Closing Balance ({formatMonthYear()})
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Remaining Casual Leaves</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Remaining Optional Leaves</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-app-border">
|
||||
@ -45,10 +42,10 @@ export default function BalanceTable({ records, isSearched, month, year }: Balan
|
||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeId}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.employeeName}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.casualOpening}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.casualClosing}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.optionalOpening}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.optionalClosing}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.openingBalance}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.closingBalance}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.casualLeave}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.optionalLeave}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import { useLeaveBalance } from '../api/useLeaveData';
|
||||
import BalanceFilters, { type BalanceFilterValues } from '../components/leave/balance/BalanceFilters';
|
||||
import BalanceTable from '../components/leave/balance/BalanceTable';
|
||||
import { useCompanies } from '../../masterData/api/useCompanyData';
|
||||
|
||||
|
||||
export default function BalanceReport() {
|
||||
const today = new Date();
|
||||
const [filters, setFilters] = useState<BalanceFilterValues>({
|
||||
companyId: '1', // Default to company 1
|
||||
month: today.getMonth() + 1,
|
||||
year: today.getFullYear(),
|
||||
});
|
||||
|
||||
const [isSearched, setIsSearched] = useState(false);
|
||||
|
||||
// Fetch companies for the dropdown
|
||||
const { data: companiesData, isLoading: compLoading, isError: compError } = useCompanies();
|
||||
const companies = companiesData?.filter(c => c.status === 'Active') || [];
|
||||
|
||||
// Fetch data from backend using month, year, and company_id.
|
||||
// It only triggers when isSearched is true.
|
||||
const { data: balanceData, isLoading: balanceLoading, isError: balanceError } = useLeaveBalance(
|
||||
{ month: filters.month, year: filters.year, companyId: Number(filters.companyId) },
|
||||
isSearched
|
||||
);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!filters.companyId) {
|
||||
alert("Please select a company");
|
||||
return;
|
||||
}
|
||||
setIsSearched(true); // Triggers the fetch
|
||||
};
|
||||
|
||||
if (compLoading || balanceLoading) return <Loader message="Loading Leave Balance data..." />;
|
||||
if (compError || balanceError) return <ErrorState message="Failed to load Leave Balance data." />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex items-center space-x-3">
|
||||
<div className="p-3 bg-primary/10 rounded-full">
|
||||
<FileText className="text-primary" size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-text-primary">Leave Balance Report</h2>
|
||||
<p className="text-sm text-text-muted">Filter by company and month to view leave balances.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BalanceFilters
|
||||
filters={filters}
|
||||
companies={companies}
|
||||
onFilterChange={setFilters}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
|
||||
<BalanceTable
|
||||
records={balanceData || []}
|
||||
isSearched={isSearched}
|
||||
month={filters.month}
|
||||
year={filters.year}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/features/leave/pages/approval/DirectorApproval.tsx
Normal file
30
src/features/leave/pages/approval/DirectorApproval.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { toast } from 'sonner';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import ApprovalTable from '../../components/approval/ApprovalTable';
|
||||
import { useDirectorApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
||||
|
||||
export default function DirectorApproval() {
|
||||
const { data: approvals, isLoading, isError } = useDirectorApprovals();
|
||||
const { mutate } = useUpdateLeaveApproval();
|
||||
|
||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||
mutate({ id, status }, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Leave request ${status.toLowerCase()} successfully.`);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to update leave status. Please try again.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader message="Loading HR Manager leave requests..." />;
|
||||
if (isError) return <ErrorState message="Failed to load leave requests." />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{approvals && <ApprovalTable records={approvals} onAction={handleAction} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/features/leave/pages/approval/HRManagerApproval.tsx
Normal file
30
src/features/leave/pages/approval/HRManagerApproval.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { toast } from 'sonner';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import ApprovalTable from '../../components/approval/ApprovalTable';
|
||||
import { useHRManagerApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
||||
|
||||
export default function HRManagerApproval() {
|
||||
const { data: approvals, isLoading, isError } = useHRManagerApprovals();
|
||||
const { mutate } = useUpdateLeaveApproval();
|
||||
|
||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||
mutate({ id, status }, {
|
||||
onSuccess: () => {
|
||||
toast.success(`Leave request ${status.toLowerCase()} successfully.`);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to update leave status. Please try again.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader message="Loading manager leave requests..." />;
|
||||
if (isError) return <ErrorState message="Failed to load leave requests." />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{approvals && <ApprovalTable records={approvals} onAction={handleAction} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
import { toast } from 'sonner';
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import ApprovalTable from '../components/approval/ApprovalTable';
|
||||
import { usePendingApprovals, useUpdateLeaveApproval } from '../api/useLeaveData';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import ApprovalTable from '../../components/approval/ApprovalTable';
|
||||
import { useManagerApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
||||
|
||||
export default function MyApprovals() {
|
||||
// This hook automatically fetches the correct pending leaves
|
||||
// based on the logged-in user's role via the Mock SSO headers.
|
||||
const { data: approvals, isLoading, isError } = usePendingApprovals();
|
||||
export default function ManagerApproval() {
|
||||
const { data: approvals, isLoading, isError } = useManagerApprovals();
|
||||
const { mutate } = useUpdateLeaveApproval();
|
||||
|
||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||
72
src/features/leave/pages/leave/BalanceReport.tsx
Normal file
72
src/features/leave/pages/leave/BalanceReport.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { useState } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import { useLeaveBalance } from '../../api/useLeaveData';
|
||||
import type { LeaveBalanceRecord } from '../../types/leave';
|
||||
import BalanceFilters, { type BalanceFilterValues } from '../../components/leave/balance/BalanceFilters';
|
||||
import BalanceTable from '../../components/leave/balance/BalanceTable';
|
||||
|
||||
export default function BalanceReport() {
|
||||
const { data: balanceData, isLoading, isError } = useLeaveBalance();
|
||||
|
||||
// Initialize filters with current month and year
|
||||
const today = new Date();
|
||||
const [filters, setFilters] = useState<BalanceFilterValues>({
|
||||
empId: '',
|
||||
empName: '',
|
||||
month: today.getMonth() + 1,
|
||||
year: today.getFullYear(),
|
||||
});
|
||||
|
||||
// Search Trigger State
|
||||
const [isSearched, setIsSearched] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<LeaveBalanceRecord[]>([]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!balanceData) return;
|
||||
|
||||
const results = balanceData.filter(record => {
|
||||
const matchesEmpId = filters.empId ? record.employeeId.toLowerCase().includes(filters.empId.toLowerCase()) : true;
|
||||
const matchesEmpName = filters.empName ? record.employeeName.toLowerCase().includes(filters.empName.toLowerCase()) : true;
|
||||
const matchesMonth = record.month === Number(filters.month);
|
||||
const matchesYear = record.year === Number(filters.year);
|
||||
|
||||
return matchesEmpId && matchesEmpName && matchesMonth && matchesYear;
|
||||
});
|
||||
|
||||
setSearchResults(results);
|
||||
setIsSearched(true);
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader message="Loading Leave Balance data..." />;
|
||||
if (isError) return <ErrorState message="Failed to load Leave Balance data." />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex items-center space-x-3">
|
||||
<div className="p-3 bg-primary/10 rounded-full">
|
||||
<FileText className="text-primary" size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-text-primary">Leave Balance Report</h2>
|
||||
<p className="text-sm text-text-muted">Filter by employee and month to view leave balances.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BalanceFilters
|
||||
filters={filters}
|
||||
onFilterChange={setFilters}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
|
||||
<BalanceTable
|
||||
records={searchResults}
|
||||
isSearched={isSearched}
|
||||
month={filters.month}
|
||||
year={filters.year}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
// src/features/leave/pages/leave/MyLeave.tsx
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import LeaveSummaryTable from '../components/leave/table/LeaveSummaryTable';
|
||||
import ApplyLeaveForm from '../components/leave/ApplyLeaveForm';
|
||||
import LeaveHistoryTable from '../components/leave/table/LeaveHistoryTable';
|
||||
import { useLeaveSummary, useLeaveHistory } from '../api/useLeaveData';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import LeaveSummaryTable from '../../components/leave/LeaveSummaryTable';
|
||||
import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||
|
||||
export default function MyLeave() {
|
||||
// The hooks automatically use the Mock SSO headers injected by the Axios interceptor!
|
||||
export default function DirectorLeaveLeave() {
|
||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||
|
||||
24
src/features/leave/pages/leave/EmployeeLeave.tsx
Normal file
24
src/features/leave/pages/leave/EmployeeLeave.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import LeaveSummaryTable from '../../components/leave/LeaveSummaryTable';
|
||||
import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||
|
||||
export default function EmployeeLeave() {
|
||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||
|
||||
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
|
||||
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{summaryData && <LeaveSummaryTable data={summaryData} />}
|
||||
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
|
||||
</div>
|
||||
{historyData && <LeaveHistoryTable records={historyData} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/features/leave/pages/leave/HRManagerLeave.tsx
Normal file
24
src/features/leave/pages/leave/HRManagerLeave.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import LeaveSummaryTable from '../../components/leave/LeaveSummaryTable';
|
||||
import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||
|
||||
export default function HRManagerLeave() {
|
||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||
|
||||
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
|
||||
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{summaryData && <LeaveSummaryTable data={summaryData} />}
|
||||
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
|
||||
</div>
|
||||
{historyData && <LeaveHistoryTable records={historyData} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import LeaveApplicationsTable from '../components/leave/table/LeaveApplicationsTable';
|
||||
import { useLeaveApplications } from '../api/useLeaveData';
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import LeaveApplicationsTable from '../../components/leave/LeaveApplicationsTable';
|
||||
import { useLeaveApplications } from '../../api/useLeaveData';
|
||||
|
||||
export default function LeaveApplications() {
|
||||
const { data: records, isLoading, isError } = useLeaveApplications();
|
||||
24
src/features/leave/pages/leave/ManagerLeave.tsx
Normal file
24
src/features/leave/pages/leave/ManagerLeave.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Loader from '../../../../components/ui/Loader';
|
||||
import ErrorState from '../../../../components/ui/ErrorState';
|
||||
import LeaveSummaryTable from '../../components/leave/LeaveSummaryTable';
|
||||
import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||
|
||||
export default function ManagerLeave() {
|
||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||
|
||||
if (summaryLoading || historyLoading) return <Loader message="Loading leave data..." />;
|
||||
if (summaryError || historyError) return <ErrorState message="Failed to load leave data." />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{summaryData && <LeaveSummaryTable data={summaryData} />}
|
||||
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
|
||||
</div>
|
||||
{historyData && <LeaveHistoryTable records={historyData} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import MyApprovals from '../pages/MyApprovals';
|
||||
import ManagerApproval from '../pages/approval/ManagerApproval';
|
||||
import HRManagerApproval from '../pages/approval/HRManagerApproval';
|
||||
import DirectorApproval from '../pages/approval/DirectorApproval';
|
||||
|
||||
export default function ApprovalRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
// Employees do not have approval access, so we block them at the router level.
|
||||
if (role === 'manager' || role === 'hrmanager' || role === 'director') {
|
||||
return <MyApprovals />;
|
||||
}
|
||||
if (role === 'manager') return <ManagerApproval />;
|
||||
if (role === 'hrmanager') return <HRManagerApproval />;
|
||||
if (role === 'director') return <DirectorApproval />;
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Managers, HR Managers, and Directors only.</div>;
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Managers and HRManagers only.</div>;
|
||||
}
|
||||
@ -1,20 +1,17 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import MyLeave from '../pages/MyLeave';
|
||||
import EmployeeLeave from '../pages/leave/EmployeeLeave';
|
||||
import ManagerLeave from '../pages/leave/ManagerLeave';
|
||||
import HRManagerLeave from '../pages/leave/HRManagerLeave';
|
||||
import DirectorLeave from '../pages/leave/DirectorLeave';
|
||||
|
||||
export default function LeaveRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
// The Leave page layout is identical for all roles.
|
||||
// The backend LMS service automatically handles role-based logic (like approval chains)
|
||||
// based on the X-Mock-User-Role header sent from Redux.
|
||||
switch (role) {
|
||||
case 'employee':
|
||||
case 'manager':
|
||||
case 'hrmanager':
|
||||
case 'director':
|
||||
return <MyLeave />;
|
||||
default:
|
||||
return <div>Unauthorized</div>;
|
||||
case 'employee': return <EmployeeLeave />;
|
||||
case 'manager': return <ManagerLeave />;
|
||||
case 'hrmanager': return <HRManagerLeave />;
|
||||
case 'director': return <DirectorLeave />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import LeaveApplications from '../pages/LeaveApplications';
|
||||
import BalanceReport from '../pages/BalanceReport';
|
||||
import LeaveApplications from '../pages/leave/LeaveApplications';
|
||||
import BalanceReport from '../pages/leave/BalanceReport';
|
||||
|
||||
export default function LeaveSummaryRouter() {
|
||||
return (
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export interface LeaveSummary {
|
||||
leaveTypeId: number;
|
||||
leaveType: string;
|
||||
credited: number;
|
||||
utilized: number;
|
||||
@ -10,7 +9,6 @@ export interface LeaveSummary {
|
||||
export type DayMode = 'Full Day' | 'First Half' | 'Second Half';
|
||||
|
||||
export interface LeaveRequestPayload {
|
||||
leaveTypeId: number;
|
||||
leaveType: string;
|
||||
fromDate: string;
|
||||
fromDayMode: DayMode;
|
||||
@ -21,7 +19,6 @@ export interface LeaveRequestPayload {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
|
||||
export type LeaveStatus = 'Approved' | 'Pending' | 'Withdrawn';
|
||||
|
||||
export interface LeaveHistoryRecord {
|
||||
@ -56,22 +53,21 @@ export interface LeaveApplicationSummary {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
employeeName: string;
|
||||
jobRole: string; // Changed from designation
|
||||
designation: string;
|
||||
department: string;
|
||||
managerName: string;
|
||||
leaveType: string;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
leaveDays: number;
|
||||
status: string;
|
||||
reason: string; // Added reason
|
||||
status: 'Approved' | 'Pending' | 'Rejected' | 'Withdrawn';
|
||||
}
|
||||
|
||||
export interface LeaveBalanceRecord {
|
||||
employeeId: string;
|
||||
employeeName: string;
|
||||
casualOpening: number;
|
||||
casualClosing: number;
|
||||
optionalOpening: number;
|
||||
optionalClosing: number;
|
||||
month: number;
|
||||
year: number;
|
||||
openingBalance: number;
|
||||
closingBalance: number;
|
||||
casualLeave: number;
|
||||
optionalLeave: number;
|
||||
}
|
||||
|
||||
@ -1,40 +1,43 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import type { RegularizationRecord } from '../../api/useAttendanceData';
|
||||
import RegularizationHistoryModal from './RegularizationHistoryModal'; // NEW IMPORT
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Trash2 } from 'lucide-react';
|
||||
import type { HRManagerEmployee } from '../../api/useHRManagerData';
|
||||
|
||||
interface RegularizationHistoryTableProps {
|
||||
records: RegularizationRecord[];
|
||||
interface HRManagerTableProps {
|
||||
records: HRManagerEmployee[];
|
||||
onToggleHRManager: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
type SortKey = 'empId' | 'empName' | 'branchName' | 'date' | 'regularizationType' | 'reqCheckIn' | 'reqCheckOut' | 'status' | 'reviewerName';
|
||||
type SortKey = 'id' | 'email' | 'name' | 'designationName' | 'deptName' | 'branchName' | 'companyName' | 'status' | 'isHRManager';
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Approved': return 'bg-present-100 text-present-700';
|
||||
case 'Pending': return 'bg-late-100 text-late-700';
|
||||
case 'Rejected': return 'bg-absent-100 text-absent-700';
|
||||
case 'Active': return 'bg-present-100 text-present-700';
|
||||
case 'Disabled': return 'bg-absent-100 text-absent-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
export default function RegularizationHistoryTable({ records }: RegularizationHistoryTableProps) {
|
||||
const [viewRecord, setViewRecord] = useState<RegularizationRecord | null>(null);
|
||||
export default function HRManagerTable({ records, onToggleHRManager, onDelete }: HRManagerTableProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||
key: 'date',
|
||||
direction: 'descending'
|
||||
key: 'name', direction: 'ascending'
|
||||
});
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchQuery) return records;
|
||||
return records.filter(record =>
|
||||
record.empId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.empName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.designationName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
record.status.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [records, searchQuery]);
|
||||
@ -42,6 +45,12 @@ export default function RegularizationHistoryTable({ records }: RegularizationHi
|
||||
const sortedData = useMemo(() => {
|
||||
const sortableData = [...filteredData];
|
||||
sortableData.sort((a, b) => {
|
||||
if (sortConfig.key === 'isHRManager') {
|
||||
const valA = a.isHRManager ? 1 : 0;
|
||||
const valB = b.isHRManager ? 1 : 0;
|
||||
return sortConfig.direction === 'ascending' ? valA - valB : valB - valA;
|
||||
}
|
||||
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
return 0;
|
||||
@ -89,25 +98,26 @@ export default function RegularizationHistoryTable({ records }: RegularizationHi
|
||||
};
|
||||
|
||||
const formatHeader = (key: string) => {
|
||||
if (key === 'empId') return 'Emp Id';
|
||||
if (key === 'empName') return 'Emp Name';
|
||||
if (key === 'id') return 'Emp CODE';
|
||||
if (key === 'name') return 'Employee Name';
|
||||
if (key === 'email') return 'Employee Email';
|
||||
if (key === 'designationName') return 'Designation';
|
||||
if (key === 'deptName') return 'Department';
|
||||
if (key === 'branchName') return 'Branch';
|
||||
if (key === 'regularizationType') return 'Type';
|
||||
if (key === 'reqCheckIn') return 'Req Check In';
|
||||
if (key === 'reqCheckOut') return 'Req Check Out';
|
||||
if (key === 'reviewerName') return 'Reviewed By';
|
||||
if (key === 'companyName') return 'Company';
|
||||
if (key === 'isHRManager') return 'HRManager Role';
|
||||
return key.charAt(0).toUpperCase() + key.slice(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||
<h2 className="text-lg font-semibold text-text-primary">All Regularization Requests</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary">HRManagers List</h2>
|
||||
<div className="relative w-full md:w-64">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search id, name, status..."
|
||||
placeholder="Search id, name, role..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
|
||||
className="pl-9 pr-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card w-full"
|
||||
@ -115,52 +125,58 @@ export default function RegularizationHistoryTable({ records }: RegularizationHi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
{(['empId', 'empName', 'branchName', 'date', 'regularizationType', 'reqCheckIn', 'reqCheckOut', 'status', 'reviewerName'] as SortKey[]).map((key) => (
|
||||
<th
|
||||
key={key}
|
||||
onClick={() => requestSort(key)}
|
||||
className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{formatHeader(key)} {getSortIcon(key)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-app-border">
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((record) => (
|
||||
<tr key={record.id} className="hover:bg-app-muted transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{record.empId}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-primary">{record.empName}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.regularizationType}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.reqCheckIn}</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.reqCheckOut}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.reviewerName || 'Pending Review'}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<button onClick={() => setViewRecord(record)} className="text-primary hover:text-primary-hover flex items-center">
|
||||
<Eye size={16} className="mr-1" /> View
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">No regularization records found.</td>
|
||||
{(['id', 'name', 'email', 'designationName', 'deptName', 'branchName', 'companyName', 'status', 'isHRManager'] as SortKey[]).map((key) => (
|
||||
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
|
||||
<div className="flex items-center">
|
||||
{formatHeader(key)} {getSortIcon(key)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider">Action</th>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-app-border">
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((record) => (
|
||||
<tr key={record.id} className="hover:bg-app-muted transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.email}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.designationName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<button
|
||||
onClick={() => onToggleHRManager(record.id)}
|
||||
className={`relative inline-flex items-center h-6 w-11 rounded-full transition-colors ${record.isHRManager ? 'bg-primary' : 'bg-app-border'}`}
|
||||
title={record.isHRManager ? "Revoke HRManager" : "Make HRManager"}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${record.isHRManager ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
|
||||
<Trash2 size={16} className="mr-1" /> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} className="px-6 py-8 text-center text-sm text-text-muted">No employees found.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mt-4 pt-4 border-t border-app-border gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -186,13 +202,6 @@ export default function RegularizationHistoryTable({ records }: RegularizationHi
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{viewRecord && (
|
||||
<RegularizationHistoryModal
|
||||
record={viewRecord}
|
||||
onClose={() => setViewRecord(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
118
src/features/masterData/pages/ManageHRManager.tsx
Normal file
118
src/features/masterData/pages/ManageHRManager.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ShieldUser, Plus } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import Loader from '../../../components/ui/Loader';
|
||||
import ErrorState from '../../../components/ui/ErrorState';
|
||||
import ConfirmationModal from '../../../components/ui/ConfirmationModal';
|
||||
import HRManagerTable from '../components/manage-hrmanager/hrmanagerTable';
|
||||
import EmployeeFormModal from '../components/employee/EmployeeFormModal';
|
||||
import { useHRManagerEmployees, type HRManagerEmployee } from '../api/useHRManagerData';
|
||||
import { useCompanies } from '../api/useCompanyData';
|
||||
import { useBranches } from '../api/useBranchData';
|
||||
import { useDepartments } from '../api/useDepartmentData';
|
||||
import { useJobRoles } from '../api/useJobRoleData';
|
||||
|
||||
export default function ManageHRManagers() {
|
||||
const { data: apiRecords, isLoading: empLoading, isError: empError } = useHRManagerEmployees();
|
||||
const { data: companies, isLoading: compLoading, isError: compError } = useCompanies();
|
||||
const { data: branches, isLoading: branchLoading, isError: branchError } = useBranches();
|
||||
const { data: departments, isLoading: depLoading, isError: depError } = useDepartments();
|
||||
const { data: designations, isLoading: dsgLoading, isError: dsgError } = useJobRoles();
|
||||
|
||||
const [localRecords, setLocalRecords] = useState<HRManagerEmployee[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
|
||||
useMemo(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-render
|
||||
if (apiRecords) setLocalRecords(apiRecords);
|
||||
}, [apiRecords]);
|
||||
|
||||
const handleToggleHRManager = (id: string) => {
|
||||
setLocalRecords(prev => prev.map(e => e.id === id ? { ...e, isHRManager: !e.isHRManager } : e));
|
||||
const emp = localRecords.find(e => e.id === id);
|
||||
if (emp) {
|
||||
toast.success(`${emp.name} is ${emp.isHRManager ? 'no longer an HRManager' : 'now an HRManager'}.`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string) => {
|
||||
setDeleteId(id);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteId) {
|
||||
setLocalRecords(prev => prev.filter(e => e.id !== deleteId));
|
||||
toast.success("Employee deleted successfully.");
|
||||
setDeleteId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddClick = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = (data: Omit<HRManagerEmployee, 'id' | 'isHRManager'>) => {
|
||||
const newId = `EMP${String(localRecords.length + 1).padStart(3, '0')}`;
|
||||
// New employees are not HRManagers by default
|
||||
setLocalRecords(prev => [...prev, { id: newId, isHRManager: false, ...data }]);
|
||||
toast.success("Employee added successfully.");
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const isLoading = empLoading || compLoading || branchLoading || depLoading || dsgLoading;
|
||||
const isError = empError || compError || branchError || depError || dsgError;
|
||||
|
||||
if (isLoading) return <Loader message="Loading HRManagers data..." />;
|
||||
if (isError) return <ErrorState message="Failed to load HRManagers data." />;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-3 bg-primary/10 rounded-full">
|
||||
<ShieldUser className="text-primary" size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-text-primary">Manage HR Managers</h2>
|
||||
<p className="text-sm text-text-muted">Toggle HRManager privileges or remove employees.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddClick}
|
||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium"
|
||||
>
|
||||
<Plus size={18} className="mr-2" />
|
||||
Add HR Manager
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<HRManagerTable
|
||||
records={localRecords}
|
||||
onToggleHRManager={handleToggleHRManager}
|
||||
onDelete={handleDeleteClick}
|
||||
/>
|
||||
|
||||
{isModalOpen && (
|
||||
<EmployeeFormModal
|
||||
employee={null}
|
||||
companies={companies || []}
|
||||
branches={branches || []}
|
||||
departments={departments || []}
|
||||
designations={designations || []}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteId && (
|
||||
<ConfirmationModal
|
||||
title="Delete Employee?"
|
||||
message="Are you sure you want to delete this employee? This action cannot be undone."
|
||||
onConfirm={confirmDelete}
|
||||
onClose={() => setDeleteId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/features/masterData/routes/ManageHRManagersRouter.tsx
Normal file
13
src/features/masterData/routes/ManageHRManagersRouter.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../../store/store';
|
||||
import ManageHRManagers from '../pages/ManageHRManager';
|
||||
|
||||
export default function ManageHRManagersRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
if (role === 'director') {
|
||||
return <ManageHRManagers />;
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Directors only.</div>;
|
||||
}
|
||||
@ -25,10 +25,10 @@ export interface RoleState {
|
||||
}
|
||||
|
||||
const initialState: RoleState = {
|
||||
currentRole: 'employee',
|
||||
mockUserId: MOCK_USERS.employee.userId,
|
||||
mockUserRole: MOCK_ROLE_MAP.employee,
|
||||
mockBranches: MOCK_USERS.employee.branches,
|
||||
currentRole: 'hrmanager', // Default to hrmanager
|
||||
mockUserId: MOCK_USERS.hrmanager.userId,
|
||||
mockUserRole: MOCK_ROLE_MAP.hrmanager,
|
||||
mockBranches: MOCK_USERS.hrmanager.branches,
|
||||
};
|
||||
|
||||
const roleSlice = createSlice({
|
||||
|
||||
@ -7,7 +7,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.60.238:8000',
|
||||
target: 'http://192.168.60.237:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user