Files
FEDEO/backend/src/modules/time/buildtimeevaluation.service.ts
2026-01-06 12:07:43 +01:00

229 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<TimeEvaluationResult> {
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,
};
}