L12 Zeiterfassung: Services für Nachtrag, Freigabe, Auto-Wechsel und Für heute beenden

time-entries.ts (manuelle Einträge, Korrekturvorschläge, Freigabe/Ablehnung mit
Team-Scope), Sessions in inTransaction mit switchFromOther, stopForToday,
Segmentwechsel und getMyActiveSession. Sync-Ops time.add_manual,
time.propose_correction, session.stop_day, session.segment inkl. optimistischer
Offline-Ansicht. Bericht, Zeiten-Tab, Dashboard und Lotse zählen nur freigegebene
Zeiten; Abrechnungsfreigabe blockiert bei offenen Zeiten. Empfänger und Links der
Zeit-Events. Notdienst-Start pausiert eine laufende Uhr automatisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 09:29:30 +02:00
co-authored by Claude Opus 5
parent 03a2ccab4f
commit 0e94eb0d9d
19 changed files with 1054 additions and 107 deletions
+5
View File
@@ -20,6 +20,11 @@ export const SYNC_OP_TYPES = [
"report.submit",
"signature.capture",
"emergency.create",
// L12 Zeiterfassung (additive, conflict-free)
"session.stop_day",
"session.segment",
"time.add_manual",
"time.propose_correction",
] as const;
export type SyncOpType = (typeof SYNC_OP_TYPES)[number];
+51 -1
View File
@@ -50,6 +50,8 @@ export const sessionStartPayload = z.object({
longitude: lng.optional(),
offline: z.boolean().default(false),
deviceInfo: z.string().max(200).optional(),
/** L12 auto switch: pause the user's running session on another order in the same transaction */
switchFromOther: z.boolean().optional(),
});
export const sessionControlPayload = z.object({
@@ -57,6 +59,50 @@ export const sessionControlPayload = z.object({
at: isoDate.optional(),
});
/** session.resume (L12): like session.start, may pause a running session on another order. */
export const sessionResumePayload = sessionControlPayload.extend({ switchFromOther: z.boolean().optional() });
/** Segment types a technician can switch to inside a running session (L12). */
export const SESSION_SEGMENT_TYPES = ["work", "return_travel", "material_procurement"] as const;
export type SessionSegmentType = (typeof SESSION_SEGMENT_TYPES)[number];
export const sessionSegmentPayload = z.object({
workOrderId: id,
type: z.enum(SESSION_SEGMENT_TYPES),
at: isoDate.optional(),
});
/** Time entry types (Prisma enum TimeEntryType). */
export const TIME_TYPES = ["travel", "work", "break", "material_procurement", "return_travel", "interruption"] as const;
export type TimeType = (typeof TIME_TYPES)[number];
/**
* time.add_manual (L12): manual time entry. Own entries become `pending` (approval by team lead /
* back office); with `field:correct_time` for a team member (`forUserId`) they are approved directly.
* Either `endedAt` or `durationMinutes` is required.
*/
export const timeAddManualPayload = z.object({
workOrderId: id,
clientId: clientId.optional(),
type: z.enum(TIME_TYPES).default("work"),
startedAt: isoDate,
endedAt: isoDate.optional(),
durationMinutes: z.number().int().min(1).max(16 * 60).optional(),
reason: z.string().trim().min(3).max(500),
note: z.string().trim().max(2000).optional(),
forUserId: id.optional(),
});
/** time.propose_correction (L12): correction proposal for an own entry; old values count until approval. */
export const timeProposeCorrectionPayload = z.object({
workOrderId: id,
timeEntryId: id,
type: z.enum(TIME_TYPES).optional(),
startedAt: isoDate,
endedAt: isoDate,
reason: z.string().trim().min(3).max(500),
});
export const workOrderTransitionPayload = z.object({
workOrderId: id,
to: z.enum(WORK_ORDER_STATUSES),
@@ -138,7 +184,7 @@ const passthrough = z.record(z.string(), z.unknown());
export const OP_PAYLOAD_SCHEMAS = {
"session.start": sessionStartPayload,
"session.pause": sessionControlPayload,
"session.resume": sessionControlPayload,
"session.resume": sessionResumePayload,
"session.end": sessionControlPayload,
"work_order.transition": workOrderTransitionPayload,
"note.create": noteCreatePayload,
@@ -150,6 +196,10 @@ export const OP_PAYLOAD_SCHEMAS = {
"report.submit": reportSubmitPayload,
"signature.capture": passthrough,
"emergency.create": emergencyCreatePayload,
"session.stop_day": sessionControlPayload,
"session.segment": sessionSegmentPayload,
"time.add_manual": timeAddManualPayload,
"time.propose_correction": timeProposeCorrectionPayload,
} satisfies Record<SyncOpType, z.ZodType>;
export type OpPayload<T extends SyncOpType> = z.input<(typeof OP_PAYLOAD_SCHEMAS)[T]>;