// src/services/buildTimeEvaluationFromSpans.ts import { FastifyInstance } from "fastify"; import { and, eq, gte, lte, inArray } from "drizzle-orm"; import { authProfiles, holidays } from "../../../db/schema"; import { DerivedSpan } from "./derivetimespans.service"; // Importiert den angereicherten Span-Typ // Definiert das erwartete Rückgabeformat export type TimeEvaluationResult = { user_id: string; tenant_id: number; from: string; to: string; // Sollzeit timeSpanWorkingMinutes: number; // Arbeitszeit Salden sumWorkingMinutesSubmitted: number; sumWorkingMinutesApproved: number; // Abwesenheiten (minuten und Tage) sumWorkingMinutesRecreationDays: number; sumRecreationDays: number; sumWorkingMinutesVacationDays: number; sumVacationDays: number; sumWorkingMinutesSickDays: number; sumSickDays: number; // Endsalden saldoApproved: number; // Saldo basierend auf genehmigter Zeit saldoSubmitted: number; // Saldo basierend auf eingereichter/genehmigter Zeit spans: DerivedSpan[]; }; // Hilfsfunktion zur Berechnung der Minuten (nur für geschlossene Spannen) const calcMinutes = (start: Date, end: Date | null): number => { if (!end) return 0; return (end.getTime() - start.getTime()) / 60000; }; export async function buildTimeEvaluationFromSpans( server: FastifyInstance, user_id: string, tenant_id: number, startDateInput: string, endDateInput: string, // Der wichtigste Unterschied: Wir nehmen die angereicherten Spannen als Input spans: DerivedSpan[] ): Promise { const startDate = server.dayjs(startDateInput); const endDate = server.dayjs(endDateInput); // ------------------------------------------------------------- // 1️⃣ Profil und Feiertage laden (WIE IM ALTEN SERVICE) // ------------------------------------------------------------- const profileRows = await server.db .select() .from(authProfiles) .where( and( eq(authProfiles.user_id, user_id), eq(authProfiles.tenant_id, tenant_id) ) ) .limit(1); const profile = profileRows[0]; if (!profile) throw new Error("Profil konnte nicht geladen werden."); const holidaysRows = await server.db .select({ date: holidays.date, }) .from(holidays) .where( and( inArray(holidays.state_code, [profile.state_code, "DE"]), gte(holidays.date, startDate.format("YYYY-MM-DD")), lte(holidays.date, endDate.add(1, "day").format("YYYY-MM-DD")) ) ); // ------------------------------------------------------------- // 2️⃣ Sollzeit berechnen (WIE IM ALTEN SERVICE) // ------------------------------------------------------------- let timeSpanWorkingMinutes = 0; const totalDays = endDate.add(1, "day").diff(startDate, "days"); for (let i = 0; i < totalDays; i++) { const date = startDate.add(i, "days"); const weekday = date.day(); timeSpanWorkingMinutes += (profile.weekly_regular_working_hours?.[weekday] || 0) * 60; } // ------------------------------------------------------------- // 3️⃣ Arbeits- und Abwesenheitszeiten berechnen (NEUE LOGIK) // ------------------------------------------------------------- let sumWorkingMinutesSubmitted = 0; let sumWorkingMinutesApproved = 0; let sumWorkingMinutesVacationDays = 0; let sumVacationDays = 0; let sumWorkingMinutesSickDays = 0; let sumSickDays = 0; // Akkumulieren der Zeiten basierend auf dem abgeleiteten Typ und Status for (const span of spans) { // **A. Arbeitszeiten (WORK)** if (span.type === "work") { const minutes = calcMinutes(span.startedAt, span.endedAt); // Zähle zur eingereichten Summe, wenn der Status submitted oder approved ist if (span.status === "submitted" || span.status === "approved") { sumWorkingMinutesSubmitted += minutes; } // Zähle zur genehmigten Summe, wenn der Status approved ist if (span.status === "approved") { sumWorkingMinutesApproved += minutes; } } // **B. Abwesenheiten (VACATION, SICK)** // Wir verwenden die Logik aus dem alten Service: Berechnung der Sollzeit // basierend auf den Tagen der Span (Voraussetzung: Spannen sind Volltages-Spannen) if (span.type === "vacation" || span.type === "sick") { // Behandle nur genehmigte Abwesenheiten für die Saldenberechnung if (span.status !== "approved") { continue; } const startDay = server.dayjs(span.startedAt).startOf('day'); // Wenn endedAt null ist (offene Span), nehmen wir das Ende des Zeitraums const endDay = span.endedAt ? server.dayjs(span.endedAt).startOf('day') : endDate.startOf('day'); // Berechnung der Tage der Span const days = endDay.diff(startDay, "day") + 1; for (let i = 0; i < days; i++) { const day = startDay.add(i, "day"); const weekday = day.day(); const hours = profile.weekly_regular_working_hours?.[weekday] || 0; if (span.type === "vacation") { sumWorkingMinutesVacationDays += hours * 60; } else if (span.type === "sick") { sumWorkingMinutesSickDays += hours * 60; } } if (span.type === "vacation") { sumVacationDays += days; } else if (span.type === "sick") { sumSickDays += days; } } // PAUSE Spannen werden ignoriert, da sie in der faktischen Ableitung bereits von WORK abgezogen wurden. } // ------------------------------------------------------------- // 4️⃣ Feiertagsausgleich (WIE IM ALTEN SERVICE) // ------------------------------------------------------------- let sumWorkingMinutesRecreationDays = 0; let sumRecreationDays = 0; if (profile.recreation_days_compensation && holidaysRows?.length) { holidaysRows.forEach(({ date }) => { const weekday = server.dayjs(date).day(); const hours = profile.weekly_regular_working_hours?.[weekday] || 0; sumWorkingMinutesRecreationDays += hours * 60; sumRecreationDays++; }); } // ------------------------------------------------------------- // 5️⃣ Salden berechnen (NEUE LOGIK) // ------------------------------------------------------------- const totalCompensatedMinutes = sumWorkingMinutesRecreationDays + sumWorkingMinutesVacationDays + sumWorkingMinutesSickDays; // Saldo basierend auf GENEHMIGTER Arbeitszeit const totalApprovedMinutes = sumWorkingMinutesApproved + totalCompensatedMinutes; const saldoApproved = totalApprovedMinutes - timeSpanWorkingMinutes; // Saldo basierend auf EINGEREICHTER und GENEHMIGTER Arbeitszeit const totalSubmittedMinutes = sumWorkingMinutesSubmitted + totalCompensatedMinutes; const saldoSubmitted = totalSubmittedMinutes - timeSpanWorkingMinutes; // ------------------------------------------------------------- // 6️⃣ Rückgabe // ------------------------------------------------------------- return { user_id, tenant_id, from: startDate.format("YYYY-MM-DD"), to: endDate.format("YYYY-MM-DD"), timeSpanWorkingMinutes, sumWorkingMinutesSubmitted, sumWorkingMinutesApproved, sumWorkingMinutesRecreationDays, sumRecreationDays, sumWorkingMinutesVacationDays, sumVacationDays, sumWorkingMinutesSickDays, sumSickDays, saldoApproved, saldoSubmitted, spans, }; }