import { matchesScheduleSearch, scheduleCoachBusy, scheduleCourseAvailable, schedulePreflightCandidates, scheduleRowActions } from './operations-schedule.component'; import { ScheduleAppointment } from '../admin.models'; import { scheduleMatchesStatus, scheduleStatusLabel, scheduleStatusTone } from './operations-schedule-status'; describe('排课搜索匹配', () => { it('支持学员姓名和课程名称的模糊匹配', () => { expect(matchesScheduleSearch('宇昊', ['万宇昊', '135****5177'])).toBeTrue(); expect(matchesScheduleSearch('pep', ['人教版(PEP)三年级起2022年录'])).toBeTrue(); }); it('支持使用完整手机号匹配脱敏手机号', () => { expect(matchesScheduleSearch('13576045177', ['135****5177'])).toBeTrue(); expect(matchesScheduleSearch('13876045177', ['135****5177'])).toBeFalse(); }); it('支持用户或课程数字 ID 搜索', () => { expect(matchesScheduleSearch('2048', ['会员甲', 2048])).toBeTrue(); expect(matchesScheduleSearch('1009', ['自然拼读', 1008])).toBeFalse(); }); }); describe('排课状态视觉分类', () => { const now = Date.parse('2026-08-20T10:00:00+08:00'); it('将已经结束但仍待开始的预约统一识别为逾期未完成', () => { const item = { cancelled: false, statusCode: 0, statusLabel: '待开始', endsAt: '2026-08-20T01:00:00.000Z' }; expect(scheduleStatusTone(item, now)).toBe('status-overdue'); expect(scheduleStatusLabel(item, now)).toBe('逾期未完成'); }); it('已开始但超过结束时间的记录使用同一个逾期状态', () => { const item = { cancelled: false, statusCode: 10, statusLabel: '上课中', endsAt: '2026-08-20T01:00:00.000Z' }; expect(scheduleStatusTone(item, now)).toBe('status-overdue'); expect(scheduleStatusLabel(item, now)).toBe('逾期未完成'); }); it('按原系统状态链区分陪练结束、学员确认和最终完课', () => { expect(scheduleStatusTone({ cancelled: false, statusCode: 0, endsAt: '2026-08-21T01:00:00.000Z' }, now)).toBe('status-upcoming'); expect(scheduleStatusTone({ cancelled: false, statusCode: 10, endsAt: '2026-08-20T03:00:00.000Z' }, now)).toBe('status-live'); expect(scheduleStatusTone({ cancelled: false, statusCode: 11, endsAt: '' }, now)).toBe('status-coach-ended'); expect(scheduleStatusLabel({ cancelled: false, statusCode: 11, statusLabel: '已完成', endsAt: '' }, now)).toBe('陪练已结束'); expect(scheduleStatusTone({ cancelled: false, statusCode: 20, endsAt: '' }, now)).toBe('status-confirmed'); expect(scheduleStatusLabel({ cancelled: false, statusCode: 20, statusLabel: '已确认', endsAt: '' }, now)).toBe('学员已确认'); expect(scheduleStatusTone({ cancelled: false, statusCode: 30, endsAt: '' }, now)).toBe('status-completed'); expect(scheduleStatusLabel({ cancelled: false, statusCode: 30, statusLabel: '已归档', endsAt: '' }, now)).toBe('已完课'); expect(scheduleStatusTone({ cancelled: false, statusCode: 40, endsAt: '' }, now)).toBe('status-archived'); expect(scheduleStatusLabel({ cancelled: false, statusCode: 40, statusLabel: '已点评', endsAt: '' }, now)).toBe('已归档'); expect(scheduleStatusTone({ cancelled: true, statusCode: 0, endsAt: '' }, now)).toBe('status-cancelled'); }); it('支持运营人员按业务状态和待分配异常筛选', () => { const upcoming = { cancelled: false, statusCode: 0, endsAt: '2026-08-21T01:00:00.000Z', coachId: 88, coachAccountMissing: false }; expect(scheduleMatchesStatus(upcoming, 'upcoming', now)).toBeTrue(); expect(scheduleMatchesStatus(upcoming, 'overdue', now)).toBeFalse(); expect(scheduleMatchesStatus({ ...upcoming, coachId: 0 }, 'unassigned', now)).toBeTrue(); expect(scheduleMatchesStatus({ ...upcoming, coachAccountMissing: true }, 'unassigned', now)).toBeTrue(); }); }); describe('排课运营操作职责', () => { it('待开始课程只能修改或取消,不能在后台直接伪造完课', () => { expect(scheduleRowActions({ cancelled: false, statusCode: 0, readOnly: false })).toEqual({ canEdit: true, canCancel: true, canRestore: false, canComplete: false, completeLabel: '', }); }); it('只在上课中或陪练已结束时提供补录入口', () => { expect(scheduleRowActions({ cancelled: false, statusCode: 10, readOnly: false })).toEqual(jasmine.objectContaining({ canComplete: true, completeLabel: '补录结束' })); expect(scheduleRowActions({ cancelled: false, statusCode: 11, readOnly: false })).toEqual(jasmine.objectContaining({ canComplete: true, completeLabel: '补录上课记录' })); expect(scheduleRowActions({ cancelled: false, statusCode: 20, readOnly: false }).canComplete).toBeFalse(); expect(scheduleRowActions({ cancelled: false, statusCode: 30, readOnly: false }).canComplete).toBeFalse(); }); it('实时旧系统记录保持只读', () => { expect(scheduleRowActions({ cancelled: false, statusCode: 10, readOnly: true })).toEqual({ canEdit: false, canCancel: false, canRestore: false, canComplete: false, completeLabel: '', }); }); }); describe('会员课程归属', () => { it('只允许选择当前会员已经绑定的课程', () => { const course = { memberIds: [101, 202], availableToAll: false }; expect(scheduleCourseAvailable(course, 101)).toBeTrue(); expect(scheduleCourseAvailable(course, 303)).toBeFalse(); expect(scheduleCourseAvailable(course, 0)).toBeFalse(); }); it('保留原系统写作课程 398 的全员可选兼容规则', () => { expect(scheduleCourseAvailable({ memberIds: [], availableToAll: true }, 303)).toBeTrue(); }); }); describe('时间优先陪练候选', () => { const appointments = [{ targetId: 'appointment-1', coachId: 88, cancelled: false, statusCode: 0, startsAt: '2026-09-02T02:00:00.000Z', endsAt: '2026-09-02T03:00:00.000Z', }] as ScheduleAppointment[]; it('按真实起止区间识别跨课型重叠', () => { expect(scheduleCoachBusy(appointments, 88, '2026-09-02T10:30:00+08:00', 30)).toBeTrue(); expect(scheduleCoachBusy(appointments, 88, '2026-09-02T11:00:00+08:00', 60)).toBeFalse(); }); it('忽略取消、归档和当前编辑记录', () => { expect(scheduleCoachBusy(appointments, 88, '2026-09-02T10:00:00+08:00', 60, 'appointment-1')).toBeFalse(); expect(scheduleCoachBusy([{ ...appointments[0], cancelled: true }] as never[], 88, '2026-09-02T10:00:00+08:00', 60)).toBeFalse(); expect(scheduleCoachBusy([{ ...appointments[0], statusCode: 30 }] as never[], 88, '2026-09-02T10:00:00+08:00', 60)).toBeFalse(); }); }); describe('批量排课逐项预检', () => { it('按课次去重冲突原因并保留可提交项', () => { const results = schedulePreflightCandidates({ candidates: [ { index: 0, startsAt: '2026-09-01T02:00:00.000Z', endsAt: '2026-09-01T03:00:00.000Z' }, { index: 1, startsAt: '2026-09-08T02:00:00.000Z', endsAt: '2026-09-08T03:00:00.000Z' }, ], conflicts: [ { index: 0, reason: '会员时间冲突' }, { index: 0, reason: '陪练时间冲突' }, { index: 0, reason: '会员时间冲突' }, ], }); expect(results[0]).toEqual(jasmine.objectContaining({ index: 0, available: false, reasons: ['会员时间冲突', '陪练时间冲突'] })); expect(results[1]).toEqual(jasmine.objectContaining({ index: 1, available: true, reasons: [] })); }); });