diff --git a/db/schema/index.ts b/db/schema/index.ts index 4208507..360363d 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -67,4 +67,5 @@ export * from "./texttemplates" export * from "./units" export * from "./user_credentials" export * from "./vehicles" -export * from "./vendors" \ No newline at end of file +export * from "./vendors" +export * from "./staff_time_events" \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a057ed7..ca5223f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,9 @@ import resourceRoutes from "./routes/resources/main"; //M2M import authM2m from "./plugins/auth.m2m"; import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email"; +import deviceRoutes from "./routes/internal/devices"; +import tenantRoutesInternal from "./routes/internal/tenant"; +import staffTimeRoutesInternal from "./routes/internal/time"; import {sendMail} from "./utils/mailer"; import {loadSecrets, secrets} from "./utils/secrets"; @@ -91,6 +94,9 @@ async function main() { await app.register(async (m2mApp) => { await m2mApp.register(authM2m) await m2mApp.register(helpdeskInboundEmailRoutes) + await m2mApp.register(deviceRoutes) + await m2mApp.register(tenantRoutesInternal) + await m2mApp.register(staffTimeRoutesInternal) },{prefix: "/internal"}) diff --git a/src/routes/internal/devices.ts b/src/routes/internal/devices.ts new file mode 100644 index 0000000..640304b --- /dev/null +++ b/src/routes/internal/devices.ts @@ -0,0 +1,41 @@ +import { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; +import { devices } from "../../../db/schema"; + +export default async function deviceRoutes(fastify: FastifyInstance) { + fastify.get<{ + Params: { + externalId: string; + }; + }>( + "/devices/by-external-id/:externalId", + async (request, reply) => { + const { externalId } = request.params; + + const device = await fastify.db + .select({ + id: devices.id, + name: devices.name, + type: devices.type, + tenant: devices.tenant, + externalId: devices.externalId, + created_at: devices.createdAt, + }) + .from(devices) + .where( + eq(devices.externalId, externalId) + + ) + .limit(1) + .then(rows => rows[0]); + + if (!device) { + return reply.status(404).send({ + message: "Device not found", + }); + } + + return reply.send(device); + } + ); +} diff --git a/src/routes/internal/tenant.ts b/src/routes/internal/tenant.ts new file mode 100644 index 0000000..ace0dc4 --- /dev/null +++ b/src/routes/internal/tenant.ts @@ -0,0 +1,107 @@ +import { FastifyInstance } from "fastify" + +import { + authTenantUsers, + authUsers, + authProfiles, + tenants +} from "../../../db/schema" + +import {and, eq, inArray} from "drizzle-orm" + + +export default async function tenantRoutesInternal(server: FastifyInstance) { + + + // ------------------------------------------------------------- + // GET CURRENT TENANT + // ------------------------------------------------------------- + server.get("/tenant/:id", async (req) => { + //@ts-ignore + const tenant = (await server.db.select().from(tenants).where(eq(tenants.id,req.params.id)).limit(1))[0] + + return tenant + }) + + + // ------------------------------------------------------------- + // TENANT USERS (auth_users + auth_profiles) + // ------------------------------------------------------------- + server.get("/tenant/users", async (req, reply) => { + try { + const authUser = req.user + if (!authUser) return reply.code(401).send({ error: "Unauthorized" }) + + const tenantId = authUser.tenant_id + + // 1) auth_tenant_users → user_ids + const tenantUsers = await server.db + .select() + .from(authTenantUsers) + .where(eq(authTenantUsers.tenant_id, tenantId)) + + const userIds = tenantUsers.map(u => u.user_id) + + if (!userIds.length) { + return { tenant_id: tenantId, users: [] } + } + + // 2) auth_users laden + const users = await server.db + .select() + .from(authUsers) + .where(inArray(authUsers.id, userIds)) + + // 3) auth_profiles pro Tenant laden + const profiles = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.tenant_id, tenantId), + inArray(authProfiles.user_id, userIds) + )) + + const combined = users.map(u => { + const profile = profiles.find(p => p.user_id === u.id) + return { + id: u.id, + email: u.email, + profile, + full_name: profile?.full_name ?? null + } + }) + + return { tenant_id: tenantId, users: combined } + + } catch (err) { + console.error("/tenant/users ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // TENANT PROFILES + // ------------------------------------------------------------- + server.get("/tenant/:id/profiles", async (req, reply) => { + try { + // @ts-ignore + const tenantId = req.params.id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const data = await server.db + .select() + .from(authProfiles) + .where(eq(authProfiles.tenant_id, tenantId)) + + return data + + } catch (err) { + console.error("/tenant/profiles ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + +} diff --git a/src/routes/internal/time.ts b/src/routes/internal/time.ts new file mode 100644 index 0000000..89b0544 --- /dev/null +++ b/src/routes/internal/time.ts @@ -0,0 +1,122 @@ +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 staffTimeRoutesInternal(server: FastifyInstance) { + + + server.post("/staff/time/event", async (req, reply) => { + try { + + const body = req.body as {user_id:string,tenant_id:number,eventtime:string,eventtype:string} + + const normalizeDate = (val: any) => { + if (!val) return null + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + const dataToInsert = { + tenant_id: body.tenant_id, + user_id: body.user_id, + actortype: "user", + actoruser_id: body.user_id, + 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 }) + } + }) + + + // GET /api/staff/time/spans + server.get("/staff/time/spans", async (req, reply) => { + try { + + // Query-Parameter: targetUserId ist optional + const { targetUserId, tenantId} = req.query as { targetUserId: string, tenantId:number }; + + // Falls eine targetUserId übergeben wurde, nutzen wir diese, sonst die eigene ID + const evaluatedUserId = targetUserId; + + // 💡 "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." }); + } + }); + + +}