import { api } from "./api.js"; const NAV = [ { key: "timeline", index: "01", label: "Zeitachse" }, { key: "capacity", index: "02", label: "Kapazitaet" }, { key: "projects", index: "03", label: "Projekte & Aufgaben" }, { key: "people", index: "04", label: "Team" } ]; const PERSON_PALETTE = ["#2F6F4F", "#C4622D", "#3E5C50", "#8A6D3B", "#4C5B7A", "#A63B2A"]; const PROJECT_PALETTE = ["#3E5C50", "#2F6F4F", "#8A6D3B", "#4C5B7A", "#A63B2A", "#C4622D"]; const PRIORITY_LABEL = { hoch: "Hoch", mittel: "Mittel", niedrig: "Niedrig" }; const MONTH_LABELS = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; const MONTH_LABELS_BY_KEY = { "01": "Jan", "02": "Feb", "03": "Mär", "04": "Apr", "05": "Mai", "06": "Jun", "07": "Jul", "08": "Aug", "09": "Sep", "10": "Okt", "11": "Nov", "12": "Dez" }; function isLeap(year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } function dayOfYear(dateStr, year) { const d = new Date(`${dateStr}T00:00:00Z`); const start = new Date(Date.UTC(year, 0, 1)); return Math.floor((d - start) / 86400000); } function capacityBarClassFor(month) { if (month.overloaded) return "danger"; if (month.utilization >= 90) return "warn"; return "ok"; } document.addEventListener("alpine:init", () => { Alpine.data("app", () => ({ // ---------- global state ---------- currentYear: new Date().getFullYear(), nav: NAV, view: "timeline", people: [], projects: [], tasks: [], absences: [], loading: true, loadError: null, personPalette: PERSON_PALETTE, projectPalette: PROJECT_PALETTE, priorityLabel: PRIORITY_LABEL, monthLabels: MONTH_LABELS, modalError: null, modalSaving: false, // ---------- person form ---------- personFormOpen: false, personEditingId: null, personDraft: { name: "", role: "", weeklyHours: 40, color: PERSON_PALETTE[0] }, // ---------- absence form ---------- absenceFormOpen: false, absenceDraft: { personId: "", startDate: "", endDate: "", note: "" }, // ---------- project form ---------- projectFormOpen: false, projectEditingId: null, projectDraft: { name: "", description: "", color: PROJECT_PALETTE[0] }, // ---------- task form ---------- taskFormOpen: false, taskEditingId: null, taskDraft: { projectId: "", name: "", startDate: "", endDate: "", estimatedHours: 8, assigneeId: "", priority: "mittel", status: "geplant", dependsOn: "" }, // ---------- timeline ---------- timelineYear: new Date().getFullYear(), // ---------- capacity ---------- capacityYear: new Date().getFullYear(), capacityData: null, capacityLoading: true, capacityError: null, init() { this.reloadAll(); }, async reloadAll() { try { const [p, pr, t, a] = await Promise.all([ api.people.list(), api.projects.list(), api.tasks.list(), api.absences.list() ]); this.people = p; this.projects = pr; this.tasks = t; this.absences = a; this.loadError = null; } catch (err) { this.loadError = err.message; } finally { this.loading = false; } }, setView(key) { this.view = key; if (key === "capacity") this.loadCapacity(); }, personById(id) { return this.people.find((p) => p.id === id); }, personNameOrUnknown(id) { return this.personById(id)?.name || "Unbekannt"; }, assigneeName(id) { return this.personById(id)?.name || "–"; }, sortedAbsences() { return [...this.absences].sort((a, b) => a.startDate.localeCompare(b.startDate)); }, tasksByProject(projectId) { return this.tasks .filter((t) => t.projectId === projectId) .sort((a, b) => a.startDate.localeCompare(b.startDate)); }, // ---------- person CRUD ---------- openPersonForm(person = null) { this.personEditingId = person ? person.id : null; this.personDraft = person ? { name: person.name, role: person.role || "", weeklyHours: person.weeklyHours ?? 40, color: person.color || PERSON_PALETTE[0] } : { name: "", role: "", weeklyHours: 40, color: PERSON_PALETTE[0] }; this.modalError = null; this.personFormOpen = true; this.$nextTick(() => document.getElementById("p-name")?.focus()); }, closePersonForm() { this.personFormOpen = false; }, async savePerson() { this.modalError = null; if (!this.personDraft.name.trim()) { this.modalError = "Bitte einen Namen eingeben."; return; } this.modalSaving = true; try { const payload = { name: this.personDraft.name, role: this.personDraft.role, weeklyHours: Number(this.personDraft.weeklyHours), color: this.personDraft.color }; if (this.personEditingId) { await api.people.update(this.personEditingId, payload); } else { await api.people.create(payload); } this.personFormOpen = false; await this.reloadAll(); } catch (err) { this.modalError = err.message; } finally { this.modalSaving = false; } }, async removePerson(id) { if (!confirm("Diese Person inklusive ihrer Abwesenheiten wirklich entfernen?")) return; await api.people.remove(id); await this.reloadAll(); }, // ---------- absence CRUD ---------- openAbsenceForm() { this.absenceDraft = { personId: this.people[0]?.id || "", startDate: "", endDate: "", note: "" }; this.modalError = null; this.absenceFormOpen = true; }, closeAbsenceForm() { this.absenceFormOpen = false; }, async saveAbsence() { this.modalError = null; const d = this.absenceDraft; if (!d.personId || !d.startDate || !d.endDate) { this.modalError = "Bitte Person, Start- und Enddatum angeben."; return; } if (d.startDate > d.endDate) { this.modalError = "Das Startdatum muss vor dem Enddatum liegen."; return; } this.modalSaving = true; try { await api.absences.create({ personId: d.personId, startDate: d.startDate, endDate: d.endDate, note: d.note }); this.absenceFormOpen = false; await this.reloadAll(); } catch (err) { this.modalError = err.message; } finally { this.modalSaving = false; } }, async removeAbsence(id) { await api.absences.remove(id); await this.reloadAll(); }, // ---------- project CRUD ---------- openProjectForm(project = null) { this.projectEditingId = project ? project.id : null; this.projectDraft = project ? { name: project.name, description: project.description || "", color: project.color || PROJECT_PALETTE[0] } : { name: "", description: "", color: PROJECT_PALETTE[0] }; this.modalError = null; this.projectFormOpen = true; this.$nextTick(() => document.getElementById("pr-name")?.focus()); }, closeProjectForm() { this.projectFormOpen = false; }, async saveProject() { this.modalError = null; if (!this.projectDraft.name.trim()) { this.modalError = "Bitte einen Projektnamen eingeben."; return; } this.modalSaving = true; try { const payload = { name: this.projectDraft.name, description: this.projectDraft.description, color: this.projectDraft.color }; if (this.projectEditingId) { await api.projects.update(this.projectEditingId, payload); } else { await api.projects.create(payload); } this.projectFormOpen = false; await this.reloadAll(); } catch (err) { this.modalError = err.message; } finally { this.modalSaving = false; } }, async removeProject(id) { if (!confirm("Projekt inklusive aller zugehoerigen Aufgaben wirklich loeschen?")) return; await api.projects.remove(id); await this.reloadAll(); }, // ---------- task CRUD ---------- openTaskForm(task = null, projectHint = null) { this.taskEditingId = task ? task.id : null; this.taskDraft = { projectId: task?.projectId || projectHint || this.projects[0]?.id || "", name: task?.name || "", startDate: task?.startDate || "", endDate: task?.endDate || "", estimatedHours: task?.estimatedHours ?? 8, assigneeId: task?.assigneeId || "", priority: task?.priority || "mittel", status: task?.status || "geplant", dependsOn: task?.dependsOn || "" }; this.modalError = null; this.taskFormOpen = true; this.$nextTick(() => document.getElementById("t-name")?.focus()); }, closeTaskForm() { this.taskFormOpen = false; }, taskDependencyOptions() { return this.tasks.filter((t) => t.id !== this.taskEditingId); }, async saveTask() { this.modalError = null; const d = this.taskDraft; if (!d.projectId || !d.name.trim() || !d.startDate || !d.endDate) { this.modalError = "Projekt, Name, Start- und Enddatum sind erforderlich."; return; } if (d.startDate > d.endDate) { this.modalError = "Das Startdatum muss vor dem Enddatum liegen."; return; } this.modalSaving = true; try { const payload = { projectId: d.projectId, name: d.name, startDate: d.startDate, endDate: d.endDate, estimatedHours: Number(d.estimatedHours), assigneeId: d.assigneeId || null, priority: d.priority, status: d.status, dependsOn: d.dependsOn || null }; if (this.taskEditingId) { await api.tasks.update(this.taskEditingId, payload); } else { await api.tasks.create(payload); } this.taskFormOpen = false; await this.reloadAll(); } catch (err) { this.modalError = err.message; } finally { this.modalSaving = false; } }, async removeTask(id) { await api.tasks.remove(id); await this.reloadAll(); }, // ---------- timeline ---------- timelineDaysInYear() { return isLeap(this.timelineYear) ? 366 : 365; }, timelineGrouped() { return this.projects .map((project) => ({ project, tasks: this.tasks .filter((t) => t.projectId === project.id) .filter((t) => { const startYear = Number(t.startDate.slice(0, 4)); const endYear = Number(t.endDate.slice(0, 4)); return startYear <= this.timelineYear && endYear >= this.timelineYear; }) .sort((a, b) => a.startDate.localeCompare(b.startDate)) })) .filter((g) => g.tasks.length > 0); }, timelineBarStyle(task) { const year = this.timelineYear; const daysInYear = this.timelineDaysInYear(); const yearStart = `${year}-01-01`; const yearEnd = `${year}-12-31`; const clippedStart = task.startDate < yearStart ? yearStart : task.startDate; const clippedEnd = task.endDate > yearEnd ? yearEnd : task.endDate; const startDay = dayOfYear(clippedStart, year); const endDay = dayOfYear(clippedEnd, year); const leftPct = (startDay / daysInYear) * 100; const widthPct = Math.max(((endDay - startDay + 1) / daysInYear) * 100, 0.6); const person = this.personById(task.assigneeId); const project = this.projects.find((p) => p.id === task.projectId); return `left: ${leftPct}%; width: ${widthPct}%; background: ${person?.color || project?.color || "var(--accent)"};`; }, // ---------- capacity ---------- async loadCapacity() { this.capacityLoading = true; try { const res = await api.capacity.get(`${this.capacityYear}-01-01`, `${this.capacityYear}-12-31`); this.capacityData = res; this.capacityError = null; } catch (err) { this.capacityError = err.message; } finally { this.capacityLoading = false; } }, capacityPrevYear() { this.capacityYear -= 1; this.loadCapacity(); }, capacityNextYear() { this.capacityYear += 1; this.loadCapacity(); }, capacityMaxScale(person) { return Math.max(1, ...person.months.map((m) => Math.max(m.capacityHours, m.assignedHours))); }, capacityBarClass(month) { return capacityBarClassFor(month); }, monthLabel(monthKey) { return MONTH_LABELS_BY_KEY[monthKey.slice(5)]; } })); });