Time Migration
This commit is contained in:
229
src/modules/time/buildtimeevaluation.service.ts
Normal file
229
src/modules/time/buildtimeevaluation.service.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
// src/services/buildTimeEvaluationFromSpans.ts
|
||||
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { and, eq, gte, lte, inArray } from "drizzle-orm";
|
||||
import { authProfiles, holidays } from "../../../db/schema";
|
||||
import { DerivedSpan } from "./derivetimespans.service"; // Importiert den angereicherten Span-Typ
|
||||
|
||||
// Definiert das erwartete Rückgabeformat
|
||||
export type TimeEvaluationResult = {
|
||||
user_id: string;
|
||||
tenant_id: number;
|
||||
from: string;
|
||||
to: string;
|
||||
|
||||
// Sollzeit
|
||||
timeSpanWorkingMinutes: number;
|
||||
|
||||
// Arbeitszeit Salden
|
||||
sumWorkingMinutesSubmitted: number;
|
||||
sumWorkingMinutesApproved: number;
|
||||
|
||||
// Abwesenheiten (minuten und Tage)
|
||||
sumWorkingMinutesRecreationDays: number;
|
||||
sumRecreationDays: number;
|
||||
sumWorkingMinutesVacationDays: number;
|
||||
sumVacationDays: number;
|
||||
sumWorkingMinutesSickDays: number;
|
||||
sumSickDays: number;
|
||||
|
||||
// Endsalden
|
||||
saldoApproved: number; // Saldo basierend auf genehmigter Zeit
|
||||
saldoSubmitted: number; // Saldo basierend auf eingereichter/genehmigter Zeit
|
||||
|
||||
spans: DerivedSpan[];
|
||||
};
|
||||
|
||||
// Hilfsfunktion zur Berechnung der Minuten (nur für geschlossene Spannen)
|
||||
const calcMinutes = (start: Date, end: Date | null): number => {
|
||||
if (!end) return 0;
|
||||
return (end.getTime() - start.getTime()) / 60000;
|
||||
};
|
||||
|
||||
|
||||
export async function buildTimeEvaluationFromSpans(
|
||||
server: FastifyInstance,
|
||||
user_id: string,
|
||||
tenant_id: number,
|
||||
startDateInput: string,
|
||||
endDateInput: string,
|
||||
// Der wichtigste Unterschied: Wir nehmen die angereicherten Spannen als Input
|
||||
spans: DerivedSpan[]
|
||||
): Promise<TimeEvaluationResult> {
|
||||
|
||||
const startDate = server.dayjs(startDateInput);
|
||||
const endDate = server.dayjs(endDateInput);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 1️⃣ Profil und Feiertage laden (WIE IM ALTEN SERVICE)
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const profileRows = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, user_id),
|
||||
eq(authProfiles.tenant_id, tenant_id)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const profile = profileRows[0];
|
||||
if (!profile) throw new Error("Profil konnte nicht geladen werden.");
|
||||
|
||||
const holidaysRows = await server.db
|
||||
.select({
|
||||
date: holidays.date,
|
||||
})
|
||||
.from(holidays)
|
||||
.where(
|
||||
and(
|
||||
inArray(holidays.state_code, [profile.state_code, "DE"]),
|
||||
gte(holidays.date, startDate.format("YYYY-MM-DD")),
|
||||
lte(holidays.date, endDate.add(1, "day").format("YYYY-MM-DD"))
|
||||
)
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 2️⃣ Sollzeit berechnen (WIE IM ALTEN SERVICE)
|
||||
// -------------------------------------------------------------
|
||||
let timeSpanWorkingMinutes = 0;
|
||||
const totalDays = endDate.add(1, "day").diff(startDate, "days");
|
||||
|
||||
for (let i = 0; i < totalDays; i++) {
|
||||
const date = startDate.add(i, "days");
|
||||
const weekday = date.day();
|
||||
timeSpanWorkingMinutes +=
|
||||
(profile.weekly_regular_working_hours?.[weekday] || 0) * 60;
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 3️⃣ Arbeits- und Abwesenheitszeiten berechnen (NEUE LOGIK)
|
||||
// -------------------------------------------------------------
|
||||
|
||||
let sumWorkingMinutesSubmitted = 0;
|
||||
let sumWorkingMinutesApproved = 0;
|
||||
|
||||
let sumWorkingMinutesVacationDays = 0;
|
||||
let sumVacationDays = 0;
|
||||
let sumWorkingMinutesSickDays = 0;
|
||||
let sumSickDays = 0;
|
||||
|
||||
// Akkumulieren der Zeiten basierend auf dem abgeleiteten Typ und Status
|
||||
for (const span of spans) {
|
||||
|
||||
// **A. Arbeitszeiten (WORK)**
|
||||
if (span.type === "work") {
|
||||
const minutes = calcMinutes(span.startedAt, span.endedAt);
|
||||
|
||||
// Zähle zur eingereichten Summe, wenn der Status submitted oder approved ist
|
||||
if (span.status === "submitted" || span.status === "approved") {
|
||||
sumWorkingMinutesSubmitted += minutes;
|
||||
}
|
||||
|
||||
// Zähle zur genehmigten Summe, wenn der Status approved ist
|
||||
if (span.status === "approved") {
|
||||
sumWorkingMinutesApproved += minutes;
|
||||
}
|
||||
}
|
||||
|
||||
// **B. Abwesenheiten (VACATION, SICK)**
|
||||
// Wir verwenden die Logik aus dem alten Service: Berechnung der Sollzeit
|
||||
// basierend auf den Tagen der Span (Voraussetzung: Spannen sind Volltages-Spannen)
|
||||
if (span.type === "vacation" || span.type === "sick") {
|
||||
|
||||
// Behandle nur genehmigte Abwesenheiten für die Saldenberechnung
|
||||
if (span.status !== "approved") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startDay = server.dayjs(span.startedAt).startOf('day');
|
||||
// Wenn endedAt null ist (offene Span), nehmen wir das Ende des Zeitraums
|
||||
const endDay = span.endedAt ? server.dayjs(span.endedAt).startOf('day') : endDate.startOf('day');
|
||||
|
||||
// Berechnung der Tage der Span
|
||||
const days = endDay.diff(startDay, "day") + 1;
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const day = startDay.add(i, "day");
|
||||
const weekday = day.day();
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0;
|
||||
|
||||
if (span.type === "vacation") {
|
||||
sumWorkingMinutesVacationDays += hours * 60;
|
||||
} else if (span.type === "sick") {
|
||||
sumWorkingMinutesSickDays += hours * 60;
|
||||
}
|
||||
}
|
||||
|
||||
if (span.type === "vacation") {
|
||||
sumVacationDays += days;
|
||||
} else if (span.type === "sick") {
|
||||
sumSickDays += days;
|
||||
}
|
||||
}
|
||||
|
||||
// PAUSE Spannen werden ignoriert, da sie in der faktischen Ableitung bereits von WORK abgezogen wurden.
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 4️⃣ Feiertagsausgleich (WIE IM ALTEN SERVICE)
|
||||
// -------------------------------------------------------------
|
||||
let sumWorkingMinutesRecreationDays = 0;
|
||||
let sumRecreationDays = 0;
|
||||
|
||||
if (profile.recreation_days_compensation && holidaysRows?.length) {
|
||||
holidaysRows.forEach(({ date }) => {
|
||||
const weekday = server.dayjs(date).day();
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0;
|
||||
sumWorkingMinutesRecreationDays += hours * 60;
|
||||
sumRecreationDays++;
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 5️⃣ Salden berechnen (NEUE LOGIK)
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const totalCompensatedMinutes =
|
||||
sumWorkingMinutesRecreationDays +
|
||||
sumWorkingMinutesVacationDays +
|
||||
sumWorkingMinutesSickDays;
|
||||
|
||||
// Saldo basierend auf GENEHMIGTER Arbeitszeit
|
||||
const totalApprovedMinutes = sumWorkingMinutesApproved + totalCompensatedMinutes;
|
||||
const saldoApproved = totalApprovedMinutes - timeSpanWorkingMinutes;
|
||||
|
||||
// Saldo basierend auf EINGEREICHTER und GENEHMIGTER Arbeitszeit
|
||||
const totalSubmittedMinutes = sumWorkingMinutesSubmitted + totalCompensatedMinutes;
|
||||
const saldoSubmitted = totalSubmittedMinutes - timeSpanWorkingMinutes;
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 6️⃣ Rückgabe
|
||||
// -------------------------------------------------------------
|
||||
return {
|
||||
user_id,
|
||||
tenant_id,
|
||||
from: startDate.format("YYYY-MM-DD"),
|
||||
to: endDate.format("YYYY-MM-DD"),
|
||||
timeSpanWorkingMinutes,
|
||||
|
||||
sumWorkingMinutesSubmitted,
|
||||
sumWorkingMinutesApproved,
|
||||
|
||||
sumWorkingMinutesRecreationDays,
|
||||
sumRecreationDays,
|
||||
sumWorkingMinutesVacationDays,
|
||||
sumVacationDays,
|
||||
sumWorkingMinutesSickDays,
|
||||
sumSickDays,
|
||||
|
||||
saldoApproved,
|
||||
saldoSubmitted,
|
||||
spans,
|
||||
};
|
||||
}
|
||||
165
src/modules/time/derivetimespans.service.ts
Normal file
165
src/modules/time/derivetimespans.service.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
type State = "IDLE" | "WORKING" | "PAUSED" | "ABSENT";
|
||||
|
||||
export type SpanStatus = "factual" | "submitted" | "approved" | "rejected";
|
||||
|
||||
export type DerivedSpan = {
|
||||
type: "work" | "pause" | "vacation" | "sick" | "overtime_compensation";
|
||||
startedAt: Date;
|
||||
endedAt: Date | null;
|
||||
sourceEventIds: string[];
|
||||
status: SpanStatus;
|
||||
statusActorId?: string;
|
||||
};
|
||||
|
||||
type TimeEvent = {
|
||||
id: string;
|
||||
eventtype: string;
|
||||
eventtime: Date;
|
||||
};
|
||||
|
||||
// Liste aller faktischen Event-Typen, die die Zustandsmaschine beeinflussen
|
||||
const FACTUAL_EVENT_TYPES = new Set([
|
||||
"work_start",
|
||||
"pause_start",
|
||||
"pause_end",
|
||||
"work_end",
|
||||
"auto_stop", // Wird als work_end behandelt
|
||||
"vacation_start",
|
||||
"vacation_end",
|
||||
"sick_start",
|
||||
"sick_end",
|
||||
"overtime_compensation_start",
|
||||
"overtime_compensation_end",
|
||||
]);
|
||||
|
||||
export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] {
|
||||
|
||||
// 1. FILTERN: Nur faktische Events verarbeiten
|
||||
const events = allValidEvents.filter(event =>
|
||||
FACTUAL_EVENT_TYPES.has(event.eventtype)
|
||||
);
|
||||
|
||||
const spans: DerivedSpan[] = [];
|
||||
|
||||
let state: State = "IDLE";
|
||||
let currentStart: Date | null = null;
|
||||
let currentType: DerivedSpan["type"] | null = null;
|
||||
let sourceEventIds: string[] = [];
|
||||
|
||||
const closeSpan = (end: Date) => {
|
||||
if (!currentStart || !currentType) return;
|
||||
|
||||
spans.push({
|
||||
type: currentType,
|
||||
startedAt: currentStart,
|
||||
endedAt: end,
|
||||
sourceEventIds: [...sourceEventIds],
|
||||
// Standardstatus ist "factual", wird später angereichert
|
||||
status: "factual"
|
||||
});
|
||||
|
||||
currentStart = null;
|
||||
currentType = null;
|
||||
sourceEventIds = [];
|
||||
};
|
||||
|
||||
const closeOpenSpanAsRunning = () => {
|
||||
if (!currentStart || !currentType) return;
|
||||
|
||||
spans.push({
|
||||
type: currentType,
|
||||
startedAt: currentStart,
|
||||
endedAt: null,
|
||||
sourceEventIds: [...sourceEventIds],
|
||||
// Standardstatus ist "factual", wird später angereichert
|
||||
status: "factual"
|
||||
});
|
||||
|
||||
currentStart = null;
|
||||
currentType = null;
|
||||
sourceEventIds = [];
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
sourceEventIds.push(event.id);
|
||||
|
||||
switch (event.eventtype) {
|
||||
/* =========================
|
||||
ARBEITSZEIT
|
||||
========================= */
|
||||
|
||||
case "work_start":
|
||||
if (state === "WORKING" || state === "PAUSED" || state === "ABSENT") {
|
||||
// Schließt die vorherige Spanne (falls z.B. work_start nach sick_start kommt)
|
||||
closeSpan(event.eventtime);
|
||||
}
|
||||
state = "WORKING";
|
||||
currentStart = event.eventtime;
|
||||
currentType = "work";
|
||||
break;
|
||||
|
||||
case "pause_start":
|
||||
if (state === "WORKING") {
|
||||
closeSpan(event.eventtime);
|
||||
state = "PAUSED";
|
||||
currentStart = event.eventtime;
|
||||
currentType = "pause";
|
||||
}
|
||||
break;
|
||||
|
||||
case "pause_end":
|
||||
if (state === "PAUSED") {
|
||||
closeSpan(event.eventtime);
|
||||
state = "WORKING";
|
||||
currentStart = event.eventtime;
|
||||
currentType = "work";
|
||||
}
|
||||
break;
|
||||
|
||||
case "work_end":
|
||||
case "auto_stop":
|
||||
if (state === "WORKING" || state === "PAUSED") {
|
||||
closeSpan(event.eventtime);
|
||||
}
|
||||
state = "IDLE";
|
||||
break;
|
||||
|
||||
/* =========================
|
||||
ABWESENHEITEN
|
||||
========================= */
|
||||
|
||||
case "vacation_start":
|
||||
case "sick_start":
|
||||
case "overtime_compensation_start":
|
||||
// Mappt den Event-Typ direkt auf den Span-Typ
|
||||
const newType = event.eventtype.split('_')[0] as DerivedSpan["type"];
|
||||
|
||||
if (state !== "IDLE") {
|
||||
closeSpan(event.eventtime);
|
||||
}
|
||||
state = "ABSENT";
|
||||
currentStart = event.eventtime;
|
||||
currentType = newType;
|
||||
break;
|
||||
|
||||
case "vacation_end":
|
||||
case "sick_end":
|
||||
case "overtime_compensation_end":
|
||||
// Extrahiert den Typ der zu beendenden Spanne
|
||||
const endedType = event.eventtype.split('_')[0] as DerivedSpan["type"];
|
||||
|
||||
if (state === "ABSENT" && currentType === endedType) {
|
||||
closeSpan(event.eventtime);
|
||||
}
|
||||
state = "IDLE";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴 WICHTIG: Offene Spannen als laufend zurückgeben
|
||||
if (state !== "IDLE") {
|
||||
closeOpenSpanAsRunning();
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
91
src/modules/time/enrichtimespanswithstatus.service.ts
Normal file
91
src/modules/time/enrichtimespanswithstatus.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// src/services/enrichSpansWithStatus.ts (Korrigierte Version)
|
||||
|
||||
import { DerivedSpan, SpanStatus } from "./derivetimespans.service";
|
||||
import { TimeEvent } from "./loadvalidevents.service"; // Jetzt mit related_event_id und actoruser_id
|
||||
|
||||
// ... (Rest der Imports)
|
||||
|
||||
export function enrichSpansWithStatus(
|
||||
factualSpans: DerivedSpan[],
|
||||
allValidEvents: TimeEvent[]
|
||||
): DerivedSpan[] {
|
||||
|
||||
// 1. Map der administrativen Aktionen erstellen
|
||||
const eventStatusMap = new Map<string, { status: SpanStatus, actorId: string }>();
|
||||
|
||||
const administrativeEvents = allValidEvents.filter(e =>
|
||||
e.eventtype === 'submitted' || e.eventtype === 'approved' || e.eventtype === 'rejected'
|
||||
);
|
||||
|
||||
// allValidEvents ist nach Zeit sortiert
|
||||
for (const event of administrativeEvents) {
|
||||
|
||||
// **Verwendung des expliziten Feldes**
|
||||
const relatedId = event.related_event_id;
|
||||
const actorId = event.actoruser_id;
|
||||
|
||||
if (relatedId) { // Nur fortfahren, wenn ein Bezugs-Event existiert
|
||||
|
||||
let status: SpanStatus = "factual";
|
||||
// Wir überschreiben den Status des relatedId basierend auf der Event-Historie
|
||||
if (event.eventtype === 'submitted') status = 'submitted';
|
||||
else if (event.eventtype === 'approved') status = 'approved';
|
||||
else if (event.eventtype === 'rejected') status = 'rejected';
|
||||
|
||||
eventStatusMap.set(relatedId, { status, actorId });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Status der Spannen bestimmen und anreichern
|
||||
return factualSpans.map(span => {
|
||||
|
||||
let approvedCount = 0;
|
||||
let rejectedCount = 0;
|
||||
let submittedCount = 0;
|
||||
let isFactualCount = 0;
|
||||
|
||||
for (const sourceId of span.sourceEventIds) {
|
||||
const statusInfo = eventStatusMap.get(sourceId);
|
||||
|
||||
if (statusInfo) {
|
||||
// Ein faktisches Event kann durch mehrere administrative Events betroffen sein
|
||||
// Wir speichern im Map nur den letzten Status (z.B. approved überschreibt submitted)
|
||||
if (statusInfo.status === 'approved') approvedCount++;
|
||||
else if (statusInfo.status === 'rejected') rejectedCount++;
|
||||
else if (statusInfo.status === 'submitted') submittedCount++;
|
||||
} else {
|
||||
// Wenn kein administratives Event existiert
|
||||
isFactualCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Regel zur Bestimmung des Span-Status:
|
||||
const totalSourceEvents = span.sourceEventIds.length;
|
||||
let finalStatus: SpanStatus = "factual";
|
||||
|
||||
if (totalSourceEvents > 0) {
|
||||
|
||||
// Priorität 1: Rejection
|
||||
if (rejectedCount > 0) {
|
||||
finalStatus = "rejected";
|
||||
}
|
||||
// Priorität 2: Full Approval
|
||||
else if (approvedCount === totalSourceEvents) {
|
||||
finalStatus = "approved";
|
||||
}
|
||||
// Priorität 3: Submitted (wenn nicht fully approved oder rejected, aber mindestens eines submitted ist)
|
||||
else if (submittedCount > 0 || approvedCount > 0) {
|
||||
finalStatus = "submitted";
|
||||
// Ein Span ist submitted, wenn es zumindest teilweise eingereicht (oder genehmigt) ist,
|
||||
// aber nicht alle Events den finalen Status "approved" haben.
|
||||
}
|
||||
// Ansonsten bleibt es "factual" (wenn z.B. nur work_start aber nicht work_end eingereicht wurde, oder nichts)
|
||||
}
|
||||
|
||||
// Rückgabe der angereicherten Span
|
||||
return {
|
||||
...span,
|
||||
status: finalStatus,
|
||||
};
|
||||
});
|
||||
}
|
||||
105
src/modules/time/loadvalidevents.service.ts
Normal file
105
src/modules/time/loadvalidevents.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// src/services/loadValidEvents.ts
|
||||
|
||||
import { stafftimeevents } from "../../../db/schema";
|
||||
import {sql, and, eq, gte, lte, inArray} from "drizzle-orm";
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
export type TimeType = "work_start" | "work_end" | "pause_start" | "pause_end" | "sick_start" | "sick_end" | "vacation_start" | "vacation_end" | "ovetime_compensation_start" | "overtime_compensation_end";
|
||||
|
||||
|
||||
// Die Definition des TimeEvent Typs, der zurückgegeben wird (muss mit dem tatsächlichen Typ übereinstimmen)
|
||||
export type TimeEvent = {
|
||||
id: string;
|
||||
eventtype: string;
|
||||
eventtime: Date;
|
||||
actoruser_id: string;
|
||||
related_event_id: string | null;
|
||||
// Fügen Sie hier alle weiteren Felder hinzu, die Sie benötigen
|
||||
};
|
||||
|
||||
export async function loadValidEvents(
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
userId: string,
|
||||
from: Date,
|
||||
to: Date
|
||||
): Promise<TimeEvent[]> {
|
||||
// Definieren Sie einen Alias für die stafftimeevents Tabelle in der äußeren Abfrage
|
||||
const baseEvents = stafftimeevents;
|
||||
|
||||
// Die Subquery, um alle IDs zu finden, die ungültig gemacht wurden
|
||||
// Wir nennen die innere Tabelle 'invalidatingEvents'
|
||||
const invalidatingEvents = server.db
|
||||
.select({
|
||||
invalidatedId: baseEvents.invalidates_event_id
|
||||
})
|
||||
.from(baseEvents)
|
||||
.as('invalidating_events');
|
||||
|
||||
// Die Hauptabfrage
|
||||
const result = await server.db
|
||||
.select()
|
||||
.from(baseEvents)
|
||||
.where(
|
||||
and(
|
||||
// 1. Tenant und User filtern
|
||||
eq(baseEvents.tenant_id, tenantId),
|
||||
eq(baseEvents.user_id, userId),
|
||||
|
||||
// 2. Zeitbereich filtern (Typensicher)
|
||||
gte(baseEvents.eventtime, from),
|
||||
lte(baseEvents.eventtime, to),
|
||||
|
||||
// 3. WICHTIG: Korrekturen ausschließen (NOT EXISTS)
|
||||
// Schließe jedes Event aus, dessen ID in der Liste der invalidates_event_id erscheint.
|
||||
sql`
|
||||
not exists (
|
||||
select 1
|
||||
from ${stafftimeevents} i
|
||||
where i.invalidates_event_id = ${baseEvents.id}
|
||||
)
|
||||
`
|
||||
)
|
||||
)
|
||||
.orderBy(
|
||||
// Wichtig für die deriveTimeSpans Logik: Event-Zeit muss primär sein
|
||||
baseEvents.eventtime,
|
||||
baseEvents.created_at, // Wenn Eventtime identisch ist, das später erstellte zuerst
|
||||
baseEvents.id
|
||||
);
|
||||
|
||||
// Mapping auf den sauberen TimeEvent Typ
|
||||
return result.map(e => ({
|
||||
id: e.id,
|
||||
eventtype: e.eventtype,
|
||||
eventtime: e.eventtime,
|
||||
// Fügen Sie hier alle weiteren benötigten Felder hinzu (z.B. metadata, actoruser_id)
|
||||
// ...
|
||||
})) as TimeEvent[];
|
||||
}
|
||||
|
||||
export async function loadRelatedAdminEvents(server, eventIds) {
|
||||
if (eventIds.length === 0) return [];
|
||||
|
||||
// Lädt alle administrativen Events, die sich auf die faktischen Event-IDs beziehen
|
||||
const adminEvents = await server.db
|
||||
.select()
|
||||
.from(stafftimeevents)
|
||||
.where(
|
||||
and(
|
||||
inArray(stafftimeevents.related_event_id, eventIds),
|
||||
// Wir müssen hier die Entkräftung prüfen, um z.B. einen abgelehnten submitted-Event auszuschließen
|
||||
sql`
|
||||
not exists (
|
||||
select 1
|
||||
from ${stafftimeevents} i
|
||||
where i.invalidates_event_id = ${stafftimeevents}.id
|
||||
)
|
||||
`,
|
||||
)
|
||||
)
|
||||
// Muss nach Zeit sortiert sein, um den Status korrekt zu bestimmen!
|
||||
.orderBy(stafftimeevents.eventtime);
|
||||
|
||||
return adminEvents;
|
||||
}
|
||||
Reference in New Issue
Block a user