- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
419 lines
20 KiB
JavaScript
419 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)
|
|
// 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();
|
|
// showTab() hat die Adresse schon vor diesem Aufruf geschrieben, als
|
|
// der Name noch fehlte (/Projektplan/?…) — jetzt mit Namen nachziehen.
|
|
if(typeof updateRouteUrl === 'function') updateRouteUrl('plan');
|
|
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 der TERMINPLAN nicht bearbeitet werden darf
|
|
// — gesperrte Sitzung oder fehlendes Recht „Terminplan bearbeiten“ (bei
|
|
// einem Planung-Plan: „Planung“; Rollen & Rechte). Rückgabe: Boolean.
|
|
function isLocked(){
|
|
return !!app.week.locked || !hatRecht(isSandboxPlan() ? 'wz.planung' : 'proj.plan');
|
|
}
|
|
// stammGesperrt: dasselbe für Projekteinstellungen, Gruppe, amtierende
|
|
// Leistungsphase und Honorar (Recht „Stammdaten und Beteiligte“).
|
|
function stammGesperrt(){
|
|
return !!app.week.locked || !hatRecht(isSandboxPlan() ? 'wz.planung' : 'proj.stamm');
|
|
}
|
|
// protokolleGesperrt: Protokolle (Recht „Protokolle bearbeiten“), in einer
|
|
// gesperrten Sitzung wie bisher gesperrt.
|
|
function protokolleGesperrt(){
|
|
return !!app.week.locked || !hatRecht('proj.protokolle');
|
|
}
|
|
|
|
// 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();
|
|
|
|
|
|
// 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), Aufgaben (…BtnList) und Leistungsphasen (…BtnPhasen),
|
|
// siehe App.html.
|
|
const locked = isLocked();
|
|
['addMilestoneBtn', 'addPhaseBtn', 'addTodoBtn', 'templateBtn', 'honorarBtn', 'protokollTodosBtn',
|
|
'addTodoBtnList', 'addMilestoneBtnList', 'templateBtnList', 'protokollTodosBtnList', 'autoScheduleBtnList',
|
|
'honorarBtnPhasen', 'addPhaseBtnPhasen']
|
|
.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()) gilt für
|
|
// Planung-Pläne UND echte Projekte, der Knopf ist immer sichtbar.
|
|
|
|
// 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.stammGesperrt = stammGesperrt;
|
|
app.protokolleGesperrt = protokolleGesperrt;
|
|
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;
|
|
})(); |