feat: add student followup report

This commit is contained in:
Codex
2026-06-30 16:42:23 +08:00
parent 7f97b52165
commit 7473c7a6d7
13 changed files with 567 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import {
bulkUpsertTenantStudents,
loadTenantClasses,
loadTenantStudentFollowups,
loadTenantStudentFollowupReport,
loadTenantStudentNotes,
loadTenantStudents,
loadTenantTeachers,
@@ -22,6 +23,7 @@ import {
type TenantStudentItem,
type TenantStudentNoteItem,
type TenantStudentCrmPushInput,
type TenantStudentFollowupReport,
type TenantTeacherItem,
type TenantMemberItem,
} from '@/services/tenantAdmin';
@@ -122,6 +124,7 @@ export default function TenantStudentsPage() {
const [bulkResult, setBulkResult] = useState<BulkOperationResult | null>(null);
const [assignResult, setAssignResult] = useState<BulkOperationResult | null>(null);
const [crmPushResult, setCrmPushResult] = useState<BulkOperationResult | null>(null);
const [followupReport, setFollowupReport] = useState<TenantStudentFollowupReport['item'] | null>(null);
const [scoped, setScoped] = useState(false);
const [busy, setBusy] = useState('');
const [error, setError] = useState('');
@@ -144,13 +147,15 @@ export default function TenantStudentsPage() {
limit: 80,
}),
loadTenantStudentFollowups({ status: 'open', limit: 30 }).catch(() => ({ items: [] })),
]).then(([classPayload, teacherPayload, memberPayload, studentPayload, followupPayload]) => {
loadTenantStudentFollowupReport({ timeRange: '30d', classId: nextClassId || undefined, limit: 8 }).catch(() => ({ item: null })),
]).then(([classPayload, teacherPayload, memberPayload, studentPayload, followupPayload, reportPayload]) => {
setClasses(classPayload.items || []);
setTeachers(teacherPayload.items || []);
setCrmAssignees((memberPayload.items || []).filter(item => crmAssignableRoles.includes(String(item.role || ''))));
setStudents(studentPayload.items || []);
setScoped(studentPayload.scoped === true);
setFollowups(followupPayload.items || []);
setFollowupReport(reportPayload.item || null);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '学生数据加载失败'));
}
@@ -538,6 +543,38 @@ export default function TenantStudentsPage() {
{!students.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-grid'>
<View className='admin-metric'>
<Text className='admin-metric-label'>30 </Text>
<Text className='admin-metric-value'>{followupReport?.summary?.total || 0}</Text>
<Text className='admin-row-meta'> {followupReport?.summary?.done || 0} · CRM {followupReport?.summary?.crmPushTasks || 0}</Text>
</View>
<View className='admin-metric'>
<Text className='admin-metric-label'></Text>
<Text className='admin-metric-value'>{followupReport?.summary?.openBacklog || 0}</Text>
<Text className='admin-row-meta'> {followupReport?.summary?.overdueBacklog || 0} · {followupReport?.summary?.highPriority || 0}</Text>
</View>
<View className='admin-metric'>
<Text className='admin-metric-label'></Text>
<Text className='admin-metric-value'>{Math.round((followupReport?.summary?.completionRate || 0) * 100)}%</Text>
<Text className='admin-row-meta'> {followupReport?.summary?.avgCompleteHours || 0} </Text>
</View>
</View>
<View className='admin-list'>
{(followupReport?.byAssignee || []).slice(0, 4).map((item, index) => (
<View className='admin-row' key={`${item.assignedToUserId || 'none'}:${index}`}>
<Text className='admin-row-main'>{String(item.assignedToName || '未指派')} · {String(item.assignedToRole || '-')}</Text>
<Text className='admin-row-meta'> {String(item.total || 0)} · {String(item.open || 0)} · {String(item.done || 0)} · {String(item.overdue || 0)}</Text>
</View>
))}
</View>
{(followupReport?.overdueItems || []).slice(0, 3).map(item => (
<Text className='admin-row-meta break-line' key={item.id}> · {item.title || '跟进任务'} · {item.studentName || item.studentUserId || '-'} · {item.assignedToName || '未指派'}</Text>
))}
</View>
<View className='admin-section'>
<Text className='admin-section-title'>CRM </Text>
<View className='admin-form-grid'>