import { randomUUID } from "node:crypto"; import { sql } from "drizzle-orm"; import { TimeService, digest, rows } from "../../modules/time-v2/service"; import { FastifyInstance } from "fastify"; import { and, desc, eq } from "drizzle-orm"; import { authProfiles, devices, stafftimeevents } from "../../../db/schema"; export default async function devicesRFIDRoutes(server: FastifyInstance) { server.post( "/rfid/createevent/:terminal_id", async (req, reply) => { try { // 1. Timestamp aus dem Body holen (optional) const { rfid_id, timestamp, request_id } = req.body as { request_id?: string, rfid_id: string, timestamp?: number // Kann undefined sein (Live) oder Zahl (Offline) }; const { terminal_id } = req.params as { terminal_id: string }; if (!rfid_id || !terminal_id) { console.log(`Missing Params`); return reply.code(400).send(`Missing Params`); } // 2. Gerät suchen const device = await server.db .select() .from(devices) .where(eq(devices.externalId, terminal_id)) .limit(1) .then(rows => rows[0]); if (!device) { console.log(`Device ${terminal_id} not found`); return reply.code(400).send(`Device ${terminal_id} not found`); } // 3. User-Profil suchen const profile = await server.db .select() .from(authProfiles) .where( and( eq(authProfiles.tenant_id, device.tenant), eq(authProfiles.token_id, rfid_id), eq(authProfiles.active, true) ) ) .limit(1) .then(rows => rows[0]); if (!profile) { console.log(`Profile for Token ${rfid_id} not found`); return reply.code(400).send(`Profile for Token ${rfid_id} not found`); } const timeService = new TimeService(server.db); const actor = { tenant: device.tenant, user: profile.user_id, trusted: true }; if (profile.user_id && await timeService.active(actor, profile.user_id)) { return await server.db.transaction(async tx => { await timeService.lock(tx, actor, profile.user_id); const scoped = new TimeService(tx); const at = timestamp && timestamp > 0 ? new Date(timestamp * 1000).toISOString() : new Date().toISOString(); const key = `rfid:${digest({ terminal_id, rfid_id, event: request_id || timestamp || randomUUID() })}`; const [cached] = await rows(tx, sql`SELECT response FROM staff_time_v2_requests WHERE tenant_id=${device.tenant} AND user_id=${profile.user_id}::uuid AND request_id=${key}`); if (cached) return cached.response; const current = await scoped.current(actor, profile.user_id); return scoped.mutate(actor, profile.user_id, key, current ? { action: 'stop', id: current.id, version: current.version, at } : { action: 'start', at }, 'terminal'); }); } // 4. Letztes Event suchen (für Status-Toggle Work Start/End) const lastEvent = await server.db .select() .from(stafftimeevents) .where(and(eq(stafftimeevents.user_id, profile.user_id), eq(stafftimeevents.tenant_id, device.tenant))) .orderBy(desc(stafftimeevents.eventtime)) .limit(1) .then(rows => rows[0]); // 5. Zeitstempel Logik (WICHTIG!) // Der ESP32 sendet Unix-Timestamp in SEKUNDEN. JS braucht MILLISEKUNDEN. // Wenn kein Timestamp kommt (0 oder undefined), nehmen wir JETZT. const actualEventTime = (timestamp && timestamp > 0) ? new Date(timestamp * 1000) : new Date(); // 6. Event Typ bestimmen (Toggle Logik) // Falls noch nie gestempelt wurde (lastEvent undefined), fangen wir mit start an. const nextEventType = (lastEvent?.eventtype === "work_start") ? "work_end" : "work_start"; const dataToInsert = { tenant_id: device.tenant, user_id: profile.user_id, actortype: "system", eventtime: actualEventTime, // Hier nutzen wir die berechnete Zeit eventtype: nextEventType, source: "TERMINAL" // Habe ich von WEB auf TERMINAL geändert (optional) }; console.log(`New Event for ${profile.user_id}: ${nextEventType} @ ${actualEventTime.toISOString()}`); 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 }); } } ); }