Files
2026-07-03 00:17:27 +02:00

540 lines
25 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jahresplanung &amp; Kapazitaet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<div id="app" x-data="app()" class="app-shell">
<aside class="sidebar">
<div class="brand">
<span class="brand-mark">Jahresplan</span>
<span class="brand-sub" x-text="`Planung &amp; Kapazitaet ${currentYear}`"></span>
</div>
<ul class="nav-list">
<template x-for="item in nav" :key="item.key">
<li>
<button
type="button"
:class="`nav-item ${view === item.key ? 'active' : ''}`"
@click="setView(item.key)"
>
<span class="nav-index" x-text="item.index"></span>
<span x-text="item.label"></span>
</button>
</li>
</template>
</ul>
<div class="sidebar-foot">
<span x-text="people.length"></span> Personen &middot; <span x-text="projects.length"></span> Projekte
<br />
<span x-text="tasks.length"></span> Aufgaben insgesamt
</div>
</aside>
<main class="main">
<div class="panel" style="border-color: var(--danger);" x-show="loadError" x-cloak>
<p class="error-text" style="margin: 0;" x-text="`Daten konnten nicht geladen werden: ${loadError}`"></p>
</div>
<!-- ---------- Zeitachse ---------- -->
<template x-if="!loading && view === 'timeline'">
<div>
<div class="view-header">
<h1 class="view-title">Zeitachse</h1>
<p class="view-desc">
Alle Aufgaben eines Jahres auf einen Blick, gruppiert nach Projekt. Balkenfarbe
folgt der zustaendigen Person (falls zugewiesen), sonst der Projektfarbe.
</p>
</div>
<div class="panel">
<div class="toolbar">
<div class="range-picker">
<button class="btn btn-small" @click="timelineYear--">&larr;</button>
<strong style="font-family: var(--font-mono); font-size: 15px;" x-text="timelineYear"></strong>
<button class="btn btn-small" @click="timelineYear++">&rarr;</button>
</div>
</div>
<div class="timeline-scale">
<div></div>
<template x-for="m in monthLabels" :key="m">
<div class="month-label" x-text="m"></div>
</template>
</div>
<p class="empty-row" x-show="timelineGrouped().length === 0" x-text="`Keine Aufgaben in ${timelineYear}. Lege Projekte und Aufgaben an.`"></p>
<template x-for="g in timelineGrouped()" :key="g.project.id">
<div>
<div class="timeline-group-title" :style="`color: ${g.project.color}`" x-text="g.project.name"></div>
<template x-for="task in g.tasks" :key="task.id">
<div class="timeline-row">
<div class="timeline-row-label">
<span x-text="task.name"></span>
<span class="proj-name" x-text="`${personById(task.assigneeId)?.name || 'nicht zugewiesen'} · ${task.estimatedHours}h`"></span>
</div>
<div class="timeline-track">
<div class="timeline-bar" :style="timelineBarStyle(task)" :title="task.name"></div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
</template>
<!-- ---------- Kapazitaet ---------- -->
<template x-if="!loading && view === 'capacity'">
<div>
<div class="view-header">
<h1 class="view-title">Kapazitaet</h1>
<p class="view-desc">
Zugewiesene Stunden je Person und Monat gegen die verfuegbare Nettokapazitaet
(Wochenstunden minus Abwesenheiten). Die gestrichelte Linie markiert 100 %
Auslastung.
</p>
</div>
<div class="panel">
<div class="toolbar">
<div class="range-picker">
<button class="btn btn-small" @click="capacityPrevYear()">&larr;</button>
<strong style="font-family: var(--font-mono); font-size: 15px;" x-text="capacityYear"></strong>
<button class="btn btn-small" @click="capacityNextYear()">&rarr;</button>
</div>
</div>
<p class="error-text" x-show="capacityError" x-text="capacityError"></p>
<p class="empty-row" x-show="!capacityLoading && people.length === 0">
Noch keine Personen angelegt. Lege zuerst Team-Mitglieder mit Wochenkapazitaet an.
</p>
<div class="ledger" x-show="!capacityLoading && capacityData && people.length > 0">
<template x-for="person in (capacityData ? capacityData.people : [])" :key="person.personId">
<div class="ledger-person">
<div class="ledger-person-head">
<span class="name" x-text="person.name"></span>
<span class="capacity-note" x-text="`${person.weeklyHours}h / Woche`"></span>
</div>
<div class="ledger-months">
<template x-for="m in person.months" :key="m.month">
<div class="ledger-month">
<div class="ledger-month-label" x-text="monthLabel(m.month)"></div>
<div class="ledger-bar-well">
<div
class="ledger-baseline"
:style="`top: ${100 - (m.capacityHours / capacityMaxScale(person)) * 100}%`"
:title="`Kapazitaet: ${m.capacityHours}h`"
></div>
<div
:class="`ledger-bar-fill ${capacityBarClass(m)}`"
:style="`height: ${Math.min((m.assignedHours / capacityMaxScale(person)) * 100, 100)}%`"
:title="`${m.assignedHours}h zugewiesen`"
></div>
</div>
<div :class="`ledger-pct ${m.overloaded ? 'danger' : ''}`" x-text="`${m.utilization}%`"></div>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="legend">
<span class="legend-item">
<span class="legend-swatch" style="background: var(--accent)"></span> Im Rahmen
</span>
<span class="legend-item">
<span class="legend-swatch" style="background: var(--warn)"></span> Ab 90 % ausgelastet
</span>
<span class="legend-item">
<span class="legend-swatch" style="background: var(--danger)"></span> Ueberlast
</span>
<span class="legend-item">
<span style="border-top: 1.5px dashed var(--ink-soft); width: 14px; display: inline-block;"></span>
100 % Kapazitaet
</span>
</div>
</div>
</div>
</template>
<!-- ---------- Projekte & Aufgaben ---------- -->
<template x-if="!loading && view === 'projects'">
<div>
<div class="view-header">
<h1 class="view-title">Projekte &amp; Aufgaben</h1>
<p class="view-desc">
Projekte in Aufgaben mit Start- und Enddatum, Aufwand und Zustaendigkeit
zerlegen. Abhaengigkeiten koennen pro Aufgabe hinterlegt werden.
</p>
</div>
<div class="toolbar">
<div></div>
<button class="btn btn-primary" @click="openProjectForm()">+ Projekt anlegen</button>
</div>
<div class="panel" x-show="projects.length === 0">
<p class="empty-row" style="padding: 0;">
Noch keine Projekte angelegt. Starte mit &bdquo;Projekt anlegen&ldquo;.
</p>
</div>
<template x-for="project in projects" :key="project.id">
<div class="panel">
<div class="toolbar">
<div>
<h2 class="panel-title" style="margin: 0; display: flex; align-items: center; gap: 8px;">
<span class="dot" :style="`background: ${project.color}; width: 10px; height: 10px;`"></span>
<span x-text="project.name"></span>
</h2>
<p x-show="project.description" style="margin: 4px 0 0; color: var(--ink-soft); font-size: 13px;" x-text="project.description"></p>
</div>
<div style="display: flex; gap: 6px;">
<button class="btn btn-small" @click="openTaskForm(null, project.id)">+ Aufgabe</button>
<button class="btn btn-ghost btn-small" @click="openProjectForm(project)">Bearbeiten</button>
<button class="btn btn-danger-ghost btn-small" @click="removeProject(project.id)">Loeschen</button>
</div>
</div>
<table>
<thead>
<tr>
<th>Aufgabe</th>
<th>Start</th>
<th>Ende</th>
<th>Aufwand</th>
<th>Zustaendig</th>
<th>Prioritaet</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr x-show="tasksByProject(project.id).length === 0">
<td colspan="8" class="empty-row">Noch keine Aufgaben in diesem Projekt.</td>
</tr>
<template x-for="task in tasksByProject(project.id)" :key="task.id">
<tr>
<td x-text="task.name"></td>
<td class="num" x-text="task.startDate"></td>
<td class="num" x-text="task.endDate"></td>
<td class="num" x-text="`${task.estimatedHours}h`"></td>
<td x-text="assigneeName(task.assigneeId)"></td>
<td x-text="priorityLabel[task.priority] || task.priority"></td>
<td x-text="task.status"></td>
<td style="text-align: right; white-space: nowrap;">
<button class="btn btn-ghost btn-small" @click="openTaskForm(task, project.id)">Bearbeiten</button>
<button class="btn btn-danger-ghost btn-small" @click="removeTask(task.id)">Loeschen</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
</template>
<!-- ---------- Team ---------- -->
<template x-if="!loading && view === 'people'">
<div>
<div class="view-header">
<h1 class="view-title">Team</h1>
<p class="view-desc">
Wochenkapazitaet pro Person hinterlegen. Sie bildet die Grundlage fuer die
Auslastungsberechnung auf der Kapazitaet-Ansicht.
</p>
</div>
<div class="panel">
<div class="toolbar">
<h2 class="panel-title" style="margin: 0;" x-text="`Personen (${people.length})`"></h2>
<button class="btn btn-primary" @click="openPersonForm()">+ Person hinzufuegen</button>
</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Rolle</th>
<th>Std. / Woche</th>
<th></th>
</tr>
</thead>
<tbody>
<tr x-show="people.length === 0">
<td colspan="4" class="empty-row">Noch keine Personen angelegt.</td>
</tr>
<template x-for="p in people" :key="p.id">
<tr>
<td>
<span class="tag">
<span class="dot" :style="`background: ${p.color}`"></span>
<span x-text="p.name"></span>
</span>
</td>
<td x-text="p.role || ''"></td>
<td class="num" x-text="`${p.weeklyHours}h`"></td>
<td style="text-align: right;">
<button class="btn btn-ghost btn-small" @click="openPersonForm(p)">Bearbeiten</button>
<button class="btn btn-danger-ghost btn-small" @click="removePerson(p.id)">Entfernen</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="panel">
<div class="toolbar">
<h2 class="panel-title" style="margin: 0;" x-text="`Abwesenheiten (${absences.length})`"></h2>
<button
class="btn btn-primary"
@click="openAbsenceForm()"
:disabled="people.length === 0"
:title="people.length === 0 ? 'Zuerst eine Person anlegen' : ''"
>
+ Abwesenheit
</button>
</div>
<table>
<thead>
<tr>
<th>Person</th>
<th>Von</th>
<th>Bis</th>
<th>Notiz</th>
<th></th>
</tr>
</thead>
<tbody>
<tr x-show="sortedAbsences().length === 0">
<td colspan="5" class="empty-row">Keine Abwesenheiten hinterlegt (Urlaub, Feiertage, etc.).</td>
</tr>
<template x-for="a in sortedAbsences()" :key="a.id">
<tr>
<td x-text="personNameOrUnknown(a.personId)"></td>
<td class="num" x-text="a.startDate"></td>
<td class="num" x-text="a.endDate"></td>
<td x-text="a.note || ''"></td>
<td style="text-align: right;">
<button class="btn btn-danger-ghost btn-small" @click="removeAbsence(a.id)">Entfernen</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</main>
<!-- ---------- Person-Modal ---------- -->
<div class="form-modal-backdrop" x-show="personFormOpen" x-cloak @mousedown.self="closePersonForm()">
<div class="form-modal" @mousedown.stop>
<h3 x-text="personEditingId ? 'Person bearbeiten' : 'Person hinzufuegen'"></h3>
<form @submit.prevent="savePerson()">
<div class="field">
<label for="p-name">Name</label>
<input id="p-name" x-model="personDraft.name" autofocus />
</div>
<div class="field-row">
<div class="field">
<label for="p-role">Rolle</label>
<input id="p-role" x-model="personDraft.role" placeholder="z. B. Entwicklerin" />
</div>
<div class="field">
<label for="p-hours">Stunden / Woche</label>
<input id="p-hours" type="number" min="1" max="60" x-model="personDraft.weeklyHours" />
</div>
</div>
<div class="field">
<label>Farbe</label>
<div style="display: flex; gap: 8px;">
<template x-for="c in personPalette" :key="c">
<button
type="button"
@click="personDraft.color = c"
:aria-label="`Farbe ${c} waehlen`"
:style="`width: 24px; height: 24px; border-radius: 50%; background: ${c}; border: ${personDraft.color === c ? '2px solid var(--ink)' : '2px solid transparent'}; cursor: pointer;`"
></button>
</template>
</div>
</div>
<p class="error-text" x-show="modalError" x-text="modalError"></p>
<div class="form-actions">
<button type="button" class="btn" @click="closePersonForm()">Abbrechen</button>
<button type="submit" class="btn btn-primary" :disabled="modalSaving" x-text="modalSaving ? 'Speichert...' : 'Speichern'"></button>
</div>
</form>
</div>
</div>
<!-- ---------- Abwesenheit-Modal ---------- -->
<div class="form-modal-backdrop" x-show="absenceFormOpen" x-cloak @mousedown.self="closeAbsenceForm()">
<div class="form-modal" @mousedown.stop>
<h3>Abwesenheit eintragen</h3>
<form @submit.prevent="saveAbsence()">
<div class="field">
<label for="a-person">Person</label>
<select id="a-person" x-model="absenceDraft.personId">
<template x-for="p in people" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
</div>
<div class="field-row">
<div class="field">
<label for="a-start">Von</label>
<input id="a-start" type="date" x-model="absenceDraft.startDate" />
</div>
<div class="field">
<label for="a-end">Bis</label>
<input id="a-end" type="date" x-model="absenceDraft.endDate" />
</div>
</div>
<div class="field">
<label for="a-note">Notiz (optional)</label>
<input id="a-note" x-model="absenceDraft.note" placeholder="z. B. Urlaub, Feiertag" />
</div>
<p class="error-text" x-show="modalError" x-text="modalError"></p>
<div class="form-actions">
<button type="button" class="btn" @click="closeAbsenceForm()">Abbrechen</button>
<button type="submit" class="btn btn-primary" :disabled="modalSaving" x-text="modalSaving ? 'Speichert...' : 'Eintragen'"></button>
</div>
</form>
</div>
</div>
<!-- ---------- Projekt-Modal ---------- -->
<div class="form-modal-backdrop" x-show="projectFormOpen" x-cloak @mousedown.self="closeProjectForm()">
<div class="form-modal" @mousedown.stop>
<h3 x-text="projectEditingId ? 'Projekt bearbeiten' : 'Projekt anlegen'"></h3>
<form @submit.prevent="saveProject()">
<div class="field">
<label for="pr-name">Projektname</label>
<input id="pr-name" x-model="projectDraft.name" autofocus />
</div>
<div class="field">
<label for="pr-desc">Beschreibung (optional)</label>
<textarea id="pr-desc" rows="2" x-model="projectDraft.description"></textarea>
</div>
<div class="field">
<label>Farbe</label>
<div style="display: flex; gap: 8px;">
<template x-for="c in projectPalette" :key="c">
<button
type="button"
@click="projectDraft.color = c"
:aria-label="`Farbe ${c} waehlen`"
:style="`width: 24px; height: 24px; border-radius: 50%; background: ${c}; border: ${projectDraft.color === c ? '2px solid var(--ink)' : '2px solid transparent'}; cursor: pointer;`"
></button>
</template>
</div>
</div>
<p class="error-text" x-show="modalError" x-text="modalError"></p>
<div class="form-actions">
<button type="button" class="btn" @click="closeProjectForm()">Abbrechen</button>
<button type="submit" class="btn btn-primary" :disabled="modalSaving" x-text="modalSaving ? 'Speichert...' : 'Speichern'"></button>
</div>
</form>
</div>
</div>
<!-- ---------- Aufgabe-Modal ---------- -->
<div class="form-modal-backdrop" x-show="taskFormOpen" x-cloak @mousedown.self="closeTaskForm()">
<div class="form-modal" @mousedown.stop>
<h3 x-text="taskEditingId ? 'Aufgabe bearbeiten' : 'Aufgabe anlegen'"></h3>
<form @submit.prevent="saveTask()">
<div class="field">
<label for="t-project">Projekt</label>
<select id="t-project" x-model="taskDraft.projectId">
<template x-for="p in projects" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
</div>
<div class="field">
<label for="t-name">Aufgabe</label>
<input id="t-name" x-model="taskDraft.name" autofocus />
</div>
<div class="field-row">
<div class="field">
<label for="t-start">Start</label>
<input id="t-start" type="date" x-model="taskDraft.startDate" />
</div>
<div class="field">
<label for="t-end">Ende / Faellig</label>
<input id="t-end" type="date" x-model="taskDraft.endDate" />
</div>
</div>
<div class="field-row">
<div class="field">
<label for="t-hours">Aufwand (Std.)</label>
<input id="t-hours" type="number" min="0" step="0.5" x-model="taskDraft.estimatedHours" />
</div>
<div class="field">
<label for="t-assignee">Zustaendig</label>
<select id="t-assignee" x-model="taskDraft.assigneeId">
<option value="">Nicht zugewiesen</option>
<template x-for="p in people" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
</div>
</div>
<div class="field-row">
<div class="field">
<label for="t-priority">Prioritaet</label>
<select id="t-priority" x-model="taskDraft.priority">
<option value="hoch">Hoch</option>
<option value="mittel">Mittel</option>
<option value="niedrig">Niedrig</option>
</select>
</div>
<div class="field">
<label for="t-status">Status</label>
<select id="t-status" x-model="taskDraft.status">
<option value="geplant">Geplant</option>
<option value="in Arbeit">In Arbeit</option>
<option value="erledigt">Erledigt</option>
</select>
</div>
</div>
<div class="field">
<label for="t-depends">Abhaengig von (optional)</label>
<select id="t-depends" x-model="taskDraft.dependsOn">
<option value="">Keine Abhaengigkeit</option>
<template x-for="t in taskDependencyOptions()" :key="t.id">
<option :value="t.id" x-text="t.name"></option>
</template>
</select>
</div>
<p class="error-text" x-show="modalError" x-text="modalError"></p>
<div class="form-actions">
<button type="button" class="btn" @click="closeTaskForm()">Abbrechen</button>
<button type="submit" class="btn btn-primary" :disabled="modalSaving" x-text="modalSaving ? 'Speichert...' : 'Speichern'"></button>
</div>
</form>
</div>
</div>
</div>
<script defer src="/js/vendor/alpine-3.15.12.min.js"></script>
<script type="module" src="/js/app.js"></script>
</body>
</html>