Protokoll-Import: KI-Erkennung (Gruppe, Teilnehmer, Status, Mehrfachpunkte), Ladeanzeige, Erstellungsdatum je Punkt

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Sonnet 5 committed 2026-10-01 10:54:43 +02:00
1 parent 6f18848f86
commit 11eabe42ae
275 files changed
+71952 -3029

No files matched your search

+108 -23
View File
@@ -1,15 +1,27 @@
// -----------------------------
// Projektübersicht — Projekte (Listen-Ansicht)
// Projektübersicht — Projektliste (Ablage-Logik)
// -----------------------------
// Diese Datei rendert den Projekte-Tab (Tab "Projekte" in `Test.html`) als
// sortierbare Listenansicht: Projekte werden nach Gruppe ("Aktive Projekte",
// "Kleine Projekte" usw.) gruppiert, inline bearbeitbar und per
// Drag-and-Drop umsortiert. Sie wird von `Test.html` nach `js/people/people.js`
// (vor `js/load/load.js`) geladen und stellt globale Funktionen wie
// `renderProjects()`, `editProjectField()`, `editProjectActiveLP()` sowie die
// Drag-and-Drop-Handler und die Sortier-Hilfe `getDragAfterElement()` bereit.
// Letztere wird auch von `js/people/people.js` (Personen-Sortierung) genutzt;
// `renderBoard()` aus `js/board/board.js` wird nach Änderungen aktualisiert.
// Der eigene Tab "Projekte" ist entfallen. Er listete dieselben Projekte ein
// zweites Mal auf und ließ dieselben Angaben — Nummer, Bezeichnung, Gruppe,
// amtierende Leistungsphase, Schlagwörter, Aufwand — ein zweites Mal
// bearbeiten. Diese Angaben haben ihren einen Ort jetzt im Projektbereich
// (js/plan/plan-settings.js, js/plan/plan-info.js); die Liste selbst ist im
// Projektboard aufgegangen (renderBoard(), js/board/board.js).
//
// Was hier bleibt, betrifft die LISTE, nicht die einzelnen Angaben:
//
// * delProjectFromList() — Projekt aus der Sitzung löschen
// * die Drag-and-Drop-Handler + reorderProjects() — Reihenfolge innerhalb
// einer Gruppe und Wechsel zwischen Gruppen. Das Board spannt dieselbe
// Ablagefläche auf (.projects-sortable mit data-group), damit diese
// Handler unverändert weiterarbeiten.
// * getDragAfterElement() — Sortier-Hilfe, auch von js/people/people.js
// genutzt
//
// editProjectField()/editProjectActiveLP()/addProjectTag()/removeProjectTag()
// stehen weiterhin bereit: Der Beauftragungs-Dialog und die Auslastungs-Maske
// rufen sie auf. renderProjects() rendert nur noch, wenn ein Element
// #projectsList vorhanden ist — sonst übernimmt renderBoard().
// delProjectFromList: löscht ein Projekt (per Bestätigungsdialog) in der
// Listen-Ansicht. Parameter `id`: Projekt-ID. Keine Rückgabe; bei gesperrter
@@ -55,16 +67,67 @@ function editProjectActiveLP(id, value){
save(); renderProjects(); renderBoard(); renderLoad();
}
// addProjectTag: fügt dem Projekt einen neuen Tag hinzu (getrimmt, nicht
// leer, keine exakten Duplikate). Initialisiert `p.tags`, falls es fehlt
// oder kein Array ist. Parameter: `projectId`, `tag` (roher Eingabetext).
// Keine Rückgabe; bei gesperrter Sitzung abgebrochen.
function addProjectTag(projectId, tag){
if(guardWeekLocked()) return;
const val = String(tag || '').trim();
if(!val) return;
const w = currentWeek();
const p = w.projects.find(x=>x.id===projectId);
if(!p) return;
if(!Array.isArray(p.tags)) p.tags = [];
if(p.tags.includes(val)) return;
p.tags.push(val);
save(); renderProjects(); renderBoard();
}
// removeProjectTag: entfernt einen Tag aus dem Projekt (exakter Textvergleich).
// Parameter: `projectId`, `tag`. Keine Rückgabe; bei gesperrter Sitzung
// abgebrochen.
function removeProjectTag(projectId, tag){
if(guardWeekLocked()) return;
const w = currentWeek();
const p = w.projects.find(x=>x.id===projectId);
if(!p) return;
p.tags = (Array.isArray(p.tags) ? p.tags : []).filter(t => t !== tag);
save(); renderProjects(); renderBoard();
}
// handleProjectTagKeydown: fügt bei Enter oder Komma im Tag-Eingabefeld den
// aktuellen Feldinhalt als neuen Tag hinzu (das Eingabefeld wird ohnehin
// beim anschließenden renderProjects() frisch/leer neu aufgebaut). Parameter:
// `event` (KeyboardEvent), `projectId`. Keine Rückgabe.
function handleProjectTagKeydown(event, projectId){
if(event.key !== 'Enter' && event.key !== ','){
return;
}
event.preventDefault();
addProjectTag(projectId, event.target.value);
}
// renderProjects: baut die HTML-Liste der Projekte auf, gruppiert nach `group`
// und sortiert je Gruppe nach `order`. Jedes Projekt erhält Felder für Nr.,
// Name, Gruppe, aktive LP sowie Drag-and-Drop-Handler. Keine Parameter, keine
// Rückgabe.
function renderProjects(){
// Ohne #projectsList gibt es den eigenen Projekte-Bereich nicht mehr — die
// Liste steht im Projektboard. Der Aufruf bleibt dennoch überall stehen
// (renderAll(), editProjectField(), …), damit kein Pfad ins Leere greift.
const listEl = document.getElementById('projectsList');
if(!listEl) return;
const w = currentWeek();
if(!w) return;
const locked = isWeekLocked(w);
let html="";
const groups = {};
// Alle Standardgruppen vorbelegen: Ohne das entsteht für eine leer geräumte
// Gruppe weder Überschrift noch Ablagefläche — ein Projekt ließ sich dann
// nur noch über das Auswahlfeld zurückverschieben, und der Hinweis
// "Keine Projekte." unten war toter Code.
['Aktive Projekte','Kleine Projekte','In Anbahnung','Allgemein'].forEach(g => { groups[g] = []; });
w.projects.forEach(p=>{ (groups[p.group]||(groups[p.group]=[])).push(p); });
Object.keys(groups).forEach(g=>{
groups[g].sort((a,b)=>(a.order||0)-(b.order||0));
@@ -74,28 +137,37 @@ function renderProjects(){
html += `<div class="section-title">${esc(g)}</div>`;
html += `<div class="projects-sortable" data-group="${g}" ${locked ? '' : 'ondragover="handleDragOver(event)" ondrop="handleDrop(event)"'}>`;
groups[g].forEach(p=>{
html += `<div class="person-list-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999;margin-right:8px">⋮⋮</span>`}
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr">
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung">
<select onchange="editProjectField('${p.id}','group',this.value)">
const tags = Array.isArray(p.tags) ? p.tags : [];
html += `<div class="person-list-item project-item" ${locked ? '' : 'draggable="true" ondragstart="handleDragStart(event)" ondragend="handleDragEnd(event)"'} data-project-id="${p.id}">
${locked ? '' : `<span class="drag-handle" style="cursor:grab;color:#999">${icon('grip')}</span>`}
<input type="text" value="${esc(p.nr)}" onchange="editProjectField('${p.id}','nr',this.value)" style="width:100px" placeholder="Proj-Nr" ${locked ? 'disabled' : ''}>
<input type="text" value="${esc(p.name)}" onchange="editProjectField('${p.id}','name',this.value)" style="width:200px" placeholder="Bezeichnung" ${locked ? 'disabled' : ''}>
<select onchange="editProjectField('${p.id}','group',this.value)" ${locked ? 'disabled' : ''}>
<option ${p.group==='Aktive Projekte'?'selected':''}>Aktive Projekte</option>
<option ${p.group==='Kleine Projekte'?'selected':''}>Kleine Projekte</option>
<option ${p.group==='In Anbahnung'?'selected':''}>In Anbahnung</option>
<option ${p.group==='Allgemein'?'selected':''}>Allgemein</option>
</select>
<select onchange="editProjectActiveLP('${p.id}',this.value)" title="Aktive Leistungsphase wählen">
<select onchange="editProjectActiveLP('${p.id}',this.value)" title="Aktive Leistungsphase wählen" ${locked ? 'disabled' : ''}>
<option value="" ${lpActiveList(p).length===0?'selected':''}>Aktive LP: keine</option>
${AUFWAND_LPS.map(l => `<option value="${l}" ${lpActiveList(p)[0]===l?'selected':''}>${l}</option>`).join('')}
</select>
<button class="secondary small" onclick="openLpModal('${p.id}')">✎ LP-Aufwand</button>
<button class="danger small" onclick="delProjectFromList('${p.id}')">Löschen</button>
<div class="project-tags-row">
<div class="tag-chip-list">
${tags.map(tag => `<span class="tag-chip" data-tag="${esc(tag)}">${esc(tag)}${locked ? '' : ` <span class="tag-remove" title="Tag entfernen" onclick="removeProjectTag('${p.id}', this.closest('.tag-chip').dataset.tag)">×</span>`}</span>`).join('')}
</div>
${locked ? '' : `<input type="text" class="tag-input" placeholder="+ Tag" onkeydown="handleProjectTagKeydown(event,'${p.id}')">`}
</div>
<div class="project-card-footer">
<button class="secondary small" onclick="openLpModal('${p.id}')" ${locked ? 'disabled' : ''}>LP-Aufwand</button>
<button class="danger small" onclick="delProjectFromList('${p.id}')" ${locked ? 'disabled' : ''}>Löschen</button>
</div>
</div>`;
});
if(groups[g].length===0) html+=`<div class="hint" style="padding:4px 2px 10px">Keine Projekte — Projekt hierher ziehen.</div>`;
html += `</div>`;
if(groups[g].length===0) html+=`<div class="hint">Keine Projekte.</div>`;
});
document.getElementById('projectsList').innerHTML=html;
listEl.innerHTML = html;
}
// Drag-and-Drop Handling für Projekte
@@ -135,7 +207,12 @@ function handleDrop(event){
sourceSortable.dataset && sortable.dataset &&
sourceSortable.dataset.group === sortable.dataset.group;
if(isSameGroup){
const afterElement = getDragAfterElement(sortable, event.clientY);
// Die Ablagefläche der Projektliste enthält .board-row-Zeilen, der
// Personen-/Projekte-Aufbau .person-list-item. getDragAfterElement()
// braucht den passenden Selektor, sonst findet es keine Nachbarn und
// hängt jedes Projekt ans Gruppenende.
const sel = sortable.querySelector('.board-row') ? '.board-row:not([style*="opacity"])' : undefined;
const afterElement = getDragAfterElement(sortable, event.clientY, sel);
if(afterElement == null){
sortable.appendChild(draggedProject);
} else {
@@ -151,7 +228,13 @@ function handleDrop(event){
// handleDragEnd: stellt die Deckkraft der gezogenen Zeile wieder her und
// setzt `draggedProject` zurück (Ende des Drag-Vorgangs).
function handleDragEnd(event){
event.currentTarget.style.opacity = '1';
// Attribut entfernen statt auf 1 setzen: getDragAfterElement() schließt jedes
// Element mit style="…opacity…" von der Positionsberechnung aus. Nach einem
// abgebrochenen Ziehen (Loslassen außerhalb der Liste) blieb das Attribut
// stehen, und die Zeile wurde beim nächsten Ablegen ignoriert — das Projekt
// landete dann am Gruppenende statt an der gewünschten Stelle.
event.currentTarget.style.removeProperty('opacity');
if(event.currentTarget.getAttribute('style') === '') event.currentTarget.removeAttribute('style');
draggedProject = null;
}
@@ -197,6 +280,8 @@ function reorderProjects(){
});
});
save();
renderProjects();
// renderProjects() nur, solange der eigene Projekte-Bereich existiert; die
// Projektliste im Board rendert renderBoard().
if(typeof renderProjects === 'function' && document.getElementById('projectsList')) renderProjects();
renderBoard();
}