Umbau auf Alpine.js
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
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)];
|
||||
}
|
||||
}));
|
||||
});
|
||||
Reference in New Issue
Block a user