Compare commits
No commits in common. "b7e206b82942a4dbc57f2e85ed55e5bb3889486c" and "a24f0e9cbf235b3d36942e4809f3ef029d52eb74" have entirely different histories.
b7e206b829
...
a24f0e9cbf
@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
{ "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." }
|
|
||||||
]
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
[
|
|
||||||
{ "leaveType": "Casual Leave", "credited": 10, "utilized": 8, "lapsed": 0, "balance": 2 },
|
|
||||||
{ "leaveType": "Optional Holiday", "credited": 4, "utilized": 3, "lapsed": 0, "balance": 1 }
|
|
||||||
]
|
|
||||||
@ -1,16 +1,10 @@
|
|||||||
// src/api/client.ts
|
// Simulates network latency and API fetching from public/mockApi/
|
||||||
|
|
||||||
// Generic GET request for any module
|
|
||||||
export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T> => {
|
export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T> => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
const response = await fetch(`/mockApi/${endpoint}`);
|
|
||||||
if (!response.ok) throw new Error(`API Error: Failed to fetch ${endpoint}`);
|
const response = await fetch(`/mockApi/${endpoint}.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API Error: Failed to fetch ${endpoint}`);
|
||||||
|
}
|
||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic POST request for any module
|
|
||||||
export const postMockData = async <T>(endpoint: string, payload: T, delay = 800): Promise<T> => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
console.log(`[Mock POST] /mockApi/${endpoint}`, payload);
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
import { getSidebarItems } from './types/sidebarConfig';
|
import { getSidebarItems } from '../../types/sidebarConfig';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { useSelector } from 'react-redux';
|
|||||||
import { fetchMockData } from '../../../api/client';
|
import { fetchMockData } from '../../../api/client';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../../../store/roleSlice';
|
||||||
import type { AttendanceRecord } from '../types/attendance';
|
import type { AttendanceRecord } from '../../../types/attendance';
|
||||||
|
|
||||||
// 1. Define endpoints per role (Easy to swap to real API URLs later)
|
// 1. Define endpoints per role (Easy to swap to real API URLs later)
|
||||||
const ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
const ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
||||||
employee: 'attendance/employee_attendance.json',
|
employee: 'employee_attendance',
|
||||||
manager: 'attendance/manager_attendance.json',
|
manager: 'manager_attendance',
|
||||||
admin: 'attendance/admin_attendance.json',
|
admin: 'admin_attendance',
|
||||||
superadmin: 'attendance/superadmin_attendance.json',
|
superadmin: 'superadmin_attendance',
|
||||||
|
|
||||||
// Future real backend example:
|
// Future real backend example:
|
||||||
// employee: '/api/v1/attendance/me',
|
// employee: '/api/v1/attendance/me',
|
||||||
@ -22,9 +22,8 @@ export const useAttendanceData = () => {
|
|||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
|
|
||||||
return useQuery<AttendanceRecord[]>({
|
return useQuery<AttendanceRecord[]>({
|
||||||
queryKey: ['attendance', role],
|
// Cache key includes the role so React Query caches them separately
|
||||||
|
queryKey: ['attendanceRecords', role],
|
||||||
queryFn: () => fetchMockData<AttendanceRecord[]>(ATTENDANCE_ENDPOINTS[role]),
|
queryFn: () => fetchMockData<AttendanceRecord[]>(ATTENDANCE_ENDPOINTS[role]),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||||
import type { AttendanceRecord, SortKey } from '../types/attendance';
|
import type { AttendanceRecord, SortKey } from '../../../types/attendance';
|
||||||
|
|
||||||
type SortDirection = 'ascending' | 'descending';
|
type SortDirection = 'ascending' | 'descending';
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData, postMockData } from '../../../api/client';
|
|
||||||
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord } from '../types/leave';
|
|
||||||
|
|
||||||
export const useLeaveSummary = () => {
|
|
||||||
return useQuery<LeaveSummary[]>({
|
|
||||||
queryKey: ['leaveSummary', 'employee'],
|
|
||||||
queryFn: () => fetchMockData<LeaveSummary[]>('leave/employee_leave_summary.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useApplyLeave = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload: LeaveRequestPayload) => postMockData('leave/employee_leave_requests', payload),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['leaveSummary', 'employee'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useLeaveHistory = () => {
|
|
||||||
return useQuery<LeaveHistoryRecord[]>({
|
|
||||||
queryKey: ['leaveHistory', 'employee'],
|
|
||||||
queryFn: () => fetchMockData<LeaveHistoryRecord[]>('leave/employee_leave_history.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useApplyLeave } from '../api/useLeaveData';
|
|
||||||
import type { LeaveSummary, LeaveRequestPayload, DayMode } from '../types/leave';
|
|
||||||
|
|
||||||
interface ApplyLeaveFormProps {
|
|
||||||
summaryData: LeaveSummary[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateLeaveDays = (from: string, fromMode: DayMode, to: string, toMode: DayMode) => {
|
|
||||||
if (!from || !to) return 0;
|
|
||||||
const start = new Date(from + 'T00:00:00');
|
|
||||||
const end = new Date(to + 'T00:00:00');
|
|
||||||
if (end < start) return 0;
|
|
||||||
const diffTime = Math.abs(end.getTime() - start.getTime());
|
|
||||||
let days = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
if (fromMode !== 'Full Day') days -= 0.5;
|
|
||||||
if (toMode !== 'Full Day') days -= 0.5;
|
|
||||||
return days;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fixed 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); // 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');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ApplyLeaveForm({ summaryData }: ApplyLeaveFormProps) {
|
|
||||||
const { mutate, isPending } = useApplyLeave();
|
|
||||||
const [showForm, setShowForm] = useState(false);
|
|
||||||
const [selectedLeaveType, setSelectedLeaveType] = useState('');
|
|
||||||
const [formData, setFormData] = useState<LeaveRequestPayload>({
|
|
||||||
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 (!selectedLeaveType) { toast.error("Please select a leave type first."); return; }
|
|
||||||
setShowForm(true);
|
|
||||||
setFormData(prev => ({ ...prev, leaveType: selectedLeaveType }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
||||||
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (calculatedDays <= 0) { toast.error("Invalid date range selected."); return; }
|
|
||||||
mutate({ ...formData, leaveDays: calculatedDays, resumptionDate: calculatedResumption }, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Leave applied successfully!");
|
|
||||||
setShowForm(false);
|
|
||||||
setSelectedLeaveType('');
|
|
||||||
setFormData({ leaveType: '', fromDate: '', fromDayMode: 'Full Day', toDate: '', toDayMode: 'Full Day', leaveDays: 0, resumptionDate: '', reason: '' });
|
|
||||||
},
|
|
||||||
onError: () => toast.error("Failed to apply leave."),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full lg:w-96 bg-app-card p-6 rounded-lg shadow-sm h-fit">
|
|
||||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Apply Leave</h2>
|
|
||||||
{!showForm ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-secondary mb-1">Leave Type</label>
|
|
||||||
<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, 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>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<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">{selectedLeaveType}</span>
|
|
||||||
<button type="button" onClick={() => setShowForm(false)} className="text-text-muted hover:text-text-primary text-sm">Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-secondary mb-1">From Date</label>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<input type="date" name="fromDate" value={formData.fromDate} onChange={handleChange} required className="flex-1 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 name="fromDayMode" value={formData.fromDayMode} onChange={handleChange} className="px-2 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>Full Day</option><option>First Half</option><option>Second Half</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-secondary mb-1">To Date</label>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<input type="date" name="toDate" value={formData.toDate} onChange={handleChange} required className="flex-1 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 name="toDayMode" value={formData.toDayMode} onChange={handleChange} className="px-2 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>Full Day</option><option>First Half</option><option>Second Half</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="bg-app-muted p-3 rounded-lg">
|
|
||||||
<span className="block text-xs text-text-muted">Leave Days</span>
|
|
||||||
<span className="text-sm font-bold text-text-primary">{calculatedDays} Days</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-app-muted p-3 rounded-lg">
|
|
||||||
<span className="block text-xs text-text-muted">Resumption Date</span>
|
|
||||||
<span className="text-sm font-bold text-text-primary">{calculatedResumption || '-'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-text-secondary mb-1">Reason</label>
|
|
||||||
<textarea name="reason" value={formData.reason} onChange={handleChange} 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 leave..."></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" disabled={isPending || calculatedDays <= 0} className="w-full px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center">
|
|
||||||
{isPending ? 'Applying...' : 'Apply Leave'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,252 +0,0 @@
|
|||||||
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 './ViewLeaveModal';
|
|
||||||
import WithdrawLeaveModal from './WithdrawLeaveModal';
|
|
||||||
|
|
||||||
interface LeaveHistoryTableProps {
|
|
||||||
records: LeaveHistoryRecord[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortDirection = 'ascending' | 'descending';
|
|
||||||
|
|
||||||
// Updated to handle empty strings safely
|
|
||||||
const getDaysDifference = (start: string, end: string) => {
|
|
||||||
if (!start || !end) return 0;
|
|
||||||
const startDate = new Date(start + 'T00:00:00');
|
|
||||||
const endDate = new Date(end + 'T00:00:00');
|
|
||||||
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
|
|
||||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
|
|
||||||
// Default to empty strings to show all records initially
|
|
||||||
const [fromDate, setFromDate] = useState('');
|
|
||||||
const [toDate, setToDate] = useState('');
|
|
||||||
|
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: LeaveSortKey; direction: SortDirection }>({
|
|
||||||
key: 'applicationDate',
|
|
||||||
direction: 'descending'
|
|
||||||
});
|
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(5);
|
|
||||||
const [viewRecord, setViewRecord] = useState<LeaveHistoryRecord | null>(null);
|
|
||||||
const [withdrawRecord, setWithdrawRecord] = useState<LeaveHistoryRecord | null>(null);
|
|
||||||
|
|
||||||
// Use an overrides object to handle instant UI updates for withdrawals
|
|
||||||
const [localOverrides, setLocalOverrides] = useState<Record<string, LeaveHistoryRecord>>({});
|
|
||||||
const combinedRecords = useMemo(() => {
|
|
||||||
return records.map(r => localOverrides[r.id] || r);
|
|
||||||
}, [records, localOverrides]);
|
|
||||||
|
|
||||||
// Filter specifically by applicationDate
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return combinedRecords.filter((record) => {
|
|
||||||
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];
|
|
||||||
sortableData.sort((a, b) => {
|
|
||||||
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;
|
|
||||||
}, [filteredData, sortConfig]);
|
|
||||||
|
|
||||||
const daysDifference = getDaysDifference(fromDate, toDate);
|
|
||||||
const isPaginationEnabled = daysDifference > 7 || sortedData.length > pageSize;
|
|
||||||
const totalPages = isPaginationEnabled ? Math.ceil(sortedData.length / pageSize) : 1;
|
|
||||||
|
|
||||||
const paginatedData = useMemo(() => {
|
|
||||||
if (!isPaginationEnabled) return sortedData;
|
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
|
||||||
return sortedData.slice(startIndex, startIndex + pageSize);
|
|
||||||
}, [sortedData, currentPage, isPaginationEnabled, pageSize]);
|
|
||||||
|
|
||||||
const pageNumbers = useMemo(() => {
|
|
||||||
const pages = [];
|
|
||||||
const maxVisiblePages = 5;
|
|
||||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
|
||||||
let endPage = startPage + maxVisiblePages - 1;
|
|
||||||
if (endPage > totalPages) {
|
|
||||||
endPage = totalPages;
|
|
||||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
|
||||||
}
|
|
||||||
for (let i = startPage; i <= endPage; i++) pages.push(i);
|
|
||||||
return pages;
|
|
||||||
}, [currentPage, totalPages]);
|
|
||||||
|
|
||||||
const requestSort = (key: LeaveSortKey) => {
|
|
||||||
let direction: SortDirection = 'ascending';
|
|
||||||
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
|
|
||||||
setSortConfig({ key, direction });
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFromDate(e.target.value);
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setToDate(e.target.value);
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setPageSize(Number(e.target.value));
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWithdraw = (id: string) => {
|
|
||||||
// Find the current record (either from original array or overrides)
|
|
||||||
const targetRecord = combinedRecords.find(r => r.id === id);
|
|
||||||
if (!targetRecord) return;
|
|
||||||
|
|
||||||
// Update local override for instant UI feedback - changes status, does NOT delete
|
|
||||||
setLocalOverrides(prev => ({
|
|
||||||
...prev,
|
|
||||||
[id]: { ...targetRecord, status: 'Withdrawn' }
|
|
||||||
}));
|
|
||||||
|
|
||||||
toast.success("Leave application withdrawn successfully.");
|
|
||||||
setWithdrawRecord(null);
|
|
||||||
setViewRecord(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSortIcon = (key: LeaveSortKey) => {
|
|
||||||
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
|
|
||||||
if (sortConfig.direction === 'ascending') return <ArrowUp size={14} className="ml-1 text-primary" />;
|
|
||||||
return <ArrowDown size={14} className="ml-1 text-primary" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 'Withdrawn': return 'bg-absent-100 text-absent-700';
|
|
||||||
default: return 'bg-app-muted text-text-secondary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6 relative">
|
|
||||||
<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">My Leaves Applications</h2>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
|
||||||
<input type="date" value={fromDate} onChange={handleFromDateChange} 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" />
|
|
||||||
</div>
|
|
||||||
<span className="text-text-muted text-sm">to</span>
|
|
||||||
<div className="relative">
|
|
||||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
|
||||||
<input type="date" value={toDate} onChange={handleToDateChange} 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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-app-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{(['leaveType', 'applicationDate', 'fromDate', 'toDate', 'leaveDays', 'status'] as LeaveSortKey[]).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)}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th className="px-6 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-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.leaveType}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{new Date(record.applicationDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{new Date(record.fromDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{new Date(record.toDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.leaveDays}</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 flex space-x-2">
|
|
||||||
<button onClick={() => setViewRecord(record)} className="text-primary hover:text-primary-hover flex items-center">
|
|
||||||
<Eye size={16} className="mr-1" /> View
|
|
||||||
</button>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="px-6 py-8 text-center text-sm text-text-muted">No records found for the selected period.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<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>
|
|
||||||
<select value={pageSize} onChange={handlePageSizeChange} className="px-2 py-1 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={5}>5</option>
|
|
||||||
<option value={10}>10</option>
|
|
||||||
<option value={15}>15</option>
|
|
||||||
<option value={20}>20</option>
|
|
||||||
</select>
|
|
||||||
<span className="text-sm text-text-muted">entries</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isPaginationEnabled && totalPages > 1 && (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span className="text-sm text-text-muted mr-2 hidden sm:inline">Page {currentPage} of {totalPages}</span>
|
|
||||||
<button onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsLeft size={16} /></button>
|
|
||||||
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronLeft size={16} /></button>
|
|
||||||
{pageNumbers.map(num => (
|
|
||||||
<button key={num} onClick={() => setCurrentPage(num)} className={`w-8 h-8 flex items-center justify-center border rounded-lg text-sm transition-colors ${currentPage === num ? 'bg-primary text-white border-primary' : 'border-app-border text-text-secondary hover:bg-app-muted'}`}>{num}</button>
|
|
||||||
))}
|
|
||||||
<button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronRight size={16} /></button>
|
|
||||||
<button onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsRight size={16} /></button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Render Separated Modals */}
|
|
||||||
{viewRecord && (
|
|
||||||
<ViewLeaveModal
|
|
||||||
record={viewRecord}
|
|
||||||
onClose={() => setViewRecord(null)}
|
|
||||||
onWithdraw={(rec) => {
|
|
||||||
setViewRecord(null);
|
|
||||||
setWithdrawRecord(rec);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{withdrawRecord && (
|
|
||||||
<WithdrawLeaveModal
|
|
||||||
record={withdrawRecord}
|
|
||||||
onClose={() => setWithdrawRecord(null)}
|
|
||||||
onConfirm={handleWithdraw}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import type { LeaveSummary } from '../types/leave';
|
|
||||||
|
|
||||||
interface LeaveSummaryTableProps {
|
|
||||||
data: LeaveSummary[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LeaveSummaryTable({ data }: LeaveSummaryTableProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex-1 bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h2 className="text-lg font-semibold text-text-primary mb-6">My Leave Summary</h2>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<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">Leave Type</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Credited</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Utilized</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Lapsed</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Balance</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-app-border">
|
|
||||||
{data.map((leave, index) => (
|
|
||||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{leave.leaveType}</td>
|
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-text-secondary">{leave.credited}</td>
|
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-text-secondary">{leave.utilized}</td>
|
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-text-secondary">{leave.lapsed}</td>
|
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm font-bold text-primary">{leave.balance}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { X } from 'lucide-react';
|
|
||||||
import type { LeaveHistoryRecord } from '../types/leave';
|
|
||||||
|
|
||||||
interface ViewLeaveModalProps {
|
|
||||||
record: LeaveHistoryRecord;
|
|
||||||
onClose: () => void;
|
|
||||||
onWithdraw: (record: LeaveHistoryRecord) => 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 'Withdrawn': return 'bg-absent-100 text-absent-700';
|
|
||||||
default: return 'bg-app-muted text-text-secondary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ViewLeaveModal({ record, onClose, onWithdraw }: ViewLeaveModalProps) {
|
|
||||||
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">
|
|
||||||
<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">Leave Details</h3>
|
|
||||||
<p className="text-sm text-text-muted mb-6">Application 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">Leave Type</span>
|
|
||||||
<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">Application Date</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{new Date(record.applicationDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'long', year: 'numeric' })}</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">{new Date(record.fromDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short' })} to {new Date(record.toDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })} ({record.leaveDays} Days)</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>
|
|
||||||
<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 space-x-3 mt-6">
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { AlertTriangle } from 'lucide-react';
|
|
||||||
import type { LeaveHistoryRecord } from '../types/leave';
|
|
||||||
|
|
||||||
interface WithdrawLeaveModalProps {
|
|
||||||
record: LeaveHistoryRecord;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WithdrawLeaveModal({ record, onClose, onConfirm }: WithdrawLeaveModalProps) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
|
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-sm p-6 relative text-center">
|
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-absent-100 mb-4">
|
|
||||||
<AlertTriangle className="h-6 w-6 text-absent-500" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-2">Withdraw Leave Application?</h3>
|
|
||||||
<p className="text-sm text-text-muted mb-6">
|
|
||||||
Are you sure you want to withdraw your <span className="font-medium text-text-primary">{record.leaveType}</span> application from <span className="font-medium text-text-primary">{new Date(record.fromDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short' })}</span> to <span className="font-medium text-text-primary">{new Date(record.toDate + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short' })}</span>? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-center space-x-3">
|
|
||||||
<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">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button onClick={() => onConfirm(record.id)} className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium">
|
|
||||||
Yes, Withdraw
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import Loader from '../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../components/ui/ErrorState';
|
|
||||||
import LeaveSummaryTable from '../components/LeaveSummaryTable';
|
|
||||||
import ApplyLeaveForm from '../components/ApplyLeaveForm';
|
|
||||||
import LeaveHistoryTable from '../components/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">
|
|
||||||
|
|
||||||
{/* Top Section: Summary & Apply Form */}
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
|
||||||
{summaryData && <LeaveSummaryTable data={summaryData} />}
|
|
||||||
{summaryData && <ApplyLeaveForm summaryData={summaryData} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section: Leave History Table */}
|
|
||||||
{historyData && <LeaveHistoryTable records={historyData} />}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import EmployeeLeave from '../pages/EmployeeLeave';
|
|
||||||
|
|
||||||
// Dummy components for Manager/Admin for now
|
const EmployeeLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">My Leaves</h2><p className="mt-2 text-text-muted">Apply for leave and view your leave history.</p></div>;
|
||||||
const ManagerLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Leave Calendar</h2></div>;
|
const ManagerLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Leave Calendar</h2><p className="mt-2 text-text-muted">See who is on leave in your team.</p></div>;
|
||||||
const AdminLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">All Leave Requests</h2></div>;
|
const AdminLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">All Leave Requests</h2><p className="mt-2 text-text-muted">Manage leave types and balances for branches.</p></div>;
|
||||||
|
|
||||||
export default function LeaveRouter() {
|
export default function LeaveRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -12,7 +11,7 @@ export default function LeaveRouter() {
|
|||||||
switch (role) {
|
switch (role) {
|
||||||
case 'employee': return <EmployeeLeave />;
|
case 'employee': return <EmployeeLeave />;
|
||||||
case 'manager': return <ManagerLeave />;
|
case 'manager': return <ManagerLeave />;
|
||||||
case 'admin':
|
case 'admin': return <ManagerLeave />;
|
||||||
case 'superadmin': return <AdminLeave />;
|
case 'superadmin': return <AdminLeave />;
|
||||||
default: return <div>Unauthorized</div>;
|
default: return <div>Unauthorized</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
export interface LeaveSummary {
|
|
||||||
leaveType: string;
|
|
||||||
credited: number;
|
|
||||||
utilized: number;
|
|
||||||
lapsed: number;
|
|
||||||
balance: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DayMode = 'Full Day' | 'First Half' | 'Second Half';
|
|
||||||
|
|
||||||
export interface LeaveRequestPayload {
|
|
||||||
leaveType: string;
|
|
||||||
fromDate: string;
|
|
||||||
fromDayMode: DayMode;
|
|
||||||
toDate: string;
|
|
||||||
toDayMode: DayMode;
|
|
||||||
leaveDays: number;
|
|
||||||
resumptionDate: string;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// New Type for Leave History
|
|
||||||
export type LeaveStatus = 'Approved' | 'Pending' | 'Withdrawn';
|
|
||||||
|
|
||||||
export interface LeaveHistoryRecord {
|
|
||||||
id: string;
|
|
||||||
leaveType: string;
|
|
||||||
applicationDate: string;
|
|
||||||
fromDate: string;
|
|
||||||
toDate: string;
|
|
||||||
leaveDays: number;
|
|
||||||
status: LeaveStatus;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LeaveSortKey = keyof LeaveHistoryRecord;
|
|
||||||
@ -1 +1,14 @@
|
|||||||
export type UserRole = 'employee' | 'manager' | 'admin' | 'superadmin';
|
export type UserRole = 'employee' | 'manager' | 'admin' | 'superadmin';
|
||||||
|
|
||||||
|
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
||||||
|
|
||||||
|
export interface AttendanceRecord {
|
||||||
|
date: string;
|
||||||
|
employeeName?: string; // Optional for employee view, required for admin/manager
|
||||||
|
timeIn: string;
|
||||||
|
timeOut: string;
|
||||||
|
loggedHours: string;
|
||||||
|
status: AttendanceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortKey = keyof AttendanceRecord;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../store/roleSlice';
|
||||||
|
|
||||||
export interface NavItem {
|
export interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
Loading…
x
Reference in New Issue
Block a user