feat: expand student archive import template
This commit is contained in:
@@ -24,7 +24,7 @@ export interface EditableCellOption {
|
||||
|
||||
export interface EditableCellProps<Value = unknown> {
|
||||
value: Value;
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
editor?: EditableCellEditor;
|
||||
options?: EditableCellOption[];
|
||||
permission?: string;
|
||||
|
||||
@@ -46,6 +46,13 @@ interface StudentInfo {
|
||||
phone: string;
|
||||
idNumber: string;
|
||||
studentNo: string;
|
||||
gender?: string;
|
||||
ethnicity?: string;
|
||||
emergencyContact?: string;
|
||||
emergencyPhone?: string;
|
||||
organizationId?: number;
|
||||
organization?: { id?: number; name?: string } | null;
|
||||
supervisor?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@@ -163,13 +170,6 @@ const RECORD_TYPE_OPTIONS = [
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
const STUDENT_STATUS_MAP: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '在读', color: 'green' },
|
||||
graduated: { text: '已毕业', color: 'blue' },
|
||||
withdrawn: { text: '已退训', color: 'red' },
|
||||
archived: { text: '已归档', color: '#999' },
|
||||
};
|
||||
|
||||
const ENROLLMENT_STATUS_MAP: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '报读中', color: 'green' },
|
||||
completed: { text: '已结课', color: 'blue' },
|
||||
@@ -209,11 +209,6 @@ const getEnrollmentStatus = (value?: string | null): { text: string; color: stri
|
||||
return ENROLLMENT_STATUS_MAP[value] || { text: value, color: 'default' };
|
||||
};
|
||||
|
||||
const getStudentStatus = (value?: string | null): { text: string; color: string } => {
|
||||
if (!value) return { text: '-', color: 'default' };
|
||||
return STUDENT_STATUS_MAP[value] || { text: value, color: 'default' };
|
||||
};
|
||||
|
||||
const formatEnrollmentDisplayName = (enrollment: EnrollmentRecord): string =>
|
||||
enrollment.className ||
|
||||
(enrollment.courseCategory
|
||||
@@ -311,72 +306,274 @@ interface TabProps {
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const ProfileTab: React.FC<{
|
||||
data: ProfileData | null;
|
||||
const InlineArchiveSummary: React.FC<{
|
||||
studentId: number;
|
||||
student: StudentInfo;
|
||||
profile: ProfileData | null;
|
||||
result: ResultData | null;
|
||||
organizations: Array<{ id: number; name: string }>;
|
||||
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);
|
||||
}
|
||||
onViewSensitive: (fieldLabel: string, value: string) => void;
|
||||
}> = ({ studentId, student, profile, result, organizations, onRefresh, onViewSensitive }) => {
|
||||
const saveStudent = async (field: keyof StudentInfo, value: unknown) => {
|
||||
await api.put(`/students/${studentId}`, { [field]: value });
|
||||
message.success('学生资料已保存');
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const saveProfile = async (field: keyof ProfileData, value: unknown) => {
|
||||
await api.put(`/archive/${studentId}/profile`, { [field]: value });
|
||||
message.success('档案已保存');
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const saveResult = async (field: keyof ResultData, value: unknown) => {
|
||||
await api.put(`/archive/${studentId}/result`, { [field]: value });
|
||||
message.success('录取信息已保存');
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const admissionStatus = getOptionLabel(
|
||||
Object.entries(ADMISSION_STATUS_MAP).map(([value, meta]) => ({
|
||||
value,
|
||||
label: meta.text,
|
||||
})),
|
||||
result?.admissionStatus,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
targetCollege: data?.targetCollege ?? undefined,
|
||||
targetMajor: data?.targetMajor ?? undefined,
|
||||
subjectDirection: data?.subjectDirection ?? undefined,
|
||||
grade: data?.grade ?? 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="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>
|
||||
<Descriptions bordered column={3} size="small" style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="手机号">
|
||||
<EditableCell
|
||||
value={student.phone}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('phone', next)}
|
||||
>
|
||||
{student.phone ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskPhone(student.phone)}</span>
|
||||
<a onClick={() => onViewSensitive('电话', student.phone)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="姓名">
|
||||
<EditableCell
|
||||
value={student.name}
|
||||
permission="student:edit"
|
||||
required
|
||||
onSave={(next) => saveStudent('name', next)}
|
||||
>
|
||||
{student.name || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="学号">
|
||||
<EditableCell
|
||||
value={student.studentNo}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('studentNo', next)}
|
||||
>
|
||||
{student.studentNo || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="性别">
|
||||
<EditableCell
|
||||
value={student.gender}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('gender', next)}
|
||||
>
|
||||
{student.gender || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证号">
|
||||
<EditableCell
|
||||
value={student.idNumber}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('idNumber', next)}
|
||||
>
|
||||
{student.idNumber ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskIdNumber(student.idNumber)}</span>
|
||||
<a onClick={() => onViewSensitive('身份证号', student.idNumber)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="民族">
|
||||
<EditableCell
|
||||
value={student.ethnicity}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('ethnicity', next)}
|
||||
>
|
||||
{student.ethnicity || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="紧急联系人">
|
||||
<EditableCell
|
||||
value={student.emergencyContact}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('emergencyContact', next)}
|
||||
>
|
||||
{student.emergencyContact || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="紧急联系人电话">
|
||||
<EditableCell
|
||||
value={student.emergencyPhone}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('emergencyPhone', next)}
|
||||
>
|
||||
{student.emergencyPhone ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskPhone(student.emergencyPhone)}</span>
|
||||
<a onClick={() => onViewSensitive('紧急联系人电话', student.emergencyPhone || '')}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="所属机构">
|
||||
<EditableCell
|
||||
value={student.organizationId}
|
||||
editor="select"
|
||||
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('organizationId', next)}
|
||||
>
|
||||
{student.organization?.name ? <Tag color="purple">{student.organization.name}</Tag> : '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="负责人">
|
||||
<EditableCell
|
||||
value={student.supervisor}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveStudent('supervisor', next)}
|
||||
>
|
||||
{student.supervisor || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="目标院校">
|
||||
<EditableCell
|
||||
value={profile?.targetCollege}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('targetCollege', next)}
|
||||
>
|
||||
{profile?.targetCollege || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="目标专业">
|
||||
<EditableCell
|
||||
value={profile?.targetMajor}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('targetMajor', next)}
|
||||
>
|
||||
{profile?.targetMajor || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="选科方向">
|
||||
<EditableCell
|
||||
value={profile?.subjectDirection}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('subjectDirection', next)}
|
||||
>
|
||||
{profile?.subjectDirection || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="年级">
|
||||
<EditableCell
|
||||
value={profile?.grade}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('grade', next)}
|
||||
>
|
||||
{profile?.grade || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="建档日期">
|
||||
<EditableCell
|
||||
value={profile?.profileDate}
|
||||
editor="date"
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('profileDate', next)}
|
||||
>
|
||||
{profile?.profileDate || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="档案备注">
|
||||
<EditableCell
|
||||
value={profile?.notes}
|
||||
editor="textarea"
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveProfile('notes', next)}
|
||||
>
|
||||
{profile?.notes || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="文化课最终分">
|
||||
<EditableCell
|
||||
value={result?.cultureFinalScore}
|
||||
editor="number"
|
||||
min={0}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveResult('cultureFinalScore', next)}
|
||||
>
|
||||
{result?.cultureFinalScore ?? '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="专业课最终分">
|
||||
<EditableCell
|
||||
value={result?.professionalFinalScore}
|
||||
editor="number"
|
||||
min={0}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveResult('professionalFinalScore', next)}
|
||||
>
|
||||
{result?.professionalFinalScore ?? '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="录取状态">
|
||||
<EditableCell
|
||||
value={result?.admissionStatus}
|
||||
editor="select"
|
||||
options={Object.entries(ADMISSION_STATUS_MAP).map(([value, meta]) => ({
|
||||
value,
|
||||
label: meta.text,
|
||||
}))}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveResult('admissionStatus', next)}
|
||||
>
|
||||
{admissionStatus}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="录取院校">
|
||||
<EditableCell
|
||||
value={result?.admittedCollege}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveResult('admittedCollege', next)}
|
||||
>
|
||||
{result?.admittedCollege || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="录取专业">
|
||||
<EditableCell
|
||||
value={result?.admittedMajor}
|
||||
permission="student:edit"
|
||||
onSave={(next) => saveResult('admittedMajor', next)}
|
||||
>
|
||||
{result?.admittedMajor || '-'}
|
||||
</EditableCell>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1018,75 +1215,6 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -1202,6 +1330,7 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const [aggregateData, setAggregateData] = useState<StudentProfileAggregate | null>(null);
|
||||
const [organizations, setOrganizations] = useState<Array<{ id: number; name: string }>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -1221,6 +1350,15 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
void fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get('/organizations', { params: { includeArchived: 'false' } })
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handlePreviewReport = useCallback(async () => {
|
||||
try {
|
||||
const { html } = await api.get<{ html: string }>(`/archive/${studentId}/report-html`);
|
||||
@@ -1238,14 +1376,8 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
if (!aggregateData) return [];
|
||||
const { profile, enrollments, examScores, learningRecords, result, attachments, attendances } =
|
||||
aggregateData;
|
||||
const { enrollments, examScores, learningRecords, attachments, attendances } = aggregateData;
|
||||
return [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '扩展档案',
|
||||
children: <ProfileTab data={profile} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'enrollments',
|
||||
label: `报读班型 (${enrollments.length})`,
|
||||
@@ -1275,11 +1407,6 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
<LearningTab data={learningRecords} studentId={studentId} onRefresh={fetchData} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '录取归档',
|
||||
children: <ResultTab data={result} studentId={studentId} onRefresh={fetchData} />,
|
||||
},
|
||||
{
|
||||
key: 'attachments',
|
||||
label: `附件 (${attachments.length})`,
|
||||
@@ -1304,7 +1431,7 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { student, profile } = aggregateData;
|
||||
const { student, profile, result } = aggregateData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -1342,51 +1469,17 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Descriptions bordered column={3} size="small" style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="学号">{student.studentNo || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="电话">
|
||||
{student.phone ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskPhone(student.phone)}</span>
|
||||
<a onClick={() => handleViewSensitive('电话', student.phone)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="身份证号">
|
||||
{student.idNumber ? (
|
||||
<span>
|
||||
<span style={{ marginRight: 8 }}>{maskIdNumber(student.idNumber)}</span>
|
||||
<a onClick={() => handleViewSensitive('身份证号', student.idNumber)}>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{(() => {
|
||||
const status = getStudentStatus(student.status);
|
||||
return <Tag color={status.color}>{status.text}</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>
|
||||
)}
|
||||
</Descriptions>
|
||||
<InlineArchiveSummary
|
||||
studentId={studentId}
|
||||
student={student}
|
||||
profile={profile}
|
||||
result={result}
|
||||
organizations={organizations}
|
||||
onRefresh={fetchData}
|
||||
onViewSensitive={handleViewSensitive}
|
||||
/>
|
||||
|
||||
<Tabs defaultActiveKey="profile" items={tabItems} />
|
||||
<Tabs defaultActiveKey="enrollments" items={tabItems} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user