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) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-21 15:56:09 +02:00
co-authored by Claude Opus 5
parent 79bfd561bd
commit e962be41ea
4 changed files with 10 additions and 20 deletions
+6 -2
View File
@@ -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;
}
+2 -1
View File
@@ -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)
-17
View File
@@ -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);
+2
View File
@@ -12,6 +12,8 @@ type ImportRole = Exclude<Role, 'admin'>;
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' },
];