From 539d06ff028b1d5802382c85376a1b34a2b63f3a Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:04:31 +0000 Subject: [PATCH 1/7] feat(substitution): add H1 and H2 tags to substitution candidates --- apps/chronos/src/routes/timetable/lesson.ts | 63 ++++++++++++++----- apps/iris/public/locales/en/translation.json | 2 + apps/iris/public/locales/hu/translation.json | 2 + .../components/admin/substitution-dialog.tsx | 34 ++++++---- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index d185f96..806ff30 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -385,7 +385,8 @@ const substitutionCandidatesRequestSchema = z.object({ }); const substitutionCandidateSchema = z.object({ - hasLessonBeforeOrAfter: z.boolean(), + hasH1: z.boolean(), + hasH2: z.boolean(), teacher: z.object({ firstName: z.string(), id: z.string(), @@ -691,7 +692,10 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( .where(arrayOverlaps(lesson.teacherIds, candidateTeacherIds)); const enrichedCandidateLessons = await enrichLessons(candidateLessons); - const candidateLessonsByTeacherId = new Map(); + const candidateLessonsByTeacherId = new Map< + string, + Array<{ period: number; subjectShort: string | null }> + >(); const candidateTeacherIdSet = new Set(candidateTeacherIds); for (const candidateLesson of enrichedCandidateLessons) { @@ -713,22 +717,24 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( continue; } + const subjectShort = candidateLesson.subject?.short ?? null; + for (const lessonTeacher of candidateLesson.teachers) { if (!candidateTeacherIdSet.has(lessonTeacher.id)) { continue; } - const periods = candidateLessonsByTeacherId.get(lessonTeacher.id) ?? []; - periods.push(currentPeriod); - candidateLessonsByTeacherId.set(lessonTeacher.id, periods); + const lessons = candidateLessonsByTeacherId.get(lessonTeacher.id) ?? []; + lessons.push({ period: currentPeriod, subjectShort }); + candidateLessonsByTeacherId.set(lessonTeacher.id, lessons); } } const substituteCandidates = candidateTeachers .map((currentTeacher) => { - const occupiedPeriods = new Set( - candidateLessonsByTeacherId.get(currentTeacher.id) ?? [] - ); + const teacherLessons = + candidateLessonsByTeacherId.get(currentTeacher.id) ?? []; + const occupiedPeriods = new Set(teacherLessons.map((l) => l.period)); const hasPeriodConflict = selectedPeriods.some((selectedPeriod) => occupiedPeriods.has(selectedPeriod) @@ -738,12 +744,38 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( return null; } - const hasLessonBeforeOrAfter = - occupiedPeriods.has(minPeriod - 1) || - occupiedPeriods.has(maxPeriod + 1); + let hasH1 = false; + let hasH2 = false; + for (const candidateLesson of teacherLessons) { + if ( + candidateLesson.period === minPeriod - 1 && + candidateLesson.subjectShort === 'H1' + ) { + hasH1 = true; + } + if ( + candidateLesson.period === minPeriod - 1 && + candidateLesson.subjectShort === 'H2' + ) { + hasH2 = true; + } + if ( + candidateLesson.period === maxPeriod + 1 && + candidateLesson.subjectShort === 'H1' + ) { + hasH1 = true; + } + if ( + candidateLesson.period === maxPeriod + 1 && + candidateLesson.subjectShort === 'H2' + ) { + hasH2 = true; + } + } return { - hasLessonBeforeOrAfter, + hasH1, + hasH2, teacher: currentTeacher, }; }) @@ -752,8 +784,11 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( candidate !== null ) .sort((a, b) => { - if (a.hasLessonBeforeOrAfter !== b.hasLessonBeforeOrAfter) { - return a.hasLessonBeforeOrAfter ? -1 : 1; + if (a.hasH1 !== b.hasH1) { + return a.hasH1 ? -1 : 1; + } + if (a.hasH2 !== b.hasH2) { + return a.hasH2 ? -1 : 1; } const aName = `${a.teacher.lastName} ${a.teacher.firstName}`; diff --git a/apps/iris/public/locales/en/translation.json b/apps/iris/public/locales/en/translation.json index d3a46ea..f78b54b 100644 --- a/apps/iris/public/locales/en/translation.json +++ b/apps/iris/public/locales/en/translation.json @@ -243,6 +243,8 @@ "selectLessonsFirst": "Select teacher and lessons first.", "loadingSubstituteTeachers": "Filtering substitute teachers...", "nearbyTeacherTag": "H1 / H2", + "h1Tag": "H1", + "h2Tag": "H2", "cancelled": "Cancelled", "merged": "Merged Lesson", "lessons": "Affected Lessons", diff --git a/apps/iris/public/locales/hu/translation.json b/apps/iris/public/locales/hu/translation.json index abdf96a..09432d9 100644 --- a/apps/iris/public/locales/hu/translation.json +++ b/apps/iris/public/locales/hu/translation.json @@ -243,6 +243,8 @@ "selectLessonsFirst": "Előbb válassz tanárt és órát.", "loadingSubstituteTeachers": "Helyettesítő tanárok szűrése…", "nearbyTeacherTag": "H1 / H2", + "h1Tag": "H1", + "h2Tag": "H2", "cancelled": "Elmarad", "merged": "Összevont óra", "lessons": "Érintett órák", diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index 19a52ce..a846057 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -199,24 +199,32 @@ export function SubstitutionDialog({ const candidates = substituteCandidatesQuery.data?.substituteCandidates ?? []; - return candidates.map((candidate) => ({ - isNearby: candidate.hasLessonBeforeOrAfter, - label: `${candidate.teacher.firstName} ${candidate.teacher.lastName} (${candidate.teacher.short})${ - candidate.hasLessonBeforeOrAfter - ? ` - ${t('substitution.nearbyTeacherTag')}` - : '' - }`, - value: candidate.teacher.id, - })); + return candidates.map((candidate) => { + let tag = ''; + if (candidate.hasH1 && candidate.hasH2) { + tag = ` - ${t('substitution.nearbyTeacherTag')}`; + } else if (candidate.hasH1) { + tag = ` - ${t('substitution.h1Tag')}`; + } else if (candidate.hasH2) { + tag = ` - ${t('substitution.h2Tag')}`; + } + + return { + hasH1: candidate.hasH1, + hasH2: candidate.hasH2, + label: `${candidate.teacher.firstName} ${candidate.teacher.lastName} (${candidate.teacher.short})${tag}`, + value: candidate.teacher.id, + }; + }); }, [substituteCandidatesQuery.data, t]); const sortedSubstituteOptions = useMemo(() => { return [...substituteOptions].sort((a, b) => { - if (a.isNearby && !b.isNearby) { - return -1; + if (a.hasH1 !== b.hasH1) { + return a.hasH1 ? -1 : 1; } - if (!a.isNearby && b.isNearby) { - return 1; + if (a.hasH2 !== b.hasH2) { + return a.hasH2 ? -1 : 1; } return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); }); From fe1b24d8fb6f584c924a4b4c1f631fe559ff6364 Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:30:58 +0000 Subject: [PATCH 2/7] refactor(substitution): simplify conflict detection logic --- apps/chronos/src/routes/timetable/lesson.ts | 80 ++++++++++--------- .../components/admin/substitution-dialog.tsx | 16 +++- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index 806ff30..8994e38 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -652,9 +652,6 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( missingTeacherId ); - const minPeriod = Math.min(...selectedPeriods); - const maxPeriod = Math.max(...selectedPeriods); - const candidateTeacherIds = normalizedTeacherIds.filter( (teacherId) => teacherId !== missingTeacherId ); @@ -734,45 +731,42 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( .map((currentTeacher) => { const teacherLessons = candidateLessonsByTeacherId.get(currentTeacher.id) ?? []; - const occupiedPeriods = new Set(teacherLessons.map((l) => l.period)); - - const hasPeriodConflict = selectedPeriods.some((selectedPeriod) => - occupiedPeriods.has(selectedPeriod) - ); - - if (hasPeriodConflict) { - return null; - } let hasH1 = false; let hasH2 = false; - for (const candidateLesson of teacherLessons) { - if ( - candidateLesson.period === minPeriod - 1 && - candidateLesson.subjectShort === 'H1' - ) { - hasH1 = true; - } - if ( - candidateLesson.period === minPeriod - 1 && - candidateLesson.subjectShort === 'H2' - ) { - hasH2 = true; - } - if ( - candidateLesson.period === maxPeriod + 1 && - candidateLesson.subjectShort === 'H1' - ) { - hasH1 = true; + let hasConflict = false; + + for (const selectedPeriod of selectedPeriods) { + const lessonsAtPeriod = teacherLessons.filter( + (l) => l.period === selectedPeriod + ); + + if (lessonsAtPeriod.length === 0) { + // Teacher is free at this period — no conflict + continue; } - if ( - candidateLesson.period === maxPeriod + 1 && - candidateLesson.subjectShort === 'H2' - ) { - hasH2 = true; + + const hasH1AtPeriod = lessonsAtPeriod.some( + (l) => l.subjectShort === 'H1' + ); + const hasH2AtPeriod = lessonsAtPeriod.some( + (l) => l.subjectShort === 'H2' + ); + + if (hasH1AtPeriod) hasH1 = true; + if (hasH2AtPeriod) hasH2 = true; + + // If none of the lessons at this period are H1 or H2, it's a real conflict + if (!hasH1AtPeriod && !hasH2AtPeriod) { + hasConflict = true; + break; } } + if (hasConflict) { + return null; + } + return { hasH1, hasH2, @@ -784,11 +778,19 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( candidate !== null ) .sort((a, b) => { - if (a.hasH1 !== b.hasH1) { - return a.hasH1 ? -1 : 1; + if (a.hasH1 && !b.hasH1) { + return -1; + } + if (!a.hasH1 && b.hasH1) { + return 1; } - if (a.hasH2 !== b.hasH2) { - return a.hasH2 ? -1 : 1; + if (!a.hasH1 && !b.hasH1) { + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; + } } const aName = `${a.teacher.lastName} ${a.teacher.firstName}`; diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index a846057..c82f32e 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -220,11 +220,19 @@ export function SubstitutionDialog({ const sortedSubstituteOptions = useMemo(() => { return [...substituteOptions].sort((a, b) => { - if (a.hasH1 !== b.hasH1) { - return a.hasH1 ? -1 : 1; + if (a.hasH1 && !b.hasH1) { + return -1; } - if (a.hasH2 !== b.hasH2) { - return a.hasH2 ? -1 : 1; + if (!a.hasH1 && b.hasH1) { + return 1; + } + if (!a.hasH1 && !b.hasH1) { + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; + } } return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); }); From 832a15d1b12a574c0d7f4a56f7f74f02bbed6d4c Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:31:25 +0000 Subject: [PATCH 3/7] fix(substitution): simplify conflict detection logic --- apps/chronos/src/routes/timetable/lesson.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index 8994e38..29ae358 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -757,7 +757,7 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( if (hasH2AtPeriod) hasH2 = true; // If none of the lessons at this period are H1 or H2, it's a real conflict - if (!hasH1AtPeriod && !hasH2AtPeriod) { + if (!(hasH1AtPeriod || hasH2AtPeriod)) { hasConflict = true; break; } @@ -784,7 +784,7 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( if (!a.hasH1 && b.hasH1) { return 1; } - if (!a.hasH1 && !b.hasH1) { + if (!(a.hasH1 || b.hasH1)) { if (a.hasH2 && !b.hasH2) { return -1; } From f8c111fc26f71c57947925b821e61259e4269ebf Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:32:28 +0000 Subject: [PATCH 4/7] refactor(substitution): simplify H1 comparison logic --- apps/iris/src/components/admin/substitution-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index c82f32e..ee694d8 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -226,7 +226,7 @@ export function SubstitutionDialog({ if (!a.hasH1 && b.hasH1) { return 1; } - if (!a.hasH1 && !b.hasH1) { + if (!(a.hasH1 || b.hasH1)) { if (a.hasH2 && !b.hasH2) { return -1; } From aec31f3e552537e68c3a413cccfd05866fc5b0b1 Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:46:20 +0000 Subject: [PATCH 5/7] refactor(substitution): extract sorting logic for substitute options --- apps/chronos/src/routes/timetable/lesson.ts | 224 ++++++++++-------- .../components/admin/substitution-dialog.tsx | 39 +-- 2 files changed, 147 insertions(+), 116 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index 29ae358..85a7ac3 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -407,6 +407,122 @@ const substitutionCandidatesResponseSchema = z.object({ const substitutionCandidatesType = '@unit SubstitutionCandidatesResult @field(.availableLessons, List) @field(.parallelLessons, List) @field(.substituteCandidates, List)'; +type CandidateLessonEntry = { + period: number; + subjectShort: string | null; +}; + +function computeCandidateFlags( + teacherLessons: CandidateLessonEntry[], + selectedPeriods: number[] +): { hasConflict: boolean; hasH1: boolean; hasH2: boolean } { + let hasH1 = false; + let hasH2 = false; + + for (const selectedPeriod of selectedPeriods) { + const lessonsAtPeriod = teacherLessons.filter( + (l) => l.period === selectedPeriod + ); + + if (lessonsAtPeriod.length === 0) { + continue; + } + + const hasH1AtPeriod = lessonsAtPeriod.some((l) => l.subjectShort === 'H1'); + const hasH2AtPeriod = lessonsAtPeriod.some((l) => l.subjectShort === 'H2'); + + if (hasH1AtPeriod) { + hasH1 = true; + } + if (hasH2AtPeriod) { + hasH2 = true; + } + + if (!(hasH1AtPeriod || hasH2AtPeriod)) { + return { hasConflict: true, hasH1, hasH2 }; + } + } + + return { hasConflict: false, hasH1, hasH2 }; +} + +type SubstitutionCandidate = { + hasH1: boolean; + hasH2: boolean; + teacher: { firstName: string; id: string; lastName: string; short: string }; +}; + +function compareSubstituteCandidates( + a: SubstitutionCandidate, + b: SubstitutionCandidate +): number { + if (a.hasH1 && !b.hasH1) { + return -1; + } + if (!a.hasH1 && b.hasH1) { + return 1; + } + if (!(a.hasH1 || b.hasH1)) { + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; + } + } + const aName = `${a.teacher.lastName} ${a.teacher.firstName}`; + const bName = `${b.teacher.lastName} ${b.teacher.firstName}`; + return aName.localeCompare(bName); +} + +async function buildCandidateLessonsMap( + candidateTeacherIds: string[], + weekday: number +): Promise> { + const candidateLessons = await db + .select() + .from(lesson) + .where(arrayOverlaps(lesson.teacherIds, candidateTeacherIds)); + + const enrichedCandidateLessons = await enrichLessons(candidateLessons); + const map = new Map(); + const candidateTeacherIdSet = new Set(candidateTeacherIds); + + for (const candidateLesson of enrichedCandidateLessons) { + if ( + !( + candidateLesson.day && + isMatchingWeekday( + weekday, + candidateLesson.day.name, + candidateLesson.day.short + ) + ) + ) { + continue; + } + + const currentPeriod = candidateLesson.period?.period; + if (typeof currentPeriod !== 'number') { + continue; + } + + const subjectShort = candidateLesson.subject?.short ?? null; + + for (const lessonTeacher of candidateLesson.teachers) { + if (!candidateTeacherIdSet.has(lessonTeacher.id)) { + continue; + } + + const lessons = map.get(lessonTeacher.id) ?? []; + lessons.push({ period: currentPeriod, subjectShort }); + map.set(lessonTeacher.id, lessons); + } + } + + return map; +} + async function getParallelLessons( selectedLessons: Awaited>, missingTeacherId: string @@ -683,93 +799,22 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( .from(teacher) .where(inArray(teacher.id, candidateTeacherIds)); - const candidateLessons = await db - .select() - .from(lesson) - .where(arrayOverlaps(lesson.teacherIds, candidateTeacherIds)); - - const enrichedCandidateLessons = await enrichLessons(candidateLessons); - const candidateLessonsByTeacherId = new Map< - string, - Array<{ period: number; subjectShort: string | null }> - >(); - const candidateTeacherIdSet = new Set(candidateTeacherIds); - - for (const candidateLesson of enrichedCandidateLessons) { - if ( - !( - candidateLesson.day && - isMatchingWeekday( - weekday, - candidateLesson.day.name, - candidateLesson.day.short - ) - ) - ) { - continue; - } - - const currentPeriod = candidateLesson.period?.period; - if (typeof currentPeriod !== 'number') { - continue; - } - - const subjectShort = candidateLesson.subject?.short ?? null; - - for (const lessonTeacher of candidateLesson.teachers) { - if (!candidateTeacherIdSet.has(lessonTeacher.id)) { - continue; - } - - const lessons = candidateLessonsByTeacherId.get(lessonTeacher.id) ?? []; - lessons.push({ period: currentPeriod, subjectShort }); - candidateLessonsByTeacherId.set(lessonTeacher.id, lessons); - } - } + const candidateLessonsByTeacherId = await buildCandidateLessonsMap( + candidateTeacherIds, + weekday + ); const substituteCandidates = candidateTeachers .map((currentTeacher) => { const teacherLessons = candidateLessonsByTeacherId.get(currentTeacher.id) ?? []; - - let hasH1 = false; - let hasH2 = false; - let hasConflict = false; - - for (const selectedPeriod of selectedPeriods) { - const lessonsAtPeriod = teacherLessons.filter( - (l) => l.period === selectedPeriod - ); - - if (lessonsAtPeriod.length === 0) { - // Teacher is free at this period — no conflict - continue; - } - - const hasH1AtPeriod = lessonsAtPeriod.some( - (l) => l.subjectShort === 'H1' - ); - const hasH2AtPeriod = lessonsAtPeriod.some( - (l) => l.subjectShort === 'H2' - ); - - if (hasH1AtPeriod) hasH1 = true; - if (hasH2AtPeriod) hasH2 = true; - - // If none of the lessons at this period are H1 or H2, it's a real conflict - if (!(hasH1AtPeriod || hasH2AtPeriod)) { - hasConflict = true; - break; - } - } - - if (hasConflict) { + const flags = computeCandidateFlags(teacherLessons, selectedPeriods); + if (flags.hasConflict) { return null; } - return { - hasH1, - hasH2, + hasH1: flags.hasH1, + hasH2: flags.hasH2, teacher: currentTeacher, }; }) @@ -777,26 +822,7 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( (candidate): candidate is NonNullable => candidate !== null ) - .sort((a, b) => { - if (a.hasH1 && !b.hasH1) { - return -1; - } - if (!a.hasH1 && b.hasH1) { - return 1; - } - if (!(a.hasH1 || b.hasH1)) { - if (a.hasH2 && !b.hasH2) { - return -1; - } - if (!a.hasH2 && b.hasH2) { - return 1; - } - } - - const aName = `${a.teacher.lastName} ${a.teacher.firstName}`; - const bName = `${b.teacher.lastName} ${b.teacher.firstName}`; - return aName.localeCompare(bName); - }); + .sort(compareSubstituteCandidates); return c.json< SuccessResponse<{ diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index ee694d8..fc3d6c4 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -89,6 +89,27 @@ const initialState = ( substituter: item?.substitution.substituter ?? null, }); +function compareSubOptions( + a: { hasH1: boolean; hasH2: boolean; label: string }, + b: { hasH1: boolean; hasH2: boolean; label: string } +): number { + if (a.hasH1 && !b.hasH1) { + return -1; + } + if (!a.hasH1 && b.hasH1) { + return 1; + } + if (!(a.hasH1 || b.hasH1)) { + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; + } + } + return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); +} + export function SubstitutionDialog({ item, onOpenChange, @@ -219,23 +240,7 @@ export function SubstitutionDialog({ }, [substituteCandidatesQuery.data, t]); const sortedSubstituteOptions = useMemo(() => { - return [...substituteOptions].sort((a, b) => { - if (a.hasH1 && !b.hasH1) { - return -1; - } - if (!a.hasH1 && b.hasH1) { - return 1; - } - if (!(a.hasH1 || b.hasH1)) { - if (a.hasH2 && !b.hasH2) { - return -1; - } - if (!a.hasH2 && b.hasH2) { - return 1; - } - } - return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); - }); + return [...substituteOptions].sort(compareSubOptions); }, [substituteOptions]); const isCreate = !item; From e94755229ede82789b4471c53d5ef6ae596a5370 Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:48:12 +0000 Subject: [PATCH 6/7] refactor(substitution): simplify comparison logic for H2 --- apps/chronos/src/routes/timetable/lesson.ts | 17 +++++++++-------- .../components/admin/substitution-dialog.tsx | 12 +++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index 85a7ac3..012c8c6 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -438,7 +438,10 @@ function computeCandidateFlags( hasH2 = true; } - if (!(hasH1AtPeriod || hasH2AtPeriod)) { + const hasConflictLesson = lessonsAtPeriod.some( + (l) => l.subjectShort !== 'H1' && l.subjectShort !== 'H2' + ); + if (hasConflictLesson) { return { hasConflict: true, hasH1, hasH2 }; } } @@ -462,13 +465,11 @@ function compareSubstituteCandidates( if (!a.hasH1 && b.hasH1) { return 1; } - if (!(a.hasH1 || b.hasH1)) { - if (a.hasH2 && !b.hasH2) { - return -1; - } - if (!a.hasH2 && b.hasH2) { - return 1; - } + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; } const aName = `${a.teacher.lastName} ${a.teacher.firstName}`; const bName = `${b.teacher.lastName} ${b.teacher.firstName}`; diff --git a/apps/iris/src/components/admin/substitution-dialog.tsx b/apps/iris/src/components/admin/substitution-dialog.tsx index fc3d6c4..80f3702 100644 --- a/apps/iris/src/components/admin/substitution-dialog.tsx +++ b/apps/iris/src/components/admin/substitution-dialog.tsx @@ -99,13 +99,11 @@ function compareSubOptions( if (!a.hasH1 && b.hasH1) { return 1; } - if (!(a.hasH1 || b.hasH1)) { - if (a.hasH2 && !b.hasH2) { - return -1; - } - if (!a.hasH2 && b.hasH2) { - return 1; - } + if (a.hasH2 && !b.hasH2) { + return -1; + } + if (!a.hasH2 && b.hasH2) { + return 1; } return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); } From 732066063e36b227bbed96956e8eef40f53ddda1 Mon Sep 17 00:00:00 2001 From: Dasa122 Date: Tue, 23 Jun 2026 21:50:45 +0000 Subject: [PATCH 7/7] refactor(substitution): exclude specific subjects from candidates --- apps/chronos/src/routes/timetable/lesson.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/chronos/src/routes/timetable/lesson.ts b/apps/chronos/src/routes/timetable/lesson.ts index 012c8c6..0e85bf6 100644 --- a/apps/chronos/src/routes/timetable/lesson.ts +++ b/apps/chronos/src/routes/timetable/lesson.ts @@ -724,8 +724,10 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( const enrichedMissingTeacherLessons = await enrichLessons( missingTeacherLessons ); - const availableLessons = enrichedMissingTeacherLessons.filter( - (currentLesson) => + const EXCLUDED_SUBJECT_SHORTS = ['H1', 'H2']; + + const availableLessons = enrichedMissingTeacherLessons + .filter((currentLesson) => currentLesson.day ? isMatchingWeekday( weekday, @@ -733,7 +735,14 @@ export const getSubstitutionCandidates = timetableFactory.createHandlers( currentLesson.day.short ) : false - ); + ) + .filter( + (currentLesson) => + !( + currentLesson.subject?.short && + EXCLUDED_SUBJECT_SHORTS.includes(currentLesson.subject.short) + ) + ); const selectedLessonIdsSet = new Set(selectedLessonIds); const selectedLessons = availableLessons.filter((currentLesson) =>