229 lines
7.9 KiB
TypeScript
229 lines
7.9 KiB
TypeScript
// 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,
|
||
};
|
||
} |