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:
@@ -62,7 +62,6 @@ interface EnrollmentInfo {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
interface StudentCreateImportResult {
|
||||
message?: string;
|
||||
imported?: number;
|
||||
@@ -75,6 +74,11 @@ interface StudentUpdateImportResult {
|
||||
skipped?: number;
|
||||
}
|
||||
|
||||
interface StudentFilterLookups {
|
||||
classes: Array<{ id: number; name: string; code?: string }>;
|
||||
teachers: Array<{ id: number; name: string; username: string }>;
|
||||
}
|
||||
|
||||
const StudentsPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
@@ -85,6 +89,10 @@ const StudentsPage: React.FC = () => {
|
||||
const [searchName, setSearchName] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
||||
const [filterOrganizationId, setFilterOrganizationId] = useState<number | undefined>(undefined);
|
||||
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
|
||||
const [filterTeacherId, setFilterTeacherId] = useState<number | undefined>(undefined);
|
||||
const [classOptions, setClassOptions] = useState<StudentFilterLookups['classes']>([]);
|
||||
const [teacherOptions, setTeacherOptions] = useState<StudentFilterLookups['teachers']>([]);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [archivedCount, setArchivedCount] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
@@ -150,6 +158,8 @@ const StudentsPage: React.FC = () => {
|
||||
};
|
||||
if (filterStatus) params.status = filterStatus;
|
||||
if (filterOrganizationId) params.organizationId = filterOrganizationId;
|
||||
if (filterClassId) params.classId = filterClassId;
|
||||
if (filterTeacherId) params.teacherId = filterTeacherId;
|
||||
const res = (await api.get('/students', { params })) as Array<Record<string, unknown>>;
|
||||
const list = res as Array<Record<string, unknown>>;
|
||||
const archived = list.filter((r) => r.status === 'archived');
|
||||
@@ -160,7 +170,7 @@ const StudentsPage: React.FC = () => {
|
||||
message.error(err?.message || '加载失败,请稍后重试');
|
||||
}
|
||||
setLoading(false);
|
||||
}, [searchName, showArchived, filterStatus, filterOrganizationId]);
|
||||
}, [searchName, showArchived, filterStatus, filterOrganizationId, filterClassId, filterTeacherId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -173,6 +183,13 @@ const StudentsPage: React.FC = () => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
api
|
||||
.get<StudentFilterLookups>('/students/filter-lookups')
|
||||
.then((res) => {
|
||||
setClassOptions(res.classes || []);
|
||||
setTeacherOptions(res.teachers || []);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
@@ -330,8 +347,15 @@ const StudentsPage: React.FC = () => {
|
||||
? '/api'
|
||||
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const token = localStorage.getItem('token');
|
||||
const params = showArchived ? '?includeArchived=true' : '';
|
||||
fetch(`${baseURL}/students/export${params}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
const params = new URLSearchParams();
|
||||
if (searchName) params.set('name', searchName);
|
||||
if (filterStatus) params.set('status', filterStatus);
|
||||
if (filterOrganizationId) params.set('organizationId', String(filterOrganizationId));
|
||||
if (showArchived) params.set('includeArchived', 'true');
|
||||
if (filterClassId) params.set('classId', String(filterClassId));
|
||||
if (filterTeacherId) params.set('teacherId', String(filterTeacherId));
|
||||
const query = params.toString() ? `?${params.toString()}` : '';
|
||||
fetch(`${baseURL}/students/export${query}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -568,6 +592,36 @@ const StudentsPage: React.FC = () => {
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="所属班级"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 160 }}
|
||||
value={filterClassId}
|
||||
onChange={(v) => {
|
||||
setFilterClassId(v);
|
||||
}}
|
||||
options={classOptions.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.code ? `${item.name}(${item.code})` : item.name,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
placeholder="所属老师"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 160 }}
|
||||
value={filterTeacherId}
|
||||
onChange={(v) => {
|
||||
setFilterTeacherId(v);
|
||||
}}
|
||||
options={teacherOptions.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name === item.username ? item.name : `${item.name}(${item.username})`,
|
||||
}))}
|
||||
/>
|
||||
<Button
|
||||
type={showArchived ? 'primary' : 'default'}
|
||||
onClick={() => setShowArchived(!showArchived)}
|
||||
@@ -609,7 +663,11 @@ const StudentsPage: React.FC = () => {
|
||||
>
|
||||
添加学生
|
||||
</PermissionButton>
|
||||
<Upload accept=".xlsx,.xls" showUploadList={false} customRequest={handleCreateStudentsImport}>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={handleCreateStudentsImport}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Upload
|
||||
@@ -679,9 +737,9 @@ const StudentsPage: React.FC = () => {
|
||||
}
|
||||
return (
|
||||
<Card title="多班型对比" size="small" style={{ margin: '8px 0' }}>
|
||||
<Row gutter={16}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{enrollments.map((enr, idx) => (
|
||||
<Col span={12} key={enr.classId}>
|
||||
<Col xs={24} md={12} key={enr.classId}>
|
||||
<Card
|
||||
size="small"
|
||||
title={enr.classType || `班型 ${idx + 1}`}
|
||||
|
||||
Reference in New Issue
Block a user