Time Migration
This commit is contained in:
@@ -10,10 +10,334 @@ import {
|
||||
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) => {
|
||||
@@ -232,5 +556,5 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
} catch (err) {
|
||||
return reply.code(400).send({ error: (err as Error).message })
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user