feat: complete student archive subsystem — 7 entities, PDF report, frontend profile page

This commit is contained in:
2026-07-06 15:58:36 +08:00
parent b6990dd11b
commit 8bd59b8ac6
15 changed files with 1539 additions and 9 deletions

View File

@@ -13,9 +13,10 @@ import BillsPage from './pages/Bills';
import RoomVisualPage from './pages/RoomVisual';
import OperationLogsPage from './pages/OperationLogs';
import UsersPage from './pages/Users';
import TeachersPage from './pages/Teachers';
import ClassroomsPage from './pages/Classrooms';
import DepositsPage from './pages/Deposits';
import ClassroomsPage from './pages/Classrooms';
import StudentProfilePage from './pages/StudentProfile';
import ClassesPage from './pages/Classes';
import ClassDetailPage from './pages/Classes/detail';
import TenantsPage from './pages/Tenants';
@@ -86,6 +87,15 @@ const App: React.FC = () => {
</PermissionRoute>
}
/>
<Route
path="students/:id/profile"
element={
<PermissionRoute permission="student:view">
<StudentProfilePage />
</PermissionRoute>
}
/>
<Route
path="rooms"
element={

View File

@@ -0,0 +1,844 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Tabs,
Card,
Descriptions,
Table,
Button,
Modal,
Form,
Input,
Select,
DatePicker,
InputNumber,
Upload,
Tag,
Space,
message,
Popconfirm,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
PlusOutlined,
UploadOutlined,
DownloadOutlined,
ArrowLeftOutlined,
DeleteOutlined,
EyeOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
// ---- Types ----
interface StudentInfo {
id: number;
name: string;
phone: string;
idNumber: string;
studentNo: string;
status: string;
}
interface ProfileData {
targetCollege?: string;
targetMajor?: string;
subjectDirection?: string;
grade?: string;
campusLocation?: string;
profileDate?: string;
notes?: string;
}
interface EnrollmentRecord {
id: number;
courseCategory: string;
classType: string;
className?: string;
headTeacher?: string;
subjectTeacher?: string;
startDate?: string;
endDate?: string;
status: string;
}
interface ExamScoreRecord {
id: number;
examType: string;
examName?: string;
subject: string;
score: number;
classAvg?: number;
rank?: number;
examDate?: string;
enrollmentId?: number;
}
interface LearningRecord {
id: number;
recordDate: string;
recordType: string;
content: string;
followUpMethod?: string;
nextStep?: string;
}
interface ResultData {
cultureFinalScore?: number;
professionalFinalScore?: number;
admissionStatus?: string;
admittedCollege?: string;
admittedMajor?: string;
}
interface AttachmentRecord {
id: number;
category: string;
fileName: string;
fileSize: number;
}
interface StudentProfileAggregate {
student: StudentInfo;
profile: ProfileData | null;
enrollments: EnrollmentRecord[];
examScores: ExamScoreRecord[];
learningRecords: LearningRecord[];
result: ResultData | null;
attachments: AttachmentRecord[];
}
// ---- Constants ----
const ADMISSION_STATUS_MAP: Record<string, { text: string; color: string }> = {
admitted: { text: '已录取', color: 'green' },
pending: { text: '待录取', color: 'orange' },
rejected: { text: '未录取', color: 'red' },
withdrawn: { text: '放弃', color: '#999' },
};
const EXAM_TYPE_OPTIONS = [
{ value: 'monthly', label: '月考' },
{ value: 'midterm', label: '期中' },
{ value: 'final', label: '期末' },
{ value: 'mock', label: '模拟考' },
{ value: 'entrance', label: '入学测试' },
{ value: 'other', label: '其他' },
];
const RECORD_TYPE_OPTIONS = [
{ value: 'study_feedback', label: '学习反馈' },
{ value: 'parent_communication', label: '家长沟通' },
{ value: 'behavior_note', label: '行为记录' },
{ value: 'meeting', label: '会议记录' },
{ value: 'other', label: '其他' },
];
const COURSE_CATEGORY_OPTIONS = [
{ value: 'culture', label: '文化课' },
{ value: 'professional', label: '专业课' },
{ value: 'comprehensive', label: '综合' },
];
const CLASS_TYPE_OPTIONS = [
{ value: 'one_on_one', label: '一对一' },
{ value: 'small_group', label: '小班' },
{ value: 'large_class', label: '大班' },
{ value: 'online', label: '线上' },
{ value: 'offline', label: '线下' },
];
const ATTACHMENT_CATEGORY_OPTIONS = [
{ value: 'id_card', label: '身份证' },
{ value: 'transcript', label: '成绩单' },
{ value: 'certificate', label: '证书' },
{ value: 'contract', label: '合同' },
{ value: 'photo', label: '照片' },
{ value: 'other', label: '其他' },
];
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// ---- Tab Components ----
interface TabProps {
studentId: number;
onRefresh: () => void;
}
const ProfileTab: React.FC<{ data: ProfileData | null; studentId: number; onRefresh: () => void }> = ({
data,
studentId,
onRefresh,
}) => {
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const handleSave = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await api.put(`/archive/${studentId}/profile`, {
...values,
profileDate: values.profileDate?.format('YYYY-MM-DD'),
});
message.success('基础档案已保存');
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
if (err?.message) message.error(err.message);
} finally {
setSaving(false);
}
};
return (
<div>
<Form
form={form}
layout="vertical"
initialValues={{
targetCollege: data?.targetCollege ?? undefined,
targetMajor: data?.targetMajor ?? undefined,
subjectDirection: data?.subjectDirection ?? undefined,
grade: data?.grade ?? undefined,
campusLocation: data?.campusLocation ?? undefined,
profileDate: data?.profileDate ? dayjs(data.profileDate) : undefined,
notes: data?.notes ?? undefined,
}}
style={{ maxWidth: 600 }}
>
<Form.Item name="targetCollege" label="目标院校">
<Input placeholder="请输入目标院校" />
</Form.Item>
<Form.Item name="targetMajor" label="目标专业">
<Input placeholder="请输入目标专业" />
</Form.Item>
<Form.Item name="subjectDirection" label="选科方向">
<Input placeholder="如:物化生、史地政" />
</Form.Item>
<Form.Item name="grade" label="年级">
<Input placeholder="如:高三" />
</Form.Item>
<Form.Item name="campusLocation" label="校区">
<Input placeholder="请输入校区" />
</Form.Item>
<Form.Item name="profileDate" label="建档日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={3} placeholder="其他备注信息" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSave} loading={saving}>
</Button>
</Form.Item>
</Form>
</div>
);
};
const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
data,
studentId,
onRefresh,
}) => {
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const handleAdd = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await api.post(`/archive/${studentId}/enrollments`, {
...values,
startDate: values.startDate?.format('YYYY-MM-DD'),
endDate: values.endDate?.format('YYYY-MM-DD'),
});
message.success('报读记录已添加');
setModalOpen(false);
form.resetFields();
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
if (err?.message) message.error(err.message);
} finally {
setSaving(false);
}
};
const columns: ColumnsType<EnrollmentRecord> = [
{ title: '课程类别', dataIndex: 'courseCategory', render: (v: string) => v || '-' },
{ title: '班型', dataIndex: 'classType', render: (v: string) => v || '-' },
{ title: '班级名称', dataIndex: 'className', render: (v: string) => v || '-' },
{ title: '班主任', dataIndex: 'headTeacher', render: (v: string) => v || '-' },
{ title: '任课教师', dataIndex: 'subjectTeacher', render: (v: string) => v || '-' },
{ title: '开始日期', dataIndex: 'startDate', render: (v: string) => v || '-' },
{ title: '结束日期', dataIndex: 'endDate', render: (v: string) => v || '-' },
{
title: '状态',
dataIndex: 'status',
render: (v: string) => {
const colorMap: Record<string, string> = {
active: 'green',
completed: 'blue',
withdrawn: 'red',
};
return <Tag color={colorMap[v] || 'default'}>{v || '-'}</Tag>;
},
},
];
return (
<div>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => {
form.resetFields();
setModalOpen(true);
}}
style={{ marginBottom: 16 }}
>
</Button>
<Table<EnrollmentRecord>
columns={columns}
dataSource={data}
rowKey="id"
pagination={{ pageSize: 15 }}
/>
<Modal
title="添加报读记录"
open={modalOpen}
onOk={handleAdd}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
<Form form={form} layout="vertical">
<Form.Item name="courseCategory" label="课程类别" rules={[{ required: true, message: '请选择课程类别' }]}>
<Select options={COURSE_CATEGORY_OPTIONS} placeholder="请选择" />
</Form.Item>
<Form.Item name="classType" label="班型" rules={[{ required: true, message: '请选择班型' }]}>
<Select options={CLASS_TYPE_OPTIONS} placeholder="请选择" />
</Form.Item>
<Form.Item name="className" label="班级名称">
<Input placeholder="如2024届冲刺班" />
</Form.Item>
<Form.Item name="headTeacher" label="班主任">
<Input placeholder="班主任姓名" />
</Form.Item>
<Form.Item name="subjectTeacher" label="任课教师">
<Input placeholder="任课教师姓名" />
</Form.Item>
<Form.Item name="startDate" label="开始日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="endDate" label="结束日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
const ExamScoresTab: React.FC<TabProps & { data: ExamScoreRecord[]; enrollments: EnrollmentRecord[] }> = ({
data,
studentId,
enrollments,
onRefresh,
}) => {
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const handleAdd = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await api.post(`/archive/${studentId}/exam-scores`, {
...values,
examDate: values.examDate?.format('YYYY-MM-DD'),
});
message.success('考试成绩已添加');
setModalOpen(false);
form.resetFields();
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
if (err?.message) message.error(err.message);
} finally {
setSaving(false);
}
};
const columns: ColumnsType<ExamScoreRecord> = [
{
title: '考试类型',
dataIndex: 'examType',
render: (v: string) => EXAM_TYPE_OPTIONS.find((o) => o.value === v)?.label || v,
},
{ title: '考试名称', dataIndex: 'examName', render: (v: string) => v || '-' },
{ title: '科目', dataIndex: 'subject' },
{ title: '成绩', dataIndex: 'score' },
{ title: '班级均分', dataIndex: 'classAvg', render: (v: number | undefined) => (v !== undefined ? v : '-') },
{ title: '排名', dataIndex: 'rank', render: (v: number | undefined) => (v !== undefined ? v : '-') },
{ title: '考试日期', dataIndex: 'examDate', render: (v: string) => v || '-' },
{
title: '关联报读',
dataIndex: 'enrollmentId',
render: (v: number | undefined) => {
if (v === undefined) return '-';
const enr = enrollments.find((e) => e.id === v);
return enr ? `${enr.className || enr.courseCategory || v}` : String(v);
},
},
];
return (
<div>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => {
form.resetFields();
setModalOpen(true);
}}
style={{ marginBottom: 16 }}
>
</Button>
<Table<ExamScoreRecord>
columns={columns}
dataSource={data}
rowKey="id"
pagination={{ pageSize: 15 }}
/>
<Modal
title="添加考试成绩"
open={modalOpen}
onOk={handleAdd}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
<Form form={form} layout="vertical">
<Form.Item name="examType" label="考试类型" rules={[{ required: true, message: '请选择考试类型' }]}>
<Select options={EXAM_TYPE_OPTIONS} placeholder="请选择" />
</Form.Item>
<Form.Item name="examName" label="考试名称">
<Input placeholder="如2024第一次月考" />
</Form.Item>
<Form.Item name="subject" label="科目" rules={[{ required: true, message: '请输入科目' }]}>
<Input placeholder="如:数学" />
</Form.Item>
<Form.Item name="score" label="成绩" rules={[{ required: true, message: '请输入成绩' }]}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="classAvg" label="班级均分">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="rank" label="排名">
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="examDate" label="考试日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="enrollmentId" label="关联报读">
<Select
allowClear
placeholder="选择关联的报读记录"
options={enrollments.map((e) => ({
value: e.id,
label: `${e.className || e.courseCategory || e.id} (${e.classType})`,
}))}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({ data, studentId, onRefresh }) => {
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const handleAdd = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await api.post(`/archive/${studentId}/learning-records`, {
...values,
recordDate: values.recordDate?.format('YYYY-MM-DD'),
});
message.success('学情记录已添加');
setModalOpen(false);
form.resetFields();
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
if (err?.message) message.error(err.message);
} finally {
setSaving(false);
}
};
const columns: ColumnsType<LearningRecord> = [
{ title: '记录日期', dataIndex: 'recordDate' },
{
title: '记录类型',
dataIndex: 'recordType',
render: (v: string) => RECORD_TYPE_OPTIONS.find((o) => o.value === v)?.label || v,
},
{ title: '内容', dataIndex: 'content', ellipsis: true },
{ title: '跟进方式', dataIndex: 'followUpMethod', render: (v: string) => v || '-' },
{ title: '下一步计划', dataIndex: 'nextStep', render: (v: string) => v || '-' },
];
return (
<div>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => {
form.resetFields();
setModalOpen(true);
}}
style={{ marginBottom: 16 }}
>
</Button>
<Table<LearningRecord>
columns={columns}
dataSource={data}
rowKey="id"
pagination={{ pageSize: 15 }}
/>
<Modal
title="添加学情记录"
open={modalOpen}
onOk={handleAdd}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
<Form form={form} layout="vertical">
<Form.Item name="recordDate" label="记录日期" rules={[{ required: true, message: '请选择日期' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="recordType" label="记录类型" rules={[{ required: true, message: '请选择记录类型' }]}>
<Select options={RECORD_TYPE_OPTIONS} placeholder="请选择" />
</Form.Item>
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
<Input.TextArea rows={4} placeholder="请记录学情内容" />
</Form.Item>
<Form.Item name="followUpMethod" label="跟进方式">
<Input placeholder="如:电话、微信、面谈" />
</Form.Item>
<Form.Item name="nextStep" label="下一步计划">
<Input placeholder="后续跟进计划" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
const ResultTab: React.FC<TabProps & { data: ResultData | null }> = ({ data, studentId, onRefresh }) => {
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const handleSave = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await api.put(`/archive/${studentId}/result`, values);
message.success('录取结果已保存');
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
if (err?.message) message.error(err.message);
} finally {
setSaving(false);
}
};
return (
<div>
<Form
form={form}
layout="vertical"
initialValues={{
cultureFinalScore: data?.cultureFinalScore ?? undefined,
professionalFinalScore: data?.professionalFinalScore ?? undefined,
admissionStatus: data?.admissionStatus ?? undefined,
admittedCollege: data?.admittedCollege ?? undefined,
admittedMajor: data?.admittedMajor ?? undefined,
}}
style={{ maxWidth: 500 }}
>
<Form.Item name="cultureFinalScore" label="文化课最终分">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="professionalFinalScore" label="专业课最终分">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="admissionStatus" label="录取状态">
<Select
allowClear
placeholder="请选择录取状态"
options={Object.entries(ADMISSION_STATUS_MAP).map(([k, v]) => ({
value: k,
label: v.text,
}))}
/>
</Form.Item>
<Form.Item name="admittedCollege" label="录取院校">
<Input placeholder="请输入录取院校" />
</Form.Item>
<Form.Item name="admittedMajor" label="录取专业">
<Input placeholder="请输入录取专业" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSave} loading={saving}>
</Button>
</Form.Item>
</Form>
</div>
);
};
const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({ data, studentId, onRefresh }) => {
const [uploading, setUploading] = useState(false);
const handleDelete = async (attachmentId: number) => {
try {
await api.delete(`/archive/${studentId}/attachments/${attachmentId}`);
message.success('已删除');
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '删除失败');
}
};
const columns: ColumnsType<AttachmentRecord> = [
{
title: '类别',
dataIndex: 'category',
render: (v: string) => ATTACHMENT_CATEGORY_OPTIONS.find((o) => o.value === v)?.label || v,
},
{ title: '文件名', dataIndex: 'fileName' },
{ title: '大小', dataIndex: 'fileSize', render: formatFileSize },
{
title: '操作',
render: (_: unknown, record: AttachmentRecord) => (
<Space>
<Button
size="small"
icon={<EyeOutlined />}
onClick={() => {
const token = localStorage.getItem('token');
window.open(`/api/archive/${studentId}/attachments/${record.id}?token=${token}`, '_blank');
}}
>
</Button>
<Popconfirm title="确定删除该附件?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Upload
showUploadList={false}
customRequest={async (options: {
file: File | Blob;
onSuccess?: (body: unknown) => void;
onError?: (error: Error) => void;
}) => {
const formData = new FormData();
formData.append('file', options.file instanceof File ? options.file : new File([options.file], 'attachment'));
setUploading(true);
try {
await api.post(`/archive/${studentId}/attachments`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
message.success('上传成功');
options.onSuccess?.({});
onRefresh();
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '上传失败');
options.onError?.(err instanceof Error ? err : new Error('Upload failed'));
} finally {
setUploading(false);
}
}}
>
<Button icon={<UploadOutlined />} loading={uploading}>
</Button>
</Upload>
<Table<AttachmentRecord>
columns={columns}
dataSource={data}
rowKey="id"
pagination={{ pageSize: 15 }}
style={{ marginTop: 16 }}
/>
</div>
);
};
// ---- Main Page ----
const StudentProfilePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [aggregateData, setAggregateData] = useState<StudentProfileAggregate | null>(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await api.get<StudentProfileAggregate>(`/archive/${id}`);
setAggregateData(res);
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '加载失败');
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
void fetchData();
}, [fetchData]);
if (!aggregateData) return null;
const studentId = Number(id);
const { student, profile, enrollments, examScores, learningRecords, result, attachments } = aggregateData;
const handleDownloadReport = () => {
const token = localStorage.getItem('token');
window.open(`/api/archive/${id}/report?token=${token}`, '_blank');
};
return (
<Card
title={
<Space>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/students')} />
<span>
{student.name}
</span>
</Space>
}
loading={loading}
extra={
<PermissionButton
permission="student:view"
type="primary"
icon={<DownloadOutlined />}
onClick={handleDownloadReport}
>
</PermissionButton>
}
>
<Descriptions bordered column={3} size="small" style={{ marginBottom: 24 }}>
<Descriptions.Item label="学号">{student.studentNo || '-'}</Descriptions.Item>
<Descriptions.Item label="电话">{student.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="身份证号">{student.idNumber || '-'}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag>{student.status || '-'}</Tag>
</Descriptions.Item>
{profile?.targetCollege && (
<Descriptions.Item label="目标院校">{profile.targetCollege}</Descriptions.Item>
)}
{profile?.targetMajor && (
<Descriptions.Item label="目标专业">{profile.targetMajor}</Descriptions.Item>
)}
{profile?.grade && (
<Descriptions.Item label="年级">{profile.grade}</Descriptions.Item>
)}
{profile?.subjectDirection && (
<Descriptions.Item label="选科方向">{profile.subjectDirection}</Descriptions.Item>
)}
{profile?.campusLocation && (
<Descriptions.Item label="校区">{profile.campusLocation}</Descriptions.Item>
)}
</Descriptions>
<Tabs
defaultActiveKey="profile"
items={[
{
key: 'profile',
label: '基础档案',
children: <ProfileTab data={profile} studentId={studentId} onRefresh={fetchData} />,
},
{
key: 'enrollments',
label: `报读记录 (${enrollments.length})`,
children: (
<EnrollmentsTab data={enrollments} studentId={studentId} onRefresh={fetchData} />
),
},
{
key: 'exams',
label: `考试成绩 (${examScores.length})`,
children: (
<ExamScoresTab
data={examScores}
studentId={studentId}
enrollments={enrollments}
onRefresh={fetchData}
/>
),
},
{
key: 'learning',
label: `学情记录 (${learningRecords.length})`,
children: (
<LearningTab data={learningRecords} studentId={studentId} onRefresh={fetchData} />
),
},
{
key: 'result',
label: '录取结果',
children: <ResultTab data={result} studentId={studentId} onRefresh={fetchData} />,
},
{
key: 'attachments',
label: `附件 (${attachments.length})`,
children: (
<AttachmentsTab data={attachments} studentId={studentId} onRefresh={fetchData} />
),
},
]}
/>
</Card>
);
};
export default StudentProfilePage;

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Table,
Button,
@@ -68,6 +69,7 @@ interface EnrollmentInfo {
const StudentsPage: React.FC = () => {
const { modal } = App.useApp();
const navigate = useNavigate();
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -296,6 +298,14 @@ const StudentsPage: React.FC = () => {
</Popconfirm>
) : (
<>
<PermissionButton
permission="student:view"
size="small"
type="link"
onClick={() => navigate(`/students/${record.id}/profile`)}
>
</PermissionButton>
<PermissionButton
permission="student:edit"
size="small"