Compare commits

..

2 Commits

10 changed files with 307 additions and 25 deletions

BIN
public/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View 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 }
]

View File

@ -5,34 +5,66 @@ import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
// Role-specific search data
const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
employee: [
{ name: 'My Dashboard', path: '/dashboard' },
{ name: 'Mark Attendance', path: '/attendance' },
{ name: 'My Attendance', path: '/attendance' },
{ name: 'Apply for Leave', path: '/leave' },
],
manager: [
{ name: 'Manager Dashboard', path: '/dashboard' },
{ name: 'Team Attendance', path: '/attendance' },
{ name: 'Team Leaves', path: '/leave' },
{ name: 'My Attendance', path: '/attendance' },
{ name: 'My Leaves', path: '/leave' },
{ name: 'Pending Approvals', path: '/approval' },
],
admin: [
{ name: 'Admin Dashboard', path: '/dashboard' },
{ name: 'All Attendance Logs', path: '/attendance' },
{ name: 'All Leave Requests', path: '/leave' },
{ name: 'Attendance Summary Report', path: '/attendance-summary' },
{ name: 'Leave Summary Report', path: '/leave-summary' },
{ name: 'Manage Employees', path: '/manage-employees' },
// 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' },
{ 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: [
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
{ name: 'Global Attendance Logs', path: '/attendance' },
{ 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 Employees', path: '/manage-employees' },
{ name: 'Policy and Rules', path: '/policies' },
],
};
@ -66,6 +98,7 @@ export default function Navbar() {
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Get search items based on the current role
const availableItems = roleSearchData[currentRole] || [];
const searchResults = availableItems.filter(item =>
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="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 className="flex items-center space-x-4">
@ -93,7 +126,7 @@ export default function Navbar() {
<input
ref={searchInputRef}
type="text"
placeholder="Search..."
placeholder="Search modules..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
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 && (
<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 ? (
<ul>
{searchResults.map((item) => (

View File

@ -6,14 +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, Receipt // Added Receipt
Database, Building, Network, Briefcase, Tag, Receipt
} 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, Receipt // Added Receipt
Database, Building, Network, Briefcase, Tag, Receipt
};
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={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
{!isCollapsed && (
<Link to="/dashboard">
<img src="clri_logo.svg" alt="Company Logo" className="h-8 w-auto cursor-pointer" />
<Link to="/dashboard" className="flex items-center">
{/* Fixed Tailwind sizing for GIF alignment */}
<img src="logo.gif" alt="Company Logo" className="h-25 w-25 object-contain cursor-pointer" />
</Link>
)}
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">

View File

@ -5,6 +5,7 @@ import type { RootState } from '../../../store/store';
import type { UserRole } from '../../../store/roleSlice';
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
import type { LeaveApplicationSummary } from '../types/leave';
import type { LeaveLedgerRecord } from '../types/leave';
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
employee: {
@ -88,4 +89,11 @@ export const useLeaveApplications = () => {
queryKey: ['leaveApplications'],
queryFn: () => fetchMockData<LeaveApplicationSummary[]>('leave/leave_applications.json'),
});
};
export const useLeaveLedger = () => {
return useQuery<LeaveLedgerRecord[]>({
queryKey: ['leaveLedger'],
queryFn: () => fetchMockData<LeaveLedgerRecord[]>('leave/leave_ledger.json'),
});
};

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -1,19 +1,13 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import LeaveApplications from '../pages/leave/LeaveApplications';
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>
);
import LedgerReport from '../pages/leave/LedgerReport'; // New Import
export default function LeaveSummaryRouter() {
return (
<Routes>
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
<Route path="/applications" element={<LeaveApplications />} />
<Route path="/ledger" element={<LedgerPlaceholder />} />
<Route path="/ledger" element={<LedgerReport />} /> {/* Updated Route */}
</Routes>
);
}

View File

@ -64,4 +64,17 @@ export interface LeaveApplicationSummary {
leaveType: string;
leaveDays: number;
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;
}