Files
projektuebersicht/public/js/planung/planung.js
erik 5b20adee48 All new
alle new
2026-10-01 06:46:51 +02:00

131 lines
6.0 KiB
JavaScript
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.
// -----------------------------
// 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 = `<div class="empty-hint">
<span class="big">Noch kein Terminplan angelegt.</span>
Unabhängig von einem echten Projekt — für Angebote, Kapazitätstests oder eigene Vorhaben.
</div>`;
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 `<div class="planung-row" onclick="openSandboxPlan('${esc(p.id)}')">
<div class="planung-row-icon ${ausHonorar ? 'honorar' : 'leer'}">${ausHonorar ? '€' : '✎'}</div>
<div class="planung-row-main">
<p class="planung-row-name">${esc(p.name || 'Ohne Namen')}</p>
<p class="planung-row-meta">${esc(meta)}</p>
</div>
<div class="planung-row-stat">zuletzt geändert<br><b>${esc(geaendert)}</b></div>
</div>`;
}).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, groesse: 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();
}
}