refactor: centralize shared date/name helpers in utils.js (#12)
New js/utils.js exports toISO, monthPrefix, getWeekday, formatDateDE and buildNameMap. algorithm.js, calendar.js, excel.js and history.js now import these instead of re-implementing ISO construction, weekday math, DD.MM.YYYY formatting and the id→name map inline. Adds tests/utils.test.mjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0efb9cb853
commit
fd9870871b
7 files changed
+71
-26
No files matched your search
+1
-10
@@ -1,13 +1,4 @@
|
|||||||
function toISO(year, month, day) {
|
import { toISO, getWeekday } from './utils.js';
|
||||||
return year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa, 7=So
|
|
||||||
function getWeekday(isoDate) {
|
|
||||||
const d = new Date(isoDate + 'T12:00:00Z');
|
|
||||||
const day = d.getUTCDay(); // 0=Sun
|
|
||||||
return day === 0 ? 7 : day;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOnVacation(employee, isoDate) {
|
function isOnVacation(employee, isoDate) {
|
||||||
return employee.constraints.vacations.some(v => v.from <= isoDate && isoDate <= v.to);
|
return employee.constraints.vacations.some(v => v.from <= isoDate && isoDate <= v.to);
|
||||||
|
|||||||
+8
-7
@@ -2,6 +2,7 @@ import { state, saveState } from './app.js';
|
|||||||
import { getWorkdays, generatePlan } from './algorithm.js';
|
import { getWorkdays, generatePlan } from './algorithm.js';
|
||||||
import { toggleHoliday, toggleClosure, addHistoryEntries, removeHistoryEntry } from './data.js';
|
import { toggleHoliday, toggleClosure, addHistoryEntries, removeHistoryEntry } from './data.js';
|
||||||
import { exportSchedule, downloadBlob } from './excel.js';
|
import { exportSchedule, downloadBlob } from './excel.js';
|
||||||
|
import { toISO, monthPrefix, getWeekday, buildNameMap } from './utils.js';
|
||||||
|
|
||||||
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
|
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
|
||||||
'Juli','August','September','Oktober','November','Dezember'];
|
'Juli','August','September','Oktober','November','Dezember'];
|
||||||
@@ -122,7 +123,7 @@ export function renderCalendar() {
|
|||||||
}
|
}
|
||||||
const workdays = getWorkdays(currentYear, currentMonth,
|
const workdays = getWorkdays(currentYear, currentMonth,
|
||||||
state.calendar.holidays, state.calendar.companyClosures);
|
state.calendar.holidays, state.calendar.companyClosures);
|
||||||
const prefix = currentYear + '-' + String(currentMonth).padStart(2, '0');
|
const prefix = monthPrefix(currentYear, currentMonth);
|
||||||
// Preserve manual picks for this month; only auto-entries get regenerated.
|
// Preserve manual picks for this month; only auto-entries get regenerated.
|
||||||
const manualEntries = state.history.filter(h => h.date.startsWith(prefix) && h.manual);
|
const manualEntries = state.history.filter(h => h.date.startsWith(prefix) && h.manual);
|
||||||
const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix));
|
const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix));
|
||||||
@@ -148,7 +149,7 @@ export function renderCalendar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPlanFromHistory() {
|
function loadPlanFromHistory() {
|
||||||
const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0');
|
const prefix = monthPrefix(currentYear, currentMonth);
|
||||||
currentPlan = state.history
|
currentPlan = state.history
|
||||||
.filter(h => h.date.startsWith(prefix))
|
.filter(h => h.date.startsWith(prefix))
|
||||||
.map(h => ({ ...h }));
|
.map(h => ({ ...h }));
|
||||||
@@ -158,7 +159,7 @@ function renderGrid(wrapper, exportBtn) {
|
|||||||
wrapper.replaceChildren();
|
wrapper.replaceChildren();
|
||||||
|
|
||||||
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
||||||
const empNameById = Object.fromEntries(state.employees.map(e => [e.id, e.name]));
|
const empNameById = buildNameMap(state.employees);
|
||||||
const updateExportVisibility = () => {
|
const updateExportVisibility = () => {
|
||||||
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
|
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
|
||||||
};
|
};
|
||||||
@@ -166,8 +167,8 @@ function renderGrid(wrapper, exportBtn) {
|
|||||||
// Build one day cell. Self-contained so a change to a single day re-renders
|
// Build one day cell. Self-contained so a change to a single day re-renders
|
||||||
// just that cell (cell.replaceWith) instead of rebuilding the whole grid.
|
// just that cell (cell.replaceWith) instead of rebuilding the whole grid.
|
||||||
function makeDayCell(d) {
|
function makeDayCell(d) {
|
||||||
const iso = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-' + String(d).padStart(2,'0');
|
const iso = toISO(currentYear, currentMonth, d);
|
||||||
const wdNum = (() => { const x = new Date(iso + 'T12:00:00Z').getUTCDay(); return x === 0 ? 7 : x; })();
|
const wdNum = getWeekday(iso);
|
||||||
const isWeekend = wdNum >= 6;
|
const isWeekend = wdNum >= 6;
|
||||||
const isHoliday = state.calendar.holidays.includes(iso);
|
const isHoliday = state.calendar.holidays.includes(iso);
|
||||||
const isClosure = state.calendar.companyClosures.includes(iso);
|
const isClosure = state.calendar.companyClosures.includes(iso);
|
||||||
@@ -291,8 +292,8 @@ function renderGrid(wrapper, exportBtn) {
|
|||||||
bodyGrid.className = 'calendar-body-grid';
|
bodyGrid.className = 'calendar-body-grid';
|
||||||
|
|
||||||
// Offset empty cells before first day
|
// Offset empty cells before first day
|
||||||
const firstISO = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-01';
|
const firstISO = toISO(currentYear, currentMonth, 1);
|
||||||
const firstWD = (() => { const d = new Date(firstISO + 'T12:00:00Z').getUTCDay(); return d === 0 ? 7 : d; })();
|
const firstWD = getWeekday(firstISO);
|
||||||
for (let i = 1; i < firstWD; i++) {
|
for (let i = 1; i < firstWD; i++) {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'cal-day empty-cell';
|
empty.className = 'cal-day empty-cell';
|
||||||
|
|||||||
+4
-5
@@ -1,3 +1,5 @@
|
|||||||
|
import { formatDateDE, buildNameMap } from './utils.js';
|
||||||
|
|
||||||
// Reads employee names from first column of xlsx (skips header row 0)
|
// Reads employee names from first column of xlsx (skips header row 0)
|
||||||
export function readEmployeeNames(arrayBuffer) {
|
export function readEmployeeNames(arrayBuffer) {
|
||||||
const wb = XLSX.read(arrayBuffer, { type: 'array' });
|
const wb = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
@@ -19,7 +21,7 @@ export function sanitizeCell(value) {
|
|||||||
// Pure helper: returns { names: [...], dates: [...] } columns for a schedule.
|
// Pure helper: returns { names: [...], dates: [...] } columns for a schedule.
|
||||||
// Multiple employees per date are joined alphabetically with " & ".
|
// Multiple employees per date are joined alphabetically with " & ".
|
||||||
export function buildScheduleColumns(assignments, employees) {
|
export function buildScheduleColumns(assignments, employees) {
|
||||||
const nameById = Object.fromEntries(employees.map(e => [e.id, e.name]));
|
const nameById = buildNameMap(employees);
|
||||||
const byDate = new Map();
|
const byDate = new Map();
|
||||||
for (const a of assignments) {
|
for (const a of assignments) {
|
||||||
if (!byDate.has(a.date)) byDate.set(a.date, []);
|
if (!byDate.has(a.date)) byDate.set(a.date, []);
|
||||||
@@ -29,10 +31,7 @@ export function buildScheduleColumns(assignments, employees) {
|
|||||||
const names = sortedDates.map(d =>
|
const names = sortedDates.map(d =>
|
||||||
sanitizeCell(byDate.get(d).slice().sort((x, y) => x.localeCompare(y, 'de')).join(' & '))
|
sanitizeCell(byDate.get(d).slice().sort((x, y) => x.localeCompare(y, 'de')).join(' & '))
|
||||||
);
|
);
|
||||||
const dates = sortedDates.map(d => {
|
const dates = sortedDates.map(formatDateDE);
|
||||||
const [y, m, day] = d.split('-');
|
|
||||||
return day + '.' + m + '.' + y;
|
|
||||||
});
|
|
||||||
return { names, dates };
|
return { names, dates };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,7 @@
|
|||||||
import { state, saveState } from './app.js';
|
import { state, saveState } from './app.js';
|
||||||
import { loadFromJSON, saveToJSON, removeHistoryEntry } from './data.js';
|
import { loadFromJSON, saveToJSON, removeHistoryEntry } from './data.js';
|
||||||
import { downloadBlob } from './excel.js';
|
import { downloadBlob } from './excel.js';
|
||||||
|
import { formatDateDE, buildNameMap } from './utils.js';
|
||||||
|
|
||||||
export function renderDataTab() {
|
export function renderDataTab() {
|
||||||
const container = document.getElementById('tab-data');
|
const container = document.getElementById('tab-data');
|
||||||
@@ -102,7 +103,7 @@ function renderHistoryTable() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameById = Object.fromEntries(state.employees.map(e => [e.id, e.name]));
|
const nameById = buildNameMap(state.employees);
|
||||||
const sorted = [...state.history].sort((a, b) => {
|
const sorted = [...state.history].sort((a, b) => {
|
||||||
const dateCmp = b.date.localeCompare(a.date);
|
const dateCmp = b.date.localeCompare(a.date);
|
||||||
if (dateCmp !== 0) return dateCmp;
|
if (dateCmp !== 0) return dateCmp;
|
||||||
@@ -124,11 +125,10 @@ function renderHistoryTable() {
|
|||||||
|
|
||||||
const tbody = document.createElement('tbody');
|
const tbody = document.createElement('tbody');
|
||||||
for (const h of sorted) {
|
for (const h of sorted) {
|
||||||
const [y, m, d] = h.date.split('-');
|
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
const dateTd = document.createElement('td');
|
const dateTd = document.createElement('td');
|
||||||
dateTd.textContent = d + '.' + m + '.' + y;
|
dateTd.textContent = formatDateDE(h.date);
|
||||||
|
|
||||||
const nameTd = document.createElement('td');
|
const nameTd = document.createElement('td');
|
||||||
nameTd.textContent = nameById[h.employeeId] ?? '(unbekannt)'; // textContent: safe
|
nameTd.textContent = nameById[h.employeeId] ?? '(unbekannt)'; // textContent: safe
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
// Shared pure helpers used across modules.
|
||||||
|
|
||||||
|
// "YYYY-MM-DD" from numeric parts.
|
||||||
|
export function toISO(year, month, day) {
|
||||||
|
return year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// "YYYY-MM" month prefix (used to filter history by month).
|
||||||
|
export function monthPrefix(year, month) {
|
||||||
|
return year + '-' + String(month).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weekday for an ISO date string: 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa, 7=So.
|
||||||
|
export function getWeekday(isoDate) {
|
||||||
|
const d = new Date(isoDate + 'T12:00:00Z').getUTCDay(); // 0=Sun
|
||||||
|
return d === 0 ? 7 : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "DD.MM.YYYY" from an ISO date string.
|
||||||
|
export function formatDateDE(isoDate) {
|
||||||
|
const [y, m, d] = isoDate.split('-');
|
||||||
|
return d + '.' + m + '.' + y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// { [id]: name } lookup map from an employees array.
|
||||||
|
export function buildNameMap(employees) {
|
||||||
|
return Object.fromEntries(employees.map(e => [e.id, e.name]));
|
||||||
|
}
|
||||||
+1
-1
@@ -5,6 +5,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx serve . -p 3001",
|
"start": "npx serve . -p 3001",
|
||||||
"test": "node tests/algorithm.test.mjs && node tests/data.test.mjs && node tests/excel.test.mjs"
|
"test": "node tests/utils.test.mjs && node tests/algorithm.test.mjs && node tests/data.test.mjs && node tests/excel.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { toISO, monthPrefix, getWeekday, formatDateDE, buildNameMap } from '../js/utils.js';
|
||||||
|
|
||||||
|
// toISO: zero-pads month and day
|
||||||
|
assert.equal(toISO(2026, 4, 1), '2026-04-01', 'toISO pads single digits');
|
||||||
|
assert.equal(toISO(2026, 12, 31), '2026-12-31', 'toISO keeps two-digit parts');
|
||||||
|
|
||||||
|
// monthPrefix: YYYY-MM
|
||||||
|
assert.equal(monthPrefix(2026, 5), '2026-05', 'monthPrefix pads month');
|
||||||
|
|
||||||
|
// getWeekday: 1=Mon … 7=Sun (2026-04-01 is a Wednesday → 3; 2026-04-05 Sunday → 7)
|
||||||
|
assert.equal(getWeekday('2026-04-01'), 3, 'Wednesday → 3');
|
||||||
|
assert.equal(getWeekday('2026-04-05'), 7, 'Sunday → 7');
|
||||||
|
assert.equal(getWeekday('2026-04-06'), 1, 'Monday → 1');
|
||||||
|
|
||||||
|
// formatDateDE: ISO → DD.MM.YYYY
|
||||||
|
assert.equal(formatDateDE('2026-04-01'), '01.04.2026', 'formatDateDE DD.MM.YYYY');
|
||||||
|
|
||||||
|
// buildNameMap: id → name lookup
|
||||||
|
assert.deepEqual(
|
||||||
|
buildNameMap([{ id: 'a', name: 'Alice' }, { id: 'b', name: 'Bob' }]),
|
||||||
|
{ a: 'Alice', b: 'Bob' },
|
||||||
|
'buildNameMap maps id to name'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('All utils tests passed.');
|
||||||
Reference in new issue
Block a user