From e962be41ea04b15ff7efa287258105cf219b5f9d Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Mon, 21 Sep 2026 15:56:09 +0200 Subject: [PATCH] fix(historie): Uebernahme alter Zeiten, Rollenzone Externe, Reportfehler Die Uebernahme der Harvest-Historie ist an drei Stellen haengen geblieben: - resolvePot hat abgelaufene Toepfe grundsaetzlich gesperrt. Fuer eine historische Buchung ist das falsch - dort zaehlt, ob der Topf am Leistungstag lief. Der Importweg setzt jetzt `historic`, das Volumen wird weiter geprueft, damit Ueberbuchungen auffallen. - In der SSO-Verwaltung fehlte die Ablagezone fuer die Rolle "Externe"; Konten liessen sich dort nicht zuordnen. - In buildProjectDetail und der Kundenauswertung stand ein Rest der Auslastungsaenderung (bisHeute/capTo) ohne Bezug im Scope. Beide Reportseiten liefen dadurch auf 500. Co-Authored-By: Claude Opus 5 (1M context) --- server/src/lib/projects.js | 8 ++++++-- server/src/routes/imports.js | 3 ++- server/src/routes/reports.js | 17 ----------------- src/components/settings/SsoDirectory.tsx | 2 ++ 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/server/src/lib/projects.js b/server/src/lib/projects.js index 689a816..da4170e 100644 --- a/server/src/lib/projects.js +++ b/server/src/lib/projects.js @@ -343,8 +343,12 @@ export class BookingError extends Error { * - ist das Volumen erreicht oder das Ablaufdatum ueberschritten: nicht buchbar * - ueberlappen mehrere Toepfe, zuerst der, der am fruehesten endet * - Projekte ohne Topf (z. B. intern, unbegrenzt) sind frei buchbar + * + * Mit `historic` entfaellt die Ablaufsperre: bei der Uebernahme alter Zeiten + * zaehlt, ob der Topf am Leistungstag lief - nicht, ob er heute noch offen ist. + * Das Volumen wird weiter geprueft, damit Ueberbuchungen auffallen. */ -export async function resolvePot(conn, project, date, minutes, excludeEntryId = null, billable = true) { +export async function resolvePot(conn, project, date, minutes, excludeEntryId = null, billable = true, { historic = false } = {}) { const onlyBillable = countsAgainstBudget(project) === 'billable'; // Nicht abrechenbare Stunden in einem Billable-Projekt verbrauchen kein Volumen. const needed = onlyBillable && !billable ? 0 : minutes; @@ -370,7 +374,7 @@ export async function resolvePot(conn, project, date, minutes, excludeEntryId = } let lastShortage = null; for (const pot of covering) { - if (pot.valid_to < today()) { + if (!historic && pot.valid_to < today()) { lastShortage = `Budgettopf „${pot.label}“ ist seit ${de(pot.valid_to)} abgelaufen.`; continue; } diff --git a/server/src/routes/imports.js b/server/src/routes/imports.js index fb18a91..2262e9c 100644 --- a/server/src/routes/imports.js +++ b/server/src/routes/imports.js @@ -84,7 +84,8 @@ router.post('/entries', async (req, res, next) => { if (Number(day.total) + minutes > 24 * 60) throw new Error('An diesem Tag waeren es mehr als 24 Stunden.'); const billable = isBillable(project, task); - const pot = await resolvePot(conn, project, date, minutes, null, billable); + // Historische Uebernahme: abgelaufene Toepfe nehmen ihre eigenen Zeiten noch an. + const pot = await resolvePot(conn, project, date, minutes, null, billable, { historic: true }); if (dryRun) return 0; const [result] = await conn.query( `INSERT INTO pulse_time_entries (user_id, project_id, task_id, pot_id, entry_date, minutes, note, billable, harvest_id) diff --git a/server/src/routes/reports.js b/server/src/routes/reports.js index 8f31a48..4cb2053 100644 --- a/server/src/routes/reports.js +++ b/server/src/routes/reports.js @@ -167,15 +167,6 @@ function buildProjectDetail(project, entries) { }; }); - /* - * Auslastung nur bis heute. Die Kapazitaet endet ohnehin am heutigen Tag - * (capTo) - werden Zeiten im Voraus gebucht, etwa eine ganze Woche Urlaub am - * Montag, stehen sonst 40 gebuchte Stunden gegen 8 Stunden Kapazitaet und die - * Auslastung springt auf 500 Prozent. Die Spalte "Stunden" zeigt weiter - * alles, was im Zeitraum gebucht ist. - */ - const bisHeute = entries.filter((e) => e.entry_date <= capTo); - const minutes = entries.reduce((s, e) => s + e.minutes, 0); const billable = entries.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0); return { @@ -228,14 +219,6 @@ router.get('/customer/:id', async (req, res, next) => { cursor.setUTCMonth(cursor.getUTCMonth() + 1); } - /* - * Auslastung nur bis heute. Die Kapazitaet endet ohnehin am heutigen Tag - * (capTo) - werden Zeiten im Voraus gebucht, etwa eine ganze Woche Urlaub am - * Montag, stehen sonst 40 gebuchte Stunden gegen 8 Stunden Kapazitaet und die - * Auslastung springt auf 500 Prozent. Die Spalte "Stunden" zeigt weiter - * alles, was im Zeitraum gebucht ist. - */ - const bisHeute = entries.filter((e) => e.entry_date <= capTo); const minutes = entries.reduce((s, e) => s + e.minutes, 0); const billable = entries.filter((e) => e.billable).reduce((s, e) => s + e.minutes, 0); diff --git a/src/components/settings/SsoDirectory.tsx b/src/components/settings/SsoDirectory.tsx index 1f0aebf..56b3413 100644 --- a/src/components/settings/SsoDirectory.tsx +++ b/src/components/settings/SsoDirectory.tsx @@ -12,6 +12,8 @@ type ImportRole = Exclude; const ZONES: { role: ImportRole; hint: string; icon: IconName; tone: string }[] = [ { role: 'user', hint: 'Bucht Zeiten auf zugewiesene Projekte', icon: 'user', tone: 'border-ink-300 bg-ink-50 text-ink-700' }, + // Gleiche Rechte wie Mitarbeiter; die Rolle trennt nur, wer zum Haus gehoert. + { role: 'external', hint: 'Wie Mitarbeiter, gilt als externe Kraft', icon: 'globe', tone: 'border-amber-200 bg-amber-50 text-amber-700' }, { role: 'lead', hint: 'Sieht und verwaltet sein Team', icon: 'team', tone: 'border-sky-200 bg-sky-50 text-sky-700' }, { role: 'management', hint: 'Sieht alles, verwaltet SSO', icon: 'shield', tone: 'border-violet-200 bg-violet-50 text-violet-700' }, ];