L10b Betrieb & Aufräumen: Sync – Berichts-Ops, Konflikt-Übernahme, eigene Session im Bundle
- Aufräumpunkt j: report.save_draft und report.submit mit Zod-Schemas (lib/sync/ops.ts) und Registry-Einträgen → services/reports/sync-ops.ts. report.submit reicht baseVersion als expectedWorkOrderVersion und aiReviewed an submitReport durch; Lotse-Entwürfe ohne Bestätigung → rejected invalid. signature.capture bleibt unregistriert (Upload-Art für Unterschriftsbild fehlt). - Aufräumpunkt b: „Übernehmen" in der Konfliktliste delegiert an den Sync-Dispatcher (apply.ts#reapplyOperation, ohne baseVersion) statt des L2-Stubs; unterstützt work_order.transition und report.submit. Hinweistext der Konfliktliste angepasst. - Aufräumpunkt c: getFieldBundle liefert je Auftrag mySession (eigene aktive WorkSession); die Offline-Ansicht leitet den Zeitstatus daraus ab (alte Bundles: Näherung über Auftragsstatus). - scripts/test-betrieb-sync.ts (Bundle, clientId je Mandant, Berichts-Ops, Konflikt-Übernahme, Mandant B, Monteur ohne Zuweisung); test-einsatz-sync.ts prüft „nicht verfügbare Op" jetzt mit signature.capture, weil report.save_draft registriert ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,52 @@ const FIELD_HANDLERS: Partial<Record<SyncOpType, Handler>> = {
|
||||
|
||||
class NotAvailable extends Error {}
|
||||
|
||||
/** Route a validated op to the field handler or the registered module of another lane. */
|
||||
async function dispatch(ctx: ServiceCtx, payload: unknown, op: SyncOperationInput): Promise<HandlerResult> {
|
||||
const handler = FIELD_HANDLERS[op.opType];
|
||||
if (handler) return handler(ctx, payload, op);
|
||||
const load = EXTERNAL_OPS[op.opType];
|
||||
const external = load ? await load() : null;
|
||||
if (!external) throw new NotAvailable(`operation ${op.opType} is not available yet (lane ${EXTERNAL_OP_OWNERS[op.opType] ?? "unknown"})`);
|
||||
return external(ctx, op);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backoffice „Übernehmen" of a stored conflict (services/work-orders/conflicts.ts, L10b): the op
|
||||
* is dispatched again against the CURRENT state — same payload validation and domain services as
|
||||
* the sync path, but without the baseVersion comparison. Only conflict-prone ops
|
||||
* (`work_order.transition`, `report.submit`) can end up as conflicts.
|
||||
*/
|
||||
export async function reapplyOperation(
|
||||
ctx: ServiceCtx,
|
||||
stored: { opType: string; entityType?: string | null; entityId: string | null; payload: unknown },
|
||||
): Promise<HandlerResult> {
|
||||
const opType = stored.opType as SyncOpType;
|
||||
if (!CONFLICTING_OPS.includes(opType)) throw new ServiceError("invalid", "reapply_unsupported", { opType: stored.opType });
|
||||
const payload: Record<string, unknown> = { ...((stored.payload ?? {}) as Record<string, unknown>) };
|
||||
// stored ops whose work order is only referenced via entityId (accepted by the former L2 stub)
|
||||
if (typeof payload.workOrderId !== "string" && stored.entityId && (!stored.entityType || stored.entityType === "work_order")) {
|
||||
payload.workOrderId = stored.entityId;
|
||||
}
|
||||
const parsed = OP_PAYLOAD_SCHEMAS[opType].safeParse(payload);
|
||||
if (!parsed.success) throw new ServiceError("invalid", "sync_payload_invalid");
|
||||
const op: SyncOperationInput = {
|
||||
clientOpId: "00000000-0000-4000-8000-000000000000", // not used by handlers; idempotency stays with the stored op
|
||||
opType,
|
||||
entityType: stored.entityType ?? undefined,
|
||||
entityId: stored.entityId ?? undefined,
|
||||
baseVersion: undefined,
|
||||
payload,
|
||||
clientCreatedAt: new Date().toISOString(),
|
||||
};
|
||||
try {
|
||||
return await dispatch(ctx, parsed.data, op);
|
||||
} catch (err) {
|
||||
if (err instanceof NotAvailable) throw new ServiceError("invalid", "reapply_unsupported", { opType });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function workOrderIdOf(op: SyncOperationInput): string | undefined {
|
||||
const fromPayload = (op.payload as { workOrderId?: unknown }).workOrderId;
|
||||
if (typeof fromPayload === "string") return fromPayload;
|
||||
@@ -149,14 +195,7 @@ async function applyOne(ctx: ServiceCtx, deviceId: string, op: SyncOperationInpu
|
||||
}
|
||||
|
||||
// 4. dispatch
|
||||
let handler = FIELD_HANDLERS[op.opType];
|
||||
if (!handler) {
|
||||
const load = EXTERNAL_OPS[op.opType];
|
||||
const external = load ? await load() : null;
|
||||
if (!external) throw new NotAvailable(`operation ${op.opType} is not available yet (lane ${EXTERNAL_OP_OWNERS[op.opType] ?? "unknown"})`);
|
||||
handler = (c, _payload, o) => external(c, o);
|
||||
}
|
||||
const result = await handler(ctx, parsed.data, op);
|
||||
const result = await dispatch(ctx, parsed.data, op);
|
||||
const rec = await record(ctx, op, deviceId, "applied", { ...result });
|
||||
if (rec === "duplicate") return { ...base, status: "duplicate", ...result };
|
||||
return { ...base, status: "applied", ...result };
|
||||
|
||||
Reference in New Issue
Block a user