306 lines
12 KiB
HTML
306 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<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(--red); }
|
||
|
||
@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="main.v2.js?v=4"></script>
|
||
<script src="project-plan.js?v=4"></script>
|
||
</body>
|
||
</html>
|