L12 Zeiterfassung: Services für Nachtrag, Freigabe, Auto-Wechsel und Für heute beenden
time-entries.ts (manuelle Einträge, Korrekturvorschläge, Freigabe/Ablehnung mit Team-Scope), Sessions in inTransaction mit switchFromOther, stopForToday, Segmentwechsel und getMyActiveSession. Sync-Ops time.add_manual, time.propose_correction, session.stop_day, session.segment inkl. optimistischer Offline-Ansicht. Bericht, Zeiten-Tab, Dashboard und Lotse zählen nur freigegebene Zeiten; Abrechnungsfreigabe blockiert bei offenen Zeiten. Empfänger und Links der Zeit-Events. Notdienst-Start pausiert eine laufende Uhr automatisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,8 @@ import { ServiceError, type ServiceCtx } from "@/server/services/context";
|
||||
import { requireVisibleWorkOrder } from "@/server/services/work-orders/visibility";
|
||||
import { CONFLICTING_OPS, type SyncOperationInput, type SyncOpResult, type SyncOpType, type SyncResponse } from "@/lib/sync/envelope";
|
||||
import { OP_PAYLOAD_SCHEMAS, type ParsedOpPayload } from "@/lib/sync/ops";
|
||||
import { endSession, pauseSession, resumeSession, startSession } from "@/server/services/field/sessions";
|
||||
import { endSession, pauseSession, resumeSession, startSession, stopForToday, switchSegment } from "@/server/services/field/sessions";
|
||||
import { addManualTimeEntry, proposeTimeCorrection } from "@/server/services/field/time-entries";
|
||||
import { createNote } from "@/server/services/field/notes";
|
||||
import { toggleChecklistItem } from "@/server/services/field/checklist";
|
||||
import { upsertMaterialUsage } from "@/server/services/field/materials";
|
||||
@@ -58,8 +59,23 @@ const FIELD_HANDLERS: Partial<Record<SyncOpType, Handler>> = {
|
||||
"material.upsert": h<"material.upsert">(async (ctx, p) => ({ idMap: idMap(p.clientId, (await upsertMaterialUsage(ctx, p)).usageId) })),
|
||||
"photo.attach": h<"photo.attach">(async (ctx, p) => ({ idMap: idMap(p.clientId, (await attachPhoto(ctx, p)).photoId) })),
|
||||
"voice.attach": h<"voice.attach">(async (ctx, p) => ({ idMap: idMap(p.clientId, (await attachVoiceNote(ctx, p)).voiceNoteId) })),
|
||||
// L12 Zeiterfassung
|
||||
"session.stop_day": h<"session.stop_day">(async (ctx, p) => ({ entityVersion: (await stopForToday(ctx, p)).workOrderVersion })),
|
||||
"session.segment": h<"session.segment">(async (ctx, p) => ({ entityVersion: (await switchSegment(ctx, p)).workOrderVersion })),
|
||||
"time.add_manual": h<"time.add_manual">(async (ctx, p) => ({ idMap: idMap(p.clientId, (await addManualTimeEntry(ctx, p)).id) })),
|
||||
"time.propose_correction": h<"time.propose_correction">(async (ctx, p) => {
|
||||
await proposeTimeCorrection(ctx, p);
|
||||
return {};
|
||||
}),
|
||||
};
|
||||
|
||||
/** Message of a rejected op: blockers as JSON, the auto-switch conflict with the other order (L12). */
|
||||
function errorMessage(err: ServiceError): string {
|
||||
if (err.code === "blocked") return JSON.stringify(err.details ?? []);
|
||||
if (err.message === "other_session_running") return `other_session_running:${JSON.stringify(err.details ?? {})}`;
|
||||
return err.message;
|
||||
}
|
||||
|
||||
class NotAvailable extends Error {}
|
||||
|
||||
/** Route a validated op to the field handler or the registered module of another lane. */
|
||||
@@ -205,12 +221,12 @@ async function applyOne(ctx: ServiceCtx, deviceId: string, op: SyncOperationInpu
|
||||
// a transition conflict detected inside the service (race after the pre-check)
|
||||
const status = err.code === "conflict" && CONFLICTING_OPS.includes(op.opType) ? "conflict" : "rejected";
|
||||
const details = err.code === "blocked" ? { blockers: err.details } : {};
|
||||
const rec = await record(ctx, op, deviceId, status, { message: err.message, ...details }, err.code);
|
||||
const rec = await record(ctx, op, deviceId, status, { message: errorMessage(err), ...details }, err.code);
|
||||
if (rec === "duplicate") return { ...base, status: "duplicate" };
|
||||
if (status === "conflict") {
|
||||
await emitEvent(ctx, { type: "sync.failed", entityType: "sync_operation", entityId: rec.id, data: { opType: op.opType, reason: "conflict" } });
|
||||
}
|
||||
return { ...base, status, errorCode: err.code, message: err.code === "blocked" ? JSON.stringify(err.details ?? []) : err.message };
|
||||
return { ...base, status, errorCode: err.code, message: errorMessage(err) };
|
||||
}
|
||||
console.error(`[sync] ${op.opType} ${op.clientOpId} failed:`, err);
|
||||
return { ...base, status: "rejected", errorCode: "internal", message: "internal error" };
|
||||
|
||||
Reference in New Issue
Block a user