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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user