import { FastifyInstance } from "fastify" import { stafftimeentries, stafftimenetryconnects } from "../../../db/schema" import { eq, and, gte, lte, desc } from "drizzle-orm" import {stafftimeevents} from "../../../db/schema/staff_time_events"; import {loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service"; import {deriveTimeSpans} from "../../modules/time/derivetimespans.service"; import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service"; import {z} from "zod"; import {enrichSpansWithStatus} from "../../modules/time/enrichtimespanswithstatus.service"; export default async function staffTimeRoutes(server: FastifyInstance) { server.post("/staff/time/event", async (req, reply) => { try { const userId = req.user.user_id const tenantId = req.user.tenant_id const body = req.body as any const normalizeDate = (val: any) => { if (!val) return null const d = new Date(val) return isNaN(d.getTime()) ? null : d } const dataToInsert = { tenant_id: tenantId, user_id: userId, actortype: "user", actoruser_id: userId, eventtime: normalizeDate(body.eventtime), eventtype: body.eventtype, source: "WEB" } console.log(dataToInsert) const [created] = await server.db .insert(stafftimeevents) //@ts-ignore .values(dataToInsert) .returning() return created } catch (err: any) { console.error(err) return reply.code(400).send({ error: err.message }) } }) // POST /staff/time/submit server.post("/staff/time/submit", async (req, reply) => { try { const userId = req.user.user_id; // Mitarbeiter, der einreicht const tenantId = req.user.tenant_id; // Erwartet eine Liste von IDs der faktischen Events (work_start, work_end, etc.) const { eventIds } = req.body as { eventIds: string[] }; if (eventIds.length === 0) { return reply.code(400).send({ error: "Keine Events zum Einreichen angegeben." }); } const inserts = eventIds.map((eventId) => ({ tenant_id: tenantId, user_id: userId, // Event gehört zum Mitarbeiter actortype: "user", actoruser_id: userId, // Mitarbeiter ist der Akteur eventtime: new Date(), eventtype: "submitted", // NEU: Event-Typ für Einreichung source: "WEB", related_event_id: eventId, // Verweis auf das faktische Event })); const createdEvents = await server.db .insert(stafftimeevents) //@ts-ignore .values(inserts) .returning(); return { submittedCount: createdEvents.length }; } catch (err: any) { console.error(err); return reply.code(500).send({ error: err.message }); } }); // POST /staff/time/approve server.post("/staff/time/approve", async (req, reply) => { try { // 🚨 Berechtigungsprüfung (Voraussetzung: req.user enthält Manager-Status) /*if (!req.user.isManager) { return reply.code(403).send({ error: "Keine Genehmigungsberechtigung." }); }*/ const actorId = req.user.user_id; // Manager ist der Akteur const tenantId = req.user.tenant_id; const { eventIds, employeeUserId } = req.body as { eventIds: string[]; employeeUserId: string; // Die ID des Mitarbeiters, dessen Zeit genehmigt wird }; if (eventIds.length === 0) { return reply.code(400).send({ error: "Keine Events zur Genehmigung angegeben." }); } const inserts = eventIds.map((eventId) => ({ tenant_id: tenantId, user_id: employeeUserId, // Event gehört zum Mitarbeiter actortype: "user", actoruser_id: actorId, // Manager ist der Akteur eventtime: new Date(), eventtype: "approved", // NEU: Event-Typ für Genehmigung source: "WEB", related_event_id: eventId, // Verweis auf das faktische Event metadata: { // Optional: Genehmigungskommentar approvedBy: req.user.email } })); const createdEvents = await server.db .insert(stafftimeevents) //@ts-ignore .values(inserts) .returning(); return { approvedCount: createdEvents.length }; } catch (err: any) { console.error(err); return reply.code(500).send({ error: err.message }); } }); // POST /staff/time/reject server.post("/staff/time/reject", async (req, reply) => { try { // 🚨 Berechtigungsprüfung /*if (!req.user.isManager) { return reply.code(403).send({ error: "Keine Zurückweisungsberechtigung." }); }*/ const actorId = req.user.user_id; // Manager ist der Akteur const tenantId = req.user.tenant_id; const { eventIds, employeeUserId, reason } = req.body as { eventIds: string[]; employeeUserId: string; reason?: string; // Optionaler Grund für die Ablehnung }; if (eventIds.length === 0) { return reply.code(400).send({ error: "Keine Events zur Ablehnung angegeben." }); } const inserts = eventIds.map((eventId) => ({ tenant_id: tenantId, user_id: employeeUserId, // Event gehört zum Mitarbeiter actortype: "user", actoruser_id: actorId, // Manager ist der Akteur eventtime: new Date(), eventtype: "rejected", // NEU: Event-Typ für Ablehnung source: "WEB", related_event_id: eventId, // Verweis auf das faktische Event metadata: { reason: reason || "Ohne Angabe" } })); const createdEvents = await server.db .insert(stafftimeevents) //@ts-ignore .values(inserts) .returning(); return { rejectedCount: createdEvents.length }; } catch (err: any) { console.error(err); return reply.code(500).send({ error: err.message }); } }); // GET /api/staff/time/spans server.get("/staff/time/spans", async (req, reply) => { try { // Der eingeloggte User (Anfragesteller) const actingUserId = req.user.user_id; const tenantId = req.user.tenant_id; // Query-Parameter: targetUserId ist optional const { targetUserId } = req.query as { targetUserId?: string }; // Falls eine targetUserId übergeben wurde, nutzen wir diese, sonst die eigene ID const evaluatedUserId = targetUserId || actingUserId; // 💡 "Unendlicher" Zeitraum, wie gewünscht const startDate = new Date(0); // 1970 const endDate = new Date("2100-12-31"); // SCHRITT 1: Lade ALLE Events für den ZIEL-USER (evaluatedUserId) const allEventsInTimeFrame = await loadValidEvents( server, tenantId, evaluatedUserId, // <--- Hier wird jetzt die variable ID genutzt startDate, endDate ); // SCHRITT 2: Filtere faktische Events const FACTUAL_EVENT_TYPES = new Set([ "work_start", "work_end", "pause_start", "pause_end", "sick_start", "sick_end", "vacation_start", "vacation_end", "overtime_compensation_start", "overtime_compensation_end", "auto_stop" ]); const factualEvents = allEventsInTimeFrame.filter(e => FACTUAL_EVENT_TYPES.has(e.eventtype)); // SCHRITT 3: Hole administrative Events const factualEventIds = factualEvents.map(e => e.id); if (factualEventIds.length === 0) { return []; } const relatedAdminEvents = await loadRelatedAdminEvents(server, factualEventIds); // SCHRITT 4: Kombinieren und Sortieren const combinedEvents = [ ...factualEvents, ...relatedAdminEvents, ].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime()); // SCHRITT 5: Spans ableiten const derivedSpans = deriveTimeSpans(combinedEvents); // SCHRITT 6: Spans anreichern const enrichedSpans = enrichSpansWithStatus(derivedSpans, combinedEvents); return enrichedSpans; } catch (error) { console.error("Fehler beim Laden der Spans:", error); return reply.code(500).send({ error: "Interner Fehler beim Laden der Zeitspannen." }); } }); server.get("/staff/time/evaluation", async (req, reply) => { try { // --- 1. Eingangsdaten und Validierung des aktuellen Nutzers --- // Daten des aktuell eingeloggten (anfragenden) Benutzers const actingUserId = req.user.user_id; const tenantId = req.user.tenant_id; // Query-Parameter extrahieren const { from, to, targetUserId } = req.query as { from: string, to: string, targetUserId?: string // Optionale ID des Benutzers, dessen Daten abgerufen werden sollen }; // Die ID, für die die Auswertung tatsächlich durchgeführt wird const evaluatedUserId = targetUserId || actingUserId; const startDate = new Date(from); const endDate = new Date(to); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return reply.code(400).send({ error: "Ungültiges Datumsformat." }); } // --- 3. Ausführung der Logik für den ermittelten Benutzer --- // SCHRITT 1: Lade ALLE gültigen Events im Zeitraum const allEventsInTimeFrame = await loadValidEvents( server, tenantId, evaluatedUserId, startDate, endDate // Verwendung der evaluatedUserId ); // 1b: Trenne Faktische und Administrative Events const FACTUAL_EVENT_TYPES = new Set([ "work_start", "work_end", "pause_start", "pause_end", "sick_start", "sick_end", "vacation_start", "vacation_end", "overtime_compensation_start", "overtime_compensation_end", "auto_stop" ]); const factualEvents = allEventsInTimeFrame.filter(e => FACTUAL_EVENT_TYPES.has(e.eventtype)); // 1c: Sammle alle IDs der faktischen Events im Zeitraum const factualEventIds = factualEvents.map(e => e.id); // SCHRITT 2: Lade die administrativen Events, die sich auf diese IDs beziehen (auch NACH dem Zeitraum) const relatedAdminEvents = await loadRelatedAdminEvents(server, factualEventIds); // SCHRITT 3: Kombiniere alle Events für die Weiterverarbeitung const combinedEvents = [ ...factualEvents, ...relatedAdminEvents, ].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime()); // SCHRITT 4: Ableiten und Anreichern const derivedSpans = deriveTimeSpans(combinedEvents); const enrichedSpans = enrichSpansWithStatus(derivedSpans, combinedEvents); // SCHRITT 5: Erstellung der finalen Auswertung (Summen und Salden) const evaluationSummary = await buildTimeEvaluationFromSpans( server, evaluatedUserId, // Verwendung der evaluatedUserId tenantId, from, to, enrichedSpans ); return { userId: evaluatedUserId, // Rückgabe der ID, für die ausgewertet wurde spans: enrichedSpans, summary: evaluationSummary }; } catch (error) { console.error("Fehler in /staff/time/evaluation:", error); return reply.code(500).send({ error: "Interner Serverfehler bei der Zeitauswertung." }); } }); /*// ------------------------------------------------------------- // ▶ Neue Zeit starten // ------------------------------------------------------------- server.post("/staff/time", async (req, reply) => { try { const userId = req.user.user_id const tenantId = req.user.tenant_id const body = req.body as any const normalizeDate = (val: any) => { if (!val) return null const d = new Date(val) return isNaN(d.getTime()) ? null : d } const dataToInsert = { tenant_id: tenantId, user_id: body.user_id || userId, type: body.type || "work", description: body.description || null, started_at: normalizeDate(body.started_at), stopped_at: normalizeDate(body.stopped_at), } const [created] = await server.db .insert(stafftimeentries) .values(dataToInsert) .returning() return created } catch (err: any) { console.error(err) return reply.code(400).send({ error: err.message }) } }) // ------------------------------------------------------------- // ▶ Zeit stoppen // ------------------------------------------------------------- server.put<{ Params: { id: string }, Body: { stopped_at: string } }>("/staff/time/:id/stop", async (req, reply) => { try { const { id } = req.params const { stopped_at } = req.body // Normalize timestamp const normalizeDate = (val: any) => { const d = new Date(val) return isNaN(d.getTime()) ? null : d } const stopTime = normalizeDate(stopped_at) if (!stopTime) { return reply.code(400).send({ error: "Invalid stopped_at timestamp" }) } const [updated] = await server.db .update(stafftimeentries) .set({ stopped_at: stopTime, updated_at: new Date(), }) .where(eq(stafftimeentries.id, id)) .returning() if (!updated) { return reply.code(404).send({ error: "Time entry not found" }) } return reply.send(updated) } catch (err: any) { console.error("STOP ERROR:", err) return reply.code(500).send({ error: err.message || "Internal server error" }) } }) // ------------------------------------------------------------- // ▶ Liste aller Zeiten // ------------------------------------------------------------- server.get("/staff/time", async (req, reply) => { try { const { from, to, type, user_id } = req.query as any const { tenant_id, user_id: currentUserId } = req.user let where = and(eq(stafftimeentries.tenant_id, tenant_id)) // Zugriffsbeschränkung if (!req.hasPermission("staff.time.read_all")) { where = and(where, eq(stafftimeentries.user_id, currentUserId)) } else if (user_id) { where = and(where, eq(stafftimeentries.user_id, user_id)) } if (from) where = and(where, gte(stafftimeentries.started_at, from)) if (to) where = and(where, lte(stafftimeentries.started_at, to)) if (type) where = and(where, eq(stafftimeentries.type, type)) const rows = await server.db .select() .from(stafftimeentries) .where(where) .orderBy(desc(stafftimeentries.started_at)) return rows } catch (err) { console.error(err) return reply.code(400).send({ error: (err as Error).message }) } }) // ------------------------------------------------------------- // ▶ Einzelne Zeit (inkl. Connects) // ------------------------------------------------------------- server.get("/staff/time/:id", async (req, reply) => { try { const { id } = req.params as any const rows = await server.db .select() .from(stafftimeentries) .where(eq(stafftimeentries.id, id)) .limit(1) if (!rows.length) return reply.code(404).send({ error: "Not found" }) const entry = rows[0] const connects = await server.db .select() .from(stafftimenetryconnects) .where(eq(stafftimenetryconnects.stafftimeentry, id)) return { ...entry, staff_time_entry_connects: connects } } catch (err) { return reply.code(400).send({ error: (err as Error).message }) } }) // ------------------------------------------------------------- // ▶ Zeit bearbeiten // ------------------------------------------------------------- // ▶ Zeit bearbeiten server.put<{ Params: { id: string }, }>("/staff/time/:id", async (req, reply) => { try { const { id } = req.params const body = req.body // Normalize all timestamp fields const normalizeDate = (val: any) => { if (!val) return null const d = new Date(val) return isNaN(d.getTime()) ? null : d } const updateData: any = { // @ts-ignore ...body, updated_at: new Date(), } // Only convert if present — avoid overriding with null unless sent // @ts-ignore if (body.started_at !== undefined) { // @ts-ignore updateData.started_at = normalizeDate(body.started_at) } // @ts-ignore if (body.stopped_at !== undefined) { // @ts-ignore updateData.stopped_at = normalizeDate(body.stopped_at) } // @ts-ignore if (body.approved_at !== undefined) { // @ts-ignore updateData.approved_at = normalizeDate(body.approved_at) } const [updated] = await server.db .update(stafftimeentries) .set(updateData) .where(eq(stafftimeentries.id, id)) .returning() if (!updated) { return reply.code(404).send({ error: "Time entry not found" }) } return reply.send(updated) } catch (err: any) { console.error("UPDATE ERROR:", err) return reply.code(500).send({ error: err.message || "Internal server error" }) } }) // ------------------------------------------------------------- // ▶ Zeit löschen // ------------------------------------------------------------- server.delete("/staff/time/:id", async (req, reply) => { try { const { id } = req.params as any await server.db .delete(stafftimeentries) .where(eq(stafftimeentries.id, id)) return { success: true } } catch (err) { return reply.code(400).send({ error: (err as Error).message }) } })*/ }