Compare commits
No commits in common. "e38b09bd2df533251042af04846877cb33a02a3a" and "2567456e538a2dcfb46ce6d8b59fa6246e9e7c30" have entirely different histories.
e38b09bd2d
...
2567456e53
BIN
public/logo.gif
BIN
public/logo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
@ -1,7 +0,0 @@
|
||||
[
|
||||
{ "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,66 +5,34 @@ 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: 'My Attendance', path: '/attendance' },
|
||||
{ name: 'Mark Attendance', path: '/attendance' },
|
||||
{ name: 'Apply for Leave', path: '/leave' },
|
||||
],
|
||||
manager: [
|
||||
{ name: 'Manager Dashboard', path: '/dashboard' },
|
||||
{ name: 'My Attendance', path: '/attendance' },
|
||||
{ name: 'My Leaves', path: '/leave' },
|
||||
{ name: 'Team Attendance', path: '/attendance' },
|
||||
{ name: 'Team 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' },
|
||||
|
||||
// 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' },
|
||||
{ name: 'Attendance Summary Report', path: '/attendance-summary' },
|
||||
{ name: 'Leave Summary Report', path: '/leave-summary' },
|
||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
||||
],
|
||||
superadmin: [
|
||||
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
|
||||
{ name: 'Global Attendance Logs', path: '/attendance' },
|
||||
{ name: 'Global Leave Requests', path: '/leave' },
|
||||
|
||||
// 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: 'Global Attendance Summary', path: '/attendance-summary' },
|
||||
{ name: 'Global Leave Summary', path: '/leave-summary' },
|
||||
{ name: 'Manage Admins', path: '/manage-admins' },
|
||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
||||
{ name: 'Policy and Rules', path: '/policies' },
|
||||
],
|
||||
};
|
||||
@ -98,7 +66,6 @@ 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())
|
||||
@ -114,7 +81,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-primary tracking-wide">CLRI HRMS</h2>
|
||||
<h2 className="text-lg font-bold text-default tracking-wide">CLRI HRMS</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
@ -126,7 +93,7 @@ export default function Navbar() {
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search modules..."
|
||||
placeholder="Search..."
|
||||
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"
|
||||
@ -148,7 +115,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 max-h-96 overflow-y-auto">
|
||||
<div className="absolute right-0 top-14 w-72 bg-app-card border border-app-border rounded-lg shadow-lg overflow-hidden z-50">
|
||||
{searchResults.length > 0 ? (
|
||||
<ul>
|
||||
{searchResults.map((item) => (
|
||||
|
||||
@ -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
|
||||
Database, Building, Network, Briefcase, Tag, Receipt // Added 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
|
||||
Database, Building, Network, Briefcase, Tag, Receipt // Added Receipt
|
||||
};
|
||||
|
||||
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
||||
@ -90,9 +90,8 @@ 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" 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 to="/dashboard">
|
||||
<img src="clri_logo.svg" alt="Company Logo" className="h-8 w-auto cursor-pointer" />
|
||||
</Link>
|
||||
)}
|
||||
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">
|
||||
|
||||
@ -5,7 +5,6 @@ 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: {
|
||||
@ -89,11 +88,4 @@ 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'),
|
||||
});
|
||||
};
|
||||
@ -1,87 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
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,13 +1,19 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
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() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
|
||||
<Route path="/applications" element={<LeaveApplications />} />
|
||||
<Route path="/ledger" element={<LedgerReport />} /> {/* Updated Route */}
|
||||
<Route path="/ledger" element={<LedgerPlaceholder />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@ -64,17 +64,4 @@ 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user