alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
// -----------------------------
// 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();
}
}