Beteiligten-Zuordnung verlagert: p.assign ist jetzt ein Stammdatum und nur noch in den Projekteinstellungen editierbar (planCycleAssign()). Wochenbericht und Projektboard zeigen sie nur noch an, mit Verweis dorthin (springZuEinstellungen()). cycleAssign() ist damit entfallen. Neu "TODOs aus Protokoll": ein externes Besprechungsprotokoll als PDF wird per KI eingelesen und daraus in einem zweiten KI-Schritt Aufgaben abgeleitet. Eigener Bereich in der Aufgaben-/GANTT-Leiste, bewusst unabhaengig von den gespeicherten Protokollen - nichts davon wird abgelegt. Drei Schritte in einem Dialog mit fester Fusszeile; die Vorschlagsliste als kompakte, aufklappbare Zeilen, weil ein echter Import leicht 20 Vorschlaege liefert. Sichtbarkeit je Aufgabe im GANTT: neue Spalte plan_todos.show_in_gantt (Vorgabe 1, idempotente Migration) und eine Checkbox im Aufgaben-Dialog. Eine terminierte Aufgabe kann damit gezielt aus dem Terminplan herausgehalten werden, bleibt sonst aber eine vollwertige Aufgabe. plan-build-gantt.js und plan-drag.js pruefen dieselbe Bedingung, damit Zeilenzahl und Umsortierung nicht auseinanderlaufen. KI-Timeout dritte Stufe: ein abweichender, aber entfernter Endpunkt (Drittanbieter-Proxy) bekommt 2 Minuten statt der 45 Sekunden der echten Anbieter-API. Gemessen: die Punkte-Erkennung eines realen Protokolls brauchte dort 39 Sekunden, lag also stets knapp unter dem alten Limit und brach bei jeder Schwankung ab. Bewegung: Hover- und Klick-Rueckmeldung als Token-Satz in :root, angewandt auf die anklickbaren Zeilen, Schalter und Dialoge. Die Akzentleiste ist ein inset-Schatten statt eines positionierten Elements, damit Drag-and-Drop unberuehrt bleibt; prefers-reduced-motion war bisher gar nicht vorhanden und kippt jetzt einfach die Tokens. Plan Checker: optionaler Projektabgleich des Plankopfs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
406 lines
20 KiB
JavaScript
406 lines
20 KiB
JavaScript
// -----------------------------
|
|
// Projektbereich — Zustand, Einstieg & Projektauswahl
|
|
// -----------------------------
|
|
// Dieses Modul wird als erstes der plan-*-Module von public/App.html geladen
|
|
// und definiert window.PlanApp als gemeinsamen Zustand aller Module des
|
|
// Projektbereichs.
|
|
//
|
|
// WICHTIG — geändert gegenüber der früheren Fassung: Der Projektbereich war
|
|
// einmal eine eigene Seite (public/project-plan.html) mit eigenem
|
|
// Einstiegspunkt. Sie las Sitzung und Projekt aus der URL und lud sich ihren
|
|
// Datenbestand über GET /load-all selbst. Beides ist entfallen: Der
|
|
// Projektbereich ist jetzt ein Bereich der Hauptseite (#plan in App.html),
|
|
// der über showTab('plan') eingeblendet wird wie jeder andere. Er arbeitet
|
|
// deshalb auf dem bereits geladenen In-Memory-Bestand db.weeks (js/store.js)
|
|
// und braucht weder einen eigenen Ladevorgang noch eine eigene
|
|
// Anmeldeprüfung.
|
|
//
|
|
// Einstieg sind zwei Schritte: setPlanTarget(weekLabel, projNr, projName)
|
|
// merkt sich das Ziel, openPlanView() sucht es in db.weeks und rendert den
|
|
// Bereich. Getrennt sind sie, weil showTab('plan') (js/tabs-auth.js) aus dem
|
|
// Ziel zuerst die Adresszeile schreibt und den Bereich einblendet — das GANTT
|
|
// kann sich erst vermessen, wenn sein Container sichtbar ist. Gesetzt wird
|
|
// das Ziel von openProjectPlan() (js/gantt.js) und beim Seitenstart aus einem
|
|
// Deep-Link "/Projektplan/<Projektname>?weekLabel=…&projNr=…"
|
|
// (applyUrlRoute() in js/ui.js, ausgewertet in js/init.js).
|
|
// Die hier definierten Funktionen (savePlan, isLocked, showToast u. a.)
|
|
// stellen sich über app.* den anderen plan-Modulen bereit, die alle in einem
|
|
// IIFE mit window.PlanApp laufen.
|
|
|
|
// PlanApp: gemeinsamer, modulübergreifender Zustand des Projektbereichs.
|
|
window.PlanApp = {
|
|
weekLabel: '',
|
|
projNr: '',
|
|
projName: '',
|
|
sandboxPlanId: '', // gesetzt von openSandboxPlan() — Sandbox-Pläne werden über id adressiert, nicht über nr/name
|
|
week: null, // planWeek
|
|
project: null, // planProject
|
|
pxPerDay: 14, // Zoomstufe GANTT (null = an Containerbreite anpassen)
|
|
criticalPathActive: false, // Hervorhebung des kritischen Pfads im GANTT aktiv
|
|
// Drag-Zustände
|
|
todoDrag: null, // Drag-and-Drop von TODO/Meilenstein-Zeilen
|
|
ganttDrag: null, // Balken/Meilenstein/Urlaub verschieben
|
|
currentMilestoneIndex: null, // Index des gerade bearbeiteten Meilensteins (Modal)
|
|
currentTodoIndex: null, // Index der gerade bearbeiteten TODO (Modal)
|
|
currentAbsencePersonId: null, // Person-ID des offenen Abwesenheits-Modals
|
|
toastTimer: null, // Timer der Toast-Benachrichtigung (plan-state.js)
|
|
resizeTimer: null // Debounce-Timer für Fenster-Resize (plan-gantt.js)
|
|
};
|
|
|
|
(function(){
|
|
const app = window.PlanApp;
|
|
|
|
// planFail: zeigt einen Fehler in der Kopfzeile des Projektbereichs an und
|
|
// blendet die Bereichsleiste aus, weil ohne Projekt jeder Unterbereich nur
|
|
// in eine leere Maske führte. Parameter: title, subtitle (Strings).
|
|
function planFail(title, subtitle){
|
|
const t = document.getElementById('planTitle');
|
|
const sub = document.getElementById('planSubtitle');
|
|
if(t) t.textContent = title;
|
|
if(sub) sub.textContent = subtitle;
|
|
const badge = document.getElementById('planIdBadge');
|
|
if(badge) badge.textContent = '';
|
|
app.project = null;
|
|
app.hidePlanNav();
|
|
}
|
|
|
|
// setPlanTarget: merkt sich, welches Projekt der Bereich zeigen soll —
|
|
// adressiert über Sitzungs-Label und Projekt-Nr./-Bezeichnung, NICHT über
|
|
// project.id, die beim Duplizieren einer Sitzung (cloneWeek(), js/weeks.js)
|
|
// neu vergeben wird. Bewusst getrennt vom Rendern: showTab('plan')
|
|
// (js/tabs-auth.js) schreibt aus diesen Werten zuerst die Adresszeile und
|
|
// blendet den Bereich ein, und erst der sichtbare Bereich lässt sich
|
|
// vermessen — das GANTT braucht die Containerbreite. Parameter:
|
|
// weekLabel, projNr, projName (Strings). Keine Rückgabe.
|
|
window.setPlanTarget = function(weekLabel, projNr, projName){
|
|
app.weekLabel = weekLabel || '';
|
|
app.projNr = projNr || '';
|
|
app.projName = projName || '';
|
|
app.sandboxPlanId = '';
|
|
};
|
|
|
|
// openSandboxPlan: das Gegenstück zu setPlanTarget()+showTab('plan') für
|
|
// einen Terminplan der Planung-Sandbox (js/planung/planung.js). Sandbox-
|
|
// Pläne haben keine Projektnummer und werden nie geklont (cloneWeek()
|
|
// betrifft nur echte Wochen) — ihre id bleibt also stabil und ist die
|
|
// einzige verlässliche Adressierung, anders als bei echten Projekten
|
|
// (dort nr/name, siehe setPlanTarget()). Parameter: planId. Keine Rückgabe.
|
|
window.openSandboxPlan = function(planId){
|
|
app.weekLabel = SANDBOX_WEEK_LABEL;
|
|
app.projNr = '';
|
|
app.projName = '';
|
|
app.sandboxPlanId = planId || '';
|
|
showTab('plan');
|
|
};
|
|
|
|
// openPlanView: sucht Sitzung und Projekt im bereits geladenen Bestand
|
|
// db.weeks (js/store.js) und rendert den Bereich. Einen eigenen
|
|
// Ladevorgang gibt es nicht mehr — der Projektbereich teilt sich den
|
|
// Datenbestand mit allen anderen Bereichen der Seite. Wird von
|
|
// showTab('plan') gerufen (und von dort aus auch nach jedem erneuten
|
|
// Laden, siehe load() in js/store.js). Rückgabe: true, wenn das Projekt
|
|
// gefunden und gerendert wurde.
|
|
window.openPlanView = function(){
|
|
app.showPlanNav();
|
|
|
|
// Planung-Sandbox: eigener, einfacherer Zweig — eine feste, reservierte
|
|
// "Sitzung" (db.sandboxWeek, nie Teil von db.weeks, siehe js/store.js)
|
|
// und Adressierung über die stabile Plan-id statt über nr/name.
|
|
if(app.weekLabel === SANDBOX_WEEK_LABEL){
|
|
if(!app.sandboxPlanId){
|
|
planFail('Kein Terminplan gewählt', 'Diesen Bereich öffnet ein Klick auf einen Terminplan im Bereich „Planung".');
|
|
return false;
|
|
}
|
|
app.week = db.sandboxWeek || null;
|
|
app.project = (app.week && Array.isArray(app.week.projects))
|
|
? app.week.projects.find(p => p.id === app.sandboxPlanId) || null
|
|
: null;
|
|
if(!app.project){
|
|
planFail('Nicht gefunden', 'Dieser Terminplan wurde nicht gefunden — er wurde möglicherweise gelöscht.');
|
|
return false;
|
|
}
|
|
app.projNr = '';
|
|
app.projName = app.project.name || '';
|
|
renderPlanPage();
|
|
return true;
|
|
}
|
|
|
|
if(!app.weekLabel || (!app.projNr && !app.projName)){
|
|
planFail('Kein Projekt gewählt', 'Diesen Bereich öffnet ein Klick auf ein Projekt im Projektboard oder im Tab „Projekte".');
|
|
return false;
|
|
}
|
|
// Vor dem ersten erfolgreichen load() ist db.weeks noch leer (Deep-Link
|
|
// direkt auf "/Projektplan/…"). Dann ist das kein Fehler, sondern nur
|
|
// noch nicht so weit: load() ruft showTab(currentTab) erneut auf.
|
|
if(!Array.isArray(db.weeks) || db.weeks.length === 0){
|
|
const t = document.getElementById('planTitle');
|
|
const sub = document.getElementById('planSubtitle');
|
|
if(t) t.textContent = app.projName || 'Projektbereich';
|
|
if(sub) sub.textContent = 'Sitzungsdaten werden geladen…';
|
|
return false;
|
|
}
|
|
app.week = db.weeks.find(w => w.label === app.weekLabel) || null;
|
|
if(!app.week){
|
|
planFail('Nicht gefunden', 'Sitzung "' + app.weekLabel + '" konnte nicht geladen werden.');
|
|
return false;
|
|
}
|
|
// Projekt-Nr. ist der stabile Schlüssel; die Bezeichnung dient nur als
|
|
// Rückfallebene für Projekte, die (noch) keine Nummer haben.
|
|
const projects = app.week.projects || [];
|
|
app.project = projects.find(p => (p.nr || '') === app.projNr && (p.name || '') === app.projName)
|
|
|| (app.projNr ? projects.find(p => (p.nr || '') === app.projNr) : null)
|
|
|| projects.find(p => (p.name || '') === app.projName)
|
|
|| null;
|
|
if(!app.project){
|
|
planFail('Nicht gefunden', 'Projekt nicht in dieser Sitzung vorhanden.');
|
|
return false;
|
|
}
|
|
app.projNr = app.project.nr || '';
|
|
app.projName = app.project.name || '';
|
|
renderPlanPage();
|
|
return true;
|
|
};
|
|
|
|
// ensurePlanLocal: stellt sicher, dass das Plan-Objekt des Projekts die
|
|
// erwartete Struktur hat (inkl. Migration alter Einzel-Abhängigkeiten).
|
|
function ensurePlanLocal(){
|
|
if(!app.project.plan) app.project.plan = { start:'', end:'', todos:[], milestones:[], people:[], progress:0 };
|
|
if(!Array.isArray(app.project.plan.todos)) app.project.plan.todos = [];
|
|
// Migration alter Einzel-Abhängigkeiten zu Array
|
|
app.project.plan.todos.forEach(t => {
|
|
if(!Array.isArray(t.dependsOn)){
|
|
t.dependsOn = t.dependsOn ? [t.dependsOn] : [];
|
|
}
|
|
if(!Array.isArray(t.assignees)) t.assignees = [];
|
|
if(t.progress === undefined) t.progress = t.done ? 100 : 0;
|
|
});
|
|
return app.project.plan;
|
|
}
|
|
|
|
// isLocked: liefert true, wenn die aktuelle Woche gesperrt ist (dann sind
|
|
// Bearbeitungen im Plan gesperrt). Rückgabe: Boolescher Wert.
|
|
function isLocked(){
|
|
return !!app.week.locked;
|
|
}
|
|
|
|
// isSandboxPlan: liefert true, wenn der aktuell geöffnete Plan zur
|
|
// Planung-Sandbox gehört statt zu einem echten Projekt. Zentraler Merker
|
|
// für alle plan-*-Module, die dort Abschnitte ausblenden, die nur bei
|
|
// einem echten Projekt Sinn ergeben (Dateien, Leistung, Auftragsstatus,
|
|
// Projektnummer, Gewerke, Projektgröße, Kommentar/Verlauf — siehe CLAUDE.md
|
|
// "Planung — projektunabhängige Terminplan-Sandbox"). Rückgabe: Boolescher Wert.
|
|
function isSandboxPlan(){
|
|
return !!app.week && app.week.label === SANDBOX_WEEK_LABEL;
|
|
}
|
|
|
|
// renderPlanPage: setzt die kompakte, einzeilige Titelzeile aus Projekt und
|
|
// Woche (Nr.-Badge, Name, Tags, Sitzungslabel, Beteiligte als
|
|
// überlappender Avatar-Stapel — siehe renderProjectAssignees()), baut das
|
|
// Projektinfo-Panel auf (renderPlanInfo(), js/plan/plan-info.js: Status,
|
|
// Gruppe, amtierende Leistungsphase, LPH-Prozentliste) sowie den
|
|
// Dateien-Bereich (initPlanFiles(), js/plan/plan-files.js: Upload, Freigabe,
|
|
// Kommentare) und den Einstellungen-Bereich (renderPlanSettings(),
|
|
// js/plan/plan-settings.js: Honorar, beauftragte Leistungsphasen), blendet
|
|
// die Schaltflächen zum Anlegen neuer Aufgaben bei gesperrter Woche aus,
|
|
// startet die GANTT-Anzeige über fitPlanGanttRange() und schaltet zuletzt
|
|
// auf den sichtbaren Bereich (app.initPlanSections(), js/plan/plan-sections.js).
|
|
function renderPlanPage(){
|
|
const p = app.project;
|
|
const plan = ensurePlanLocal();
|
|
const sandbox = isSandboxPlan();
|
|
const planIdBadge = document.getElementById('planIdBadge');
|
|
if(planIdBadge) planIdBadge.textContent = p.nr || '';
|
|
document.getElementById('planTitle').textContent = p.name;
|
|
// Für die Sandbox NICHT das rohe week.label ("__sandbox__") anzeigen —
|
|
// das ist ein interner Marker, kein für Nutzer bestimmter Sitzungsname.
|
|
document.getElementById('planSubtitle').textContent = sandbox
|
|
? 'Planung — unabhängig von einem laufenden Projekt'
|
|
: app.week.label + (app.week.date ? ' · ' + formatWeekDate(app.week.date) : '');
|
|
|
|
const planPeopleList = document.getElementById('planPeopleList');
|
|
if(planPeopleList) planPeopleList.innerHTML = renderProjectAssignees();
|
|
|
|
const tags = Array.isArray(p.tags) ? p.tags : [];
|
|
const planTagsList = document.getElementById('planTagsList');
|
|
if(planTagsList){
|
|
planTagsList.innerHTML = tags.map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
|
|
}
|
|
|
|
// Projektinfo-Panel (Status, Gruppe, amtierende Leistungsphase, LPH-
|
|
// Prozentliste) — siehe js/plan/plan-info.js.
|
|
renderPlanInfo();
|
|
|
|
// Dateien-Bereich (Upload, Freigabe, Kommentare) — siehe js/plan/plan-files.js.
|
|
initPlanFiles();
|
|
|
|
// Protokolle-Bereich (Besprechungsprotokolle) — siehe js/plan/plan-protokoll.js.
|
|
// Wie initPlanFiles() unbedingt aufgerufen (nicht erst beim Öffnen des
|
|
// Bereichs), lädt bei vorhandener Projektnummer die Protokollliste.
|
|
if(typeof initPlanProtokoll === 'function') initPlanProtokoll();
|
|
|
|
// Verlauf über alle Sitzungen (Kommentar-Historie) — siehe js/plan/plan-history.js.
|
|
renderPlanHistory();
|
|
|
|
// Einstellungen-Bereich (Honorar, beauftragte Leistungsphasen,
|
|
// Projektangaben) — siehe js/plan/plan-settings.js.
|
|
renderPlanSettings();
|
|
|
|
// Schaltflächen, die neue Aufgaben/Meilensteine erzeugen — in gesperrten
|
|
// Sitzungen nicht möglich. Die Liste enthält die Knöpfe beider Bereiche:
|
|
// Terminplan (…Btn) und Aufgaben (…BtnList), siehe App.html.
|
|
const locked = isLocked();
|
|
['addMilestoneBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'protokollTodosBtn', 'autoScheduleBtn',
|
|
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'honorarBtnList', 'protokollTodosBtnList', 'autoScheduleBtnList']
|
|
.forEach(id => {
|
|
const btn = document.getElementById(id);
|
|
if(btn) btn.style.display = locked ? 'none' : '';
|
|
});
|
|
|
|
// "Dateien" (keine Projektnummer, kein sinnvoller Prüf-Workflow),
|
|
// "Leistung" (echte Abrechnung/Auftragsstatus-abhängig) und "Protokolle"
|
|
// (hängt wie Dateien an project.nr) ergeben für einen Terminplan ohne
|
|
// echtes Projekt keinen Sinn — Bereichsknöpfe ausblenden.
|
|
document.querySelectorAll('#planNav .plan-nav-item[data-section="dateien"], #planNav .plan-nav-item[data-section="leistung"], #planNav .plan-nav-item[data-section="protokolle"]')
|
|
.forEach(btn => { btn.style.display = sandbox ? 'none' : ''; });
|
|
|
|
// Gantt-PDF-Export (js/pdf.js, exportSandboxPlanPDF()) gibt es nur für
|
|
// Sandbox-Pläne der Planung — ein echtes Projekt hat kein Äquivalent.
|
|
const sandboxPdfBtn = document.getElementById('sandboxPdfBtn');
|
|
if(sandboxPdfBtn) sandboxPdfBtn.style.display = sandbox ? '' : 'none';
|
|
|
|
// GANTT inkl. TODOs (rendert am Ende auch Aufgabenliste und Dashboard mit)
|
|
fitPlanGanttRange();
|
|
|
|
// Sichtbaren Bereich wählen (URL-Fragment, sonst Dashboard) — siehe
|
|
// js/plan/plan-sections.js. Steht bewusst am Ende: erst wenn alle
|
|
// Bereiche befüllt sind, wird einer davon eingeblendet.
|
|
app.initPlanSections();
|
|
}
|
|
|
|
// Entfernt eine gelöschte ID aus allen dependsOn-Listen (TODOs und Meilensteine)
|
|
function removeDependencyRefs(plan, deletedId){
|
|
(plan.todos || []).forEach(t => {
|
|
if(Array.isArray(t.dependsOn)) t.dependsOn = t.dependsOn.filter(id => id !== deletedId);
|
|
});
|
|
(plan.milestones || []).forEach(m => {
|
|
if(Array.isArray(m.dependsOn)) m.dependsOn = m.dependsOn.filter(id => id !== deletedId);
|
|
});
|
|
}
|
|
|
|
// planRoleClass: Kopie von roleClass() aus js/board/board.js (dort nicht
|
|
// geladen) — erzeugt dieselben CSS-Klassen wie im Board, damit die
|
|
// Avatar-Kreise hier dieselben Rollenfarben (.board-avatar in styles.css)
|
|
// nutzen. Parameter: role (String). Rückgabe: Klassen-String.
|
|
function planRoleClass(role){
|
|
return 'role-' + String(role || '').toLowerCase().replace(/\s+/g, '-');
|
|
}
|
|
|
|
// Beteiligte des Projekts (Haupt + Optional) mit E-Mail aus der globalen
|
|
// Liste, als überlappender Avatar-Stapel für die kompakte Titelzeile
|
|
// (dieselben Kreise/Farben wie im Projektboard-Detailpanel, siehe
|
|
// .board-avatar in styles.css). Voller Name + Rolle stehen im Tooltip.
|
|
function renderProjectAssignees(){
|
|
const assignees = projectAssignees(app.project, app.week);
|
|
if(assignees.length === 0) return '';
|
|
return assignees.map(a => {
|
|
const title = (a.person.fullName || a.person.name) + (a.role ? ' · ' + a.role : '');
|
|
return `<span class="board-avatar ${planRoleClass(a.role)} state-${a.state}" title="${esc(title)}">${esc(a.person.name)}</span>`;
|
|
}).join('');
|
|
}
|
|
|
|
// savePlan: merkt die Sitzung des Projektbereichs als geändert vor und
|
|
// stößt den gewöhnlichen, entprellten Speicherlauf an (js/store.js).
|
|
//
|
|
// Früher schrieb der Projektbereich mit einem eigenen sendWeekToServer()
|
|
// direkt zum Server, weil er eine eigene Seite war und store.js dort gar
|
|
// nicht geladen wurde. In einer Seite wären daraus zwei Schreiber auf
|
|
// dieselbe Sitzung geworden: beide senden dieselbe Revisions-Basis, und der
|
|
// langsamere bekäme einen scheinbaren 409-Konflikt gemeldet. Deshalb geht
|
|
// jetzt auch der Projektbereich durch syncAllToServer() — mitsamt dessen
|
|
// In-Flight-Schutz, Wiederholung nach Netzfehlern, Konfliktbehandlung und
|
|
// Anmelde-Prüfung. markWeekDirty() bekommt bewusst app.week.label statt
|
|
// save() zu rufen: save() markiert die AKTUELL gewählte Sitzung, und ein
|
|
// Deep-Link kann den Projektbereich auf eine andere gestellt haben.
|
|
function savePlan(){
|
|
if(!app.week || !app.project) return;
|
|
markWeekDirty(app.week.label);
|
|
scheduleSessionSync();
|
|
}
|
|
|
|
// userIdByPersonIdPromise: Cache der Zuordnung Person-ID (String, z. B.
|
|
// "p_max_mustermann") -> numerische users.id. Seit der Ablösung von
|
|
// people/person_roles/person_absences gibt es keinen /save-people-Schreibpfad
|
|
// mehr; Abwesenheiten werden direkt gegen /users/:id/absences geschrieben
|
|
// (siehe plan-modals.js/plan-drag.js), wofür die numerische User-ID benötigt
|
|
// wird. Lazy geladen über GET /users (liefert seit Phase 1 auch personId)
|
|
// und für die Lebensdauer der Seite zwischengespeichert.
|
|
let userIdByPersonIdPromise = null;
|
|
function loadUserIdByPersonId(){
|
|
if(!userIdByPersonIdPromise){
|
|
userIdByPersonIdPromise = fetch(baseUrl() + '/users')
|
|
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
|
|
.then(data => {
|
|
const map = {};
|
|
(Array.isArray(data.users) ? data.users : []).forEach(u => {
|
|
if(u.personId) map[u.personId] = u.id;
|
|
});
|
|
return map;
|
|
})
|
|
.catch(err => {
|
|
// Cache zurücksetzen, damit ein späterer Aufruf es erneut versucht.
|
|
userIdByPersonIdPromise = null;
|
|
throw err;
|
|
});
|
|
}
|
|
return userIdByPersonIdPromise;
|
|
}
|
|
|
|
// getUserIdForPersonId: liefert die numerische users.id für eine Personen-ID
|
|
// oder null, falls keine passende (aktive) Benutzer-Zuordnung existiert (z. B.
|
|
// eine Person ohne Login-Konto). Rückgabe: Promise.
|
|
function getUserIdForPersonId(personId){
|
|
return loadUserIdByPersonId().then(map => (map && Object.prototype.hasOwnProperty.call(map, personId)) ? map[personId] : null);
|
|
}
|
|
|
|
// Person in der globalen Liste finden (dort werden Abwesenheiten gepflegt)
|
|
function findGlobalPerson(personId){
|
|
return (db.globalPeople || []).find(p => p.id === personId) || null;
|
|
}
|
|
|
|
// countPersonAbsences: zählt die Abwesenheiten einer Person anhand der
|
|
// globalen Liste. Rückgabe: Anzahl (0 bei fehlender Person).
|
|
function countPersonAbsences(personId){
|
|
const p = findGlobalPerson(personId);
|
|
return p && Array.isArray(p.absences) ? p.absences.length : 0;
|
|
}
|
|
|
|
// Toast-Benachrichtigung
|
|
function showToast(message){
|
|
let toast = document.getElementById('planToast');
|
|
if(!toast){
|
|
toast = document.createElement('div');
|
|
toast.id = 'planToast';
|
|
toast.className = 'plan-toast';
|
|
document.body.appendChild(toast);
|
|
}
|
|
toast.textContent = message;
|
|
toast.classList.add('show');
|
|
// Vorherigen Timer abbrechen, sonst verschwindet ein neuer Toast zu früh
|
|
if(app.toastTimer) clearTimeout(app.toastTimer);
|
|
app.toastTimer = setTimeout(() => { toast.classList.remove('show'); app.toastTimer = null; }, 2500);
|
|
}
|
|
|
|
// Export der Hilfsfunktionen auf PlanApp, damit die anderen plan-Module
|
|
// (plan-drag, plan-modals, plan-schedule) sie direkt nutzen können.
|
|
app.ensurePlanLocal = ensurePlanLocal;
|
|
app.isLocked = isLocked;
|
|
app.isSandboxPlan = isSandboxPlan;
|
|
app.removeDependencyRefs = removeDependencyRefs;
|
|
app.renderProjectAssignees = renderProjectAssignees;
|
|
app.planRoleClass = planRoleClass;
|
|
app.savePlan = savePlan;
|
|
app.getUserIdForPersonId = getUserIdForPersonId;
|
|
app.findGlobalPerson = findGlobalPerson;
|
|
app.countPersonAbsences = countPersonAbsences;
|
|
app.showToast = showToast;
|
|
})(); |