feat: refine attendance and finance workflows
This commit is contained in:
@@ -74,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[]>([]);
|
||||
@@ -84,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[]>([]);
|
||||
@@ -149,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');
|
||||
@@ -159,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();
|
||||
@@ -172,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();
|
||||
@@ -329,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);
|
||||
@@ -567,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)}
|
||||
|
||||
Reference in New Issue
Block a user