diff --git a/js/algorithm.js b/js/algorithm.js index 63fe059..0e3458a 100644 --- a/js/algorithm.js +++ b/js/algorithm.js @@ -76,9 +76,10 @@ function scoreCandidate(employee, isoDate, allHistory, randomFn) { return score; } -// Returns [{ date, employeeId, manual: false }] for each assigned workday +// Returns { assignments: [{ date, employeeId, manual: false }], skippedDates: [] } export function generatePlan(workdays, employees, history, randomFn = Math.random) { const monthAssignments = []; + const skippedDates = []; for (const date of workdays) { const candidates = employees.filter(e => @@ -93,5 +94,5 @@ export function generatePlan(workdays, employees, history, randomFn = Math.rando monthAssignments.push({ date, employeeId: scored[0].employee.id, manual: false }); } - return monthAssignments; + return { assignments: monthAssignments, skippedDates }; } diff --git a/js/calendar.js b/js/calendar.js index 94c5013..8aa784e 100644 --- a/js/calendar.js +++ b/js/calendar.js @@ -98,7 +98,8 @@ export function renderCalendar() { state.calendar.holidays, state.calendar.companyClosures); const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0'); const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix)); - currentPlan = generatePlan(workdays, state.employees, historyWithoutMonth); + const result = generatePlan(workdays, state.employees, historyWithoutMonth); + currentPlan = result.assignments; addHistoryEntries(state, currentPlan); saveState(); renderGrid(wrapper, exportBtn); diff --git a/tests/algorithm.test.mjs b/tests/algorithm.test.mjs index 8e07b79..f0203c8 100644 --- a/tests/algorithm.test.mjs +++ b/tests/algorithm.test.mjs @@ -35,15 +35,15 @@ function emp(id, constraints = {}) { // generatePlan — basic assignment { const result = generatePlan(['2026-04-01'], [emp('alice'), emp('bob')], [], fixed); - assert.equal(result.length, 1, 'One workday → one assignment'); - assert.ok(['alice','bob'].includes(result[0].employeeId), 'Assigned to alice or bob'); + assert.equal(result.assignments.length, 1, 'One workday → one assignment'); + assert.ok(['alice','bob'].includes(result.assignments[0].employeeId), 'Assigned to alice or bob'); } // neverDays blocks employee { // 2026-04-01 is Wednesday (3) const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]}), emp('bob')], [], fixed); - assert.equal(result[0].employeeId, 'bob', 'alice blocked on Wednesday → bob assigned'); + assert.equal(result.assignments[0].employeeId, 'bob', 'alice blocked on Wednesday → bob assigned'); } // onlyDays restricts to specific days @@ -51,7 +51,7 @@ function emp(id, constraints = {}) { // 2026-04-01 = Wednesday (3), 2026-04-02 = Thursday (4) const employees = [emp('alice', {onlyDays:[3]}), emp('bob')]; const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed); - const aliceOnThursday = result.find(r => r.date === '2026-04-02' && r.employeeId === 'alice'); + const aliceOnThursday = result.assignments.find(r => r.date === '2026-04-02' && r.employeeId === 'alice'); assert.ok(!aliceOnThursday, 'alice with onlyDays:[3] not assigned on Thursday'); } @@ -62,7 +62,7 @@ function emp(id, constraints = {}) { emp('bob') ]; const result = generatePlan(['2026-04-01'], employees, [], fixed); - assert.equal(result[0].employeeId, 'bob', 'alice on vacation → bob assigned'); + assert.equal(result.assignments[0].employeeId, 'bob', 'alice on vacation → bob assigned'); } // maxPerMonth cap @@ -70,7 +70,7 @@ function emp(id, constraints = {}) { const days = ['2026-04-01','2026-04-02','2026-04-03']; const employees = [emp('alice', {maxPerMonth:1}), emp('bob'), emp('clara')]; const result = generatePlan(days, employees, [], fixed); - const aliceCount = result.filter(r => r.employeeId === 'alice').length; + const aliceCount = result.assignments.filter(r => r.employeeId === 'alice').length; assert.ok(aliceCount <= 1, 'alice maxPerMonth=1 → assigned at most once'); } @@ -80,14 +80,14 @@ function emp(id, constraints = {}) { const history = [{date:'2026-04-02', employeeId:'alice', manual:false}]; const employees = [emp('alice', {minGapDays:5}), emp('bob')]; const result = generatePlan(['2026-04-06'], employees, history, fixed); - assert.equal(result[0].employeeId, 'bob', 'alice within minGap (4<5) → bob assigned'); + assert.equal(result.assignments[0].employeeId, 'bob', 'alice within minGap (4<5) → bob assigned'); } // No candidates → day skipped { // 2026-04-01 = Wednesday (3), only employee blocked const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]})], [], fixed); - assert.equal(result.length, 0, 'No candidate → day skipped'); + assert.equal(result.assignments.length, 0, 'No candidate → day skipped'); } // Fairness: less history = higher priority @@ -96,7 +96,7 @@ function emp(id, constraints = {}) { date: '2026-04-0' + i, employeeId: 'alice', manual: false })); const result = generatePlan(['2026-05-04'], [emp('alice'), emp('bob')], history, fixed); - assert.equal(result[0].employeeId, 'bob', 'bob (0 history) beats alice (5 history)'); + assert.equal(result.assignments[0].employeeId, 'bob', 'bob (0 history) beats alice (5 history)'); } // companyClosures excluded from workdays @@ -112,13 +112,13 @@ function emp(id, constraints = {}) { // Both have same history (0) — alice's score gets -5 penalty, bob wins const employees = [emp('alice', {lowPriorityDays:[3]}), emp('bob')]; const result = generatePlan(['2026-04-01'], employees, [], fixed); - assert.equal(result[0].employeeId, 'bob', 'alice low priority on Wednesday → bob wins'); + assert.equal(result.assignments[0].employeeId, 'bob', 'alice low priority on Wednesday → bob wins'); } // manual: false on auto-assigned entries { const result = generatePlan(['2026-04-01'], [emp('alice')], [], fixed); - assert.equal(result[0].manual, false, 'auto-assignment sets manual: false'); + assert.equal(result.assignments[0].manual, false, 'auto-assignment sets manual: false'); } console.log('All algorithm tests passed.');