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)
+ }
+ }}
+ >
+
- {days.map((day) => {
- const date = formatDate(day)
- const daySchedules = schedules.filter((item) => item.teamId === team.id && sliceScheduleForDay(item, day))
- return (
-
+ )
+ })}
)
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)}
+ />
+
查询
@@ -345,7 +370,9 @@ export default function DashboardAgentTeamSchedulesPage() {
{viewMode === "calendar" ? (