Files
projektuebersicht/public/project-plan.html
T

332 lines
14 KiB
HTML
Raw 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>
<!--
PROJEKTPLAN-ANSICHT: zeigt für ein einzelnes Projekt ein GANTT-Diagramm mit TODO- und
Meilenstein-Zeilen, Abhängigkeiten, Zoom, kritischem Pfad, Auto-Schedule, Ressourcen-
Auslastung und Abwesenheiten der Projektbeteiligten. Die Seite wird über openProjectPlan()
in js/gantt.js in einem neuen Tab geöffnet und per Deep-Link-Route
"/<Abteilung>/Projektplan/<Projektname>" adressiert; Woche und Projekt-Nr.
kommen als URL-Parameter (weekLabel, projNr). Sie lädt styles.css und ausschließlich die gemeinsamen
Module js/globals.js, js/utils.js, js/ui.js, js/date-picker.js, js/csv.js, js/server.js
und js/gantt.js sowie danach die Plan-Module js/plan/plan-state.js,
js/plan/plan-build-gantt.js, js/plan/plan-gantt.js, js/plan/plan-drag.js,
js/plan/plan-modals.js und js/plan/plan-schedule.js. WICHTIG: store, weeks, board, people,
projects, load, showmode, tabs-auth, pdf und init dürfen hier NICHT geladen werden, da sie
Elemente der Hauptseite Test.html voraussetzen. Änderungen werden über savePlan() in
js/plan/plan-state.js nur für die aktuelle Woche direkt an den Server geschrieben.
-->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projektplan</title>
<link rel="stylesheet" href="/styles.css?v=3">
<style>
.plan-page { min-height: 100vh; display: flex; flex-direction: column; }
.plan-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 24px; background: var(--surface); border-bottom: 1px solid var(--line-soft);
position: sticky; top: 0; z-index: 20;
}
.plan-header h1 { font-size: 17px; margin: 0; font-weight: 600; }
.plan-header .subtitle { color: var(--text-muted); font-size: 13px; }
.plan-body { flex: 1; display: flex; gap: 0; overflow: hidden; }
.plan-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--bg); min-width: 0; }
.plan-main-header {
padding: 16px 20px 10px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
}
.plan-main-content { flex: 1; overflow: auto; padding: 0 20px 20px; min-width: 0; }
.todo-item {
background: var(--surface-alt); border: 1px solid var(--line-soft); border-radius: var(--r-md);
padding: 10px 12px; margin-bottom: 10px;
}
.todo-item.done .todo-title { text-decoration: line-through; opacity: .6; }
.todo-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.todo-row:last-child { margin-bottom: 0; }
.todo-title { flex: 1; font-weight: 500; font-size: 13px; }
.todo-dates { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted); }
.todo-dates input[type=date] { padding: 4px 7px; font-size: 12px; }
.todo-assignee-list { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.todo-assignee {
font-size: 11px; padding: 2px 8px; border-radius: var(--r-pill);
background: var(--surface-sunk); border: 1px solid var(--line-soft); color: var(--text-muted);
}
.todo-assignee.main { background: var(--green); color: #fff; border-color: transparent; }
.todo-assignee.optional { background: #d18b00; color: #fff; border-color: transparent; }
.empty-todos { color: var(--text-subtle); font-size: 13px; text-align: center; padding: 24px 0; }
.plan-people { margin-top: 14px; }
.plan-people-title { font-size: 12px; font-weight: 600; color: var(--text-muted); margin-bottom: 6px; }
.plan-people-list { display: flex; flex-wrap: wrap; gap: 6px; }
.plan-people-chip {
font-size: 12px; padding: 4px 10px; border-radius: var(--r-pill);
background: var(--accent-soft); color: var(--text); border: 1px solid var(--line-soft);
}
.milestone-list { display: flex; flex-direction: column; gap: 6px; }
.milestone-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.milestone-row input[type=date] { padding: 4px 7px; font-size: 12px; }
.todo-list-main {
margin-top: 18px;
background: var(--surface);
border: 1px solid var(--line-soft);
border-radius: var(--r-lg);
padding: 14px 18px;
}
.todo-list-title {
font-size: 15px; font-weight: 600; margin: 0 0 10px;
}
.todo-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-row-item {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
padding: 8px 10px;
background: var(--surface-alt);
border: 1px solid var(--line-soft);
border-radius: var(--r-md);
}
.todo-row-item.done { opacity: .55; text-decoration: line-through; }
.todo-row-item input[type=text] { flex: 1 1 200px; min-width: 140px; }
.todo-row-item input[type=date] { padding: 4px 7px; font-size: 12px; }
.todo-add-row {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--line-soft);
}
.plan-toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
background: var(--text); color: #fff; padding: 10px 18px; border-radius: var(--r-pill);
font-size: 13px; opacity: 0; pointer-events: none; transition: opacity .25s, transform .25s;
z-index: 1000; box-shadow: var(--sh-3);
}
.plan-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.plan-resource-grid {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(140px, max-content)) !important;
gap: 10px !important;
justify-content: start !important;
}
.plan-resource-card {
padding: 8px 10px !important;
min-height: 0 !important;
width: 140px !important;
max-width: 140px !important;
}
.plan-resource-card h3 {
font-size: 12px !important;
margin-bottom: 2px !important;
}
.plan-resource-clickable { cursor: pointer; transition: box-shadow .15s, transform .15s; }
.plan-resource-clickable:hover { box-shadow: var(--sh-2); transform: translateY(-1px); }
.todo-depends-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 180px;
overflow-y: auto;
border: 1px solid var(--line-soft);
border-radius: var(--r-md);
padding: 8px;
background: var(--surface-alt);
}
.todo-depends-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 6px;
border-radius: var(--r-sm);
cursor: pointer;
font-size: 12px;
transition: background .15s;
}
.todo-depends-item:hover { background: var(--surface-sunk); }
.todo-depends-item input { flex-shrink: 0; }
.todo-depends-group {
font-size: 11px;
color: var(--text-muted);
margin-top: 6px;
margin-bottom: 2px;
text-transform: uppercase;
letter-spacing: .03em;
}
.todo-depends-group:first-child { margin-top: 0; }
/* Drag-Handle für TODO- und Meilenstein-Zeilen direkt im GANTT */
.gantt-row.gantt-todo,
.gantt-row.gantt-milestone-row { cursor: default; }
.gantt-row.gantt-todo .gantt-label,
.gantt-row.gantt-milestone-row .gantt-label { display: flex; align-items: center; gap: 8px; }
.gantt-todo-drag {
color: var(--text-subtle); cursor: grab; font-size: 13px; line-height: 1;
user-select: none; flex-shrink: 0;
}
.gantt-row.gantt-todo.dragging,
.gantt-row.gantt-milestone-row.dragging { opacity: .5; }
.gantt-todo-drag:active { cursor: grabbing; }
.gantt-row.gantt-milestone-row .gantt-todo-drag { color: var(--text-subtle); }
@media (max-width: 900px) {
.plan-body { flex-direction: column; overflow: auto; }
}
</style>
</head>
<body>
<div class="plan-page">
<div class="plan-header">
<div>
<h1 id="planTitle">Projektplan</h1>
<div class="subtitle" id="planSubtitle"></div>
</div>
<div class="toolbar">
<button class="secondary" onclick="todoMail()">E-Mail erstellen</button>
<button class="secondary" onclick="window.close()">Schließen</button>
</div>
</div>
<div class="plan-body">
<div class="plan-main">
<div class="plan-main-header">
<label>Zeitraum:</label>
<input type="text" id="planGanttRange" class="range-picker-display" placeholder="Zeitraum wählen…" readonly
onclick="openRangePicker({ anchor: this, startInputId: 'planGanttStart', endInputId: 'planGanttEnd', displayId: 'planGanttRange', onApply: renderPlanGantt })">
<input type="hidden" id="planGanttStart">
<input type="hidden" id="planGanttEnd">
<button class="secondary" onclick="fitPlanGanttRange()">Zeitraum anpassen</button>
<label><input type="checkbox" id="planShowVacation" checked onchange="renderPlanGantt()"> Urlaub anzeigen</label>
<button class="secondary" onclick="zoomPlanGantt(-1)" title="Verkleinern">−</button>
<button class="secondary" onclick="zoomPlanGantt(1)" title="Vergrößern">+</button>
<button onclick="addPlanMilestone()" id="addMilestoneBtn">+ Meilenstein</button>
<button onclick="addTodo()" id="addTodoBtn">+ TODO</button>
<button onclick="autoScheduleTodos()" id="autoScheduleBtn" title="Aufgaben nach Abhängigkeiten automatisch anordnen">Auto-Schedule</button>
<button onclick="toggleCriticalPath()" id="criticalPathBtn" title="Kritischen Pfad ein-/ausblenden">Kritischer Pfad</button>
</div>
<div class="plan-main-content plan-main-content-full">
<div id="planGanttContainer" class="gantt-container"></div>
<div id="resourceLoadContainer" style="margin-top:16px"></div>
</div>
</div>
</div>
</div>
<!-- Meilenstein-Modal -->
<div class="modal-bg" id="milestoneModal" style="display:none">
<div class="modal">
<h2>Meilenstein</h2>
<div class="form-row">
<label>Name</label>
<input type="text" id="milestoneEditName" placeholder="Name">
</div>
<div class="form-row">
<label>Datum</label>
<input type="text" id="milestoneEditDate" class="range-picker-display" placeholder="Datum wählen…" readonly onclick="openDatePicker({ anchor: this, inputId: 'milestoneEditDate' })">
</div>
<div class="form-row">
<label>Position</label>
<select id="milestoneEditPosition">
<option value="">Oben in der Projektzeile</option>
</select>
</div>
<div class="form-row">
<label>Vorgänger (mehrfach möglich)</label>
<div id="milestoneEditDependsOn" class="todo-depends-list"></div>
</div>
<div class="toolbar">
<button onclick="saveMilestoneEdit()">Speichern</button>
<button class="danger" onclick="deleteMilestoneEdit()">Löschen</button>
<button class="secondary" onclick="closeMilestoneModal()">Abbrechen</button>
</div>
</div>
</div>
<!-- TODO-Modal -->
<div class="modal-bg" id="todoModal" style="display:none">
<div class="modal">
<h2>TODO</h2>
<div class="form-row">
<label>Titel</label>
<input type="text" id="todoEditTitle" placeholder="Titel">
</div>
<div class="form-row">
<label>Zeitraum</label>
<input type="text" id="todoEditRange" class="range-picker-display" placeholder="Start – Ende wählen…" readonly
onclick="openRangePicker({ anchor: this, startInputId: 'todoEditStart', endInputId: 'todoEditEnd', displayId: 'todoEditRange' })">
<input type="hidden" id="todoEditStart">
<input type="hidden" id="todoEditEnd">
</div>
<div class="form-row">
<label>Vorgänger (mehrfach möglich)</label>
<div id="todoEditDependsOn" class="todo-depends-list">
<label class="todo-depends-item">
<input type="checkbox" value="" checked> (Keine Abhängigkeit)
</label>
</div>
</div>
<div class="form-row">
<label>Zuständige</label>
<div id="todoEditAssignees" class="role-checkboxes"></div>
</div>
<div class="form-row">
<label>Fortschritt (%)</label>
<input type="range" id="todoEditProgress" min="0" max="100" value="0" oninput="document.getElementById('todoEditProgressVal').textContent=this.value+'%'"><span id="todoEditProgressVal">0%</span>
</div>
<div class="form-row">
<label><input type="checkbox" id="todoEditDone"> Erledigt</label>
</div>
<div class="toolbar">
<button onclick="saveTodoEdit()">Speichern</button>
<button class="danger" onclick="deleteTodoEdit()">Löschen</button>
<button class="secondary" onclick="closeTodoModal()">Abbrechen</button>
</div>
</div>
</div>
<!-- Abwesenheits-Modal (Urlaube der Projektbeteiligten) -->
<div class="modal-bg" id="absenceModal" style="display:none">
<div class="modal">
<h2 id="absenceModalTitle">Abwesenheiten</h2>
<div class="form-row">
<div id="absenceModalList"></div>
</div>
<div class="absence-form" id="absenceModalForm">
<input type="text" id="absenceEditRange" class="range-picker-display" placeholder="Zeitraum wählen…" readonly
onclick="openRangePicker({ anchor: this, startInputId: 'absenceEditStart', endInputId: 'absenceEditEnd', displayId: 'absenceEditRange' })">
<input type="hidden" id="absenceEditStart">
<input type="hidden" id="absenceEditEnd">
<select id="absenceEditType">
<option>Urlaub</option>
<option>Krank</option>
<option>Fortbildung</option>
<option>Sonstiges</option>
</select>
<input type="text" id="absenceEditNote" placeholder="Notiz (optional)" style="width:140px">
<button class="secondary small" onclick="addPlanAbsence()">+ Abwesenheit</button>
</div>
<div class="toolbar" style="margin-top:12px">
<button class="secondary" onclick="closeAbsenceModal()">Schließen</button>
</div>
</div>
</div>
<script src="/js/globals.js?v=5"></script>
<script src="/js/utils.js?v=5"></script>
<script src="/js/ui.js?v=5"></script>
<script src="/js/date-picker.js?v=5"></script>
<script src="/js/csv.js?v=5"></script>
<script src="/js/server.js?v=5"></script>
<script src="/js/gantt.js?v=5"></script>
<script src="/js/plan/plan-state.js?v=5"></script>
<script src="/js/plan/plan-build-gantt.js?v=5"></script>
<script src="/js/plan/plan-gantt.js?v=5"></script>
<script src="/js/plan/plan-drag.js?v=5"></script>
<script src="/js/plan/plan-modals.js?v=5"></script>
<script src="/js/plan/plan-schedule.js?v=5"></script>
</body>
</html>