feat(time): add span-based attendance tracking under 2.0

This commit is contained in:
2026-10-03 15:55:45 +02:00
parent 4c67ddd908
commit 677ae0f4fb
32 changed files with 1681 additions and 47 deletions

View File

@@ -1,3 +1,4 @@
import { TimeService } from "./time-v2/service";
import { FastifyInstance } from 'fastify';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
@@ -200,6 +201,14 @@ export const publicLinkService = {
},
async submitFormData(server: FastifyInstance, token: string, payload: any, providedPin?: string) {
return server.db.transaction(async tx => {
const scoped = Object.create(server);
scoped.db = tx;
return publicLinkService.submitFormDataTransaction(scoped, token, payload, providedPin);
});
},
async submitFormDataTransaction(server: FastifyInstance, token: string, payload: any, providedPin?: string) {
// 1. Validierung (Token & PIN)
const linkConfig = await server.db.query.publicLinks.findFirst({
where: eq(schema.publicLinks.token, token)
@@ -223,7 +232,7 @@ export const publicLinkService = {
// Profil laden, um die user_id zu bekommen
const authProfile = await server.db.query.authProfiles.findFirst({
where: eq(schema.authProfiles.id, rawProfileId)
where: and(eq(schema.authProfiles.id, rawProfileId), eq(schema.authProfiles.tenant_id, tenantId))
});
if (!authProfile) throw new Error("Profile_Not_Found");
@@ -245,7 +254,7 @@ export const publicLinkService = {
// SCHRITT A: Stammdaten laden
// =================================================================
const project = await server.db.query.projects.findFirst({
where: eq(schema.projects.id, payload.project)
where: and(eq(schema.projects.id, payload.project), eq(schema.projects.tenant, tenantId))
});
if (!project) throw new Error("Project not found");
@@ -351,29 +360,42 @@ export const publicLinkService = {
generatedDocumentId: createdDoc.id
};
// 1. START EVENT
await server.db.insert(schema.stafftimeevents).values({
tenant_id: tenantId,
user_id: userId, // WICHTIG: User ID
actortype: "user",
actoruser_id: userId, // WICHTIG: User ID
eventtime: startDate,
eventtype: "START",
source: "PUBLIC_LINK",
metadata: eventMetadata // WICHTIG: Schema heißt 'metadata', nicht 'payload'
});
const timeService = new TimeService(server.db);
const actor = { tenant: tenantId, user: userId, trusted: true };
await timeService.lock(server.db, actor, userId);
if (await timeService.active(actor, userId)) {
await timeService.mutate(actor, userId, `public-link:${createdDoc.id}`, {
action: 'create', data: {
type: 'work', started_at: startDate.toISOString(), ended_at: endDate.toISOString(),
description: eventMetadata.description, breaks: [],
projects: [{ project_id: project.id, started_at: startDate.toISOString(), ended_at: endDate.toISOString(), notes: eventMetadata.description }],
},
}, 'public_link');
} else {
// 1. START EVENT
await server.db.insert(schema.stafftimeevents).values({
tenant_id: tenantId,
user_id: userId, // WICHTIG: User ID
actortype: "user",
actoruser_id: userId, // WICHTIG: User ID
eventtime: startDate,
eventtype: "START",
source: "PUBLIC_LINK",
metadata: eventMetadata // WICHTIG: Schema heißt 'metadata', nicht 'payload'
});
// 2. STOP EVENT
await server.db.insert(schema.stafftimeevents).values({
tenant_id: tenantId,
user_id: userId,
actortype: "user",
actoruser_id: userId,
eventtime: endDate,
eventtype: "STOP",
source: "PUBLIC_LINK",
metadata: eventMetadata
});
// 2. STOP EVENT
await server.db.insert(schema.stafftimeevents).values({
tenant_id: tenantId,
user_id: userId,
actortype: "user",
actoruser_id: userId,
eventtime: endDate,
eventtype: "STOP",
source: "PUBLIC_LINK",
metadata: eventMetadata
});
}
}
// =================================================================