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) {
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
"scripts": {
|
||||
"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