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:
2026-09-14 18:19:19 +02:00
co-authored by Claude Opus 5
parent 5f08df324f
commit 85bae832d0
12 changed files with 316 additions and 40 deletions
+19 -1
View File
@@ -301,9 +301,27 @@ export async function getFieldBundle(ctx: ServiceCtx, since?: Date | null) {
const histories = Object.fromEntries(
await Promise.all(siteIds.map(async (id) => [id, await fieldSiteHistory(ctx, id, 5).catch(() => [])] as const)),
);
// L10b (L7 offene Punkte 3/4): the caller's own running session per order, so the offline view
// shows the correct time actions on team orders with several technicians.
const ownSessions = orders.length
? await ctx.db.workSession.findMany({
where: { workOrderId: { in: orders.map((o) => o.id) }, userId: ctx.userId, status: { in: ACTIVE_SESSION_STATUSES } },
orderBy: { startedAt: "desc" },
select: { id: true, workOrderId: true, status: true, startedAt: true },
})
: [];
const mySessions = new Map<string, { id: string; status: string; startedAt: string }>();
for (const s of ownSessions) {
if (!mySessions.has(s.workOrderId)) mySessions.set(s.workOrderId, { id: s.id, status: s.status, startedAt: s.startedAt.toISOString() });
}
return {
serverTime: serverTime.toISOString(),
since: since?.toISOString() ?? null,
orders: orders.map((o) => ({ ...o, statusGroup: STATUS_GROUP[o.status], siteHistory: o.site ? histories[o.site.id] ?? [] : [] })),
orders: orders.map((o) => ({
...o,
statusGroup: STATUS_GROUP[o.status],
siteHistory: o.site ? histories[o.site.id] ?? [] : [],
mySession: mySessions.get(o.id) ?? null,
})),
};
}