// ----------------------------- // Planung — Terminplan-Sandbox // ----------------------------- // Der Tab "Planung" (js/tabs-auth.js, Slug "Planung" in TAB_SLUGS) zeigt eine // einfache Liste projektunabhängiger Terminpläne — Kapazitätstests, Angebote // vor der Beauftragung, eigene Vorhaben. Jeder Plan ist ein ganz normales // Projekt-Objekt (dieselbe Form wie ein echtes Projekt), liegt aber in der // reservierten Sitzung SANDBOX_WEEK_LABEL statt in einer echten (siehe // db.sandboxWeek, js/store.js, und CLAUDE.md "Planung — projektunabhängige // Terminplan-Sandbox"). Das Bearbeiten selbst — Terminplan, Aufgaben, // Meilensteine, Honorar-Rechner — läuft über denselben Projektbereich wie bei // einem echten Projekt (openSandboxPlan(), js/plan/plan-state.js); dieses // Modul stellt nur die Liste und den schlanken Anlege-Dialog bereit. // planungGewaehlteArt: welche Anlege-Art im Dialog gerade markiert ist // ('honorar'|'leer'). Zustand nur für die Dauer des offenen Dialogs. let planungGewaehlteArt = 'honorar'; // renderPlanung: füllt die Terminplan-Liste des Tabs "Planung". Keine // Parameter, keine Rückgabe. function renderPlanung(){ const el = document.getElementById('planungList'); if(!el) return; const plaene = (db.sandboxWeek && Array.isArray(db.sandboxWeek.projects)) ? db.sandboxWeek.projects : []; if(plaene.length === 0){ el.innerHTML = `
Noch kein Terminplan angelegt. Unabhängig von einem echten Projekt — für Angebote, Kapazitätstests oder eigene Vorhaben.
`; return; } // Zuletzt geändert zuerst — genau die Frage, die die Liste beantworten soll. const sortiert = [...plaene].sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || ''))); el.innerHTML = sortiert.map(p => { const honorar = Number(p.honorar) || 0; const ausHonorar = honorar > 0; const beauftragt = Array.isArray(p.beauftragteLP) ? p.beauftragteLP : []; const meta = ausHonorar ? 'Aus Honorar errechnet · ' + honorar.toLocaleString('de-DE') + ' € · ' + (beauftragt.length ? beauftragt.join(', ') : 'keine Leistungsphasen gewählt') : 'Frei begonnen · ' + ((p.plan && Array.isArray(p.plan.todos)) ? p.plan.todos.length : 0) + ' Aufgabe(n), ' + ((p.plan && Array.isArray(p.plan.milestones)) ? p.plan.milestones.length : 0) + ' Meilenstein(e)'; const geaendert = p.updatedAt ? formatCommentStamp(p.updatedAt) : '–'; return `
${ausHonorar ? '€' : '✎'}

${esc(p.name || 'Ohne Namen')}

${esc(meta)}

zuletzt geändert
${esc(geaendert)}
`; }).join(''); } // oeffneNeuerTerminplanDialog: zeigt den Anlege-Dialog mit leerem Namensfeld. // Keine Parameter, keine Rückgabe. function oeffneNeuerTerminplanDialog(){ const nameEl = document.getElementById('planungName'); if(nameEl) nameEl.value = ''; planungGewaehlteArt = 'honorar'; aktualisierePlanungArtAuswahl(); const modal = document.getElementById('neuerTerminplanModal'); if(modal) modal.style.display = 'flex'; if(nameEl) nameEl.focus(); } function schliesseNeuerTerminplanDialog(){ const modal = document.getElementById('neuerTerminplanModal'); if(modal) modal.style.display = 'none'; } // waehlePlanungArt: markiert eine der beiden Anlege-Karten ("Aus Honorar // berechnen" / "Leer beginnen"). Parameter: art ('honorar'|'leer'). function waehlePlanungArt(art){ planungGewaehlteArt = art; aktualisierePlanungArtAuswahl(); } function aktualisierePlanungArtAuswahl(){ document.querySelectorAll('#neuerTerminplanModal .choice-card').forEach(card => { card.classList.toggle('is-gewaehlt', card.dataset.art === planungGewaehlteArt); }); } // erstellePlanungTerminplan: legt den Sandbox-Plan an (Name-Pflichtfeld, // keine doppelten Namen — die einzige Identität eines Sandbox-Plans, siehe // CLAUDE.md) und öffnet ihn direkt im Projektbereich. Bei "Aus Honorar // berechnen" landet man in den Einstellungen mit fokussiertem Honorar-Feld; // den eigentlichen Rechenweg übernimmt der bereits vorhandene "Terminplan // aus Honorar"-Knopf dort — kein eigener Rechen-Code. Keine Parameter, keine // Rückgabe. function erstellePlanungTerminplan(){ const nameEl = document.getElementById('planungName'); const name = (nameEl ? nameEl.value : '').trim(); if(!name){ alert('Bitte einen Namen für den Terminplan eingeben.'); return; } const plaene = (db.sandboxWeek && Array.isArray(db.sandboxWeek.projects)) ? db.sandboxWeek.projects : []; if(plaene.some(p => (p.name || '').toLowerCase() === name.toLowerCase())){ alert('Es gibt bereits einen Terminplan mit dem Namen "' + name + '". Bitte einen anderen Namen wählen.'); return; } const neu = { id: uid(), projectUid: uid(), nr: '', name, group: '', gewerke: [], tags: [], status: '', order: plaene.length, lph: {}, assign: {}, plan: null, auftragsstatus: 'beauftragt', honorar: 0, beauftragteLP: [], aktuelleLP: [], besprochen: false, updatedAt: new Date().toISOString() }; plaene.push(neu); markWeekDirty(SANDBOX_WEEK_LABEL); scheduleSessionSync(); const gewaehlteArt = planungGewaehlteArt; schliesseNeuerTerminplanDialog(); renderPlanung(); openSandboxPlan(neu.id); if(gewaehlteArt === 'honorar'){ // Kleine UX-Abkürzung: direkt in den Einstellungen landen und das // Honorar-Feld fokussieren, statt den Nutzer selbst dorthin navigieren // zu lassen. showPlanSection() rendert synchron, das Feld existiert // danach im DOM. if(typeof showPlanSection === 'function') showPlanSection('einstellungen'); const honorarEl = document.getElementById('settingsHonorar'); if(honorarEl) honorarEl.focus(); } else if(typeof showPlanSection === 'function'){ // "Leer beginnen" führt direkt in den (leeren) Terminplan. showPlanSection('terminplan'); } }