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'; const MONTHS = ['Januar','Februar','März','April','Mai','Juni', 'Juli','August','September','Oktober','November','Dezember']; const WD_LABELS = ['Mo','Di','Mi','Do','Fr','Sa','So']; let currentYear = 2026; let currentMonth = Math.min(Math.max(new Date().getMonth() + 1, 1), 12); let currentPlan = []; // [{ date, employeeId, manual }] for displayed month export function renderCalendar() { const container = document.getElementById('tab-planning'); container.replaceChildren(); // Title const title = document.createElement('h2'); title.className = 'section-title'; title.textContent = 'Monatsplanung'; container.appendChild(title); // Controls row const controls = document.createElement('div'); controls.className = 'row'; const monthLabel = document.createElement('label'); monthLabel.textContent = 'Monat:'; const monthSel = document.createElement('select'); MONTHS.forEach((m, i) => { const opt = document.createElement('option'); opt.value = String(i + 1); opt.textContent = m; if (i + 1 === currentMonth) opt.selected = true; monthSel.appendChild(opt); }); const yearLabel = document.createElement('label'); yearLabel.textContent = 'Jahr:'; const yearSel = document.createElement('select'); const yearOpt = document.createElement('option'); yearOpt.value = '2026'; yearOpt.textContent = '2026'; yearOpt.selected = true; yearSel.appendChild(yearOpt); const genBtn = document.createElement('button'); genBtn.type = 'button'; genBtn.className = 'primary'; genBtn.textContent = 'Plan generieren'; const exportBtn = document.createElement('button'); exportBtn.type = 'button'; exportBtn.textContent = 'Als Excel exportieren'; exportBtn.style.display = 'none'; controls.append(monthLabel, monthSel, yearLabel, yearSel, genBtn, exportBtn); container.appendChild(controls); // Legend const legend = document.createElement('div'); legend.className = 'row'; legend.style.cssText = 'font-size:12px;color:#6b7280'; legend.textContent = 'FT = Feiertag markieren | BU = Betriebsurlaub markieren'; container.appendChild(legend); // Calendar grid wrapper const wrapper = document.createElement('div'); wrapper.className = 'calendar-outer'; wrapper.id = 'calendar-wrapper'; container.appendChild(wrapper); loadPlanFromHistory(); renderGrid(wrapper, exportBtn); monthSel.addEventListener('change', () => { currentMonth = parseInt(monthSel.value); loadPlanFromHistory(); renderGrid(wrapper, exportBtn); }); yearSel.addEventListener('change', () => { currentYear = parseInt(yearSel.value); loadPlanFromHistory(); renderGrid(wrapper, exportBtn); }); genBtn.addEventListener('click', () => { if (state.employees.length === 0) { alert('Bitte zuerst Mitarbeiter hinzufügen.'); return; } const workdays = getWorkdays(currentYear, currentMonth, state.calendar.holidays, state.calendar.companyClosures); const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0'); const historyWithoutMonth = state.history.filter(h => !h.date.startsWith(prefix)); currentPlan = generatePlan(workdays, state.employees, historyWithoutMonth); addHistoryEntries(state, currentPlan); saveState(); renderGrid(wrapper, exportBtn); }); exportBtn.addEventListener('click', () => { const blob = exportSchedule(currentPlan, state.employees); downloadBlob(blob, 'Moderationsplan-' + MONTHS[currentMonth-1] + '-' + currentYear + '.xlsx'); }); } function loadPlanFromHistory() { const prefix = currentYear + '-' + String(currentMonth).padStart(2,'0'); currentPlan = state.history .filter(h => h.date.startsWith(prefix)) .map(h => ({ ...h })); } function renderGrid(wrapper, exportBtn) { wrapper.replaceChildren(); const daysInMonth = new Date(currentYear, currentMonth, 0).getDate(); const nameById = Object.fromEntries(state.employees.map(e => [e.id, e.name])); const planByDate = Object.fromEntries(currentPlan.map(a => [a.date, a.employeeId])); // 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.style.cssText = 'text-align:center;font-weight:600;font-size:12px;color:#6b7280;padding:4px'; 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 wdNum = (() => { const x = new Date(iso + 'T12:00:00Z').getUTCDay(); return x === 0 ? 7 : x; })(); const isWeekend = wdNum >= 6; const isHoliday = state.calendar.holidays.includes(iso); const isClosure = state.calendar.companyClosures.includes(iso); const assigneeId = planByDate[iso]; const cell = document.createElement('div'); cell.className = 'cal-day'; if (isWeekend) cell.classList.add('weekend'); else if (isHoliday) cell.classList.add('holiday'); else if (isClosure) cell.classList.add('closure'); // Day number + weekday label const dayNum = document.createElement('div'); dayNum.className = 'day-num'; dayNum.textContent = d + ' '; const wdSpan = document.createElement('span'); wdSpan.className = 'day-tag'; wdSpan.textContent = WD_LABELS[wdNum - 1]; dayNum.appendChild(wdSpan); cell.appendChild(dayNum); if (!isWeekend) { if (isHoliday || isClosure) { // Show label + remove button const tag = document.createElement('div'); tag.style.cssText = 'font-size:11px;margin-top:4px'; tag.textContent = isHoliday ? 'Feiertag' : 'Betriebsurlaub'; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.textContent = '✕'; removeBtn.style.cssText = 'font-size:9px;padding:1px 4px;margin-left:4px'; removeBtn.addEventListener('click', () => { if (isHoliday) toggleHoliday(state, iso); else toggleClosure(state, iso); saveState(); renderGrid(wrapper, exportBtn); }); tag.appendChild(removeBtn); cell.appendChild(tag); } else { // Assignee dropdown const sel = document.createElement('select'); sel.style.cssText = 'width:100%;margin-top:4px;font-size:11px'; const emptyOpt = document.createElement('option'); emptyOpt.value = ''; emptyOpt.textContent = '— Leer —'; sel.appendChild(emptyOpt); for (const emp of state.employees) { const opt = document.createElement('option'); opt.value = emp.id; opt.textContent = emp.name; // textContent: safe if (emp.id === assigneeId) opt.selected = true; sel.appendChild(opt); } sel.addEventListener('change', () => { const empId = sel.value; if (empId) { const entry = { date: iso, employeeId: empId, manual: true }; addHistoryEntries(state, [entry]); const idx = currentPlan.findIndex(a => a.date === iso); if (idx >= 0) currentPlan[idx] = entry; else currentPlan.push(entry); } else { removeHistoryEntry(state, iso); currentPlan = currentPlan.filter(a => a.date !== iso); } saveState(); exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none'; }); cell.appendChild(sel); // FT / BU mark buttons const btnRow = document.createElement('div'); btnRow.style.cssText = 'margin-top:4px;display:flex;gap:2px'; const ftBtn = document.createElement('button'); ftBtn.type = 'button'; ftBtn.textContent = 'FT'; ftBtn.style.cssText = 'font-size:9px;padding:1px 4px'; ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); renderGrid(wrapper, exportBtn); }); const buBtn = document.createElement('button'); buBtn.type = 'button'; buBtn.textContent = 'BU'; buBtn.style.cssText = 'font-size:9px;padding:1px 4px'; buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); renderGrid(wrapper, exportBtn); }); btnRow.append(ftBtn, buBtn); cell.appendChild(btnRow); } } bodyGrid.appendChild(cell); } wrapper.appendChild(bodyGrid); exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none'; }