import { z } from "zod"; import type { SyncOpType } from "./envelope"; import { WORK_ORDER_STATUSES } from "@/lib/work-orders/status"; /** * Payload schemas per sync opType (ARCHITEKTUR §4.6). Client-safe: used by the mobile UI to * build ops and by src/server/services/sync/apply.ts to validate them. * `clientId` fields are device-generated ids (uuid) that make additive creates idempotent * and are mapped to server ids in SyncOpResult.idMap. */ const id = z.string().min(1).max(64); const clientId = z.string().uuid(); const isoDate = z.string().datetime({ offset: true }); const lat = z.number().min(-90).max(90); const lng = z.number().min(-180).max(180); const quantity = z.number().min(0).max(1_000_000); export const NOTE_KINDS = [ "work_done", "deviation", "problem", "additional_work", "not_executable", "follow_up", "recommendation", "customer_note", "general", ] as const; export type NoteKind = (typeof NOTE_KINDS)[number]; export const PHOTO_PHASES = ["before", "during", "after"] as const; export type PhotoPhase = (typeof PHOTO_PHASES)[number]; export const MATERIAL_USAGE_STATUSES = ["fully_used", "partially_used", "not_used", "additional"] as const; export type MaterialUsageStatus = (typeof MATERIAL_USAGE_STATUSES)[number]; /** Unit suggestions for the material stepper (free text stays allowed). */ export const UNIT_SUGGESTIONS = ["Stk", "m", "m²", "m³", "kg", "l", "Pkg", "Rolle", "Satz", "h"] as const; export const sessionStartPayload = z.object({ workOrderId: id, clientId: clientId.optional(), /** travel = "Losfahren" (in Anfahrt), work = "Arbeit starten" */ mode: z.enum(["travel", "work"]).default("work"), at: isoDate.optional(), latitude: lat.optional(), longitude: lng.optional(), offline: z.boolean().default(false), deviceInfo: z.string().max(200).optional(), }); export const sessionControlPayload = z.object({ workOrderId: id, at: isoDate.optional(), }); export const workOrderTransitionPayload = z.object({ workOrderId: id, to: z.enum(WORK_ORDER_STATUSES), reason: z.string().max(1000).optional(), }); export const noteCreatePayload = z.object({ workOrderId: id, clientId: clientId.optional(), kind: z.enum(NOTE_KINDS).default("general"), text: z.string().trim().min(1).max(10_000), }); export const checklistTogglePayload = z.object({ workOrderId: id, itemId: id, checked: z.boolean(), comment: z.string().max(2000).nullish(), }); export const materialUpsertPayload = z.object({ workOrderId: id, clientId: clientId.optional(), materialPlanId: id.nullish(), name: z.string().trim().max(200).optional(), articleNumber: z.string().trim().max(100).nullish(), quantity, unit: z.string().trim().min(1).max(20), usageStatus: z.enum(MATERIAL_USAGE_STATUSES), deviationReason: z.string().trim().max(2000).nullish(), notes: z.string().trim().max(2000).nullish(), photoId: id.nullish(), }); export const photoAttachPayload = z.object({ workOrderId: id, clientId: clientId.optional(), documentId: id, phase: z.enum(PHOTO_PHASES).nullish(), photoRequirementId: id.nullish(), checklistItemId: id.nullish(), comment: z.string().trim().max(2000).nullish(), takenAt: isoDate.optional(), latitude: lat.optional(), longitude: lng.optional(), }); export const voiceAttachPayload = z.object({ workOrderId: id, clientId: clientId.optional(), documentId: id, durationSeconds: z.number().int().min(0).max(300).optional(), recordedAt: isoDate.optional(), /** optional note kind the transcript is filed under */ kind: z.enum(NOTE_KINDS).optional(), }); /** Schemas of ops owned by other lanes are validated there (reports: L5, emergency: L8). */ const passthrough = z.record(z.string(), z.unknown()); export const OP_PAYLOAD_SCHEMAS = { "session.start": sessionStartPayload, "session.pause": sessionControlPayload, "session.resume": sessionControlPayload, "session.end": sessionControlPayload, "work_order.transition": workOrderTransitionPayload, "note.create": noteCreatePayload, "checklist.toggle": checklistTogglePayload, "material.upsert": materialUpsertPayload, "photo.attach": photoAttachPayload, "voice.attach": voiceAttachPayload, "report.save_draft": passthrough, "report.submit": passthrough, "signature.capture": passthrough, "emergency.create": passthrough, } satisfies Record; export type OpPayload = z.input<(typeof OP_PAYLOAD_SCHEMAS)[T]>; export type ParsedOpPayload = z.output<(typeof OP_PAYLOAD_SCHEMAS)[T]>;