UX 缺陷修复: - 校验失败不再卡死弹窗按钮(Users/Roles/Bills) - 押金收取/批量收取/添加分期防重复提交;切换房型重置勾选 - AI 表单/批量确认不再出现"假成功" - Dashboard 各数据模块独立加载,单接口失败不再整页清零 - 房间可视化加载失败显示错误态而非永久转圈 - 学生编辑表单回填前重置,避免字段残留污染 - 覆盖式导入增加二次确认;恢复默认考勤时段确认并同步表单 - 金数据匹配关闭前确认,同步中禁止误关 体验提升: - 新增统一 QueryErrorState/QueryEmpty,20+ 页面加载失败显示错误态与重试 - 全局 ErrorBoundary + RouteKeeper 逐页兜底 - 新增 usePageVisible/useVisibleRefetch,保活页面切回自动刷新数据 - 新增首次登录角色引导 RoleTour 与业务闭环 NextStepHint 引导卡 - 重构 A2UI:useSubmissionState/useXCardSurface 收敛状态与命令生命周期, ArtifactErrorBoundary 渲染降级,图表空数据占位 - AI 助手欢迎语与建议话术按角色定制,会话列表空态引导 - 更新 a2ui-contract.md 契约文档说明实现现状
593 lines
20 KiB
TypeScript
593 lines
20 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 { 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 [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);
|
|
} else {
|
|
message.success(classification.message);
|
|
}
|
|
} 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 () => {
|
|
if (modalMode === 'create' && !selectedCell) 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' ? '排课更新成功,请重新同步到钉钉排班' : '排课创建成功',
|
|
);
|
|
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>
|
|
<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;
|