feat(schedule): add validation to prevent cross-day and historical schedule entries
This commit is contained in:
@@ -165,6 +165,12 @@ func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt,
|
||||
if !endAtValue.After(startAtValue) {
|
||||
return nil, errorsx.InvalidParam("结束时间必须晚于开始时间")
|
||||
}
|
||||
if !sameLocalDay(startAtValue, endAtValue) {
|
||||
return nil, errorsx.InvalidParam("单条排班记录不能跨天")
|
||||
}
|
||||
if startAtValue.Before(startOfLocalDay(time.Now())) {
|
||||
return nil, errorsx.InvalidParam("不能添加或修改历史日期的排班")
|
||||
}
|
||||
var count int64
|
||||
sqls.DB().Model(&models.AgentTeamSchedule{}).
|
||||
Where("team_id = ? AND id <> ? AND start_at < ? AND end_at > ?", teamID, id, endAtValue, startAtValue).
|
||||
@@ -208,6 +214,17 @@ func parseDateTimeValue(value string) (time.Time, error) {
|
||||
return time.Time{}, errorsx.InvalidParam("时间格式错误")
|
||||
}
|
||||
|
||||
func startOfLocalDay(value time.Time) time.Time {
|
||||
year, month, day := value.In(time.Local).Date()
|
||||
return time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
||||
}
|
||||
|
||||
func sameLocalDay(a, b time.Time) bool {
|
||||
aYear, aMonth, aDay := a.In(time.Local).Date()
|
||||
bYear, bMonth, bDay := b.In(time.Local).Date()
|
||||
return aYear == bYear && aMonth == bMonth && aDay == bDay
|
||||
}
|
||||
|
||||
func (s *agentTeamScheduleService) dispatchPendingConversationsIfActive(item *models.AgentTeamSchedule) {
|
||||
if item == nil {
|
||||
return
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/dto"
|
||||
"cs-agent/internal/pkg/dto/request"
|
||||
"cs-agent/internal/pkg/enums"
|
||||
"cs-agent/internal/services"
|
||||
@@ -76,6 +77,109 @@ func TestAgentTeamScheduleServiceFindCalendarSchedulesValidatesTimeRange(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceCreateRejectsCrossDaySchedule(t *testing.T) {
|
||||
setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestTeams(t, sqls.DB())
|
||||
|
||||
tomorrow := time.Now().AddDate(0, 0, 1)
|
||||
_, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
|
||||
TeamID: 1,
|
||||
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
|
||||
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
|
||||
SourceType: "manual",
|
||||
}, testOperator())
|
||||
if err == nil {
|
||||
t.Fatalf("expected cross-day schedule to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "不能跨天") {
|
||||
t.Fatalf("expected cross-day error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceCreateRejectsHistoricalScheduleByDay(t *testing.T) {
|
||||
setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestTeams(t, sqls.DB())
|
||||
|
||||
yesterday := time.Now().AddDate(0, 0, -1)
|
||||
_, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
|
||||
TeamID: 1,
|
||||
StartAt: formatTestDateTime(yesterday, "09:00:00"),
|
||||
EndAt: formatTestDateTime(yesterday, "18:00:00"),
|
||||
SourceType: "manual",
|
||||
}, testOperator())
|
||||
if err == nil {
|
||||
t.Fatalf("expected historical schedule to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "历史日期") {
|
||||
t.Fatalf("expected historical date error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceCreateAllowsTodayEarlierThanCurrentTime(t *testing.T) {
|
||||
setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestTeams(t, sqls.DB())
|
||||
|
||||
today := time.Now()
|
||||
item, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(request.CreateAgentTeamScheduleRequest{
|
||||
TeamID: 1,
|
||||
StartAt: formatTestDateTime(today, "00:00:00"),
|
||||
EndAt: formatTestDateTime(today, "01:00:00"),
|
||||
SourceType: "manual",
|
||||
}, testOperator())
|
||||
if err != nil {
|
||||
t.Fatalf("expected today's schedule to pass, got %v", err)
|
||||
}
|
||||
if item == nil || item.ID == 0 {
|
||||
t.Fatalf("expected created schedule, got %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceUpdateRejectsCrossDaySchedule(t *testing.T) {
|
||||
db := setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestTeams(t, db)
|
||||
existingID := createFutureAgentTeamSchedule(t, db)
|
||||
tomorrow := time.Now().AddDate(0, 0, 1)
|
||||
|
||||
err := services.AgentTeamScheduleService.UpdateAgentTeamSchedule(request.UpdateAgentTeamScheduleRequest{
|
||||
ID: existingID,
|
||||
CreateAgentTeamScheduleRequest: request.CreateAgentTeamScheduleRequest{
|
||||
TeamID: 1,
|
||||
StartAt: formatTestDateTime(tomorrow, "22:00:00"),
|
||||
EndAt: formatTestDateTime(tomorrow.AddDate(0, 0, 1), "08:00:00"),
|
||||
SourceType: "manual",
|
||||
},
|
||||
}, testOperator())
|
||||
if err == nil {
|
||||
t.Fatalf("expected cross-day update to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "不能跨天") {
|
||||
t.Fatalf("expected cross-day error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTeamScheduleServiceUpdateRejectsHistoricalScheduleByDay(t *testing.T) {
|
||||
db := setupAgentTeamScheduleTestDB(t)
|
||||
createAgentTeamScheduleTestTeams(t, db)
|
||||
existingID := createFutureAgentTeamSchedule(t, db)
|
||||
yesterday := time.Now().AddDate(0, 0, -1)
|
||||
|
||||
err := services.AgentTeamScheduleService.UpdateAgentTeamSchedule(request.UpdateAgentTeamScheduleRequest{
|
||||
ID: existingID,
|
||||
CreateAgentTeamScheduleRequest: request.CreateAgentTeamScheduleRequest{
|
||||
TeamID: 1,
|
||||
StartAt: formatTestDateTime(yesterday, "09:00:00"),
|
||||
EndAt: formatTestDateTime(yesterday, "18:00:00"),
|
||||
SourceType: "manual",
|
||||
},
|
||||
}, testOperator())
|
||||
if err == nil {
|
||||
t.Fatalf("expected historical update to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "历史日期") {
|
||||
t.Fatalf("expected historical date error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAgentTeamScheduleTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
@@ -105,13 +209,7 @@ func setupAgentTeamScheduleTestDB(t *testing.T) *gorm.DB {
|
||||
func createAgentTeamScheduleTestData(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
teams := []models.AgentTeam{
|
||||
{ID: 1, Name: "售前组", Status: enums.StatusOk},
|
||||
{ID: 2, Name: "售后组", Status: enums.StatusOk},
|
||||
}
|
||||
if err := db.Create(&teams).Error; err != nil {
|
||||
t.Fatalf("create teams error = %v", err)
|
||||
}
|
||||
createAgentTeamScheduleTestTeams(t, db)
|
||||
|
||||
parse := func(value string) time.Time {
|
||||
t.Helper()
|
||||
@@ -132,3 +230,47 @@ func createAgentTeamScheduleTestData(t *testing.T, db *gorm.DB) {
|
||||
t.Fatalf("create schedules error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createAgentTeamScheduleTestTeams(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
teams := []models.AgentTeam{
|
||||
{ID: 1, Name: "售前组", Status: enums.StatusOk},
|
||||
{ID: 2, Name: "售后组", Status: enums.StatusOk},
|
||||
}
|
||||
if err := db.Create(&teams).Error; err != nil {
|
||||
t.Fatalf("create teams error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func formatTestDateTime(date time.Time, clock string) string {
|
||||
return date.Format(time.DateOnly) + " " + clock
|
||||
}
|
||||
|
||||
func createFutureAgentTeamSchedule(t *testing.T, db *gorm.DB) int64 {
|
||||
t.Helper()
|
||||
tomorrow := time.Now().AddDate(0, 0, 1)
|
||||
item := models.AgentTeamSchedule{
|
||||
TeamID: 1,
|
||||
StartAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "09:00:00")),
|
||||
EndAt: parseTestDateTime(t, formatTestDateTime(tomorrow, "18:00:00")),
|
||||
SourceType: "manual",
|
||||
Status: enums.StatusOk,
|
||||
}
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
t.Fatalf("create future schedule error = %v", err)
|
||||
}
|
||||
return item.ID
|
||||
}
|
||||
|
||||
func parseTestDateTime(t *testing.T, value string) time.Time {
|
||||
t.Helper()
|
||||
ret, err := time.ParseInLocation(time.DateTime, value, time.Local)
|
||||
if err != nil {
|
||||
t.Fatalf("parse time %q error = %v", value, err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func testOperator() *dto.AuthPrincipal {
|
||||
return &dto.AuthPrincipal{UserID: 1, Username: "tester", Status: enums.StatusOk}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,14 @@ function getDropCell(event: PointerEvent) {
|
||||
return element?.closest("[data-schedule-cell]")
|
||||
}
|
||||
|
||||
function isHistoricalDay(day: Date) {
|
||||
return startOfDay(day).getTime() < startOfDay(new Date()).getTime()
|
||||
}
|
||||
|
||||
function isSameLocalDay(a: Date, b: Date) {
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
||||
}
|
||||
|
||||
function buildMovePayload(item: AdminAgentTeamSchedule, date: string): UpdateAdminAgentTeamSchedulePayload {
|
||||
const originalStart = parseLocalDateTime(item.startAt)
|
||||
const originalEnd = parseLocalDateTime(item.endAt)
|
||||
@@ -152,6 +160,9 @@ function buildResizePayload(
|
||||
): UpdateAdminAgentTeamSchedulePayload | null {
|
||||
const startAt = parseLocalDateTime(item.startAt)
|
||||
const endAt = parseLocalDateTime(item.endAt)
|
||||
if (!isSameLocalDay(startAt, nextTime)) {
|
||||
return null
|
||||
}
|
||||
if (edge === "start") {
|
||||
if (endAt.getTime() - nextTime.getTime() < minDurationMs) {
|
||||
return null
|
||||
@@ -254,6 +265,16 @@ export function ScheduleCalendar({
|
||||
if (!date) {
|
||||
return null
|
||||
}
|
||||
if (isHistoricalDay(parseLocalDateTime(`${date} 00:00:00`))) {
|
||||
return {
|
||||
itemId: state.item.id,
|
||||
date,
|
||||
label: "不能修改历史日期",
|
||||
invalid: true,
|
||||
x: pointerEvent.clientX,
|
||||
y: pointerEvent.clientY,
|
||||
}
|
||||
}
|
||||
|
||||
const point = { x: pointerEvent.clientX, y: pointerEvent.clientY }
|
||||
if (state.type === "move") {
|
||||
@@ -261,7 +282,7 @@ export function ScheduleCalendar({
|
||||
}
|
||||
|
||||
const payload = buildResizePayload(state.item, state.edge, getPointerDateInCell(pointerEvent, cell))
|
||||
return buildPreviewFromPayload(state.item.id, date, payload, point, "至少保留 15 分钟")
|
||||
return buildPreviewFromPayload(state.item.id, date, payload, point, "不能跨天或少于 15 分钟")
|
||||
}
|
||||
|
||||
function cleanupPointerInteraction(
|
||||
@@ -324,6 +345,9 @@ export function ScheduleCalendar({
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
if (isHistoricalDay(parseLocalDateTime(`${date} 00:00:00`))) {
|
||||
return
|
||||
}
|
||||
await onMove(buildMovePayload(item, date))
|
||||
return
|
||||
}
|
||||
@@ -363,6 +387,7 @@ export function ScheduleCalendar({
|
||||
{days.map((day, dayIndex) => {
|
||||
const date = formatDate(day)
|
||||
const inMonth = day.getMonth() === monthStart.getMonth()
|
||||
const historical = isHistoricalDay(day)
|
||||
const daySchedules = schedules
|
||||
.filter((item) => intersectsDay(item, day))
|
||||
.sort((a, b) => parseLocalDateTime(a.startAt).getTime() - parseLocalDateTime(b.startAt).getTime())
|
||||
@@ -378,6 +403,7 @@ export function ScheduleCalendar({
|
||||
"min-h-36 border-l border-t bg-background p-2 text-left outline-none transition-colors first:border-l-0 hover:bg-muted/20 focus-visible:ring-2 focus-visible:ring-ring",
|
||||
dayIndex % 7 === 0 && "border-l-0",
|
||||
!inMonth && "bg-muted/20 text-muted-foreground",
|
||||
historical && "cursor-not-allowed bg-muted/30 hover:bg-muted/30",
|
||||
interactionPreview?.date === date &&
|
||||
(interactionPreview.invalid ? "bg-destructive/5 ring-2 ring-destructive/30" : "bg-primary/5 ring-2 ring-primary/35")
|
||||
)}
|
||||
@@ -385,9 +411,15 @@ export function ScheduleCalendar({
|
||||
if ((event.target as HTMLElement).closest("[data-schedule-block]")) {
|
||||
return
|
||||
}
|
||||
if (historical) {
|
||||
return
|
||||
}
|
||||
handleBlankCellClick(day)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (historical) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault()
|
||||
handleBlankCellClick(day)
|
||||
@@ -401,7 +433,7 @@ export function ScheduleCalendar({
|
||||
<div className="mt-0.5 text-[10px] leading-none text-muted-foreground">{dayTimeLayout.rangeLabel}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<CalendarPlusIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
||||
{historical ? null : <CalendarPlusIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{daySchedules.slice(0, 5).map((item) => {
|
||||
@@ -409,6 +441,7 @@ export function ScheduleCalendar({
|
||||
const busy = savingId === item.id
|
||||
const active = interactionPreview?.itemId === item.id
|
||||
const timeLayout = dayTimeLayout.items.get(item.id)
|
||||
const readonly = historical || isHistoricalDay(parseLocalDateTime(item.startAt))
|
||||
return (
|
||||
<div key={`${item.id}-${date}`} className="relative h-10 rounded-sm bg-muted/25">
|
||||
<div
|
||||
@@ -420,6 +453,7 @@ export function ScheduleCalendar({
|
||||
className={cn(
|
||||
"absolute inset-y-0 cursor-grab overflow-hidden rounded-md border border-primary/20 bg-primary/10 px-2 py-1.5 pl-4 pr-4 text-primary shadow-sm outline-none transition active:cursor-grabbing",
|
||||
active && "scale-[0.98] border-primary/50 bg-primary/15 opacity-80 ring-2 ring-primary/30",
|
||||
readonly && "cursor-not-allowed opacity-60",
|
||||
busy && "pointer-events-none opacity-60"
|
||||
)}
|
||||
style={{
|
||||
@@ -427,8 +461,18 @@ export function ScheduleCalendar({
|
||||
width: `${timeLayout?.widthPercent ?? 100}%`,
|
||||
minWidth: 34,
|
||||
}}
|
||||
onPointerDown={(event) => handlePointerDown(event, item, "move")}
|
||||
onPointerDown={(event) => {
|
||||
if (readonly) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
handlePointerDown(event, item, "move")
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault()
|
||||
onEdit(item)
|
||||
@@ -437,13 +481,27 @@ export function ScheduleCalendar({
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 flex h-full w-3 cursor-ew-resize items-center justify-center bg-primary/15"
|
||||
onPointerDown={(event) => handlePointerDown(event, item, "resize", "start")}
|
||||
onPointerDown={(event) => {
|
||||
if (readonly) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
handlePointerDown(event, item, "resize", "start")
|
||||
}}
|
||||
>
|
||||
<GripVerticalIcon className="size-3" />
|
||||
</div>
|
||||
<div
|
||||
className="absolute right-0 top-0 flex h-full w-3 cursor-ew-resize items-center justify-center bg-primary/15"
|
||||
onPointerDown={(event) => handlePointerDown(event, item, "resize", "end")}
|
||||
onPointerDown={(event) => {
|
||||
if (readonly) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
handlePointerDown(event, item, "resize", "end")
|
||||
}}
|
||||
>
|
||||
<GripVerticalIcon className="size-3" />
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,34 @@ const editFormSchema = z.object({
|
||||
endAt: z.string().trim().min(1, "结束时间不能为空"),
|
||||
sourceType: z.enum(["manual", "batch_import", "template_generate"], { message: "请选择排班来源" }),
|
||||
remark: z.string().trim(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const startAt = parseDateTimeLocal(value.startAt)
|
||||
const endAt = parseDateTimeLocal(value.endAt)
|
||||
if (!startAt || !endAt) {
|
||||
return
|
||||
}
|
||||
if (!endAt || endAt <= startAt) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["endAt"],
|
||||
message: "结束时间必须晚于开始时间",
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!isSameLocalDay(startAt, endAt)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["endAt"],
|
||||
message: "单条排班记录不能跨天",
|
||||
})
|
||||
}
|
||||
if (startAt < startOfLocalDay(new Date())) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["startAt"],
|
||||
message: "不能添加或修改历史日期的排班",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type EditForm = z.infer<typeof editFormSchema>
|
||||
@@ -77,6 +105,28 @@ function toDateTimeLocal(value?: string) {
|
||||
return value.replace(" ", "T").slice(0, 16)
|
||||
}
|
||||
|
||||
function parseDateTimeLocal(value: string) {
|
||||
const ret = new Date(value)
|
||||
return Number.isNaN(ret.getTime()) ? null : ret
|
||||
}
|
||||
|
||||
function startOfLocalDay(value: Date) {
|
||||
const ret = new Date(value)
|
||||
ret.setHours(0, 0, 0, 0)
|
||||
return ret
|
||||
}
|
||||
|
||||
function isSameLocalDay(a: Date, b: Date) {
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
||||
}
|
||||
|
||||
function todayDateTimeLocalMin() {
|
||||
const today = startOfLocalDay(new Date())
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(today.getDate()).padStart(2, "0")
|
||||
return `${today.getFullYear()}-${month}-${day}T00:00`
|
||||
}
|
||||
|
||||
function buildForm(item: AdminAgentTeamSchedule | null, defaultValues?: Partial<CreateAdminAgentTeamSchedulePayload> | null): EditForm {
|
||||
if (!item) {
|
||||
return {
|
||||
@@ -167,6 +217,7 @@ function ScheduleEditDialogBody({
|
||||
register,
|
||||
formState: { errors },
|
||||
} = form
|
||||
const minDateTime = todayDateTimeLocalMin()
|
||||
|
||||
useEffect(() => {
|
||||
async function loadDetail() {
|
||||
@@ -235,14 +286,14 @@ function ScheduleEditDialogBody({
|
||||
<Field data-invalid={!!errors.startAt}>
|
||||
<FieldLabel htmlFor="agent-team-schedule-start-at">开始时间</FieldLabel>
|
||||
<FieldContent>
|
||||
<Input id="agent-team-schedule-start-at" type="datetime-local" {...register("startAt")} />
|
||||
<Input id="agent-team-schedule-start-at" type="datetime-local" min={minDateTime} {...register("startAt")} />
|
||||
<FieldError errors={[errors.startAt]} />
|
||||
</FieldContent>
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.endAt}>
|
||||
<FieldLabel htmlFor="agent-team-schedule-end-at">结束时间</FieldLabel>
|
||||
<FieldContent>
|
||||
<Input id="agent-team-schedule-end-at" type="datetime-local" {...register("endAt")} />
|
||||
<Input id="agent-team-schedule-end-at" type="datetime-local" min={minDateTime} {...register("endAt")} />
|
||||
<FieldError errors={[errors.endAt]} />
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
@@ -101,6 +101,16 @@ function formatDateTimeValue(date: Date) {
|
||||
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)
|
||||
@@ -220,6 +230,10 @@ export default function DashboardAgentTeamSchedulesPage() {
|
||||
}
|
||||
|
||||
function openEditDialog(item: AdminAgentTeamSchedule) {
|
||||
if (isHistoricalSchedule(item)) {
|
||||
toast.error("不能修改历史日期的排班")
|
||||
return
|
||||
}
|
||||
setDialogDefaults(null)
|
||||
setEditingItem(item)
|
||||
setDialogOpen(true)
|
||||
@@ -281,6 +295,11 @@ export default function DashboardAgentTeamSchedulesPage() {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -398,7 +417,7 @@ export default function DashboardAgentTeamSchedulesPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.results.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableRow key={item.id} className={isHistoricalSchedule(item) ? "opacity-60" : undefined}>
|
||||
<TableCell>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex size-10 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
@@ -419,7 +438,7 @@ export default function DashboardAgentTeamSchedulesPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Button variant="outline" size="sm" onClick={() => openEditDialog(item)}>
|
||||
<Button variant="outline" size="sm" onClick={() => openEditDialog(item)} disabled={isHistoricalSchedule(item)}>
|
||||
编辑
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user