Files
craftvia/src/lib/sync/envelope.ts
T
msolarczekandClaude Opus 5 ac2d857e26 L14 Abrechnungsübersicht: Services für Einträge, Aufstellung, Abrechnen/Stornieren, Meilensteine und PDF
syncBillingCandidates (idempotent, Event-Hook für work_order.released_for_billing und report.approved), Aufstellung ohne Preise (nur freigegebene Zeiten, Fahrzeit getrennt, Pausen informativ, Anfahrten-Zählung, Material), markBilled mit eingefrorenem Snapshot und Positionszuordnung, voidBilling mit Grund und neuem offenen Eintrag (billed → released_for_billing nur über applyTransition-Option billingVoid), Meilenstein-Flow mit Events, Job billing-pdf (Dokument other/backoffice_only), Sync-Op milestone.reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:38 +02:00

65 lines
2.0 KiB
TypeScript

import { z } from "zod";
/**
* Offline sync envelope (ARCHITEKTUR §4.6). The per-op payload schemas live in
* src/lib/sync/ops.ts (owned by lane field); this file only fixes the wire format.
*/
export const SYNC_OP_TYPES = [
"session.start",
"session.pause",
"session.resume",
"session.end",
"work_order.transition",
"note.create",
"checklist.toggle",
"material.upsert",
"photo.attach",
"voice.attach",
"report.save_draft",
"report.submit",
"signature.capture",
"emergency.create",
// L12 Zeiterfassung (additive, conflict-free)
"session.stop_day",
"session.segment",
"time.add_manual",
"time.propose_correction",
// L14 Abrechnungsübersicht (additive, conflict-free; registered in services/sync/external-ops.ts)
"milestone.reach",
] as const;
export type SyncOpType = (typeof SYNC_OP_TYPES)[number];
/** Ops that compare WorkOrder.version and may produce a conflict. All others are additive. */
export const CONFLICTING_OPS: readonly SyncOpType[] = ["work_order.transition", "report.submit"];
export const syncOperationSchema = z.object({
clientOpId: z.string().uuid(),
opType: z.enum(SYNC_OP_TYPES),
entityType: z.string().max(40).optional(),
entityId: z.string().max(64).optional(),
baseVersion: z.number().int().positive().optional(),
payload: z.record(z.string(), z.unknown()),
clientCreatedAt: z.string().datetime(),
});
export type SyncOperationInput = z.infer<typeof syncOperationSchema>;
export const syncRequestSchema = z.object({
deviceId: z.string().max(64),
operations: z.array(syncOperationSchema).min(1).max(100),
});
export type SyncOpResult = {
clientOpId: string;
status: "applied" | "duplicate" | "conflict" | "rejected";
/** server ids created by the op, keyed by the client id they replace */
idMap?: Record<string, string>;
entityVersion?: number;
errorCode?: "not_found" | "forbidden" | "invalid" | "conflict" | "blocked" | "internal";
message?: string;
};
export type SyncResponse = { results: SyncOpResult[]; serverTime: string };