import { useState, useMemo } from 'react' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Link } from 'react-router-dom' import { CalendarDays, Plus, Trash2, ChevronLeft, ChevronRight, X, MapPin, User } from 'lucide-react' import { dashboardApi, calendarApi } from '../lib/api-services' import Card from '../components/ui/Card' import Button from '../components/ui/Button' import { Input, Label } from '../components/ui/Input' const EVENT_TYPE_COLORS: Record = { CONTRACT_EXPIRY: 'bg-red-100 text-red-700 border-red-200', PROBATION_END: 'bg-amber-100 text-amber-700 border-amber-200', TERMINATION: 'bg-red-100 text-red-700 border-red-200', ANNIVERSARY: 'bg-green-100 text-green-700 border-green-200', RISK_DEADLINE: 'bg-orange-100 text-orange-700 border-orange-200', RETIREMENT: 'bg-purple-100 text-purple-700 border-purple-200', CUSTOM: 'bg-blue-100 text-blue-700 border-blue-200', MEETING: 'bg-cyan-100 text-cyan-700 border-cyan-200', TEAM_BUILDING: 'bg-pink-100 text-pink-700 border-pink-200', TRAINING: 'bg-indigo-100 text-indigo-700 border-indigo-200', INTERVIEW: 'bg-teal-100 text-teal-700 border-teal-200', } const EVENT_TYPE_LABELS: Record = { CONTRACT_EXPIRY: '合同到期', PROBATION_END: '试用期到期', TERMINATION: '离职/解聘', ANNIVERSARY: '入职周年', RISK_DEADLINE: '风险截止', RETIREMENT: '退休', CUSTOM: '自定义', MEETING: '会议', TEAM_BUILDING: '团建', TRAINING: '培训', INTERVIEW: '面试', } const PRIORITY_DOT: Record = { high: 'bg-red-500', medium: 'bg-amber-500', low: 'bg-gray-400', } const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'] /** 本地日期格式化为 YYYY-MM-DD,避免 toISOString() 的 UTC 时区偏移 */ const fmtDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` /** 本地月份格式化为 YYYY-MM */ const fmtMonth = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` export default function Calendar() { const queryClient = useQueryClient() const [calendarMonth, setCalendarMonth] = useState(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`) const [showEventForm, setShowEventForm] = useState(false) const [selectedDate, setSelectedDate] = useState(null) const [typeFilter, setTypeFilter] = useState(null) const [eventForm, setEventForm] = useState({ title: '', date: `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}`, type: 'CUSTOM', priority: 'medium', location: '', description: '', }) const { data: calendarData } = useQuery({ queryKey: ['calendar', calendarMonth], queryFn: () => dashboardApi.calendar(calendarMonth), }) const { data: customEvents } = useQuery({ queryKey: ['custom-events', calendarMonth], queryFn: () => calendarApi.events(calendarMonth), }) const createEventMutation = useMutation({ mutationFn: (data: any) => calendarApi.createEvent(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['custom-events'] }) queryClient.invalidateQueries({ queryKey: ['calendar'] }) setShowEventForm(false) setEventForm({ title: '', date: selectedDate || fmtDate(new Date()), type: 'CUSTOM', priority: 'medium', location: '', description: '' }) toast.success('事件已创建') }, onError: () => toast.error('创建事件失败'), }) const deleteEventMutation = useMutation({ mutationFn: (id: string) => calendarApi.removeEvent(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['custom-events'] }) queryClient.invalidateQueries({ queryKey: ['calendar'] }) toast.success('事件已删除') }, }) const calendarGrid = useMemo(() => { const [year, mon] = calendarMonth.split('-').map(Number) const firstDay = new Date(year, mon - 1, 1) const lastDay = new Date(year, mon, 0) const startWeekday = firstDay.getDay() const daysInMonth = lastDay.getDate() const todayStr = fmtDate(new Date()) const cells: Array<{ day: number | null; date: string | null; events: any[]; isToday: boolean }> = [] for (let i = 0; i < startWeekday; i++) cells.push({ day: null, date: null, events: [], isToday: false }) for (let d = 1; d <= daysInMonth; d++) { const dateStr = `${calendarMonth}-${String(d).padStart(2, '0')}` let dayEvents = (calendarData?.events || []).filter((ev: any) => ev.date === dateStr) if (typeFilter) dayEvents = dayEvents.filter((ev: any) => ev.type === typeFilter) cells.push({ day: d, date: dateStr, events: dayEvents, isToday: dateStr === todayStr }) } return cells }, [calendarMonth, calendarData, typeFilter]) const allEvents = useMemo(() => { let events = calendarData?.events || [] if (typeFilter) events = events.filter((ev: any) => ev.type === typeFilter) return events }, [calendarData, typeFilter]) const customEventMap = useMemo(() => { const map: Record = {} for (const ev of (customEvents || [])) { map[ev.id] = ev } return map }, [customEvents]) const prevMonth = () => { const [y, m] = calendarMonth.split('-').map(Number) const d = new Date(y, m - 2, 1) setCalendarMonth(fmtMonth(d)) } const nextMonth = () => { const [y, m] = calendarMonth.split('-').map(Number) const d = new Date(y, m, 1) setCalendarMonth(fmtMonth(d)) } const goToday = () => setCalendarMonth(fmtMonth(new Date())) const handleDayClick = (date: string | null) => { if (!date) return setSelectedDate(date) setEventForm({ ...eventForm, date }) setShowEventForm(true) } const handleSubmitEvent = () => { if (!eventForm.title.trim()) { toast.error('请输入事件标题') return } createEventMutation.mutate(eventForm) } const isCustomEvent = (ev: any) => { return !!customEventMap[ev.id] || ['CUSTOM', 'MEETING', 'TEAM_BUILDING', 'TRAINING', 'INTERVIEW'].includes(ev.type) } return (
{/* 顶部工具栏 */}

