diff --git a/web/app/dashboard/agent-team-schedules/_components/calendar.tsx b/web/app/dashboard/agent-team-schedules/_components/calendar.tsx index f41cf58..d218ee2 100644 --- a/web/app/dashboard/agent-team-schedules/_components/calendar.tsx +++ b/web/app/dashboard/agent-team-schedules/_components/calendar.tsx @@ -10,13 +10,15 @@ import type { } from "@/lib/api/admin" import { cn, formatDateTime } from "@/lib/utils" -const dayNames = ["一", "二", "三", "四", "五", "六", "日"] +const weekDayNames = ["一", "二", "三", "四", "五", "六", "日"] const dayMs = 24 * 60 * 60 * 1000 const minuteMs = 60 * 1000 const minDurationMs = 15 * minuteMs type ScheduleCalendarProps = { - weekStart: Date + monthStart: Date + calendarStart: Date + calendarEnd: Date teams: AdminAgentTeam[] schedules: AdminAgentTeamSchedule[] loading: boolean @@ -84,10 +86,6 @@ function formatDateTimeValue(date: Date) { return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}` } -function formatDayTitle(date: Date) { - return `${date.getMonth() + 1}/${date.getDate()}` -} - function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max) } @@ -101,25 +99,19 @@ function roundToQuarterHour(date: Date) { return ret } -function getPointerDateInCell(event: PointerEvent | React.PointerEvent, cell: Element) { +function getPointerDateInCell(event: PointerEvent, cell: Element) { const rect = cell.getBoundingClientRect() const day = startOfDay(parseLocalDateTime(`${cell.getAttribute("data-date")} 00:00:00`)) const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1) return roundToQuarterHour(new Date(day.getTime() + ratio * dayMs)) } -function getDropCell(event: PointerEvent | React.PointerEvent) { +function getDropCell(event: PointerEvent) { const element = document.elementFromPoint(event.clientX, event.clientY) return element?.closest("[data-schedule-cell]") } -function getCellTeamAndDate(cell: Element) { - const teamID = Number(cell.getAttribute("data-team-id")) - const date = cell.getAttribute("data-date") ?? "" - return { teamID, date } -} - -function buildMovePayload(item: AdminAgentTeamSchedule, teamId: number, date: string): UpdateAdminAgentTeamSchedulePayload { +function buildMovePayload(item: AdminAgentTeamSchedule, date: string): UpdateAdminAgentTeamSchedulePayload { const originalStart = parseLocalDateTime(item.startAt) const originalEnd = parseLocalDateTime(item.endAt) const duration = originalEnd.getTime() - originalStart.getTime() @@ -130,7 +122,7 @@ function buildMovePayload(item: AdminAgentTeamSchedule, teamId: number, date: st return { id: item.id, - teamId, + teamId: item.teamId, startAt: formatDateTimeValue(nextStart), endAt: formatDateTimeValue(nextEnd), sourceType: item.sourceType, @@ -166,23 +158,26 @@ function buildResizePayload( } } -function sliceScheduleForDay(item: AdminAgentTeamSchedule, day: Date) { +function intersectsDay(item: AdminAgentTeamSchedule, day: Date) { const dayStart = startOfDay(day) const dayEnd = addDays(dayStart, 1) const scheduleStart = parseLocalDateTime(item.startAt) const scheduleEnd = parseLocalDateTime(item.endAt) - const visibleStart = new Date(Math.max(scheduleStart.getTime(), dayStart.getTime())) - const visibleEnd = new Date(Math.min(scheduleEnd.getTime(), dayEnd.getTime())) - if (!visibleEnd.getTime() || visibleEnd <= visibleStart) { - return null + return scheduleStart < dayEnd && scheduleEnd > dayStart +} + +function buildCalendarDays(calendarStart: Date, calendarEnd: Date) { + const days: Date[] = [] + for (let current = startOfDay(calendarStart); current < calendarEnd; current = addDays(current, 1)) { + days.push(current) } - const left = ((visibleStart.getTime() - dayStart.getTime()) / dayMs) * 100 - const width = ((visibleEnd.getTime() - visibleStart.getTime()) / dayMs) * 100 - return { left, width, visibleStart, visibleEnd } + return days } export function ScheduleCalendar({ - weekStart, + monthStart, + calendarStart, + calendarEnd, teams, schedules, loading, @@ -192,15 +187,16 @@ export function ScheduleCalendar({ onMove, onResize, }: ScheduleCalendarProps) { - const days = Array.from({ length: 7 }, (_, index) => startOfDay(addDays(weekStart, index))) + const days = buildCalendarDays(calendarStart, calendarEnd) + const defaultTeamID = teams[0]?.id ?? 0 - function handleBlankCellClick(teamId: number, day: Date) { + function handleBlankCellClick(day: Date) { const startAt = new Date(day) startAt.setHours(9, 0, 0, 0) const endAt = new Date(day) endAt.setHours(18, 0, 0, 0) onCreate({ - teamId, + teamId: defaultTeamID || undefined, startAt: formatDateTimeValue(startAt), endAt: formatDateTimeValue(endAt), sourceType: "manual", @@ -241,11 +237,11 @@ export function ScheduleCalendar({ return } if (state.type === "move") { - const next = getCellTeamAndDate(cell) - if (!next.teamID || !next.date) { + const date = cell.getAttribute("data-date") + if (!date) { return } - await onMove(buildMovePayload(item, next.teamID, next.date)) + await onMove(buildMovePayload(item, date)) return } const payload = buildResizePayload(item, state.edge, getPointerDateInCell(upEvent, cell)) @@ -267,107 +263,97 @@ export function ScheduleCalendar({ } return ( -
-
-
-
客服组
- {days.map((day, index) => ( -
-
周{dayNames[index]}
-
{formatDayTitle(day)}
-
- ))} -
- -
- {teams.map((team) => ( -
-
-
-
{team.name}
-
组ID:{team.id}
-
+
+
+ {weekDayNames.map((name) => ( +
+ 周{name} +
+ ))} +
+
+ {days.map((day, dayIndex) => { + const date = formatDate(day) + const inMonth = day.getMonth() === monthStart.getMonth() + const daySchedules = schedules.filter((item) => intersectsDay(item, day)) + return ( +
{ + if ((event.target as HTMLElement).closest("[data-schedule-block]")) { + return + } + handleBlankCellClick(day) + }} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + handleBlankCellClick(day) + } + }} + > +
+
{day.getDate()}
+
- {days.map((day) => { - const date = formatDate(day) - const daySchedules = schedules.filter((item) => item.teamId === team.id && sliceScheduleForDay(item, day)) - return ( - - ) - })} +
handlePointerDown(event, item, "resize", "end")} + > + +
+
{teamName}
+
+ {formatDateTime(item.startAt).slice(11, 16)} - {formatDateTime(item.endAt).slice(11, 16)} +
+ {item.remark ?
{item.remark}
: null} +
+ ) + })} + {daySchedules.length > 5 ? ( +
还有 {daySchedules.length - 5} 条
+ ) : null} +
- ))} -
+ ) + })}
) diff --git a/web/app/dashboard/agent-team-schedules/page.tsx b/web/app/dashboard/agent-team-schedules/page.tsx index 80c94b2..ffb2c20 100644 --- a/web/app/dashboard/agent-team-schedules/page.tsx +++ b/web/app/dashboard/agent-team-schedules/page.tsx @@ -18,13 +18,13 @@ 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Table, TableBody, @@ -66,6 +66,26 @@ function startOfWeek(date: Date) { 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) @@ -81,16 +101,21 @@ function formatDateTimeValue(date: Date) { return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}` } -function formatWeekRange(weekStart: Date) { - const weekEnd = addDays(weekStart, 6) - return `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, "0")}-${String(weekStart.getDate()).padStart(2, "0")} 至 ${String(weekEnd.getMonth() + 1).padStart(2, "0")}-${String(weekEnd.getDate()).padStart(2, "0")}` +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 [weekStart, setWeekStart] = useState(() => startOfWeek(new Date())) + const [monthStart, setMonthStart] = useState(() => startOfMonth(new Date())) const [page, setPage] = useState(1) const [limit, setLimit] = useState(20) const [loading, setLoading] = useState(true) @@ -134,8 +159,8 @@ export default function DashboardAgentTeamSchedulesPage() { setCalendarLoading(true) try { const data = await fetchAgentTeamScheduleCalendar({ - startAt: formatDateTimeValue(weekStart), - endAt: formatDateTimeValue(addDays(weekStart, 7)), + startAt: formatDateTimeValue(startOfMonthCalendar(monthStart)), + endAt: formatDateTimeValue(endOfMonthCalendar(monthStart)), teamId: teamFilter === "all" ? undefined : teamFilter, }) setCalendarItems(data) @@ -144,7 +169,7 @@ export default function DashboardAgentTeamSchedulesPage() { } finally { setCalendarLoading(false) } - }, [teamFilter, weekStart]) + }, [monthStart, teamFilter]) const loadTeams = useCallback(async () => { try { @@ -294,36 +319,36 @@ export default function DashboardAgentTeamSchedulesPage() { {viewMode === "calendar" ? ( - - - ) : null} {viewMode === "calendar" ? ( -
{formatWeekRange(weekStart)}
+
{formatMonthTitle(monthStart)}
) : null}
- +
+ ({ value: String(team.id), label: team.name })), + ]} + placeholder="筛选客服组" + searchPlaceholder="搜索客服组" + emptyText="未找到客服组" + onChange={(value) => setTeamFilterInput(value)} + /> +