diff --git a/js/calendar.js b/js/calendar.js new file mode 100644 index 0000000..f1340b1 --- /dev/null +++ b/js/calendar.js @@ -0,0 +1,245 @@ +import { state } from './app.js'; +import { getWorkdays, generatePlan } from './algorithm.js'; +import { toggleHoliday, toggleClosure, addHistoryEntries } 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); + }); + + 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); + 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); + 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 { + state.history = state.history.filter(h => h.date !== iso); + currentPlan = currentPlan.filter(a => a.date !== iso); + } + 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); 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); 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'; +}