Re_Figma_Code/src/components/admin/HolidayManager.tsx

500 lines
21 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Calendar,
Plus,
Trash2,
Edit2,
Loader2,
AlertCircle,
CheckCircle,
} from 'lucide-react';
import { getAllHolidays, createHoliday, updateHoliday, deleteHoliday, Holiday } from '@/services/adminApi';
import { formatDateShort } from '@/utils/dateFormatter';
import { toast } from 'sonner';
export function HolidayManager() {
const [holidays, setHolidays] = useState<Holiday[]>([]);
const [loading, setLoading] = useState(true);
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const [showAddDialog, setShowAddDialog] = useState(false);
const [editingHoliday, setEditingHoliday] = useState<Holiday | null>(null);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [formData, setFormData] = useState({
holidayDate: '',
holidayName: '',
description: '',
holidayType: 'ORGANIZATIONAL' as Holiday['holidayType'],
isRecurring: false
});
useEffect(() => {
loadHolidays();
}, [selectedYear]);
const loadHolidays = async () => {
try {
setLoading(true);
setError(null);
const data = await getAllHolidays(selectedYear);
setHolidays(data);
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to load holidays');
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setFormData({
holidayDate: '',
holidayName: '',
description: '',
holidayType: 'ORGANIZATIONAL',
isRecurring: false
});
setEditingHoliday(null);
setShowAddDialog(true);
};
const handleEdit = (holiday: Holiday) => {
setFormData({
holidayDate: holiday.holidayDate,
holidayName: holiday.holidayName,
description: holiday.description || '',
holidayType: holiday.holidayType,
isRecurring: holiday.isRecurring
});
setEditingHoliday(holiday);
setShowAddDialog(true);
};
// Calculate minimum date (tomorrow for new holidays, no restriction for editing)
const getMinDate = () => {
// Only enforce minimum date when adding new holidays, not when editing existing ones
if (editingHoliday) {
return undefined; // Allow editing past holidays
}
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString().split('T')[0];
};
const handleSave = async () => {
try {
setError(null);
if (!formData.holidayDate || !formData.holidayName) {
setError('Holiday date and name are required');
return;
}
if (editingHoliday) {
// Update existing
await updateHoliday(editingHoliday.holidayId, formData);
toast.success('Holiday updated successfully');
} else {
// Create new
await createHoliday(formData);
toast.success('Holiday created successfully');
}
await loadHolidays();
setShowAddDialog(false);
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to save holiday');
}
};
const handleDelete = async (holiday: Holiday) => {
if (!confirm(`Delete "${holiday.holidayName}"?`)) {
return;
}
try {
setError(null);
await deleteHoliday(holiday.holidayId);
toast.success('Holiday deleted successfully');
await loadHolidays();
} catch (err: any) {
const errorMsg = err.response?.data?.error || 'Failed to delete holiday';
setError(errorMsg);
toast.error(errorMsg);
}
};
const getTypeColor = (type: Holiday['holidayType']) => {
switch (type) {
case 'NATIONAL':
return 'bg-gradient-to-r from-red-50 to-rose-50 text-red-800 border-red-300';
case 'REGIONAL':
return 'bg-gradient-to-r from-blue-50 to-cyan-50 text-blue-800 border-blue-300';
case 'ORGANIZATIONAL':
return 'bg-gradient-to-r from-purple-50 to-violet-50 text-purple-800 border-purple-300';
case 'OPTIONAL':
return 'bg-gradient-to-r from-slate-50 to-gray-50 text-slate-700 border-slate-300';
default:
return 'bg-gradient-to-r from-red-50 to-rose-50 text-red-800 border-red-300';
}
};
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 1 + i);
// Group holidays by month
const holidaysByMonth = holidays.reduce((acc, holiday) => {
const month = new Date(holiday.holidayDate).toLocaleString('default', { month: 'long' });
if (!acc[month]) {
acc[month] = [];
}
acc[month].push(holiday);
return acc;
}, {} as Record<string, Holiday[]>);
// Sort months chronologically
const sortedMonths = Object.keys(holidaysByMonth).sort((a, b) => {
const monthA = new Date(Date.parse(a + " 1, 2000")).getMonth();
const monthB = new Date(Date.parse(b + " 1, 2000")).getMonth();
return monthA - monthB;
});
return (
<div className="space-y-6">
{/* Success Message */}
{successMessage && (
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-300 rounded-md shadow-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="p-1.5 bg-green-500 rounded-md">
<CheckCircle className="w-4 h-4 text-white shrink-0" />
</div>
<p className="text-sm font-medium text-green-900">{successMessage}</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="p-4 bg-gradient-to-r from-red-50 to-rose-50 border border-red-300 rounded-md shadow-sm flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="p-1.5 bg-red-500 rounded-md">
<AlertCircle className="w-4 h-4 text-white shrink-0" />
</div>
<p className="text-sm font-medium text-red-900">{error}</p>
<Button
size="sm"
variant="ghost"
onClick={() => setError(null)}
className="ml-auto hover:bg-red-100"
>
Dismiss
</Button>
</div>
)}
{/* Header */}
<Card className="shadow-lg border-0 rounded-md">
<CardHeader className="border-b border-slate-100 py-4 sm:py-5">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-md shadow-md">
<Calendar className="w-5 h-5 text-white" />
</div>
<div>
<CardTitle className="text-lg sm:text-xl font-semibold text-slate-900">Holiday Calendar</CardTitle>
<CardDescription className="text-sm">
Manage organization holidays for TAT calculations
</CardDescription>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3 w-full sm:w-auto">
<Select value={selectedYear.toString()} onValueChange={(v) => setSelectedYear(parseInt(v))}>
<SelectTrigger className="w-24 sm:w-32 border-slate-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 rounded-md transition-all shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-md">
{years.map(year => (
<SelectItem key={year} value={year.toString()}>
{year}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={handleAdd}
className="gap-2 bg-re-green hover:bg-re-green/90 text-white shadow-sm flex-1 sm:flex-initial"
>
<Plus className="w-4 h-4" />
<span className="hidden xs:inline">Add Holiday</span>
<span className="xs:hidden">Add</span>
</Button>
</div>
</div>
</CardHeader>
</Card>
{/* Holidays List */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
</div>
) : holidays.length === 0 ? (
<Card className="shadow-lg border-0 rounded-md">
<CardContent className="p-12 text-center">
<div className="p-4 bg-slate-100 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-4">
<Calendar className="w-10 h-10 text-slate-400" />
</div>
<p className="text-slate-700 font-medium text-lg">No holidays found for {selectedYear}</p>
<p className="text-sm text-slate-500 mt-2 mb-6">Add holidays to exclude them from TAT calculations</p>
<Button
onClick={handleAdd}
variant="outline"
className="gap-2 border-slate-300 hover:bg-slate-50 hover:border-slate-400 shadow-sm"
>
<Plus className="w-4 h-4" />
Add First Holiday
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-4 sm:space-y-6">
{sortedMonths.map(month => (
<Card key={month} className="shadow-lg border-0 rounded-md">
<CardHeader className="pb-3 sm:pb-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base sm:text-lg font-semibold text-slate-900">{month} {selectedYear}</CardTitle>
<CardDescription className="text-xs sm:text-sm">
{holidaysByMonth[month]?.length || 0} holiday{(holidaysByMonth[month]?.length || 0) !== 1 ? 's' : ''}
</CardDescription>
</div>
<div className="p-2 bg-blue-50 rounded-md">
<Calendar className="w-4 h-4 text-blue-600" />
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 pt-4">
{holidaysByMonth[month]?.map(holiday => (
<div
key={holiday.holidayId}
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 p-3 sm:p-4 bg-slate-50 border border-slate-200 rounded-md hover:bg-slate-100 hover:border-slate-300 transition-all shadow-sm"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<p className="font-semibold text-slate-900 text-sm sm:text-base truncate">{holiday.holidayName}</p>
<Badge variant="outline" className={`${getTypeColor(holiday.holidayType)} text-[10px] sm:text-xs font-medium shadow-sm`}>
{holiday.holidayType}
</Badge>
{holiday.isRecurring && (
<Badge variant="outline" className="bg-gradient-to-r from-indigo-50 to-purple-50 text-indigo-700 border-indigo-300 text-[10px] sm:text-xs font-medium shadow-sm">
Recurring
</Badge>
)}
</div>
<p className="text-xs sm:text-sm text-slate-600 font-medium">
{formatDateShort(holiday.holidayDate)}
</p>
{holiday.description && (
<p className="text-xs text-slate-500 mt-1.5 line-clamp-2">{holiday.description}</p>
)}
</div>
<div className="flex items-center gap-2 sm:gap-2 self-end sm:self-auto">
<Button
size="sm"
variant="ghost"
onClick={() => handleEdit(holiday)}
className="gap-1.5 hover:bg-blue-50 border border-transparent hover:border-blue-200 text-xs sm:text-sm"
>
<Edit2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="hidden xs:inline">Edit</span>
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(holiday)}
className="gap-1.5 text-red-600 hover:text-red-700 hover:bg-red-50 border border-transparent hover:border-red-200 text-xs sm:text-sm"
>
<Trash2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="hidden xs:inline">Delete</span>
</Button>
</div>
</div>
))}
</CardContent>
</Card>
))}
</div>
)}
{/* Add/Edit Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="sm:max-w-[550px] max-h-[90vh] rounded-lg flex flex-col p-0">
<DialogHeader className="pb-4 border-b border-slate-100 px-6 pt-6 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-gradient-to-br from-slate-600 to-slate-700 rounded-lg shadow-md">
<Calendar className="w-5 h-5 text-white" />
</div>
<div className="flex-1">
<DialogTitle className="text-xl font-semibold text-slate-900">
{editingHoliday ? 'Edit Holiday' : 'Add New Holiday'}
</DialogTitle>
<DialogDescription className="text-sm text-slate-600 mt-1">
{editingHoliday ? 'Update holiday information' : 'Add a new holiday to the calendar for TAT calculations'}
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="space-y-5 py-6 px-6 overflow-y-auto flex-1 min-h-0">
{/* Date Field */}
<div className="space-y-2">
<Label htmlFor="date" className="text-sm font-semibold text-slate-900 flex items-center gap-1">
Date <span className="text-red-500">*</span>
</Label>
<Input
id="date"
type="date"
value={formData.holidayDate}
onChange={(e) => setFormData({ ...formData, holidayDate: e.target.value })}
min={getMinDate()}
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
/>
<p className="text-xs text-slate-500">
{editingHoliday ? 'Select the holiday date' : 'Select the holiday date (minimum: tomorrow)'}
</p>
</div>
{/* Holiday Name Field */}
<div className="space-y-2">
<Label htmlFor="name" className="text-sm font-semibold text-slate-900 flex items-center gap-1">
Holiday Name <span className="text-red-500">*</span>
</Label>
<Input
id="name"
placeholder="e.g., Diwali, Republic Day, Christmas"
value={formData.holidayName}
onChange={(e) => setFormData({ ...formData, holidayName: e.target.value })}
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
/>
<p className="text-xs text-slate-500">Enter the official name of the holiday</p>
</div>
{/* Description Field */}
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-semibold text-slate-900">
Description <span className="text-slate-400 font-normal text-xs">(Optional)</span>
</Label>
<Input
id="description"
placeholder="Add additional details about this holiday..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
/>
<p className="text-xs text-slate-500">Optional description or notes about the holiday</p>
</div>
{/* Holiday Type Field */}
<div className="space-y-2">
<Label htmlFor="type" className="text-sm font-semibold text-slate-900">
Holiday Type
</Label>
<Select
value={formData.holidayType}
onValueChange={(value: Holiday['holidayType']) =>
setFormData({ ...formData, holidayType: value })
}
>
<SelectTrigger id="type" className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-lg">
<SelectItem value="NATIONAL" className="p-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500"></div>
<span>National</span>
</div>
</SelectItem>
<SelectItem value="REGIONAL" className="p-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span>Regional</span>
</div>
</SelectItem>
<SelectItem value="ORGANIZATIONAL" className="p-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
<span>Organizational</span>
</div>
</SelectItem>
<SelectItem value="OPTIONAL" className="p-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-slate-500"></div>
<span>Optional</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-slate-500">Select the category of this holiday</p>
</div>
{/* Recurring Checkbox */}
<div className="flex items-start gap-3 p-4 bg-gradient-to-br from-slate-50 to-slate-100/50 border-2 border-slate-200 rounded-lg hover:border-slate-300 hover:bg-slate-100 transition-all cursor-pointer group" onClick={() => setFormData({ ...formData, isRecurring: !formData.isRecurring })}>
<input
type="checkbox"
id="recurring"
checked={formData.isRecurring}
onChange={(e) => setFormData({ ...formData, isRecurring: e.target.checked })}
className="mt-0.5 rounded border-slate-300 text-re-green focus:ring-2 focus:ring-re-green/20 focus:ring-offset-0 w-4 h-4 cursor-pointer"
/>
<div className="flex-1">
<Label htmlFor="recurring" className="font-semibold cursor-pointer text-sm text-slate-900 block mb-1">
Recurring Holiday
</Label>
<p className="text-xs text-slate-600">
This holiday will automatically repeat every year on the same date
</p>
</div>
</div>
</div>
<DialogFooter className="gap-3 pt-4 border-t border-slate-100 px-6 pb-6 flex-shrink-0">
<Button
variant="outline"
onClick={() => setShowAddDialog(false)}
className="h-11 border-slate-300 hover:bg-slate-50 hover:border-slate-400 shadow-sm"
>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={!formData.holidayDate || !formData.holidayName}
className="h-11 bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<Calendar className="w-4 h-4 mr-2" />
{editingHoliday ? 'Update Holiday' : 'Add Holiday'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}