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

@@ -9341,6 +9341,21 @@ async function testTenantStudentOperations() {
assert.equal(completedFollowup.item?.status, 'done', 'teacher should complete scoped follow-up');
assert.equal(completedFollowup.item?.completedBy, TENANT_TEACHER_USER_ID, 'completed follow-up should record completer');
const teacherFollowupReport = await request('/api/tenant-admin/students/followups/report', {
userId: TENANT_TEACHER_USER_ID,
query: { timeRange: '30d', limit: 10 },
});
assert.equal(teacherFollowupReport.item?.filters?.scoped, true, 'teacher follow-up report should be class scoped');
assert.equal(teacherFollowupReport.item?.summary?.total >= 1, true, 'teacher follow-up report should include scoped follow-up');
assert.ok(
teacherFollowupReport.item?.byAssignee?.some(item => item.assignedToUserId === TENANT_TEACHER_USER_ID),
'teacher follow-up report should include scoped assignee summary',
);
assert.ok(
!teacherFollowupReport.item?.overdueItems?.some(item => item.studentUserId === SECOND_STUDENT_USER_ID),
'teacher follow-up report must not expose unscoped students',
);
const teacherOtherFollowupDenied = await request('/api/tenant-admin/students/followups', {
userId: TENANT_TEACHER_USER_ID,
method: 'PUT',
@@ -9354,6 +9369,20 @@ async function testTenantStudentOperations() {
});
assert.equal(teacherOtherFollowupDenied.code, 'STUDENT_SCOPE_REQUIRED', 'teacher should not create follow-up for unscoped student');
const invalidFollowupReportRange = await request('/api/tenant-admin/students/followups/report', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '365d' },
expectStatus: 400,
});
assert.equal(invalidFollowupReportRange.code, 'INVALID_FOLLOWUP_REPORT_RANGE', 'follow-up report should validate time range');
const invalidFollowupReportDate = await request('/api/tenant-admin/students/followups/report', {
userId: TENANT_ADMIN_USER_ID,
query: { startDate: '2026-02-31', endDate: '2026-03-01' },
expectStatus: 400,
});
assert.equal(invalidFollowupReportDate.code, 'INVALID_DATE_RANGE', 'follow-up report should reject impossible calendar dates');
const studentStatusDenied = await request('/api/tenant-admin/students/status', {
userId: TENANT_TEACHER_USER_ID,
method: 'POST',
@@ -9525,6 +9554,35 @@ async function testReferralAndCrmGrowth() {
'student CRM class scoped push should reject students outside the selected class',
);
const followupReport = await request('/api/tenant-admin/students/followups/report', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', limit: 10 },
});
assert.equal(followupReport.item?.summary?.crmPushTasks >= 2, true, 'follow-up report should count CRM push tasks');
assert.equal(followupReport.item?.crmQueue?.total >= 2, true, 'follow-up report should include student CRM queue totals');
assert.ok(
followupReport.item?.byAssignee?.some(item => item.assignedToUserId === TENANT_SALES_USER_ID && Number(item.crmPushTasks || 0) >= 2),
'follow-up report should group CRM push tasks by assignee',
);
assert.ok(
followupReport.item?.dailyTrend?.some(item => Number(item.crmPushCreated || 0) >= 1),
'follow-up report should include CRM push daily trend',
);
const classFollowupReport = await request('/api/tenant-admin/students/followups/report', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '30d', classId: ids.tenantClass, limit: 10 },
});
assert.equal(
classFollowupReport.item?.summary?.crmPushTasks >= 1,
true,
'class-scoped follow-up report should include in-class CRM push tasks',
);
assert.ok(
!classFollowupReport.item?.overdueItems?.some(item => item.studentUserId === SECOND_STUDENT_USER_ID),
'class-scoped follow-up report should not expose another class student overdue items',
);
const operatorCrmPushDenied = await request('/api/tenant-admin/students/crm-push', {
userId: TENANT_OPERATOR_USER_ID,
method: 'POST',