feat: refine admin forms, attendance and finance workflows
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
This commit is contained in:
@@ -59,22 +59,22 @@ const AttendanceDevicesPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const classroomOptions = useMemo(
|
||||
() => classrooms.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.building ? `${item.name}(${item.building})` : item.name,
|
||||
})),
|
||||
() =>
|
||||
classrooms.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.building ? `${item.name}(${item.building})` : item.name,
|
||||
})),
|
||||
[classrooms],
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
const text = keyword.trim().toLocaleLowerCase('zh-CN');
|
||||
if (!text) return data;
|
||||
return data.filter((item) => [
|
||||
item.deviceSn,
|
||||
item.deviceName,
|
||||
item.classroom?.name,
|
||||
item.location,
|
||||
].some((value) => (value || '').toLocaleLowerCase('zh-CN').includes(text)));
|
||||
return data.filter((item) =>
|
||||
[item.deviceSn, item.deviceName, item.classroom?.name, item.location].some((value) =>
|
||||
(value || '').toLocaleLowerCase('zh-CN').includes(text),
|
||||
),
|
||||
);
|
||||
}, [data, keyword]);
|
||||
|
||||
const openCreate = () => {
|
||||
@@ -131,17 +131,48 @@ const AttendanceDevicesPage: React.FC = () => {
|
||||
|
||||
const columns: ColumnsType<AttendanceDeviceRow> = [
|
||||
{ title: '设备名称', dataIndex: 'deviceName', width: 180 },
|
||||
{ title: 'SN 码', dataIndex: 'deviceSn', width: 220, render: (value) => <span style={{ fontFamily: 'monospace' }}>{value}</span> },
|
||||
{ title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}` },
|
||||
{ title: '位置', dataIndex: 'location', render: (value) => value || <span style={{ color: '#999' }}>—</span> },
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (value: keyof typeof statusMeta) => <Tag color={statusMeta[value]?.color}>{statusMeta[value]?.text || value}</Tag> },
|
||||
{ title: '备注', dataIndex: 'notes', ellipsis: true, render: (value) => value || <span style={{ color: '#999' }}>—</span> },
|
||||
{
|
||||
title: 'SN 码',
|
||||
dataIndex: 'deviceSn',
|
||||
width: 220,
|
||||
render: (value) => <span style={{ fontFamily: 'monospace' }}>{value}</span>,
|
||||
},
|
||||
{
|
||||
title: '绑定教室',
|
||||
dataIndex: ['classroom', 'name'],
|
||||
width: 160,
|
||||
render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}`,
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
dataIndex: 'location',
|
||||
render: (value) => value || <span style={{ color: '#999' }}>—</span>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (value: keyof typeof statusMeta) => (
|
||||
<Tag color={statusMeta[value]?.color}>{statusMeta[value]?.text || value}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'notes',
|
||||
ellipsis: true,
|
||||
render: (value) => value || <span style={{ color: '#999' }}>—</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<PermissionButton permission="classroom:edit" size="small" type="link" onClick={() => openEdit(record)}>
|
||||
<PermissionButton
|
||||
permission="classroom:edit"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => openEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm title="确定停用此考勤机绑定?" onConfirm={() => handleDelete(record.id)}>
|
||||
@@ -156,7 +187,15 @@ const AttendanceDevicesPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索设备/SN/教室"
|
||||
@@ -164,7 +203,12 @@ const AttendanceDevicesPage: React.FC = () => {
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
/>
|
||||
<PermissionButton permission="classroom:edit" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
<PermissionButton
|
||||
permission="classroom:edit"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={openCreate}
|
||||
>
|
||||
添加考勤机
|
||||
</PermissionButton>
|
||||
</div>
|
||||
@@ -188,17 +232,39 @@ const AttendanceDevicesPage: React.FC = () => {
|
||||
okText="保存"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="deviceName" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Form.Item
|
||||
name="deviceName"
|
||||
label="设备名称"
|
||||
rules={[{ required: true, message: '请输入设备名称' }]}
|
||||
>
|
||||
<Input placeholder="如:彼岸游境_N1604" />
|
||||
</Form.Item>
|
||||
<Form.Item name="deviceSn" label="SN 码" rules={[{ required: true, message: '请输入钉钉返回的 deviceSN' }]}>
|
||||
<Form.Item
|
||||
name="deviceSn"
|
||||
label="SN 码"
|
||||
rules={[{ required: true, message: '请输入钉钉返回的 deviceSN' }]}
|
||||
>
|
||||
<Input placeholder="如:300419260325WN1604" />
|
||||
</Form.Item>
|
||||
<Form.Item name="classroomId" label="绑定教室" rules={[{ required: true, message: '请选择绑定教室' }]}>
|
||||
<Select showSearch optionFilterProp="label" options={classroomOptions} placeholder="选择教室" />
|
||||
<Form.Item
|
||||
name="classroomId"
|
||||
label="绑定教室"
|
||||
rules={[{ required: true, message: '请选择绑定教室' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={classroomOptions}
|
||||
placeholder="选择教室"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" initialValue="active">
|
||||
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '停用' }]} />
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'active', label: '启用' },
|
||||
{ value: 'disabled', label: '停用' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="location" label="位置">
|
||||
<Input placeholder="如:教学楼一楼东侧" />
|
||||
|
||||
Reference in New Issue
Block a user