refactor: generatePlan returns {assignments, skippedDates}

This commit is contained in:
ferdi2go committed 2026-04-28 09:55:25 +00:00
1 parent e4ae866375
commit 790ea72987
3 files changed
+16 -14

No files matched your search

+3 -2
View File
@@ -76,9 +76,10 @@ function scoreCandidate(employee, isoDate, allHistory, randomFn) {
return score; 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) { export function generatePlan(workdays, employees, history, randomFn = Math.random) {
const monthAssignments = []; const monthAssignments = [];
const skippedDates = [];
for (const date of workdays) { for (const date of workdays) {
const candidates = employees.filter(e => 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 }); monthAssignments.push({ date, employeeId: scored[0].employee.id, manual: false });
} }
return monthAssignments; return { assignments: monthAssignments, skippedDates };
} }
+2 -1
View File
@@ -98,7 +98,8 @@ export function renderCalendar() {
state.calendar.holidays, state.calendar.companyClosures); state.calendar.holidays, state.calendar.companyClosures);
const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0'); const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0');
const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix)); 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); addHistoryEntries(state, currentPlan);
saveState(); saveState();
renderGrid(wrapper, exportBtn); renderGrid(wrapper, exportBtn);
+11 -11
View File
@@ -35,15 +35,15 @@ function emp(id, constraints = {}) {
// generatePlan — basic assignment // generatePlan — basic assignment
{ {
const result = generatePlan(['2026-04-01'], [emp('alice'), emp('bob')], [], fixed); const result = generatePlan(['2026-04-01'], [emp('alice'), emp('bob')], [], fixed);
assert.equal(result.length, 1, 'One workday → one assignment'); assert.equal(result.assignments.length, 1, 'One workday → one assignment');
assert.ok(['alice','bob'].includes(result[0].employeeId), 'Assigned to alice or bob'); assert.ok(['alice','bob'].includes(result.assignments[0].employeeId), 'Assigned to alice or bob');
} }
// neverDays blocks employee // neverDays blocks employee
{ {
// 2026-04-01 is Wednesday (3) // 2026-04-01 is Wednesday (3)
const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]}), emp('bob')], [], fixed); 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 // onlyDays restricts to specific days
@@ -51,7 +51,7 @@ function emp(id, constraints = {}) {
// 2026-04-01 = Wednesday (3), 2026-04-02 = Thursday (4) // 2026-04-01 = Wednesday (3), 2026-04-02 = Thursday (4)
const employees = [emp('alice', {onlyDays:[3]}), emp('bob')]; const employees = [emp('alice', {onlyDays:[3]}), emp('bob')];
const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed); 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'); assert.ok(!aliceOnThursday, 'alice with onlyDays:[3] not assigned on Thursday');
} }
@@ -62,7 +62,7 @@ function emp(id, constraints = {}) {
emp('bob') emp('bob')
]; ];
const result = generatePlan(['2026-04-01'], employees, [], fixed); 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 // maxPerMonth cap
@@ -70,7 +70,7 @@ function emp(id, constraints = {}) {
const days = ['2026-04-01','2026-04-02','2026-04-03']; const days = ['2026-04-01','2026-04-02','2026-04-03'];
const employees = [emp('alice', {maxPerMonth:1}), emp('bob'), emp('clara')]; const employees = [emp('alice', {maxPerMonth:1}), emp('bob'), emp('clara')];
const result = generatePlan(days, employees, [], fixed); 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'); 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 history = [{date:'2026-04-02', employeeId:'alice', manual:false}];
const employees = [emp('alice', {minGapDays:5}), emp('bob')]; const employees = [emp('alice', {minGapDays:5}), emp('bob')];
const result = generatePlan(['2026-04-06'], employees, history, fixed); 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 // No candidates → day skipped
{ {
// 2026-04-01 = Wednesday (3), only employee blocked // 2026-04-01 = Wednesday (3), only employee blocked
const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]})], [], fixed); 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 // Fairness: less history = higher priority
@@ -96,7 +96,7 @@ function emp(id, constraints = {}) {
date: '2026-04-0' + i, employeeId: 'alice', manual: false date: '2026-04-0' + i, employeeId: 'alice', manual: false
})); }));
const result = generatePlan(['2026-05-04'], [emp('alice'), emp('bob')], history, fixed); 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 // 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 // Both have same history (0) — alice's score gets -5 penalty, bob wins
const employees = [emp('alice', {lowPriorityDays:[3]}), emp('bob')]; const employees = [emp('alice', {lowPriorityDays:[3]}), emp('bob')];
const result = generatePlan(['2026-04-01'], employees, [], fixed); 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 // manual: false on auto-assigned entries
{ {
const result = generatePlan(['2026-04-01'], [emp('alice')], [], fixed); 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.'); console.log('All algorithm tests passed.');