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:
2026-07-18 12:54:10 +00:00
parent 92d303ed01
commit 375c7ec60b
64 changed files with 5169 additions and 2404 deletions

View File

@@ -1,7 +1,19 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import {
Table, Button, Input, Select, Space, Tag, Modal, Form, InputNumber,
DatePicker, Popconfirm, Card, Switch, Empty,
Table,
Button,
Input,
Select,
Space,
Tag,
Modal,
Form,
InputNumber,
DatePicker,
Popconfirm,
Card,
Switch,
Empty,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { PlusOutlined, SearchOutlined, TeamOutlined, InboxOutlined } from '@ant-design/icons';
@@ -96,13 +108,14 @@ const ClassesPage: React.FC = () => {
setData(res);
} catch (e: any) {
message.error(e?.message || '加载失败,请稍后重试');
}
finally {
} finally {
setLoading(false);
}
}, [filterStatus, filterType, showArchived]);
useEffect(() => { fetchData(); }, [fetchData]);
useEffect(() => {
fetchData();
}, [fetchData]);
const filtered = useMemo(() => {
if (!searchText) return data;
@@ -154,58 +167,86 @@ const ClassesPage: React.FC = () => {
}
};
const columns: ColumnsType<ClassItem> = useMemo(() => [
{
title: '班级名称', dataIndex: 'name', width: 120,
sorter: (a, b) => a.name.localeCompare(b.name),
},
{ title: '编码', dataIndex: 'code', width: 140 },
{
title: '班型', dataIndex: 'classType', width: 100,
render: (v: string) => <Tag>{TYPE_MAP[v] || v}</Tag>,
},
{
title: '开班日期', dataIndex: 'startDate', width: 110,
render: (v: string | null) => v || '-',
},
{
title: '学员', width: 100,
render: (_: unknown, r: ClassItem) => `${r.studentCount || 0}/${r.maxStudents || '-'}`,
},
{
title: '状态', dataIndex: 'status', width: 100,
render: (v: string) => {
const cfg = STATUS_MAP[v] || { color: 'default', text: v };
return <Tag color={cfg.color}>{cfg.text}</Tag>;
const columns: ColumnsType<ClassItem> = useMemo(
() => [
{
title: '班级名称',
dataIndex: 'name',
width: 120,
sorter: (a, b) => a.name.localeCompare(b.name),
},
},
{
title: '操作', width: 280,
render: (_: unknown, r: ClassItem) => (
<Space>
<Button size="small" icon={<TeamOutlined />} onClick={() => navigate(`/classes/${r.id}`)}>
</Button>
<PermissionButton permission="class:edit" size="small" onClick={() => handleEdit(r)}>
</PermissionButton>
{r.isArchived ? (
<Popconfirm title="确认恢复?" onConfirm={() => handleArchive(r.id, false)}>
<PermissionButton permission="class:edit" size="small"></PermissionButton>
</Popconfirm>
) : (
<Popconfirm title="归档后可恢复,确认归档?" onConfirm={() => handleArchive(r.id, true)}>
<PermissionButton permission="class:edit" size="small"></PermissionButton>
</Popconfirm>
)}
</Space>
),
},
], []);
{ title: '编码', dataIndex: 'code', width: 140 },
{
title: '班型',
dataIndex: 'classType',
width: 100,
render: (v: string) => <Tag>{TYPE_MAP[v] || v}</Tag>,
},
{
title: '开班日期',
dataIndex: 'startDate',
width: 110,
render: (v: string | null) => v || '-',
},
{
title: '学员',
width: 100,
render: (_: unknown, r: ClassItem) => `${r.studentCount || 0}/${r.maxStudents || '-'}`,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (v: string) => {
const cfg = STATUS_MAP[v] || { color: 'default', text: v };
return <Tag color={cfg.color}>{cfg.text}</Tag>;
},
},
{
title: '操作',
width: 280,
render: (_: unknown, r: ClassItem) => (
<Space>
<Button
size="small"
icon={<TeamOutlined />}
onClick={() => navigate(`/classes/${r.id}`)}
>
</Button>
<PermissionButton permission="class:edit" size="small" onClick={() => handleEdit(r)}>
</PermissionButton>
{r.isArchived ? (
<Popconfirm title="确认恢复?" onConfirm={() => handleArchive(r.id, false)}>
<PermissionButton permission="class:edit" size="small">
</PermissionButton>
</Popconfirm>
) : (
<Popconfirm
title="归档后可恢复,确认归档?"
onConfirm={() => handleArchive(r.id, true)}
>
<PermissionButton permission="class:edit" size="small">
</PermissionButton>
</Popconfirm>
)}
</Space>
),
},
],
[],
);
return (
<Card>
<Space style={{ marginBottom: 16 }} wrap className="responsive-toolbar responsive-toolbar--single">
<Space
style={{ marginBottom: 16 }}
wrap
className="responsive-toolbar responsive-toolbar--single"
>
<Input
placeholder="搜索名称/编码"
prefix={<SearchOutlined />}
@@ -229,7 +270,12 @@ const ClassesPage: React.FC = () => {
onChange={setFilterStatus}
options={Object.entries(STATUS_MAP).map(([k, v]) => ({ value: k, label: v.text }))}
/>
<PermissionButton permission="class:create" type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
<PermissionButton
permission="class:create"
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
</PermissionButton>
<span style={{ marginLeft: 8 }}>
@@ -287,7 +333,9 @@ const ClassesPage: React.FC = () => {
</Form.Item>
</Space>
<Form.Item name="status" label="状态" initialValue="enrolling">
<Select options={Object.entries(STATUS_MAP).map(([k, v]) => ({ value: k, label: v.text }))} />
<Select
options={Object.entries(STATUS_MAP).map(([k, v]) => ({ value: k, label: v.text }))}
/>
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={3} />