由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - Roles 多组勾选、Schedules 月视图新建、useDirtyGuard 稳定引用 - JinshujuMatchModal success:false、Attendance store 订阅、EditableCell Enter、DynamicChart 空数据、渲染期 ref - AttachmentsTab 附件预览不再 window.open(防存储型 XSS)+ 请求乱序防护 - 考勤 late 计入出勤率与主状态 Reviewed-by: OCR (open-codereview.ai)
608 lines
21 KiB
TypeScript
608 lines
21 KiB
TypeScript
// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化
|
||
import React, { useMemo, useState } from 'react';
|
||
import { Button, Card, Form, Segmented, Select, Space } from 'antd';
|
||
import { CalendarOutlined, CloudSyncOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||
import dayjs, { Dayjs } from 'dayjs';
|
||
import api from '../../api';
|
||
import PermissionButton from '../../components/PermissionButton';
|
||
import { QueryErrorState } from '../../components/QueryState';
|
||
import { NextStepHint } from '../../components/NextStepHint';
|
||
import { usePermission } from '../../hooks/usePermission';
|
||
import { useVisibleRefetch } from '../../hooks/usePageVisible';
|
||
import { message } from '../../ui/app-message';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { useApiMutation } from '../../hooks/useApiMutation';
|
||
import { validateResponse } from '../../utils/validate';
|
||
import { scheduleLookupsSchema, weeklyScheduleSchema } from '../../api/schemas';
|
||
import {
|
||
buildSchedulePayload,
|
||
scheduleToFormValues,
|
||
type ScheduleFormValues,
|
||
} from './schedule-form';
|
||
import { filterSchedulesForClass, isMaskedSchedule } from './schedule-visibility';
|
||
import { classifySyncResult } from './sync-result';
|
||
import { getErrorMessage } from '../../utils/error';
|
||
import { ScheduleGrid } from './ScheduleGrids';
|
||
import type {
|
||
ClassItem,
|
||
ClassScheduleItem,
|
||
ClassTeacherOption,
|
||
ClassroomItem,
|
||
} from './ScheduleGrids';
|
||
import { ScheduleModal, SyncModal } from './ScheduleModals';
|
||
|
||
interface ScheduleSyncResult {
|
||
scheduleCount: number;
|
||
shiftCount: number;
|
||
groupCount: number;
|
||
syncedItems: number;
|
||
skippedNoMapping: number;
|
||
failedBatchCount: number;
|
||
failedItems: number;
|
||
errors: string[];
|
||
groups: Array<{ className: string; groupId: number; itemCount: number }>;
|
||
}
|
||
|
||
const SchedulesPage: React.FC = () => {
|
||
const { hasPermission } = usePermission();
|
||
const [viewMode, setViewMode] = useState<'week' | 'month'>('week');
|
||
const [viewDate, setViewDate] = useState<Dayjs>(() => dayjs().weekday(1).startOf('day'));
|
||
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
|
||
const [classTeachers, setClassTeachers] = useState<ClassTeacherOption[]>([]);
|
||
const [filterClassroomIds, setFilterClassroomIds] = useState<number[]>([]);
|
||
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [modalMode, setModalMode] = useState<'create' | 'edit' | 'detail'>('create');
|
||
const [editingSchedule, setEditingSchedule] = useState<ClassScheduleItem | null>(null);
|
||
const [selectedCell, setSelectedCell] = useState<{
|
||
classroomId: number;
|
||
weekDay: number;
|
||
} | null>(null);
|
||
const [selectedSchedules, setSelectedSchedules] = useState<ClassScheduleItem[]>([]);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [syncModalOpen, setSyncModalOpen] = useState(false);
|
||
const [syncing, setSyncing] = useState(false);
|
||
// 排课创建成功后的「下一步:同步钉钉」引导
|
||
const [syncHint, setSyncHint] = useState(false);
|
||
const [syncStatus, setSyncStatus] = useState<{
|
||
activeSchedules: number;
|
||
mappedClasses: number;
|
||
totalClasses: number;
|
||
} | null>(null);
|
||
const [syncStatusError, setSyncStatusError] = useState(false);
|
||
const [syncResult, setSyncResult] = useState<ScheduleSyncResult | null>(null);
|
||
const [syncDateFrom, setSyncDateFrom] = useState<Dayjs>(dayjs);
|
||
const [syncDays, setSyncDays] = useState(30);
|
||
const [attendanceMachineOnly, setAttendanceMachineOnly] = useState(false);
|
||
const [form] = Form.useForm<ScheduleFormValues>();
|
||
|
||
const openSyncModal = async () => {
|
||
setSyncModalOpen(true);
|
||
setSyncResult(null);
|
||
setSyncStatusError(false);
|
||
try {
|
||
const res = await api.get<{
|
||
success: boolean;
|
||
data: { activeSchedules: number; mappedClasses: number; totalClasses: number };
|
||
}>('/sync/schedule/status');
|
||
setSyncStatus(res.data);
|
||
} catch {
|
||
setSyncStatusError(true);
|
||
setSyncStatus(null);
|
||
}
|
||
};
|
||
|
||
const handleSyncSchedule = async () => {
|
||
setSyncing(true);
|
||
try {
|
||
const res = await api.post<{
|
||
success: boolean;
|
||
data: ScheduleSyncResult;
|
||
}>('/sync/schedule/sync', null, {
|
||
params: {
|
||
dateFrom: syncDateFrom.format('YYYY-MM-DD'),
|
||
days: syncDays,
|
||
attendanceMachineOnly,
|
||
},
|
||
});
|
||
setSyncResult(res.data);
|
||
const classification = classifySyncResult(res.data);
|
||
if (classification.level === 'error') {
|
||
message.error(classification.message);
|
||
} else if (classification.level === 'warning') {
|
||
message.warning(classification.message);
|
||
setSyncHint(false);
|
||
} else {
|
||
message.success(classification.message);
|
||
setSyncHint(false);
|
||
}
|
||
} catch (e: unknown) {
|
||
message.error(getErrorMessage(e, '同步失败'));
|
||
} finally {
|
||
setSyncing(false);
|
||
}
|
||
};
|
||
|
||
const weekStart = useMemo(() => viewDate.weekday(1).startOf('day'), [viewDate]);
|
||
const monthStart = useMemo(() => viewDate.startOf('month'), [viewDate]);
|
||
const weekEnd = useMemo(() => weekStart.add(6, 'day'), [weekStart]);
|
||
const weekNum = useMemo(() => weekStart.week(), [weekStart]);
|
||
const weekYear = useMemo(() => weekStart.year(), [weekStart]);
|
||
const calendarDays = useMemo(() => {
|
||
const monthEnd = monthStart.endOf('month');
|
||
const startOffset = (monthStart.day() + 6) % 7;
|
||
const endOffset = (7 - monthEnd.day()) % 7;
|
||
const start = monthStart.subtract(startOffset, 'day');
|
||
const end = monthEnd.add(endOffset, 'day');
|
||
const totalDays = end.diff(start, 'day') + 1;
|
||
const days: Dayjs[] = [];
|
||
for (let i = 0; i < totalDays; i++) {
|
||
days.push(start.add(i, 'day'));
|
||
}
|
||
return days;
|
||
}, [monthStart]);
|
||
|
||
const weeks = useMemo(() => {
|
||
const result: Dayjs[][] = [];
|
||
for (let i = 0; i < calendarDays.length; i += 7) {
|
||
result.push(calendarDays.slice(i, i + 7));
|
||
}
|
||
return result;
|
||
}, [calendarDays]);
|
||
|
||
const startDateStr = useMemo(() => {
|
||
if (viewMode === 'month') return calendarDays[0].format('YYYY-MM-DD');
|
||
return weekStart.format('YYYY-MM-DD');
|
||
}, [viewMode, weekStart, calendarDays]);
|
||
|
||
const endDateStr = useMemo(() => {
|
||
if (viewMode === 'month') return calendarDays[calendarDays.length - 1].format('YYYY-MM-DD');
|
||
return weekEnd.format('YYYY-MM-DD');
|
||
}, [viewMode, weekEnd, calendarDays]);
|
||
|
||
const {
|
||
data: fetchResult = { classrooms: [], classes: [], matrix: {} },
|
||
isLoading,
|
||
isFetching,
|
||
isError,
|
||
refetch,
|
||
} = useQuery<{
|
||
classrooms: ClassroomItem[];
|
||
classes: ClassItem[];
|
||
matrix: Record<number, Record<number, ClassScheduleItem[]>>;
|
||
}>({
|
||
queryKey: ['class-schedules', startDateStr, endDateStr, filterClassroomIds],
|
||
queryFn: async () => {
|
||
const [lookups, schedulesRes] = await Promise.all([
|
||
api.get('/class-schedules/lookups') as Promise<{
|
||
classrooms: ClassroomItem[];
|
||
classes: ClassItem[];
|
||
}>,
|
||
api.get('/class-schedules/weekly', {
|
||
params: {
|
||
startDate: startDateStr,
|
||
endDate: endDateStr,
|
||
...(filterClassroomIds.length === 1 ? { classroomId: filterClassroomIds[0] } : {}),
|
||
},
|
||
}) as Promise<Record<string, Record<string, ClassScheduleItem[]>>>,
|
||
]);
|
||
const validatedLookups = validateResponse<{
|
||
classrooms: ClassroomItem[];
|
||
classes: ClassItem[];
|
||
}>(scheduleLookupsSchema, lookups);
|
||
const validatedWeekly = validateResponse<
|
||
Record<string, Record<string, ClassScheduleItem[]>>
|
||
>(weeklyScheduleSchema, schedulesRes);
|
||
|
||
const typedMatrix: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||
for (const [cId, dayMap] of Object.entries(validatedWeekly)) {
|
||
const classroomId = Number(cId);
|
||
typedMatrix[classroomId] = {};
|
||
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||
typedMatrix[classroomId][Number(wd)] = schedules;
|
||
}
|
||
}
|
||
return {
|
||
classrooms: validatedLookups.classrooms,
|
||
classes: validatedLookups.classes,
|
||
matrix: typedMatrix,
|
||
};
|
||
},
|
||
});
|
||
const classrooms = fetchResult.classrooms;
|
||
const classes = fetchResult.classes;
|
||
const matrix = fetchResult.matrix;
|
||
const loading = isLoading || isFetching;
|
||
// RouteKeeper 保活页面切回时刷新排课数据
|
||
useVisibleRefetch(['class-schedules']);
|
||
|
||
const saveMutation = useApiMutation(
|
||
async (payload: Record<string, unknown>) =>
|
||
modalMode === 'edit' && editingSchedule
|
||
? api.put(`/class-schedules/${editingSchedule.id}`, payload)
|
||
: api.post('/class-schedules', payload),
|
||
{ invalidate: [['class-schedules']] },
|
||
);
|
||
const disableMutation = useApiMutation(
|
||
async (id: number) => api.put(`/class-schedules/${id}`, { status: 'inactive' }),
|
||
{ invalidate: [['class-schedules']] },
|
||
);
|
||
|
||
const filteredClassrooms = useMemo(() => {
|
||
if (filterClassroomIds.length === 0) return classrooms;
|
||
const idSet = new Set(filterClassroomIds);
|
||
return classrooms.filter((c) => idSet.has(c.id));
|
||
}, [classrooms, filterClassroomIds]);
|
||
|
||
const displayMatrix = useMemo(() => {
|
||
if (filterClassId == null) return matrix;
|
||
const filtered: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||
for (const [cId, dayMap] of Object.entries(matrix)) {
|
||
const classroomId = Number(cId);
|
||
filtered[classroomId] = {};
|
||
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||
const matched = filterSchedulesForClass(schedules, filterClassId);
|
||
if (matched.length > 0) {
|
||
filtered[classroomId][Number(wd)] = matched;
|
||
}
|
||
}
|
||
}
|
||
return filtered;
|
||
}, [matrix, filterClassId]);
|
||
|
||
const monthScheduleMap = useMemo(() => {
|
||
const map: Record<string, ClassScheduleItem[]> = {};
|
||
for (const day of calendarDays) {
|
||
const wd = day.day() === 0 ? 7 : day.day();
|
||
const dateStr = day.format('YYYY-MM-DD');
|
||
const result: ClassScheduleItem[] = [];
|
||
for (const classroom of filteredClassrooms) {
|
||
const daySchedules = displayMatrix[classroom.id]?.[wd] || [];
|
||
for (const s of daySchedules) {
|
||
if (dateStr >= s.startDate && dateStr <= s.endDate) {
|
||
result.push(s);
|
||
}
|
||
}
|
||
}
|
||
map[dateStr] = result;
|
||
}
|
||
return map;
|
||
}, [calendarDays, displayMatrix, filteredClassrooms]);
|
||
|
||
const handleCellClick = (classroomId: number, weekDay: number) => {
|
||
const schedules = displayMatrix[classroomId]?.[weekDay] || [];
|
||
setSelectedCell({ classroomId, weekDay });
|
||
setSelectedDate(null);
|
||
if (schedules.length > 0) {
|
||
setSelectedSchedules(schedules);
|
||
setModalMode('detail');
|
||
setModalOpen(true);
|
||
} else if (hasPermission('schedule:create')) {
|
||
setSelectedSchedules([]);
|
||
setEditingSchedule(null);
|
||
setModalMode('create');
|
||
form.resetFields();
|
||
form.setFieldsValue({ classroomId, weekDay, attendanceAdvanceMinutes: 30 });
|
||
setModalOpen(true);
|
||
}
|
||
};
|
||
|
||
const getSchedulesForDate = (date: Dayjs): ClassScheduleItem[] => {
|
||
const wd = date.day() === 0 ? 7 : date.day();
|
||
const dateStr = date.format('YYYY-MM-DD');
|
||
const result: ClassScheduleItem[] = [];
|
||
for (const classroom of filteredClassrooms) {
|
||
const daySchedules = displayMatrix[classroom.id]?.[wd] || [];
|
||
for (const s of daySchedules) {
|
||
if (dateStr >= s.startDate && dateStr <= s.endDate) {
|
||
result.push(s);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const handleDateClick = (date: Dayjs) => {
|
||
const dateKey = date.format('YYYY-MM-DD');
|
||
const schedules = monthScheduleMap[dateKey] || getSchedulesForDate(date);
|
||
setSelectedDate(date);
|
||
setSelectedCell(null);
|
||
setSelectedSchedules(schedules);
|
||
setModalMode('detail');
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const loadClassTeachers = async (classId: number) => {
|
||
try {
|
||
const teachers = await api.get<ClassTeacherOption[]>(
|
||
`/class-schedules/classes/${classId}/teachers`,
|
||
);
|
||
setClassTeachers(teachers);
|
||
return teachers;
|
||
} catch {
|
||
setClassTeachers([]);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
const applyClassTeacherDefaults = async (classId: number, subject?: string) => {
|
||
const teachers = await loadClassTeachers(classId);
|
||
const subjectTeachers = teachers.filter((teacher) => teacher.roleType === 'subject_teacher');
|
||
const matchedBySubject = subject
|
||
? subjectTeachers.filter((teacher) => teacher.subject && teacher.subject === subject)
|
||
: [];
|
||
const matched = matchedBySubject.length > 0 ? matchedBySubject : subjectTeachers;
|
||
if (matched.length === 1) {
|
||
form.setFieldValue('teacherId', matched[0].userId);
|
||
if (!subject && matched[0].subject) form.setFieldValue('subject', matched[0].subject);
|
||
} else {
|
||
form.setFieldValue('teacherId', undefined);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
// 周视图走 selectedCell;月视图走 selectedDate(此时 selectedCell 为 null)
|
||
if (modalMode === 'create' && !selectedCell && !selectedDate) return;
|
||
if (modalMode === 'edit' && !editingSchedule) return;
|
||
try {
|
||
const values = (await form.validateFields()) as ScheduleFormValues;
|
||
setSubmitting(true);
|
||
const payload = buildSchedulePayload(values);
|
||
await saveMutation.mutateAsync(payload);
|
||
message.success(
|
||
modalMode === 'edit' ? '排课更新成功,请重新同步到钉钉排班' : '排课创建成功',
|
||
);
|
||
if (modalMode === 'create') setSyncHint(true);
|
||
setModalOpen(false);
|
||
setEditingSchedule(null);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const openEditSchedule = (schedule: ClassScheduleItem) => {
|
||
if (isMaskedSchedule(schedule) || schedule.id === null || schedule.classId === null) return;
|
||
if (schedule.scheduleType === 'RENTAL') {
|
||
message.warning('租赁排课请在租赁订单中修改');
|
||
return;
|
||
}
|
||
setEditingSchedule(schedule);
|
||
setModalMode('edit');
|
||
form.setFieldsValue(
|
||
scheduleToFormValues({
|
||
...schedule,
|
||
id: schedule.id ?? undefined,
|
||
classId: schedule.classId ?? undefined,
|
||
}),
|
||
);
|
||
void loadClassTeachers(schedule.classId);
|
||
};
|
||
|
||
const removeScheduleFromSelection = (id: number) => {
|
||
const remaining = selectedSchedules.filter((s) => s.id !== id);
|
||
setSelectedSchedules(remaining);
|
||
if (remaining.length === 0) {
|
||
setModalOpen(false);
|
||
}
|
||
};
|
||
|
||
const handleDisable = async (id: number | null) => {
|
||
if (id === null) return;
|
||
try {
|
||
await disableMutation.mutateAsync(id);
|
||
message.success('排课已停用,历史考勤记录已保留,教室占用已释放');
|
||
removeScheduleFromSelection(id);
|
||
} catch {
|
||
// 错误提示由 useApiMutation 统一处理
|
||
}
|
||
};
|
||
|
||
const classroomOptions = useMemo(
|
||
() =>
|
||
classrooms.map((c) => ({
|
||
value: c.id,
|
||
label: `${c.building ? c.building + ' · ' : ''}${c.name}`,
|
||
})),
|
||
[classrooms],
|
||
);
|
||
|
||
const classOptions = useMemo(
|
||
() =>
|
||
classes.map((c) => ({
|
||
value: c.id,
|
||
label: `${c.name}${c.code ? ` (${c.code})` : ''}`,
|
||
})),
|
||
[classes],
|
||
);
|
||
|
||
const selectedClassroom = selectedCell
|
||
? classrooms.find((c) => c.id === selectedCell.classroomId)
|
||
: undefined;
|
||
|
||
return (
|
||
<div>
|
||
{syncHint && (
|
||
<NextStepHint
|
||
title="排课已创建"
|
||
description="同步到钉钉后,教师端与考勤设备才能看到新排课。"
|
||
action={{ label: '同步钉钉', onClick: () => void handleSyncSchedule() }}
|
||
onClose={() => setSyncHint(false)}
|
||
/>
|
||
)}
|
||
<div
|
||
style={{
|
||
marginBottom: 16,
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
flexWrap: 'wrap',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<Space>
|
||
<CalendarOutlined style={{ fontSize: 20 }} />
|
||
<h3 style={{ margin: 0 }}>排课管理</h3>
|
||
</Space>
|
||
<Space>
|
||
<Segmented
|
||
value={viewMode}
|
||
onChange={(v) => {
|
||
setViewMode(v as 'week' | 'month');
|
||
setSelectedDate(null);
|
||
}}
|
||
options={[
|
||
{ label: '周视图', value: 'week' },
|
||
{ label: '月视图', value: 'month' },
|
||
]}
|
||
/>
|
||
<PermissionButton
|
||
permission="sync:trigger"
|
||
type="primary"
|
||
icon={<CloudSyncOutlined />}
|
||
onClick={() => void openSyncModal()}
|
||
>
|
||
同步到钉钉排班
|
||
</PermissionButton>
|
||
{viewMode === 'week' ? (
|
||
<>
|
||
<Button icon={<LeftOutlined />} onClick={() => setViewDate(viewDate.subtract(7, 'day'))}>
|
||
上一周
|
||
</Button>
|
||
<span style={{ fontWeight: 500, fontSize: 15 }}>
|
||
{weekYear} W{weekNum}
|
||
<span style={{ color: '#8c8c8c', fontWeight: 400, fontSize: 13, marginLeft: 6 }}>
|
||
({startDateStr} ~ {endDateStr})
|
||
</span>
|
||
</span>
|
||
<Button icon={<RightOutlined />} onClick={() => setViewDate(viewDate.add(7, 'day'))}>
|
||
下一周
|
||
</Button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Button icon={<LeftOutlined />} onClick={() => setViewDate(viewDate.subtract(1, 'month'))}>
|
||
上一月
|
||
</Button>
|
||
<span style={{ fontWeight: 500, fontSize: 15 }}>{monthStart.format('YYYY年 M月')}</span>
|
||
<Button icon={<RightOutlined />} onClick={() => setViewDate(viewDate.add(1, 'month'))}>
|
||
下一月
|
||
</Button>
|
||
</>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<Space wrap>
|
||
<Select
|
||
mode="multiple"
|
||
placeholder="筛选教室"
|
||
allowClear
|
||
style={{ minWidth: 200 }}
|
||
value={filterClassroomIds}
|
||
onChange={(v) => setFilterClassroomIds(v)}
|
||
options={classroomOptions}
|
||
maxTagCount={3}
|
||
/>
|
||
<Select
|
||
placeholder="筛选班级"
|
||
allowClear
|
||
style={{ minWidth: 160 }}
|
||
value={filterClassId}
|
||
onChange={(v) => setFilterClassId(v)}
|
||
options={classOptions}
|
||
/>
|
||
</Space>
|
||
</Card>
|
||
|
||
{isError ? (
|
||
<QueryErrorState
|
||
title="排课数据加载失败"
|
||
description="请检查网络后重试。"
|
||
onRetry={() => void refetch()}
|
||
/>
|
||
) : (
|
||
<ScheduleGrid
|
||
loading={loading}
|
||
viewMode={viewMode}
|
||
classrooms={classrooms}
|
||
filteredClassrooms={filteredClassrooms}
|
||
displayMatrix={displayMatrix}
|
||
weeks={weeks}
|
||
monthStart={monthStart}
|
||
monthScheduleMap={monthScheduleMap}
|
||
onCellClick={handleCellClick}
|
||
onDateClick={handleDateClick}
|
||
/>
|
||
)}
|
||
|
||
<ScheduleModal
|
||
open={modalOpen}
|
||
mode={modalMode}
|
||
submitting={submitting}
|
||
form={form}
|
||
selectedCell={selectedCell}
|
||
selectedDate={selectedDate}
|
||
selectedSchedules={selectedSchedules}
|
||
editingSchedule={editingSchedule}
|
||
selectedClassroom={selectedClassroom}
|
||
classOptions={classOptions}
|
||
classroomOptions={classroomOptions}
|
||
classTeachers={classTeachers}
|
||
classes={classes}
|
||
onCancel={() => {
|
||
setModalOpen(false);
|
||
setEditingSchedule(null);
|
||
}}
|
||
onSubmit={handleSubmit}
|
||
onStartCreate={() => {
|
||
setEditingSchedule(null);
|
||
setModalMode('create');
|
||
form.resetFields();
|
||
form.setFieldsValue({
|
||
classroomId: selectedCell?.classroomId,
|
||
weekDay:
|
||
selectedCell?.weekDay ?? (selectedDate ? selectedDate.day() || 7 : undefined),
|
||
dateRange: selectedDate ? [selectedDate, selectedDate] : undefined,
|
||
attendanceAdvanceMinutes: 30,
|
||
});
|
||
}}
|
||
onEdit={openEditSchedule}
|
||
onDisable={handleDisable}
|
||
onClassChange={(classId) => {
|
||
form.setFieldValue('teacherId', undefined);
|
||
void applyClassTeacherDefaults(classId, form.getFieldValue('subject'));
|
||
}}
|
||
onSubjectBlur={(value) => {
|
||
const classId = form.getFieldValue('classId');
|
||
if (classId) void applyClassTeacherDefaults(classId, value);
|
||
}}
|
||
/>
|
||
|
||
<SyncModal
|
||
open={syncModalOpen}
|
||
syncing={syncing}
|
||
syncStatus={syncStatus}
|
||
syncStatusError={syncStatusError}
|
||
syncResult={syncResult}
|
||
syncDateFrom={syncDateFrom}
|
||
syncDays={syncDays}
|
||
attendanceMachineOnly={attendanceMachineOnly}
|
||
onClose={() => {
|
||
setSyncModalOpen(false);
|
||
setSyncResult(null);
|
||
}}
|
||
onRetryStatus={() => void openSyncModal()}
|
||
onSync={handleSyncSchedule}
|
||
onDateChange={setSyncDateFrom}
|
||
onDaysChange={setSyncDays}
|
||
onMachineOnlyChange={setAttendanceMachineOnly}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SchedulesPage;
|