"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { CalendarClockIcon, CalendarDaysIcon, ChevronLeftIcon, ChevronRightIcon, ListIcon, MoreHorizontalIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, } from "lucide-react" import { toast } from "sonner" import { ListPagination } from "@/components/list-pagination" import { Button } from "@/components/ui/button" import { ButtonGroup } from "@/components/ui/button-group" import { OptionCombobox } from "@/components/option-combobox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { createAgentTeamSchedule, deleteAgentTeamSchedule, fetchAgentTeamScheduleCalendar, fetchAgentTeamSchedules, fetchAgentTeamsAll, updateAgentTeamSchedule, type AdminAgentTeam, type AdminAgentTeamSchedule, type CreateAdminAgentTeamSchedulePayload, type PageResult, type UpdateAdminAgentTeamSchedulePayload, } from "@/lib/api/admin" import { formatDateTime } from "@/lib/utils" import { ScheduleCalendar } from "./_components/calendar" import { EditDialog } from "./_components/edit" type ViewMode = "calendar" | "list" function startOfDay(date: Date) { const ret = new Date(date) ret.setHours(0, 0, 0, 0) return ret } function startOfWeek(date: Date) { const ret = startOfDay(date) const day = ret.getDay() const offset = day === 0 ? -6 : 1 - day ret.setDate(ret.getDate() + offset) return ret } function startOfMonth(date: Date) { const ret = startOfDay(date) ret.setDate(1) return ret } function startOfMonthCalendar(date: Date) { return startOfWeek(startOfMonth(date)) } function endOfMonthCalendar(date: Date) { const monthEnd = startOfMonth(date) monthEnd.setMonth(monthEnd.getMonth() + 1) const ret = startOfWeek(monthEnd) if (ret.getTime() < monthEnd.getTime()) { ret.setDate(ret.getDate() + 7) } return ret } function addDays(date: Date, days: number) { const ret = new Date(date) ret.setDate(ret.getDate() + days) return ret } function formatDateTimeValue(date: Date) { const month = String(date.getMonth() + 1).padStart(2, "0") const day = String(date.getDate()).padStart(2, "0") const hour = String(date.getHours()).padStart(2, "0") const minute = String(date.getMinutes()).padStart(2, "0") const second = String(date.getSeconds()).padStart(2, "0") return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}` } function parseLocalDateTime(value: string) { const ret = new Date(value.replace(" ", "T")) return Number.isNaN(ret.getTime()) ? null : ret } function isHistoricalSchedule(item: AdminAgentTeamSchedule) { const startAt = parseLocalDateTime(item.startAt) return !!startAt && startAt < startOfDay(new Date()) } function addMonths(date: Date, months: number) { const ret = startOfMonth(date) ret.setMonth(ret.getMonth() + months) return ret } function formatMonthTitle(monthStart: Date) { return `${monthStart.getFullYear()}年${String(monthStart.getMonth() + 1).padStart(2, "0")}月` } export default function DashboardAgentTeamSchedulesPage() { const [viewMode, setViewMode] = useState("calendar") const [teamFilterInput, setTeamFilterInput] = useState("all") const [teamFilter, setTeamFilter] = useState("all") const [monthStart, setMonthStart] = useState(() => startOfMonth(new Date())) const [page, setPage] = useState(1) const [limit, setLimit] = useState(20) const [loading, setLoading] = useState(true) const [calendarLoading, setCalendarLoading] = useState(true) const [saving, setSaving] = useState(false) const [actionLoadingId, setActionLoadingId] = useState(null) const [dialogOpen, setDialogOpen] = useState(false) const [editingItem, setEditingItem] = useState(null) const [dialogDefaults, setDialogDefaults] = useState | null>(null) const [teams, setTeams] = useState([]) const [calendarItems, setCalendarItems] = useState([]) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const visibleTeams = useMemo(() => { if (teamFilter === "all") { return teams } return teams.filter((team) => String(team.id) === teamFilter) }, [teamFilter, teams]) const loadData = useCallback(async () => { setLoading(true) try { const data = await fetchAgentTeamSchedules({ teamId: teamFilter === "all" ? undefined : teamFilter, page, limit, }) setResult(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载客服组排班失败") } finally { setLoading(false) } }, [limit, page, teamFilter]) const loadCalendarData = useCallback(async () => { setCalendarLoading(true) try { const data = await fetchAgentTeamScheduleCalendar({ startAt: formatDateTimeValue(startOfMonthCalendar(monthStart)), endAt: formatDateTimeValue(endOfMonthCalendar(monthStart)), teamId: teamFilter === "all" ? undefined : teamFilter, }) setCalendarItems(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载客服组排班日历失败") } finally { setCalendarLoading(false) } }, [monthStart, teamFilter]) const loadTeams = useCallback(async () => { try { const data = await fetchAgentTeamsAll() setTeams(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载客服组选项失败") } }, []) const refreshActiveView = useCallback(async () => { await Promise.all([ loadCalendarData(), viewMode === "list" ? loadData() : Promise.resolve(), ]) }, [loadCalendarData, loadData, viewMode]) useEffect(() => { void loadCalendarData() }, [loadCalendarData]) useEffect(() => { if (viewMode === "list") { void loadData() } }, [loadData, viewMode]) useEffect(() => { void loadTeams() }, [loadTeams]) function applyFilters() { setTeamFilter(teamFilterInput) setPage(1) } function handlePageChange(nextPage: number) { if (nextPage < 1 || nextPage === page) { return } setPage(nextPage) } function openCreateDialog(defaults?: Partial) { setEditingItem(null) setDialogDefaults(defaults ?? null) setDialogOpen(true) } function openEditDialog(item: AdminAgentTeamSchedule) { if (isHistoricalSchedule(item)) { toast.error("不能修改历史日期的排班") return } setDialogDefaults(null) setEditingItem(item) setDialogOpen(true) } function handleDialogOpenChange(open: boolean) { if (saving) { return } if (!open) { setEditingItem(null) setDialogDefaults(null) } setDialogOpen(open) } async function handleSubmit(payload: CreateAdminAgentTeamSchedulePayload) { if (saving) { return } setSaving(true) try { if (editingItem) { await updateAgentTeamSchedule({ id: editingItem.id, ...payload }) toast.success("已更新客服组排班") } else { await createAgentTeamSchedule(payload) toast.success("已创建客服组排班") } setDialogOpen(false) setEditingItem(null) setDialogDefaults(null) await refreshActiveView() } catch (error) { toast.error(error instanceof Error ? error.message : "保存客服组排班失败") } finally { setSaving(false) } } async function handleDeleteById(id: number) { setActionLoadingId(id) try { await deleteAgentTeamSchedule(id) toast.success("已删除客服组排班") setDialogOpen(false) setEditingItem(null) setDialogDefaults(null) await refreshActiveView() } catch (error) { toast.error(error instanceof Error ? error.message : "删除客服组排班失败") } finally { setActionLoadingId(null) } } async function handleDelete(item: AdminAgentTeamSchedule) { await handleDeleteById(item.id) } async function handleCalendarUpdate(payload: UpdateAdminAgentTeamSchedulePayload) { const startAt = parseLocalDateTime(payload.startAt) if (startAt && startAt < startOfDay(new Date())) { toast.error("不能修改历史日期的排班") return } setActionLoadingId(payload.id) try { await updateAgentTeamSchedule(payload) toast.success("已更新客服组排班") await loadCalendarData() } catch (error) { toast.error(error instanceof Error ? error.message : "更新客服组排班失败") await loadCalendarData() } finally { setActionLoadingId(null) } } return ( <>
{viewMode === "calendar" ? ( ) : null} {viewMode === "calendar" ? (
{formatMonthTitle(monthStart)}
) : null}
({ value: String(team.id), label: team.name })), ]} placeholder="筛选客服组" searchPlaceholder="搜索客服组" emptyText="未找到客服组" onChange={(value) => setTeamFilterInput(value)} />
{viewMode === "calendar" ? (
) : (
客服组 时间范围 来源 操作 {result.results.map((item) => (
{item.teamName || `客服组#${item.teamId}`}
组ID:{item.teamId}
{formatDateTime(item.startAt)}
{formatDateTime(item.endAt)}
{item.sourceType}
} aria-label={`更多操作 ${item.startAt}`} > void handleDelete(item)} className="text-destructive focus:text-destructive" > {actionLoadingId === item.id ? "删除中..." : "删除"}
))} {!loading && result.results.length === 0 ? ( 没有匹配的客服组排班 ) : null}
{ setLimit(nextLimit) setPage(1) }} />
)}
) }