// src/services/loadValidEvents.ts import { stafftimeevents } from "../../../db/schema"; import {sql, and, eq, gte, lte, inArray, asc} from "drizzle-orm"; import { FastifyInstance } from "fastify"; export type TimeType = "work_start" | "work_end" | "pause_start" | "pause_end" | "sick_start" | "sick_end" | "vacation_start" | "vacation_end" | "ovetime_compensation_start" | "overtime_compensation_end"; // Die Definition des TimeEvent Typs, der zurückgegeben wird (muss mit dem tatsächlichen Typ übereinstimmen) export type TimeEvent = { id: string; eventtype: string; eventtime: Date; actoruser_id?: string; related_event_id: string | null; payload?: Record | null; created_at?: Date | null; }; const EVENT_TYPE_ORDER: Record = { auto_stop: 10, work_end: 10, pause_end: 10, vacation_end: 10, sick_end: 10, overtime_compensation_end: 10, work_start: 20, pause_start: 20, vacation_start: 20, sick_start: 20, overtime_compensation_start: 20, submitted: 30, approved: 30, rejected: 30, invalidated: 40, }; export function compareTimeEvents(a: TimeEvent, b: TimeEvent) { const eventTimeDiff = a.eventtime.getTime() - b.eventtime.getTime(); if (eventTimeDiff !== 0) return eventTimeDiff; const typeOrderDiff = (EVENT_TYPE_ORDER[a.eventtype] ?? 999) - (EVENT_TYPE_ORDER[b.eventtype] ?? 999); if (typeOrderDiff !== 0) return typeOrderDiff; const createdAtDiff = (a.created_at?.getTime() ?? 0) - (b.created_at?.getTime() ?? 0); if (createdAtDiff !== 0) return createdAtDiff; return a.id.localeCompare(b.id); } export async function loadValidEvents( server: FastifyInstance, tenantId: number, userId: string, from: Date, to: Date ): Promise { // Definieren Sie einen Alias für die stafftimeevents Tabelle in der äußeren Abfrage const baseEvents = stafftimeevents; // Die Subquery, um alle IDs zu finden, die ungültig gemacht wurden // Wir nennen die innere Tabelle 'invalidatingEvents' const invalidatingEvents = server.db .select({ invalidatedId: baseEvents.invalidates_event_id }) .from(baseEvents) .as('invalidating_events'); // Die Hauptabfrage const result = await server.db .select() .from(baseEvents) .where( and( // 1. Tenant und User filtern eq(baseEvents.tenant_id, tenantId), eq(baseEvents.user_id, userId), // 2. Zeitbereich filtern (Typensicher) gte(baseEvents.eventtime, from), lte(baseEvents.eventtime, to), // 3. WICHTIG: Korrekturen ausschließen (NOT EXISTS) // Schließe jedes Event aus, dessen ID in der Liste der invalidates_event_id erscheint. sql` not exists ( select 1 from ${stafftimeevents} i where i.invalidates_event_id = ${baseEvents.id} ) ` ) ) .orderBy( asc(baseEvents.eventtime), asc(baseEvents.created_at), asc(baseEvents.id) ); // Mapping auf den sauberen TimeEvent Typ return result.map(e => ({ id: e.id, eventtype: e.eventtype, eventtime: e.eventtime, actoruser_id: e.actoruser_id, related_event_id: e.related_event_id, payload: e.payload, created_at: e.created_at, })) as TimeEvent[]; } export async function loadRelatedAdminEvents(server, eventIds) { if (eventIds.length === 0) return []; // Lädt alle administrativen Events, die sich auf die faktischen Event-IDs beziehen const adminEvents = await server.db .select() .from(stafftimeevents) .where( and( inArray(stafftimeevents.related_event_id, eventIds), // Wir müssen hier die Entkräftung prüfen, um z.B. einen abgelehnten submitted-Event auszuschließen sql` not exists ( select 1 from ${stafftimeevents} i where i.invalidates_event_id = ${stafftimeevents}.id ) `, ) ) // Muss nach Zeit sortiert sein, um den Status korrekt zu bestimmen! .orderBy( asc(stafftimeevents.eventtime), asc(stafftimeevents.created_at), asc(stafftimeevents.id) ); return adminEvents as TimeEvent[]; }