L8 Notdienst: Services für Erfassung, Suche, Abschluss-Event und Sync-Op
createEmergencyOrder in einer Transaktion (vorläufiger Kunde/Objekt, Auftrag N-… in_progress, Team/Zuweisung, WorkSession), searchCustomersForEmergency mit minimalem Feldumfang + Audit, emergency.completed nach Abschlussbericht, Sync-Op emergency.create (Registry + Zod-Schema), Review-Services. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Client-safe input schemas of the emergency module (lane L8, spec §19).
|
||||
* `emergencyCreatePayload` is both the sync op payload (`emergency.create`, src/lib/sync/ops.ts)
|
||||
* and the input of `createEmergencyOrder`. The client ids make the op idempotent: a replay with
|
||||
* the same `clientIds.session` returns the order that was already created.
|
||||
*/
|
||||
|
||||
const id = z.string().min(1).max(64);
|
||||
const clientId = z.string().uuid();
|
||||
const req = (max: number) => z.string().trim().min(1).max(max);
|
||||
const opt = (max: number) =>
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(max)
|
||||
.nullish()
|
||||
.transform((v) => (v ? v : null));
|
||||
|
||||
export const EMERGENCY_REASON_MAX = 2000;
|
||||
|
||||
const newCustomer = z.object({
|
||||
mode: z.literal("new"),
|
||||
companyName: opt(200),
|
||||
firstName: opt(100),
|
||||
lastName: opt(100),
|
||||
phone: req(50),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(200)
|
||||
.nullish()
|
||||
.transform((v) => (v ? v.toLowerCase() : null))
|
||||
.pipe(z.email().nullable()),
|
||||
street: opt(200),
|
||||
houseNumber: opt(20),
|
||||
postalCode: opt(12),
|
||||
city: opt(100),
|
||||
});
|
||||
|
||||
const existingCustomer = z.object({ mode: z.literal("existing"), customerId: id });
|
||||
|
||||
const newSite = z.object({
|
||||
mode: z.literal("new"),
|
||||
name: opt(200),
|
||||
street: req(200),
|
||||
houseNumber: opt(20),
|
||||
postalCode: opt(12),
|
||||
city: req(100),
|
||||
});
|
||||
|
||||
const existingSite = z.object({ mode: z.literal("existing"), siteId: id });
|
||||
|
||||
export const emergencyCreatePayload = z
|
||||
.object({
|
||||
clientIds: z.object({
|
||||
workOrder: clientId,
|
||||
session: clientId,
|
||||
customer: clientId.optional(),
|
||||
site: clientId.optional(),
|
||||
}),
|
||||
customer: z.discriminatedUnion("mode", [existingCustomer, newCustomer]),
|
||||
site: z.discriminatedUnion("mode", [existingSite, newSite]),
|
||||
onSiteContact: z.object({ name: req(200), phone: req(50) }),
|
||||
reason: req(EMERGENCY_REASON_MAX),
|
||||
/** Einsatzbeginn (default: now) */
|
||||
startedAt: z.string().datetime({ offset: true }).optional(),
|
||||
/** null/undefined → the user's own team (first active team) */
|
||||
teamId: id.nullish(),
|
||||
/** colleagues of the chosen team; the creating user is always assigned */
|
||||
assigneeIds: z.array(id).max(20).default([]),
|
||||
offline: z.boolean().default(false),
|
||||
deviceInfo: z.string().max(200).optional(),
|
||||
})
|
||||
.superRefine((v, c) => {
|
||||
if (v.customer.mode === "new" && !v.customer.companyName && !v.customer.lastName) {
|
||||
c.addIssue({ code: "custom", path: ["customer", "lastName"], message: "name_required" });
|
||||
}
|
||||
});
|
||||
|
||||
export type EmergencyCreateInput = z.input<typeof emergencyCreatePayload>;
|
||||
export type EmergencyCreateParsed = z.output<typeof emergencyCreatePayload>;
|
||||
|
||||
/** Result keys of the op's idMap besides the client ids. */
|
||||
export type EmergencyCreateResult = {
|
||||
workOrderId: string;
|
||||
number: string;
|
||||
customerId: string;
|
||||
siteId: string;
|
||||
contactId: string;
|
||||
sessionId: string;
|
||||
version: number;
|
||||
replayed: boolean;
|
||||
};
|
||||
|
||||
/** Backoffice: "Auftrag ergänzen". */
|
||||
export const emergencyOrderPatchSchema = z.object({
|
||||
title: req(200),
|
||||
description: opt(10_000),
|
||||
orderTypeId: id.nullish(),
|
||||
billingType: z.enum(["fixed", "time_material", "maintenance_contract", "warranty"]).nullish(),
|
||||
});
|
||||
|
||||
/** The five review steps shown as "x von 5 Prüfschritten erledigt". */
|
||||
export const REVIEW_STEPS = ["customer", "site", "order", "report", "billing"] as const;
|
||||
export type ReviewStep = (typeof REVIEW_STEPS)[number];
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { SyncOpType } from "./envelope";
|
||||
import { WORK_ORDER_STATUSES } from "@/lib/work-orders/status";
|
||||
import { emergencyCreatePayload } from "@/lib/emergency/schemas";
|
||||
|
||||
/**
|
||||
* Payload schemas per sync opType (ARCHITEKTUR §4.6). Client-safe: used by the mobile UI to
|
||||
@@ -129,7 +130,7 @@ export const OP_PAYLOAD_SCHEMAS = {
|
||||
"report.save_draft": passthrough,
|
||||
"report.submit": passthrough,
|
||||
"signature.capture": passthrough,
|
||||
"emergency.create": passthrough,
|
||||
"emergency.create": emergencyCreatePayload,
|
||||
} satisfies Record<SyncOpType, z.ZodType>;
|
||||
|
||||
export type OpPayload<T extends SyncOpType> = z.input<(typeof OP_PAYLOAD_SCHEMAS)[T]>;
|
||||
|
||||
Reference in New Issue
Block a user