工作日历

setCalendarMonth(e.target.value)} className="text-sm font-medium rounded-md border border-input bg-background px-2 py-1 min-w-[120px] text-center" />
{/* 类型筛选 */}
{Object.entries(EVENT_TYPE_LABELS).map(([type, label]) => ( ))}
{/* 月历网格 */}
{WEEKDAYS.map(wd => (
{wd}
))}
{calendarGrid.map((cell, i) => (
handleDayClick(cell.date)} className={`min-h-[72px] p-1.5 rounded-md cursor-pointer transition-colors border ${ cell.day === null ? 'bg-gray-50/50 border-transparent cursor-default' : cell.isToday ? 'bg-primary/5 border-primary/30 hover:bg-primary/10' : 'border-gray-100 hover:bg-gray-50' }`} > {cell.day && ( <>
{cell.day}
{cell.events.slice(0, 3).map((ev: any, idx: number) => (
{ev.title}
))} {cell.events.length > 3 && (
+{cell.events.length - 3} 更多
)}
)}
))}
{/* 事件列表 */}

{calendarMonth} 事件列表 ({allEvents.length})

{allEvents.length > 0 ? (
{allEvents.map((ev: any, i: number) => (
{ev.date.slice(5)} {EVENT_TYPE_LABELS[ev.type] || ev.type}
{ev.title} {ev.employeeName && — {ev.employeeName}}
{ev.actionUrl && ev.actionUrl !== '/dashboard' && ( 查看详情 → )}
{isCustomEvent(ev) && customEventMap[ev.id] && ( )}
))}
) : (
本月暂无事件
)}
{/* 新建事件弹窗 */} {showEventForm && (
setShowEventForm(false)}> e.stopPropagation()}>

新建日历事件

setEventForm({ ...eventForm, title: e.target.value })} placeholder="如:月度全员会议" />
setEventForm({ ...eventForm, date: e.target.value })} />
setEventForm({ ...eventForm, location: e.target.value })} placeholder="可选" />
setEventForm({ ...eventForm, description: e.target.value })} placeholder="可选" />
)}
) }