Compare commits
2 Commits
2567456e53
...
e38b09bd2d
| Author | SHA1 | Date | |
|---|---|---|---|
| e38b09bd2d | |||
| 66017309aa |
BIN
public/logo.gif
Normal file
BIN
public/logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
7
public/mockApi/leave/leave_ledger.json
Normal file
7
public/mockApi/leave/leave_ledger.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{ "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 }
|
||||||
|
]
|
||||||
@ -5,34 +5,66 @@ import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Role-specific search data
|
||||||
const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
|
const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
|
||||||
employee: [
|
employee: [
|
||||||
{ name: 'My Dashboard', path: '/dashboard' },
|
{ name: 'My Dashboard', path: '/dashboard' },
|
||||||
{ name: 'Mark Attendance', path: '/attendance' },
|
{ name: 'My Attendance', path: '/attendance' },
|
||||||
{ name: 'Apply for Leave', path: '/leave' },
|
{ name: 'Apply for Leave', path: '/leave' },
|
||||||
],
|
],
|
||||||
manager: [
|
manager: [
|
||||||
{ name: 'Manager Dashboard', path: '/dashboard' },
|
{ name: 'Manager Dashboard', path: '/dashboard' },
|
||||||
{ name: 'Team Attendance', path: '/attendance' },
|
{ name: 'My Attendance', path: '/attendance' },
|
||||||
{ name: 'Team Leaves', path: '/leave' },
|
{ name: 'My Leaves', path: '/leave' },
|
||||||
{ name: 'Pending Approvals', path: '/approval' },
|
{ name: 'Pending Approvals', path: '/approval' },
|
||||||
],
|
],
|
||||||
admin: [
|
admin: [
|
||||||
{ name: 'Admin Dashboard', path: '/dashboard' },
|
{ name: 'Admin Dashboard', path: '/dashboard' },
|
||||||
{ name: 'All Attendance Logs', path: '/attendance' },
|
{ name: 'All Attendance Logs', path: '/attendance' },
|
||||||
{ name: 'All Leave Requests', path: '/leave' },
|
{ name: 'All Leave Requests', path: '/leave' },
|
||||||
{ name: 'Attendance Summary Report', path: '/attendance-summary' },
|
|
||||||
{ name: 'Leave Summary Report', path: '/leave-summary' },
|
// Attendance Summary
|
||||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
{ name: 'Attendance Summary Dashboard', path: '/attendance-summary/dashboard' },
|
||||||
|
{ name: 'Monthly Attendance Report', path: '/attendance-summary/monthly' },
|
||||||
|
{ name: 'Daily Attendance Report', path: '/attendance-summary/daily' },
|
||||||
|
{ name: 'Attendance Regularization', path: '/attendance-summary/regularization' },
|
||||||
|
|
||||||
|
// Leave Summary
|
||||||
|
{ name: 'Leave Applications', path: '/leave-summary/applications' },
|
||||||
|
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
|
||||||
|
|
||||||
|
// Master Data
|
||||||
|
{ name: 'Master Data Dashboard', path: '/master-data/dashboard' },
|
||||||
|
{ name: 'Manage Company', path: '/master-data/company' },
|
||||||
|
{ name: 'Manage Branches', path: '/master-data/branches' },
|
||||||
|
{ name: 'Manage Departments', path: '/master-data/departments' },
|
||||||
|
{ name: 'Manage Designations', path: '/master-data/designations' },
|
||||||
|
{ name: 'Manage Employees', path: '/master-data/employees' },
|
||||||
],
|
],
|
||||||
superadmin: [
|
superadmin: [
|
||||||
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
|
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
|
||||||
{ name: 'Global Attendance Logs', path: '/attendance' },
|
{ name: 'Global Attendance Logs', path: '/attendance' },
|
||||||
{ name: 'Global Leave Requests', path: '/leave' },
|
{ name: 'Global Leave Requests', path: '/leave' },
|
||||||
{ name: 'Global Attendance Summary', path: '/attendance-summary' },
|
|
||||||
{ name: 'Global Leave Summary', path: '/leave-summary' },
|
// Attendance Summary
|
||||||
|
{ name: 'Attendance Summary Dashboard', path: '/attendance-summary/dashboard' },
|
||||||
|
{ name: 'Monthly Attendance Report', path: '/attendance-summary/monthly' },
|
||||||
|
{ name: 'Daily Attendance Report', path: '/attendance-summary/daily' },
|
||||||
|
|
||||||
|
// Leave Summary
|
||||||
|
{ name: 'Leave Applications', path: '/leave-summary/applications' },
|
||||||
|
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
|
||||||
|
|
||||||
|
// Master Data
|
||||||
|
{ name: 'Master Data Dashboard', path: '/master-data/dashboard' },
|
||||||
|
{ name: 'Manage Company', path: '/master-data/company' },
|
||||||
|
{ name: 'Manage Branches', path: '/master-data/branches' },
|
||||||
|
{ name: 'Manage Departments', path: '/master-data/departments' },
|
||||||
|
{ name: 'Manage Designations', path: '/master-data/designations' },
|
||||||
|
{ name: 'Manage Employees', path: '/master-data/employees' },
|
||||||
|
|
||||||
|
// SuperAdmin Specific
|
||||||
{ name: 'Manage Admins', path: '/manage-admins' },
|
{ name: 'Manage Admins', path: '/manage-admins' },
|
||||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
|
||||||
{ name: 'Policy and Rules', path: '/policies' },
|
{ name: 'Policy and Rules', path: '/policies' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -66,6 +98,7 @@ export default function Navbar() {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Get search items based on the current role
|
||||||
const availableItems = roleSearchData[currentRole] || [];
|
const availableItems = roleSearchData[currentRole] || [];
|
||||||
const searchResults = availableItems.filter(item =>
|
const searchResults = availableItems.filter(item =>
|
||||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
@ -81,7 +114,7 @@ export default function Navbar() {
|
|||||||
<div className="bg-app-card shadow-sm h-16 flex items-center justify-between px-6 border-b border-app-border relative z-30">
|
<div className="bg-app-card shadow-sm h-16 flex items-center justify-between px-6 border-b border-app-border relative z-30">
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h2 className="text-lg font-bold text-default tracking-wide">CLRI HRMS</h2>
|
<h2 className="text-lg font-bold text-primary tracking-wide">CLRI HRMS</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
@ -93,7 +126,7 @@ export default function Navbar() {
|
|||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search modules..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-56 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary"
|
className="w-56 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary"
|
||||||
@ -115,7 +148,7 @@ export default function Navbar() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isSearchOpen && searchQuery && (
|
{isSearchOpen && searchQuery && (
|
||||||
<div className="absolute right-0 top-14 w-72 bg-app-card border border-app-border rounded-lg shadow-lg overflow-hidden z-50">
|
<div className="absolute right-0 top-14 w-72 bg-app-card border border-app-border rounded-lg shadow-lg overflow-hidden z-50 max-h-96 overflow-y-auto">
|
||||||
{searchResults.length > 0 ? (
|
{searchResults.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{searchResults.map((item) => (
|
{searchResults.map((item) => (
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
Menu, Users, ShieldUser, ScrollText, ChevronDown, FileText, CalendarDays, ClipboardCheck,
|
Menu, Users, ShieldUser, ScrollText, ChevronDown, FileText, CalendarDays, ClipboardCheck,
|
||||||
Database, Building, Network, Briefcase, Tag, Receipt // Added Receipt
|
Database, Building, Network, Briefcase, Tag, Receipt
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const iconMap: Record<string, any> = {
|
const iconMap: Record<string, any> = {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
|
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
|
||||||
Database, Building, Network, Briefcase, Tag, Receipt // Added Receipt
|
Database, Building, Network, Briefcase, Tag, Receipt
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
||||||
@ -90,8 +90,9 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
|
|||||||
<div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}>
|
<div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}>
|
||||||
<div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
<div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Link to="/dashboard">
|
<Link to="/dashboard" className="flex items-center">
|
||||||
<img src="clri_logo.svg" alt="Company Logo" className="h-8 w-auto cursor-pointer" />
|
{/* Fixed Tailwind sizing for GIF alignment */}
|
||||||
|
<img src="logo.gif" alt="Company Logo" className="h-25 w-25 object-contain cursor-pointer" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">
|
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { RootState } from '../../../store/store';
|
|||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../../../store/roleSlice';
|
||||||
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
||||||
import type { LeaveApplicationSummary } from '../types/leave';
|
import type { LeaveApplicationSummary } from '../types/leave';
|
||||||
|
import type { LeaveLedgerRecord } from '../types/leave';
|
||||||
|
|
||||||
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
||||||
employee: {
|
employee: {
|
||||||
@ -89,3 +90,10 @@ export const useLeaveApplications = () => {
|
|||||||
queryFn: () => fetchMockData<LeaveApplicationSummary[]>('leave/leave_applications.json'),
|
queryFn: () => fetchMockData<LeaveApplicationSummary[]>('leave/leave_applications.json'),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useLeaveLedger = () => {
|
||||||
|
return useQuery<LeaveLedgerRecord[]>({
|
||||||
|
queryKey: ['leaveLedger'],
|
||||||
|
queryFn: () => fetchMockData<LeaveLedgerRecord[]>('leave/leave_ledger.json'),
|
||||||
|
});
|
||||||
|
};
|
||||||
87
src/features/leave/components/leave/ledger/LedgerFilters.tsx
Normal file
87
src/features/leave/components/leave/ledger/LedgerFilters.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface LedgerFilterValues {
|
||||||
|
empId: string;
|
||||||
|
empName: string;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LedgerFiltersProps {
|
||||||
|
filters: LedgerFilterValues;
|
||||||
|
onFilterChange: (newFilters: LedgerFilterValues) => void;
|
||||||
|
onSearch: (e: React.FormEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthsArray = [
|
||||||
|
{ value: 1, label: 'January' }, { value: 2, label: 'February' }, { value: 3, label: 'March' },
|
||||||
|
{ value: 4, label: 'April' }, { value: 5, label: 'May' }, { value: 6, label: 'June' },
|
||||||
|
{ value: 7, label: 'July' }, { value: 8, label: 'August' }, { value: 9, label: 'September' },
|
||||||
|
{ value: 10, label: 'October' }, { value: 11, label: 'November' }, { value: 12, label: 'December' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const yearsArray = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
||||||
|
|
||||||
|
export default function LedgerFilters({ filters, onFilterChange, onSearch }: LedgerFiltersProps) {
|
||||||
|
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-5 gap-4 items-end">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Employee ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="empId"
|
||||||
|
value={filters.empId}
|
||||||
|
onChange={(e) => onFilterChange({ ...filters, empId: e.target.value })}
|
||||||
|
className={inputClass}
|
||||||
|
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">
|
||||||
|
<label className={labelClass}>Month</label>
|
||||||
|
<select
|
||||||
|
value={filters.month}
|
||||||
|
onChange={(e) => onFilterChange({ ...filters, month: Number(e.target.value) })}
|
||||||
|
className={inputClass}
|
||||||
|
>
|
||||||
|
{monthsArray.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Year</label>
|
||||||
|
<select
|
||||||
|
value={filters.year}
|
||||||
|
onChange={(e) => onFilterChange({ ...filters, year: Number(e.target.value) })}
|
||||||
|
className={inputClass}
|
||||||
|
>
|
||||||
|
{yearsArray.map(y => <option key={y} value={y}>{y}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex items-center justify-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors font-medium h-[38px]"
|
||||||
|
>
|
||||||
|
<Search size={18} className="mr-2" />
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/features/leave/components/leave/ledger/LedgerTable.tsx
Normal file
64
src/features/leave/components/leave/ledger/LedgerTable.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { LeaveLedgerRecord } from '../../../types/leave';
|
||||||
|
|
||||||
|
interface LedgerTableProps {
|
||||||
|
records: LeaveLedgerRecord[];
|
||||||
|
isSearched: boolean;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LedgerTable({ records, isSearched, month, year }: LedgerTableProps) {
|
||||||
|
// Helper to format the month number into a readable name (e.g., "July 2026")
|
||||||
|
const formatMonthYear = () => {
|
||||||
|
const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'long' });
|
||||||
|
return `${monthName} ${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only render if the search button has been clicked
|
||||||
|
if (!isSearched) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary mb-4">
|
||||||
|
Search Results ({records.length})
|
||||||
|
</h3>
|
||||||
|
<table className="min-w-full divide-y divide-app-border">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp ID</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Employee Name</th>
|
||||||
|
{/* Dynamically display Month/Year in the headers */}
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||||
|
Opening Balance ({formatMonthYear()})
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">
|
||||||
|
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">
|
||||||
|
{records.length > 0 ? (
|
||||||
|
records.map((record, index) => (
|
||||||
|
<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.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>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
|
No ledger records found for the selected filters.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
src/features/leave/pages/leave/LedgerReport.tsx
Normal file
75
src/features/leave/pages/leave/LedgerReport.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { FileText } from 'lucide-react';
|
||||||
|
import Loader from '../../../../components/ui/Loader';
|
||||||
|
import ErrorState from '../../../../components/ui/ErrorState';
|
||||||
|
import { useLeaveLedger } from '../../api/useLeaveData';
|
||||||
|
import type { LeaveLedgerRecord } from '../../types/leave';
|
||||||
|
import LedgerFilters, { type LedgerFilterValues } from '../../components/leave/ledger/LedgerFilters';
|
||||||
|
import LedgerTable from '../../components/leave/ledger/LedgerTable';
|
||||||
|
|
||||||
|
export default function LedgerReport() {
|
||||||
|
const { data: ledgerData, isLoading, isError } = useLeaveLedger();
|
||||||
|
|
||||||
|
// Initialize filters with current month and year
|
||||||
|
const today = new Date();
|
||||||
|
const [filters, setFilters] = useState<LedgerFilterValues>({
|
||||||
|
empId: '',
|
||||||
|
empName: '',
|
||||||
|
month: today.getMonth() + 1,
|
||||||
|
year: today.getFullYear(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search Trigger State
|
||||||
|
const [isSearched, setIsSearched] = useState(false);
|
||||||
|
const [searchResults, setSearchResults] = useState<LeaveLedgerRecord[]>([]);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!ledgerData) return;
|
||||||
|
|
||||||
|
const results = ledgerData.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 ledger data..." />;
|
||||||
|
if (isError) return <ErrorState message="Failed to load ledger data." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<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 Ledger Report</h2>
|
||||||
|
<p className="text-sm text-text-muted">Filter by employee and month to view leave balances.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reusable Filters Component */}
|
||||||
|
<LedgerFilters
|
||||||
|
filters={filters}
|
||||||
|
onFilterChange={setFilters}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Reusable Table Component */}
|
||||||
|
<LedgerTable
|
||||||
|
records={searchResults}
|
||||||
|
isSearched={isSearched}
|
||||||
|
month={filters.month}
|
||||||
|
year={filters.year}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,19 +1,13 @@
|
|||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import LeaveApplications from '../pages/leave/LeaveApplications';
|
import LeaveApplications from '../pages/leave/LeaveApplications';
|
||||||
|
import LedgerReport from '../pages/leave/LedgerReport'; // New Import
|
||||||
const LedgerPlaceholder = () => (
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Ledger Report</h2>
|
|
||||||
<p className="text-text-muted mt-2">Ledger Report UI will be implemented here.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function LeaveSummaryRouter() {
|
export default function LeaveSummaryRouter() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
|
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
|
||||||
<Route path="/applications" element={<LeaveApplications />} />
|
<Route path="/applications" element={<LeaveApplications />} />
|
||||||
<Route path="/ledger" element={<LedgerPlaceholder />} />
|
<Route path="/ledger" element={<LedgerReport />} /> {/* Updated Route */}
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,3 +65,16 @@ export interface LeaveApplicationSummary {
|
|||||||
leaveDays: number;
|
leaveDays: number;
|
||||||
status: 'Approved' | 'Pending' | 'Rejected' | 'Withdrawn';
|
status: 'Approved' | 'Pending' | 'Rejected' | 'Withdrawn';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add this to your existing leave types
|
||||||
|
export interface LeaveLedgerRecord {
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
openingBalance: number;
|
||||||
|
closingBalance: number;
|
||||||
|
casualLeave: number;
|
||||||
|
optionalLeave: number;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user