perf(calendar): re-render only the changed day cell, not the whole grid (#10)

Extracts the day-cell construction into a self-contained makeDayCell(d). Slot
changes and holiday/closure toggles now replace just that one cell
(cell.replaceWith) plus update export-button visibility, instead of rebuilding
the entire month grid and re-attaching every listener on each interaction.
Structural changes (month/year/slot-count) still do a full render.

Faithful extraction; verified headless that the page loads without JS errors
and tests pass. Interactive render coverage to follow in the test-layer issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Opus 4.8 committed 2026-06-03 12:51:16 +00:00
1 parent c076b5e1ce
commit 8cd8908f38
1 file changed
+49 -51
+49 -51
View File
@@ -159,44 +159,13 @@ function renderGrid(wrapper, exportBtn) {
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 = Object.fromEntries(state.employees.map(e => [e.id, e.name]));
const entriesByDate = {}; const updateExportVisibility = () => {
for (const a of currentPlan) { exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
if (!entriesByDate[a.date]) entriesByDate[a.date] = []; };
entriesByDate[a.date].push(a);
}
// Sort each date's entries alphabetically by employee name (stable slot order)
for (const date of Object.keys(entriesByDate)) {
entriesByDate[date].sort((x, y) =>
(empNameById[x.employeeId] ?? '').localeCompare(empNameById[y.employeeId] ?? '', 'de')
);
}
// Day-of-week header row // Build one day cell. Self-contained so a change to a single day re-renders
const headerGrid = document.createElement('div'); // just that cell (cell.replaceWith) instead of rebuilding the whole grid.
headerGrid.className = 'calendar-header-grid'; function makeDayCell(d) {
WD_LABELS.forEach(lbl => {
const cell = document.createElement('div');
cell.className = 'cal-header-cell';
cell.textContent = lbl;
headerGrid.appendChild(cell);
});
wrapper.appendChild(headerGrid);
// Body grid
const bodyGrid = document.createElement('div');
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; })();
for (let i = 1; i < firstWD; i++) {
const empty = document.createElement('div');
empty.className = 'cal-day empty-cell';
bodyGrid.appendChild(empty);
}
// Day cells
for (let d = 1; d <= daysInMonth; d++) {
const iso = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-' + String(d).padStart(2,'0'); 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 wdNum = (() => { const x = new Date(iso + 'T12:00:00Z').getUTCDay(); return x === 0 ? 7 : x; })();
const isWeekend = wdNum >= 6; const isWeekend = wdNum >= 6;
@@ -209,7 +178,6 @@ function renderGrid(wrapper, exportBtn) {
else if (isHoliday) cell.classList.add('holiday'); else if (isHoliday) cell.classList.add('holiday');
else if (isClosure) cell.classList.add('closure'); else if (isClosure) cell.classList.add('closure');
// Day number + weekday label
const dayNum = document.createElement('div'); const dayNum = document.createElement('div');
dayNum.className = 'day-num'; dayNum.className = 'day-num';
dayNum.textContent = d + ' '; dayNum.textContent = d + ' ';
@@ -219,9 +187,11 @@ function renderGrid(wrapper, exportBtn) {
dayNum.appendChild(wdSpan); dayNum.appendChild(wdSpan);
cell.appendChild(dayNum); cell.appendChild(dayNum);
// Re-render only this cell after a change affecting this day
const refresh = () => { cell.replaceWith(makeDayCell(d)); updateExportVisibility(); };
if (!isWeekend) { if (!isWeekend) {
if (isHoliday || isClosure) { if (isHoliday || isClosure) {
// Show label + remove button
const tag = document.createElement('div'); const tag = document.createElement('div');
tag.className = 'cal-day-tag-label'; tag.className = 'cal-day-tag-label';
tag.textContent = isHoliday ? 'Feiertag' : 'Betriebsurlaub'; tag.textContent = isHoliday ? 'Feiertag' : 'Betriebsurlaub';
@@ -232,18 +202,18 @@ function renderGrid(wrapper, exportBtn) {
if (isHoliday) toggleHoliday(state, iso); if (isHoliday) toggleHoliday(state, iso);
else toggleClosure(state, iso); else toggleClosure(state, iso);
saveState(); saveState();
renderGrid(wrapper, exportBtn); refresh();
}); });
tag.appendChild(removeBtn); tag.appendChild(removeBtn);
cell.appendChild(tag); cell.appendChild(tag);
} else { } else {
// N slots per day
const slotCount = state.settings.moderatorsPerDay; const slotCount = state.settings.moderatorsPerDay;
const dayEntries = entriesByDate[iso] ?? []; // already alphabetically sorted const dayEntries = currentPlan
.filter(a => a.date === iso)
.sort((x, y) =>
(empNameById[x.employeeId] ?? '').localeCompare(empNameById[y.employeeId] ?? '', 'de'));
const slotValues = []; // employeeId per slot index, '' if empty const slotValues = []; // employeeId per slot index, '' if empty
for (let s = 0; s < slotCount; s++) { for (let s = 0; s < slotCount; s++) slotValues.push(dayEntries[s]?.employeeId ?? '');
slotValues.push(dayEntries[s]?.employeeId ?? '');
}
for (let slotIdx = 0; slotIdx < slotCount; slotIdx++) { for (let slotIdx = 0; slotIdx < slotCount; slotIdx++) {
const sel = document.createElement('select'); const sel = document.createElement('select');
@@ -278,36 +248,64 @@ function renderGrid(wrapper, exportBtn) {
currentPlan.push(entry); currentPlan.push(entry);
} }
saveState(); saveState();
renderGrid(wrapper, exportBtn); refresh();
}); });
cell.appendChild(sel); cell.appendChild(sel);
} }
// FT / BU mark buttons
const btnRow = document.createElement('div'); const btnRow = document.createElement('div');
btnRow.className = 'cal-btn-row'; btnRow.className = 'cal-btn-row';
const ftBtn = document.createElement('button'); const ftBtn = document.createElement('button');
ftBtn.type = 'button'; ftBtn.type = 'button';
ftBtn.textContent = 'Feiertag'; ftBtn.textContent = 'Feiertag';
ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); renderGrid(wrapper, exportBtn); }); ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); refresh(); });
const buBtn = document.createElement('button'); const buBtn = document.createElement('button');
buBtn.type = 'button'; buBtn.type = 'button';
buBtn.textContent = 'Betriebsurl.'; buBtn.textContent = 'Betriebsurl.';
buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); renderGrid(wrapper, exportBtn); }); buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); refresh(); });
btnRow.append(ftBtn, buBtn); btnRow.append(ftBtn, buBtn);
cell.appendChild(btnRow); cell.appendChild(btnRow);
} }
} }
bodyGrid.appendChild(cell); return cell;
}
// Day-of-week header row
const headerGrid = document.createElement('div');
headerGrid.className = 'calendar-header-grid';
WD_LABELS.forEach(lbl => {
const cell = document.createElement('div');
cell.className = 'cal-header-cell';
cell.textContent = lbl;
headerGrid.appendChild(cell);
});
wrapper.appendChild(headerGrid);
// Body grid
const bodyGrid = document.createElement('div');
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; })();
for (let i = 1; i < firstWD; i++) {
const empty = document.createElement('div');
empty.className = 'cal-day empty-cell';
bodyGrid.appendChild(empty);
}
// Day cells — each cell can re-render itself; no full-grid rebuild per click
for (let d = 1; d <= daysInMonth; d++) {
bodyGrid.appendChild(makeDayCell(d));
} }
wrapper.appendChild(bodyGrid); wrapper.appendChild(bodyGrid);
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none'; updateExportVisibility();
} }
function renderSkippedWarning(box, skippedDates) { function renderSkippedWarning(box, skippedDates) {