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:
ferdi2goandClaude Opus 4.8 committed 2026-06-03 13:01:28 +00:00
1 parent 0efb9cb853
commit fd9870871b
7 files changed
+71 -26

No files matched your search

+1 -10
View File
@@ -1,13 +1,4 @@
function toISO(year, month, day) {
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;
}
import { toISO, getWeekday } from './utils.js';
function isOnVacation(employee, isoDate) {
return employee.constraints.vacations.some(v => v.from <= isoDate && isoDate <= v.to);
+8 -7
View File
@@ -2,6 +2,7 @@ import { state, saveState } from './app.js';
import { getWorkdays, generatePlan } from './algorithm.js';
import { toggleHoliday, toggleClosure, addHistoryEntries, removeHistoryEntry } from './data.js';
import { exportSchedule, downloadBlob } from './excel.js';
import { toISO, monthPrefix, getWeekday, buildNameMap } from './utils.js';
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
@@ -122,7 +123,7 @@ export function renderCalendar() {
}
const workdays = getWorkdays(currentYear, currentMonth,
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.
const manualEntries = state.history.filter(h => h.date.startsWith(prefix) && h.manual);
const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix));
@@ -148,7 +149,7 @@ export function renderCalendar() {
}
function loadPlanFromHistory() {
const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0');
const prefix = monthPrefix(currentYear, currentMonth);
currentPlan = state.history
.filter(h => h.date.startsWith(prefix))
.map(h => ({ ...h }));
@@ -158,7 +159,7 @@ function renderGrid(wrapper, exportBtn) {
wrapper.replaceChildren();
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 = () => {
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
// just that cell (cell.replaceWith) instead of rebuilding the whole grid.
function makeDayCell(d) {
const iso = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-' + String(d).padStart(2,'0');
const wdNum = (() => { const x = new Date(iso + 'T12:00:00Z').getUTCDay(); return x === 0 ? 7 : x; })();
const iso = toISO(currentYear, currentMonth, d);
const wdNum = getWeekday(iso);
const isWeekend = wdNum >= 6;
const isHoliday = state.calendar.holidays.includes(iso);
const isClosure = state.calendar.companyClosures.includes(iso);
@@ -291,8 +292,8 @@ function renderGrid(wrapper, exportBtn) {
bodyGrid.className = 'calendar-body-grid';
// Offset empty cells before first day
const firstISO = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-01';
const firstWD = (() => { const d = new Date(firstISO + 'T12:00:00Z').getUTCDay(); return d === 0 ? 7 : d; })();
const firstISO = toISO(currentYear, currentMonth, 1);
const firstWD = getWeekday(firstISO);
for (let i = 1; i < firstWD; i++) {
const empty = document.createElement('div');
empty.className = 'cal-day empty-cell';
+4 -5
View File
@@ -1,3 +1,5 @@
import { formatDateDE, buildNameMap } from './utils.js';
// Reads employee names from first column of xlsx (skips header row 0)
export function readEmployeeNames(arrayBuffer) {
const wb = XLSX.read(arrayBuffer, { type: 'array' });
@@ -19,7 +21,7 @@ export function sanitizeCell(value) {
// Pure helper: returns { names: [...], dates: [...] } columns for a schedule.
// Multiple employees per date are joined alphabetically with " & ".
export function buildScheduleColumns(assignments, employees) {
const nameById = Object.fromEntries(employees.map(e => [e.id, e.name]));
const nameById = buildNameMap(employees);
const byDate = new Map();
for (const a of assignments) {
if (!byDate.has(a.date)) byDate.set(a.date, []);
@@ -29,10 +31,7 @@ export function buildScheduleColumns(assignments, employees) {
const names = sortedDates.map(d =>
sanitizeCell(byDate.get(d).slice().sort((x, y) => x.localeCompare(y, 'de')).join(' & '))
);
const dates = sortedDates.map(d => {
const [y, m, day] = d.split('-');
return day + '.' + m + '.' + y;
});
const dates = sortedDates.map(formatDateDE);
return { names, dates };
}
+3 -3
View File
@@ -1,6 +1,7 @@
import { state, saveState } from './app.js';
import { loadFromJSON, saveToJSON, removeHistoryEntry } from './data.js';
import { downloadBlob } from './excel.js';
import { formatDateDE, buildNameMap } from './utils.js';
export function renderDataTab() {
const container = document.getElementById('tab-data');
@@ -102,7 +103,7 @@ function renderHistoryTable() {
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 dateCmp = b.date.localeCompare(a.date);
if (dateCmp !== 0) return dateCmp;
@@ -124,11 +125,10 @@ function renderHistoryTable() {
const tbody = document.createElement('tbody');
for (const h of sorted) {
const [y, m, d] = h.date.split('-');
const tr = document.createElement('tr');
const dateTd = document.createElement('td');
dateTd.textContent = d + '.' + m + '.' + y;
dateTd.textContent = formatDateDE(h.date);
const nameTd = document.createElement('td');
nameTd.textContent = nameById[h.employeeId] ?? '(unbekannt)'; // textContent: safe
+28
View File
@@ -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]));
}