pmtool / src /components /leaves /LeaveForm.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
50.9 kB
import React, { useState, useRef, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { format, addDays, differenceInCalendarDays, eachDayOfInterval, isWeekend, isSameDay } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { holidayApi } from "@/services/holidayApi";
import { LeaveType, LeaveCreateRequest, LeaveMaster, LeaveBalance, Holiday } from "@/types";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import {
Calendar as CalendarIcon,
Loader2,
ChevronDown,
Briefcase,
Heart,
Moon,
Sun,
Baby,
DollarSign,
HelpCircle,
Check,
Clock,
FileText,
Leaf,
Droplets,
AlertCircle,
RefreshCw,
Gift,
CalendarDays,
} from "lucide-react";
import { toast } from "sonner";
import { leaveMasterApi } from "@/services/leaveMasterApi";
import { leaveApi } from "@/services/leaveApi";
import { Badge } from "@/components/ui/badge";
import { useAuth } from "@/lib/auth-context";
// Create a more dynamic schema based on available leave types
const createLeaveFormSchema = (availableTypes: string[]) => z.object({
startDate: z.date({
required_error: "Start date is required",
}),
endDate: z.date({
required_error: "End date is required",
}),
type: z.string({
required_error: "Leave type is required",
}),
reason: z.string().min(5, {
message: "Reason must be at least 5 characters",
}),
documents: z.instanceof(FileList).optional(),
halfDay: z.boolean().default(false),
halfDayPortion: z.enum(['first', 'second']).optional(),
leaveMasterId: z.number().optional(),
compOff: z.boolean().default(false),
dayType: z.enum(['half', 'full']).default('full'),
});
type LeaveFormValues = {
startDate: Date;
endDate: Date;
type: string;
reason: string;
documents?: FileList;
halfDay: boolean;
halfDayPortion?: 'first' | 'second';
leaveMasterId?: number;
compOff: boolean;
dayType: 'half' | 'full';
};
interface LeaveFormProps {
employeeId: number;
onSubmit: (data: LeaveCreateRequest) => Promise<void>;
onCancel: () => void;
isSubmitting: boolean;
defaultType?: string;
defaultCompOff?: boolean;
hideLeaveType?: boolean;
duplicateDateWarning?: string;
onDateChange?: () => void;
}
const LeaveForm: React.FC<LeaveFormProps> = ({
employeeId,
onSubmit,
onCancel,
isSubmitting,
defaultType = "",
defaultCompOff = false,
hideLeaveType = false,
duplicateDateWarning = "",
onDateChange,
}) => {
const { userData } = useAuth();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [showStartDateCalendar, setShowStartDateCalendar] = useState(false);
const [showEndDateCalendar, setShowEndDateCalendar] = useState(false);
const [showTypeDropdown, setShowTypeDropdown] = useState(false);
const [showLeaveMasterDropdown, setShowLeaveMasterDropdown] = useState(false);
const [leaveMasters, setLeaveMasters] = useState<LeaveMaster[]>([]);
const [isLoadingMasters, setIsLoadingMasters] = useState(false);
const [availableLeaveTypes, setAvailableLeaveTypes] = useState<string[]>([]);
const [leaveBalances, setLeaveBalances] = useState<LeaveBalance[]>([]);
const [isLoadingBalances, setIsLoadingBalances] = useState(false);
const [currentYear] = useState<number>(new Date().getFullYear());
const [showHalfDayDropdown, setShowHalfDayDropdown] = useState(false);
const [holidays, setHolidays] = useState<Holiday[]>([]);
const [calculatedWorkingDays, setCalculatedWorkingDays] = useState<number>(0);
const [holidayWarnings, setHolidayWarnings] = useState<string[]>([]);
// Add refs for the date pickers
const startDateRef = useRef<HTMLDivElement>(null);
const endDateRef = useRef<HTMLDivElement>(null);
const typeDropdownRef = useRef<HTMLDivElement>(null);
const leaveMasterDropdownRef = useRef<HTMLDivElement>(null);
const halfDayDropdownRef = useRef<HTMLDivElement>(null);
// Fallback leave types in case the API fails
const fallbackLeaveTypes = [
"Annual",
"Sick",
"Personal",
"Unpaid",
"Maternity",
"Paternity",
"Other"
];
const form = useForm<LeaveFormValues>({
resolver: zodResolver(createLeaveFormSchema(availableLeaveTypes.length > 0 ? availableLeaveTypes : fallbackLeaveTypes)),
defaultValues: {
startDate: new Date(),
endDate: addDays(new Date(), 1),
type: defaultType,
reason: "",
halfDay: false,
halfDayPortion: 'first',
leaveMasterId: undefined,
compOff: defaultCompOff,
dayType: 'full',
},
});
// Fetch leave masters and leave balances on component mount
useEffect(() => {
const fetchData = async () => {
setIsLoadingMasters(true);
setIsLoadingBalances(true);
try {
// Get all leave masters
const masters = await leaveMasterApi.getAll();
console.log('Fetched all leave masters:', masters);
// Filter leave masters based on user role if available
let filteredMasters = masters;
if (userData?.roleId) {
filteredMasters = masters.filter(master => {
const roles = master.applyToRoles.split(',').map(r => parseInt(r.trim(), 10));
return roles.includes(userData.roleId) && master.isActive;
});
console.log(`Filtered leave masters for role ${userData.roleId}:`, filteredMasters);
} else {
// Only use active leave masters
filteredMasters = masters.filter(master => master.isActive);
}
setLeaveMasters(filteredMasters);
// Extract available leave types from the masters
const types = filteredMasters.map(m => m.leaveType);
setAvailableLeaveTypes(types);
// Set default selection if available
if (filteredMasters.length > 0) {
// Priority: 1. defaultType if provided, 2. Annual, 3. First available
const defaultMaster =
(defaultType && filteredMasters.find(m => m.leaveType.toLowerCase() === defaultType.toLowerCase())) ||
filteredMasters.find(m => m.leaveType === 'Annual') ||
filteredMasters[0];
form.setValue('type', defaultMaster.leaveType);
form.setValue('leaveMasterId', defaultMaster.id);
// If it's compensatory and defaultCompOff is true, ensure it's set
if (defaultCompOff && defaultMaster.leaveType.toLowerCase() === "compensatory") {
form.setValue('compOff', true);
}
}
// Get leave balances for the current employee
if (employeeId) {
try {
const balances = await leaveApi.getLeaveBalance(employeeId, currentYear);
console.log(`Fetched leave balances for employee ${employeeId}:`, balances);
if (Array.isArray(balances)) {
setLeaveBalances(balances);
}
} catch (error) {
console.error('Error fetching leave balances:', error);
toast.error('Failed to load leave balances');
}
// Get holidays
try {
const fetchedHolidays = await holidayApi.getAll();
setHolidays(fetchedHolidays);
} catch (error) {
console.error('Error fetching holidays:', error);
}
}
} catch (error) {
console.error('Error fetching leave masters:', error);
toast.error('Failed to load leave types');
setAvailableLeaveTypes(fallbackLeaveTypes);
} finally {
setIsLoadingMasters(false);
setIsLoadingBalances(false);
}
};
fetchData();
}, [userData?.roleId, employeeId, currentYear]);
// Update the useEffect for click outside handling
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (startDateRef.current && !startDateRef.current.contains(event.target as Node)) {
setShowStartDateCalendar(false);
}
if (endDateRef.current && !endDateRef.current.contains(event.target as Node)) {
setShowEndDateCalendar(false);
}
if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target as Node)) {
setShowTypeDropdown(false);
}
if (leaveMasterDropdownRef.current && !leaveMasterDropdownRef.current.contains(event.target as Node)) {
setShowLeaveMasterDropdown(false);
}
if (halfDayDropdownRef.current && !halfDayDropdownRef.current.contains(event.target as Node)) {
setShowHalfDayDropdown(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
setSelectedFile(files[0]);
}
};
// Helper function to find leave master ID based on leave type
const findLeaveMasterId = (type: string): number => {
// Try to find the matching leave master from fetched masters
const master = leaveMasters.find(m =>
m.leaveType.toLowerCase() === type.toLowerCase()
);
if (master) {
return master.id;
}
// No matching master found, return a default
return 0;
};
// Add an effect to automatically set halfDay to true when the leave type is "Half Day"
useEffect(() => {
const selectedMasterId = form.watch('leaveMasterId');
if (selectedMasterId) {
const selectedMaster = leaveMasters.find(m => m.id === selectedMasterId);
if (selectedMaster && selectedMaster.leaveType.toLowerCase() === "half day") {
form.setValue('halfDay', true);
} else {
form.setValue('halfDay', false);
}
// Only set compOff to false when changing away from Compensatory
// Don't automatically set it to true just because Compensatory is selected
// It should only be true when explicitly creating a Comp Off Request (defaultCompOff prop)
if (selectedMaster && selectedMaster.leaveType.toLowerCase() !== "compensatory") {
form.setValue('compOff', false);
}
}
}, [form.watch('leaveMasterId'), leaveMasters]);
// Add an effect to ensure end date equals start date for Half Day leave
useEffect(() => {
const selectedMasterId = form.watch('leaveMasterId');
const startDate = form.watch('startDate');
if (selectedMasterId) {
const selectedMaster = leaveMasters.find(m => m.id === selectedMasterId);
// For Half Day leave type, always set end date equal to start date
if (selectedMaster && selectedMaster.leaveType.toLowerCase() === "half day") {
form.setValue('halfDay', true);
form.setValue('endDate', startDate);
// Disable end date calendar for Half Day leave type
setShowEndDateCalendar(false);
}
}
}, [form.watch('startDate'), form.watch('leaveMasterId'), leaveMasters]);
// Calculate working days whenever dates or leave type changes
useEffect(() => {
const startDate = form.watch("startDate");
const endDate = form.watch("endDate");
const leaveMasterId = form.watch("leaveMasterId");
const compOff = form.watch("compOff");
const dayType = form.watch("dayType");
console.log("Working days calculation triggered:", {
startDate: startDate?.toDateString(),
endDate: endDate?.toDateString(),
leaveMasterId,
compOff,
dayType,
holidaysCount: holidays.length,
});
if (startDate && endDate && leaveMasterId) {
const selectedMaster = leaveMasters.find((m) => m.id === leaveMasterId);
const isHalfDayLeave = selectedMaster?.leaveType.toLowerCase() === "half day";
const isCompOffLeave =
compOff || selectedMaster?.leaveType.toLowerCase() === "compensatory";
console.log("Leave type checks:", {
selectedMaster: selectedMaster?.leaveType,
isHalfDayLeave,
isCompOffLeave,
dayType,
});
if (isHalfDayLeave || dayType === "half") {
console.log("Setting 0.5 days for half day leave");
setCalculatedWorkingDays(0.5);
setHolidayWarnings([]);
} else {
let actualWorkingDays = 0;
try {
// Normalize both dates to midnight to avoid time comparison issues
const validStartDate = new Date(startDate);
const validEndDate = new Date(endDate);
validStartDate.setHours(0, 0, 0, 0);
validEndDate.setHours(0, 0, 0, 0);
console.log("Validating dates:", {
originalStart: startDate,
originalEnd: endDate,
normalizedStart: validStartDate,
normalizedEnd: validEndDate,
isValidStart: !isNaN(validStartDate.getTime()),
isValidEnd: !isNaN(validEndDate.getTime()),
});
if (isNaN(validStartDate.getTime()) || isNaN(validEndDate.getTime())) {
console.error("Invalid dates detected");
setCalculatedWorkingDays(0);
setHolidayWarnings([]);
return;
}
// Ensure start date is not after end date after normalization
if (validStartDate > validEndDate) {
console.error("Start date is after end date after normalization");
setCalculatedWorkingDays(0);
setHolidayWarnings([]);
return;
}
const days = eachDayOfInterval({
start: validStartDate,
end: validEndDate,
});
console.log(
"Days in interval:",
days.length,
days.map((d) => d.toDateString())
);
const foundHolidays: string[] = [];
days.forEach((day) => {
const isWeekendDay = isWeekend(day);
const holiday = holidays.find((h) =>
isSameDay(new Date(h.date), day)
);
// For all leave types (including regular leaves), count all days except holidays
if (!holiday) {
actualWorkingDays++;
} else {
const holidayInfo = `${holiday.name} (${format(
new Date(holiday.date),
"MMM d"
)})`;
if (!foundHolidays.includes(holidayInfo)) {
foundHolidays.push(holidayInfo);
}
}
});
console.log("Final calculated working days:", actualWorkingDays);
setCalculatedWorkingDays(actualWorkingDays);
setHolidayWarnings(foundHolidays);
} catch (error) {
console.error("Error calculating working days:", error);
setCalculatedWorkingDays(0);
setHolidayWarnings([]);
}
}
} else {
console.log("Missing required data, setting 0 days");
setCalculatedWorkingDays(0);
setHolidayWarnings([]);
}
}, [
form.watch("startDate"),
form.watch("endDate"),
form.watch("leaveMasterId"),
form.watch("compOff"),
form.watch("dayType"),
holidays,
leaveMasters,
]);
// Clear end date error when dates are valid
useEffect(() => {
const startDate = form.watch('startDate');
const endDate = form.watch('endDate');
if (startDate && endDate) {
// Normalize both dates to midnight for accurate comparison
const normalizedStart = new Date(startDate);
const normalizedEnd = new Date(endDate);
normalizedStart.setHours(0, 0, 0, 0);
normalizedEnd.setHours(0, 0, 0, 0);
console.log('Date comparison:', {
originalStart: startDate,
originalEnd: endDate,
normalizedStart,
normalizedEnd,
comparison: normalizedStart <= normalizedEnd
});
if (normalizedStart <= normalizedEnd) {
if (form.formState.errors.endDate) {
console.log('Clearing end date error');
form.clearErrors("endDate");
}
}
}
}, [form.watch('startDate'), form.watch('endDate')]);
// Update the handleSubmit function to handle working days calculation correctly for Half Day leave type
const handleSubmit = async (values: LeaveFormValues) => {
// Clear any previous end date error
if (form.formState.errors.endDate) {
form.clearErrors("endDate");
}
// Validate end date is not before start date (allow same date)
// Normalize both dates to midnight for accurate comparison
const normalizedStart = new Date(values.startDate);
const normalizedEnd = new Date(values.endDate);
normalizedStart.setHours(0, 0, 0, 0);
normalizedEnd.setHours(0, 0, 0, 0);
console.log('Submit validation:', {
originalStart: values.startDate,
originalEnd: values.endDate,
normalizedStart,
normalizedEnd,
isValid: normalizedEnd >= normalizedStart,
dayType: values.dayType
});
if (normalizedEnd < normalizedStart) {
form.setError("endDate", {
type: "manual",
message: "End date cannot be before start date",
});
return;
}
try {
// Calculate working days
const selectedMaster = leaveMasters.find(m => m.id === values.leaveMasterId);
const isHalfDayLeave = selectedMaster?.leaveType.toLowerCase() === "half day";
const isCompOffLeave = values.compOff || selectedMaster?.leaveType.toLowerCase() === "compensatory";
let actualWorkingDays = 0;
const days = eachDayOfInterval({ start: normalizedStart, end: normalizedEnd });
days.forEach(day => {
const isWeekendDay = isWeekend(day);
const isHoliday = holidays.some(h => isSameDay(new Date(h.date), day));
// For comp off requests, count weekends as working days (since they worked on weekends)
// For regular leaves, exclude weekends and holidays
if (isCompOffLeave) {
// For comp off, count all days including weekends, but exclude holidays
if (!isHoliday) {
actualWorkingDays++;
}
} else {
// For regular leaves, exclude both weekends and holidays
if (!isWeekendDay && !isHoliday) {
actualWorkingDays++;
}
}
});
// If it's Half Day leave type or dayType is 'half', count it as 0.5 days, otherwise use full days
const workingDays = (isHalfDayLeave || values.dayType === 'half') ? 0.5 : actualWorkingDays;
// Concat first or second half in leave reason for halfday leave (excluding Comp Off)
let finalReason = values.reason;
if (isHalfDayLeave && values.halfDayPortion) {
const portionLabel = values.halfDayPortion === 'first' ? 'First Half' : 'Second Half';
finalReason = `(${portionLabel}) ${values.reason}`;
}
const leaveData: LeaveCreateRequest = {
id: 0,
employeeId,
leaveMasterId: values.leaveMasterId || findLeaveMasterId(values.type),
startDate: format(values.startDate, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
endDate: format(values.endDate, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
reason: finalReason,
type: values.type as LeaveType,
status: "Pending",
appliedOn: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
comments: "",
halfDay: isHalfDayLeave,
halfDayPortion: (isHalfDayLeave || values.dayType === 'half') ? values.halfDayPortion : undefined,
workingDays: workingDays,
compOff: values.compOff,
};
// If a document was attached, we'll handle it separately
if (selectedFile) {
leaveData.documents = "pending_upload"; // This will be replaced with the actual URL after upload
}
await onSubmit(leaveData);
} catch (error) {
console.error("Error submitting leave request:", error);
toast.error("Failed to submit leave request");
}
};
// Helper function to get icon for leave type
const getLeaveTypeIcon = (type: string) => {
switch (type.toLowerCase()) {
case "annual":
return <Sun className="h-4 w-4 mr-2" />;
case "sick":
return <Heart className="h-4 w-4 mr-2" />;
case "personal":
return <Briefcase className="h-4 w-4 mr-2" />;
case "unpaid":
return <DollarSign className="h-4 w-4 mr-2" />;
case "maternity":
return <Baby className="h-4 w-4 mr-2" />;
case "paternity":
return <Baby className="h-4 w-4 mr-2" />;
case "compensatory":
return <FileText className="h-4 w-4 mr-2" />;
case "bereavement":
return <FileText className="h-4 w-4 mr-2" />;
case "half day":
return <Clock className="h-4 w-4 mr-2" />;
case "casual":
return <Leaf className="h-4 w-4 mr-2" />;
default:
return <HelpCircle className="h-4 w-4 mr-2" />;
}
};
// Helper function to get color based on leave type
const getLeaveTypeColor = (type: string): string => {
switch (type.toLowerCase()) {
case "annual":
return "bg-blue-500 hover:bg-blue-600";
case "sick":
return "bg-red-500 hover:bg-red-600";
case "personal":
return "bg-green-500 hover:bg-green-600";
case "maternity":
return "bg-purple-500 hover:bg-purple-600";
case "paternity":
return "bg-indigo-500 hover:bg-indigo-600";
case "unpaid":
return "bg-gray-500 hover:bg-gray-600";
case "compensatory":
return "bg-orange-500 hover:bg-orange-600";
case "bereavement":
return "bg-slate-500 hover:bg-slate-600";
case "half day":
return "bg-amber-500 hover:bg-amber-600";
case "casual":
return "bg-emerald-500 hover:bg-emerald-600";
default:
return "bg-neutral-500 hover:bg-neutral-600";
}
};
// Helper function to get color based on days available
const getLeavePolicyColor = (days: number): string => {
if (days >= 15) return "bg-green-100 text-green-800";
if (days >= 7) return "bg-blue-100 text-blue-800";
if (days >= 3) return "bg-yellow-100 text-yellow-800";
return "bg-red-100 text-red-800";
};
// Helper function to get leave balance for selected leave type
const getLeaveBalance = (leaveMasterId: number): LeaveBalance | undefined => {
return leaveBalances.find(balance => balance.leavesMasterId === leaveMasterId);
};
// Add this helper function to check if currently selected leave type is Half Day
const isHalfDayLeaveType = (): boolean => {
const selectedMasterId = form.watch('leaveMasterId');
if (!selectedMasterId) return false;
const selectedMaster = leaveMasters.find(m => m.id === selectedMasterId);
return selectedMaster?.leaveType.toLowerCase() === "half day";
};
// Helper function to check if leave includes weekends (sandwich leave)
const isSandwichLeave = (): boolean => {
const startDate = form.watch('startDate');
const endDate = form.watch('endDate');
if (!startDate || !endDate) return false;
// Ensure start date is not after end date to avoid Invalid interval error
if (startDate > endDate) return false;
// Check if the leave period includes any weekend days
const days = eachDayOfInterval({ start: startDate, end: endDate });
// Check if any day in the period is a weekend (Saturday = 6, Sunday = 0)
return days.some(day => isWeekend(day));
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-24">
{/* Duplicate Date Warning */}
{duplicateDateWarning && (
<div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-500 rounded-lg p-4 flex items-start gap-3 animate-pulse">
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-red-800 dark:text-red-300 mb-1">Duplicate Leave Request</h4>
<p className="text-sm text-red-700 dark:text-red-400">{duplicateDateWarning}</p>
</div>
</div>
)}
{/* Holiday Warning */}
{holidayWarnings.length > 0 && (
<div className="bg-indigo-50 dark:bg-indigo-900/20 border-2 border-indigo-500 rounded-lg p-4 flex items-start gap-3">
<Gift className="h-5 w-5 text-indigo-600 dark:text-indigo-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-indigo-800 dark:text-indigo-300 mb-1">Holiday Intersection</h4>
<p className="text-sm text-indigo-700 dark:text-indigo-400">
The selected date range includes the following holiday(s): <strong>{holidayWarnings.join(", ")}</strong>.
These days will not be deducted from your leave balance.
</p>
</div>
</div>
)}
{/* Sandwich Leave Policy Warning */}
{isSandwichLeave() && (
<div className="bg-amber-50 dark:bg-amber-900/20 border-2 border-amber-500 rounded-lg p-4 flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-amber-800 dark:text-amber-300 mb-1">Sandwich Leave Policy</h4>
<p className="text-sm text-amber-700 dark:text-amber-400">
Your leave request includes a weekend between Friday and Monday. According to the sandwich leave policy,
the weekend days (Saturday and Sunday) will be counted as part of your leave balance.
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Start Date Picker */}
<div className="space-y-2">
<FormLabel htmlFor="startDate">Start Date</FormLabel>
<div ref={startDateRef} className="relative">
<Button
type="button"
variant="outline"
className="w-full justify-start text-left font-normal"
onClick={() => {
setShowEndDateCalendar(false);
setShowStartDateCalendar(!showStartDateCalendar);
}}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{form.getValues("startDate")
? format(form.getValues("startDate"), "PPP")
: <span>Pick a start date</span>}
</Button>
{showStartDateCalendar && (
<div
className="absolute top-full left-0 mt-2 bg-background border rounded-lg shadow-md p-3 z-[100]"
style={{ minWidth: '320px' }}
>
<Calendar
mode="single"
selected={form.getValues("startDate")}
onSelect={(date) => {
if (date) {
form.setValue("startDate", date);
setShowStartDateCalendar(false);
// Clear duplicate warning when date changes
if (onDateChange) {
onDateChange();
}
}
}}
initialFocus
/>
</div>
)}
</div>
{form.formState.errors.startDate && (
<p className="text-sm font-medium text-destructive">
{form.formState.errors.startDate.message}
</p>
)}
</div>
{/* End Date Picker */}
<div className="space-y-2">
<FormLabel htmlFor="endDate">End Date</FormLabel>
<div ref={endDateRef} className="relative">
<Button
type="button"
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
// Add disabled styling when Half Day is selected
(isHalfDayLeaveType() || form.watch('dayType') === 'half') && "opacity-60 cursor-not-allowed"
)}
onClick={() => {
// Only allow opening the calendar if not Half Day leave type or dayType is not half
if (!isHalfDayLeaveType() && form.watch('dayType') !== 'half') {
setShowStartDateCalendar(false);
setShowEndDateCalendar(!showEndDateCalendar);
}
}}
disabled={isHalfDayLeaveType() || form.watch('dayType') === 'half'}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{form.getValues("endDate")
? format(form.getValues("endDate"), "PPP")
: <span>Pick an end date</span>}
</Button>
{showEndDateCalendar && !isHalfDayLeaveType() && form.watch('dayType') !== 'half' && (
<div
className="absolute top-full left-0 mt-2 bg-background border rounded-lg shadow-md p-3 z-[100]"
style={{ minWidth: '320px' }}
>
<Calendar
mode="single"
selected={form.getValues("endDate")}
onSelect={(date) => {
if (date) {
form.setValue("endDate", date);
setShowEndDateCalendar(false);
}
}}
disabled={(date) => {
const startDate = form.getValues("startDate");
// Normalize both dates to midnight for proper comparison
const normalizedStartDate = new Date(startDate);
normalizedStartDate.setHours(0, 0, 0, 0);
const normalizedDate = new Date(date);
normalizedDate.setHours(0, 0, 0, 0);
// Allow same day selection by only disabling dates strictly before start date
return normalizedDate < normalizedStartDate;
}}
initialFocus
/>
</div>
)}
</div>
{form.formState.errors.endDate && (
<p className="text-sm font-medium text-destructive">
{form.formState.errors.endDate.message}
</p>
)}
{(isHalfDayLeaveType() || form.watch('dayType') === 'half') && (
<p className="text-xs text-muted-foreground">
End date is automatically set to match start date for Half Day leave
</p>
)}
{/* Calculated Working Days Display */}
{form.watch('startDate') && form.watch('endDate') && (
<div className="mt-2 flex items-center gap-2 p-2 rounded-md bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-100 dark:border-indigo-900/50">
<CalendarDays className="h-4 w-4 text-indigo-600 dark:text-indigo-400" />
<span className="text-sm font-medium text-indigo-700 dark:text-indigo-300">
Total Working Days: <span className="text-lg ml-1 font-bold">{calculatedWorkingDays}</span>
</span>
</div>
)}
</div>
{/* Day Type Selection - Half Day or Full Day - Only show for Comp Off requests */}
{defaultCompOff && (
<div className="col-span-1 md:col-span-2">
<FormField
control={form.control}
name="dayType"
render={({ field }) => (
<FormItem>
<FormLabel>Day Type</FormLabel>
<div className="grid grid-cols-2 gap-4">
<div
className={cn(
"border-2 rounded-lg p-4 cursor-pointer transition-all",
field.value === 'half'
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/30"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
)}
onClick={() => {
field.onChange('half');
// Set end date to start date for half day
const startDate = form.getValues('startDate');
form.setValue('endDate', startDate);
}}
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-lg">Half Day</span>
{field.value === 'half' && (
<Check className="h-5 w-5 text-indigo-600" />
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">0.5 working days</p>
</div>
<div
className={cn(
"border-2 rounded-lg p-4 cursor-pointer transition-all",
field.value === 'full'
? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/30"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
)}
onClick={() => field.onChange('full')}
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-lg">Full Day</span>
{field.value === 'full' && (
<Check className="h-5 w-5 text-indigo-600" />
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">1 working day</p>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Leave Type Field */}
{!defaultCompOff && !hideLeaveType && (
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Leave Type</FormLabel>
<div ref={typeDropdownRef} className="relative">
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
showTypeDropdown && "ring-2 ring-ring ring-offset-2",
isLoadingMasters && "opacity-70 cursor-not-allowed"
)}
onClick={() => !isLoadingMasters && setShowTypeDropdown(!showTypeDropdown)}
>
{isLoadingMasters ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading leave types...
</span>
) : field.value ? (
<div className="flex items-center gap-2">
<Badge className={getLeaveTypeColor(field.value)}>
<span className="flex items-center gap-1">
{getLeaveTypeIcon(field.value)}
<span>{field.value}</span>
</span>
</Badge>
</div>
) : (
<span className="text-muted-foreground">Select leave type</span>
)}
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
{showTypeDropdown && !isLoadingMasters && (
<div className="absolute z-[90] mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
{(leaveMasters.length > 0 ?
leaveMasters.map(master => master.leaveType).filter(type => type.toLowerCase() !== "work from home" && type.toLowerCase() !== "unpaid" && type.toLowerCase() !== "compensatory") :
fallbackLeaveTypes.filter(type => type.toLowerCase() !== "work from home" && type.toLowerCase() !== "unpaid" && type.toLowerCase() !== "compensatory")
).map((type) => (
<div
key={type}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
field.value === type && "bg-accent text-accent-foreground"
)}
onClick={() => {
field.onChange(type);
setShowTypeDropdown(false);
// When type changes, try to find corresponding master ID
const master = leaveMasters.find(m =>
m.leaveType.toLowerCase() === type.toLowerCase()
);
if (master) {
form.setValue('leaveMasterId', master.id);
// For Half Day leave type, set end date equal to start date
if (master.leaveType.toLowerCase() === "half day") {
const startDate = form.getValues("startDate");
form.setValue("endDate", startDate);
}
}
}}
>
<div className="flex items-center gap-2">
<Badge className={getLeaveTypeColor(type)}>
<span className="flex items-center gap-1">
{getLeaveTypeIcon(type)}
<span>{type}</span>
</span>
</Badge>
</div>
{field.value === type && <Check className="h-4 w-4" />}
</div>
))}
</div>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
)}
{/* Half Day Options - Show only when "Half Day" is selected */}
{leaveMasters.length > 0 && form.watch('leaveMasterId') &&
(() => {
const selectedMaster = leaveMasters.find(m => m.id === form.watch('leaveMasterId'));
return selectedMaster && selectedMaster.leaveType.toLowerCase() === "half day";
})() && (
<div className="col-span-1 md:col-span-2">
<div className="rounded-md border p-4 bg-slate-50">
<h3 className="font-medium text-sm mb-3">Half Day Options</h3>
<div className="space-y-4">
{/* Time Slot Selection */}
<div className="space-y-2">
<label className="text-sm font-medium">
Time Slot
</label>
<div className="grid grid-cols-2 gap-3">
<div
className={cn(
"border rounded-md p-3 cursor-pointer transition-colors",
form.watch('halfDayPortion') === 'first'
? "bg-primary text-primary-foreground border-primary"
: "bg-card hover:bg-accent"
)}
onClick={() => form.setValue('halfDayPortion', 'first')}
>
<div className="flex items-center space-x-2">
<Sun className="h-4 w-4" />
<div>
<p className="text-sm font-medium">First Half</p>
<p className="text-xs opacity-90">Morning Session</p>
</div>
</div>
</div>
<div
className={cn(
"border rounded-md p-3 cursor-pointer transition-colors",
form.watch('halfDayPortion') === 'second'
? "bg-primary text-primary-foreground border-primary"
: "bg-card hover:bg-accent"
)}
onClick={() => form.setValue('halfDayPortion', 'second')}
>
<div className="flex items-center space-x-2">
<Moon className="h-4 w-4" />
<div>
<p className="text-sm font-medium">Second Half</p>
<p className="text-xs opacity-90">Afternoon Session</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Leave Policy Details */}
{!defaultCompOff && !hideLeaveType && leaveMasters.length > 0 && form.watch('leaveMasterId') && (
<div className="col-span-1 md:col-span-2">
<div className="rounded-md border p-4 bg-muted/30">
<h3 className="font-medium text-sm mb-2">Leave Policy Details</h3>
{(() => {
const selectedMaster = leaveMasters.find(m => m.id === form.watch('leaveMasterId'));
if (!selectedMaster) return null;
const isHalfDayLeave = selectedMaster.leaveType.toLowerCase() === "half day";
return (
<div className="space-y-3 text-sm">
{/* Show special information for Half Day leave */}
{isHalfDayLeave && (
<div className="flex items-center text-emerald-600 bg-emerald-50 p-2 rounded">
<Clock className="h-4 w-4 mr-2" />
<span>This is a half day leave request ({form.watch('halfDayPortion') === 'first' ? 'Morning' : 'Afternoon'} session)</span>
</div>
)}
{/* Available Days */}
{/* Description */}
{selectedMaster.description && (
<div>
<span className="font-medium">Description: </span>
<span>{selectedMaster.description}</span>
</div>
)}
{/* Documentation required */}
{selectedMaster.requiresDocumentation && (
<div className="flex items-center text-amber-600">
<FileText className="h-4 w-4 mr-1" />
<span>Documentation required</span>
</div>
)}
{/* Advance notice required */}
{selectedMaster.minDaysBeforeApply > 0 && (
<div className="flex items-center text-blue-600">
<span>Must apply {selectedMaster.minDaysBeforeApply} days in advance</span>
</div>
)}
</div>
);
})()}
</div>
</div>
)}
</div>
{/* Reason Field */}
<FormField
control={form.control}
name="reason"
render={({ field }) => (
<FormItem>
<FormLabel>{defaultCompOff ? "Comp Off Reason / Project Details*" : hideLeaveType ? "Reason for Work From Home*" : "Reason for Leave*"}</FormLabel>
<FormControl>
<Textarea
placeholder={defaultCompOff
? "Please provide details about your compensatory request (e.g. Project Name, dates worked, etc.)"
: hideLeaveType
? "Please provide details about your work from home request"
: "Please provide details about your leave request"}
className="resize-none min-h-[100px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* File Upload Field */}
{/* Leave Balance Information */}
{leaveMasters.length > 0 && form.watch('leaveMasterId') && (
<div className="border border-slate-200 rounded-md p-4 bg-slate-50">
{!defaultCompOff && <h3 className="font-medium text-sm mb-2">Leave Balance Information</h3>}
{isLoadingBalances ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading balance information...</span>
</div>
) : (
<>
{(() => {
const selectedMaster = leaveMasters.find(m => m.id === form.watch('leaveMasterId'));
const balance = selectedMaster ? getLeaveBalance(selectedMaster.id) : undefined;
if (!selectedMaster) {
return <p className="text-sm text-slate-500">Select a leave type to view balance</p>;
}
if (!balance) {
return <p className="text-sm text-slate-500">No balance information available for this leave type</p>;
}
if (defaultCompOff) {
return (
<div className="text-sm">
<p className="text-slate-500">Total Comp Off</p>
<p className="font-medium text-lg text-orange-600">
{balance.pending} days
</p>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<p className="text-slate-500">Allocated</p>
<p className="font-medium">{balance.allocated} days</p>
</div>
<div>
<p className="text-slate-500">Used</p>
<p className="font-medium">{balance.used} days</p>
</div>
<div>
<p className="text-slate-500">Pending</p>
<p className="font-medium">{balance.pending} days</p>
</div>
<div>
<p className="text-slate-500">Carry Forward</p>
<p className="font-medium">{balance.carryForward} days</p>
</div>
<div className="col-span-2">
<p className="text-slate-500">Available Balance</p>
<p className="font-medium text-lg">
<Badge variant={balance.pending > 0 ? "default" : "destructive"}>
{balance.pending} days
</Badge>
</p>
</div>
</div>
);
})()}
</>
)}
</div>
)}
<div className="absolute bottom-0 left-0 right-0 w-full bg-background border-t px-6 py-4 flex justify-end gap-2 z-10">
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting || isLoadingMasters}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : "Submit Request"}
</Button>
</div>
</form>
</Form>
);
};
export default LeaveForm;