Time Migration
This commit is contained in:
@@ -67,4 +67,5 @@ export * from "./texttemplates"
|
||||
export * from "./units"
|
||||
export * from "./user_credentials"
|
||||
export * from "./vehicles"
|
||||
export * from "./vendors"
|
||||
export * from "./vendors"
|
||||
export * from "./staff_time_events"
|
||||
@@ -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"})
|
||||
|
||||
|
||||
|
||||
41
src/routes/internal/devices.ts
Normal file
41
src/routes/internal/devices.ts
Normal file
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
107
src/routes/internal/tenant.ts
Normal file
107
src/routes/internal/tenant.ts
Normal file
@@ -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" })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
122
src/routes/internal/time.ts
Normal file
122
src/routes/internal/time.ts
Normal file
@@ -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." });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user