forked from wangziqi/gongxue-base
fix: release beds/lockers in batchCheckOut
The batchCheckOut method was not releasing assigned beds and lockers after checkout, leaving them orphaned as 'occupied'. Added the same release pattern used in checkOut() — using runner.manager.update() since batchCheckOut operates inside a QueryRunner transaction.
This commit is contained in:
@@ -2,13 +2,14 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Card, Button, Select, Modal, Form, Input, DatePicker, TimePicker,
|
||||
Popconfirm, message, Space, Spin, Empty, Tag, Tooltip, Segmented,
|
||||
Badge,
|
||||
Badge, Row, Col, Statistic, Alert,
|
||||
} from 'antd';
|
||||
import {
|
||||
CalendarOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
DeleteOutlined,
|
||||
CloudSyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
@@ -56,6 +57,16 @@ interface ScheduleFormValues {
|
||||
dateRange: [Dayjs, Dayjs];
|
||||
}
|
||||
|
||||
/** 排班同步返回结果 */
|
||||
interface ScheduleSyncResult {
|
||||
scheduleCount: number;
|
||||
shiftCount: number;
|
||||
groupCount: number;
|
||||
syncedItems: number;
|
||||
skippedNoMapping: number;
|
||||
groups: Array<{ deptName: string; groupId: number; itemCount: number }>;
|
||||
}
|
||||
|
||||
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
const WEEKDAY_NUMBERS = [1, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
@@ -88,6 +99,56 @@ const SchedulesPage: React.FC = () => {
|
||||
} | 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; mappedTeachers: number; totalTeachers: number;
|
||||
} | null>(null);
|
||||
const [syncResult, setSyncResult] = useState<{
|
||||
scheduleCount: number; shiftCount: number; groupCount: number;
|
||||
syncedItems: number; skippedNoMapping: number;
|
||||
groups: Array<{ deptName: string; groupId: number; itemCount: number }>;
|
||||
} | null>(null);
|
||||
const [syncDateFrom, setSyncDateFrom] = useState<Dayjs>(dayjs);
|
||||
const [syncDays, setSyncDays] = useState(30);
|
||||
|
||||
/** 打开同步弹窗时先查询就绪状态 */
|
||||
const openSyncModal = useCallback(async () => {
|
||||
setSyncModalOpen(true);
|
||||
setSyncResult(null);
|
||||
try {
|
||||
const res = await api.get<{
|
||||
success: boolean; data: { activeSchedules: number; mappedTeachers: number; totalTeachers: number };
|
||||
}>('/sync/schedule/status');
|
||||
setSyncStatus(res.data);
|
||||
} catch {
|
||||
setSyncStatus(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 执行排班同步 */
|
||||
const handleSyncSchedule = useCallback(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,
|
||||
},
|
||||
});
|
||||
setSyncResult(res.data);
|
||||
message.success(`同步完成:${res.data.syncedItems} 条排班已写入钉钉`);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string };
|
||||
message.error(err?.message || '同步失败');
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
}, [syncDateFrom, syncDays]);
|
||||
const [form] = Form.useForm<ScheduleFormValues>();
|
||||
|
||||
// Derived week/month info
|
||||
@@ -368,6 +429,9 @@ const SchedulesPage: React.FC = () => {
|
||||
{ label: '月视图', value: 'month' },
|
||||
]}
|
||||
/>
|
||||
<PermissionButton permission="sync:trigger" type="primary" icon={<CloudSyncOutlined />} onClick={openSyncModal}>
|
||||
同步到钉钉排班
|
||||
</PermissionButton>
|
||||
{viewMode === 'week' ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -833,6 +897,125 @@ const SchedulesPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* ── 钉钉排班同步 Modal ── */}
|
||||
<Modal
|
||||
title="同步排课到钉钉考勤排班"
|
||||
open={syncModalOpen}
|
||||
onCancel={() => { setSyncModalOpen(false); setSyncResult(null); }}
|
||||
footer={syncResult ? [
|
||||
<Button key="close" onClick={() => { setSyncModalOpen(false); setSyncResult(null); }}>关闭</Button>,
|
||||
] : [
|
||||
<Button key="cancel" onClick={() => { setSyncModalOpen(false); setSyncResult(null); }}>取消</Button>,
|
||||
<Button
|
||||
key="sync"
|
||||
type="primary"
|
||||
icon={<CloudSyncOutlined />}
|
||||
loading={syncing}
|
||||
onClick={handleSyncSchedule}
|
||||
disabled={!syncStatus || syncStatus.activeSchedules === 0}
|
||||
>
|
||||
开始同步
|
||||
</Button>,
|
||||
]}
|
||||
width={560}
|
||||
>
|
||||
{syncResult ? (
|
||||
/* ── 同步结果 ── */
|
||||
<div>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Statistic title="排课" value={syncResult.scheduleCount} suffix="条" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic title="班次" value={syncResult.shiftCount} suffix="个" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic title="考勤组" value={syncResult.groupCount} suffix="个" />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="排班数"
|
||||
value={syncResult.syncedItems}
|
||||
suffix="条"
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{syncResult.skippedNoMapping > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`${syncResult.skippedNoMapping} 条排课因教师未绑定钉钉而跳过`}
|
||||
style={{ marginBottom: 16 }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
{syncResult.groups.length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, marginBottom: 8 }}>按部门分组:</div>
|
||||
{syncResult.groups.map((g) => (
|
||||
<Tag key={g.groupId} color="blue" style={{ marginBottom: 4 }}>
|
||||
{g.deptName}:{g.itemCount} 条排班
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : syncStatus ? (
|
||||
/* ── 同步确认信息 ── */
|
||||
<div>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={8}>
|
||||
<Statistic title="活跃排课" value={syncStatus.activeSchedules} suffix="条" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="已绑定教师"
|
||||
value={syncStatus.mappedTeachers}
|
||||
suffix={`/ ${syncStatus.totalTeachers}`}
|
||||
valueStyle={{ color: syncStatus.mappedTeachers < syncStatus.totalTeachers ? '#faad14' : '#3f8600' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="未绑定" value={syncStatus.totalTeachers - syncStatus.mappedTeachers} suffix="人" />
|
||||
</Col>
|
||||
</Row>
|
||||
{syncStatus.mappedTeachers < syncStatus.totalTeachers && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`${syncStatus.totalTeachers - syncStatus.mappedTeachers} 位教师未绑定钉钉,其排课将被跳过。请先执行组织架构同步。`}
|
||||
style={{ marginBottom: 16 }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>同步参数</div>
|
||||
<Space>
|
||||
<span>起始日期:</span>
|
||||
<DatePicker value={syncDateFrom} onChange={(d) => d && setSyncDateFrom(d)} allowClear={false} />
|
||||
<span>天数:</span>
|
||||
<Select
|
||||
value={syncDays}
|
||||
onChange={setSyncDays}
|
||||
style={{ width: 100 }}
|
||||
options={[
|
||||
{ value: 7, label: '7 天' },
|
||||
{ value: 14, label: '14 天' },
|
||||
{ value: 30, label: '30 天' },
|
||||
{ value: 60, label: '60 天' },
|
||||
{ value: 90, label: '90 天' },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
{syncStatus.activeSchedules === 0 && (
|
||||
<Alert type="info" message="当前没有活跃排课。请先在排课页面创建排课记录。" showIcon />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Spin tip="查询同步状态..." />
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user