| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- 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: [] }));
- });
- });
|