feat(calendar): refactor to support month view and improve scheduling logic

This commit is contained in:
mlogclub
2026-04-29 10:30:27 +08:00
parent 8646b002c1
commit 944221a76c
2 changed files with 169 additions and 156 deletions
+54 -27
View File
@@ -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<ViewMode>("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() {
</ButtonGroup>
{viewMode === "calendar" ? (
<ButtonGroup>
<Button variant="outline" size="icon-sm" onClick={() => setWeekStart(addDays(weekStart, -7))} aria-label="上一">
<Button variant="outline" size="icon-sm" onClick={() => setMonthStart(addMonths(monthStart, -1))} aria-label="上一">
<ChevronLeftIcon />
</Button>
<Button variant="outline" size="sm" onClick={() => setWeekStart(startOfWeek(new Date()))}>
<Button variant="outline" size="sm" onClick={() => setMonthStart(startOfMonth(new Date()))}>
</Button>
<Button variant="outline" size="icon-sm" onClick={() => setWeekStart(addDays(weekStart, 7))} aria-label="下一">
<Button variant="outline" size="icon-sm" onClick={() => setMonthStart(addMonths(monthStart, 1))} aria-label="下一">
<ChevronRightIcon />
</Button>
</ButtonGroup>
) : null}
{viewMode === "calendar" ? (
<div className="text-sm text-muted-foreground">{formatWeekRange(weekStart)}</div>
<div className="text-sm text-muted-foreground">{formatMonthTitle(monthStart)}</div>
) : null}
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center xl:justify-end">
<Select value={teamFilterInput} onValueChange={(value) => setTeamFilterInput(value ?? "all")}>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="筛选客服组" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{teams.map((team) => (
<SelectItem key={team.id} value={String(team.id)}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="w-full sm:w-48">
<OptionCombobox
value={teamFilterInput}
options={[
{ value: "all", label: "全部客服组" },
...teams.map((team) => ({ value: String(team.id), label: team.name })),
]}
placeholder="筛选客服组"
searchPlaceholder="搜索客服组"
emptyText="未找到客服组"
onChange={(value) => setTeamFilterInput(value)}
/>
</div>
<Button variant="outline" onClick={applyFilters} disabled={loading || calendarLoading}>
<SearchIcon />
@@ -345,7 +370,9 @@ export default function DashboardAgentTeamSchedulesPage() {
{viewMode === "calendar" ? (
<ScheduleCalendar
weekStart={weekStart}
monthStart={monthStart}
calendarStart={startOfMonthCalendar(monthStart)}
calendarEnd={endOfMonthCalendar(monthStart)}
teams={visibleTeams}
schedules={calendarItems}
loading={calendarLoading}