Files
Meeting-Mixer/js/calendar.js

255 lines
9.2 KiB
JavaScript

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';
}