L2 Aufträge & Backoffice: Server Actions und /api/v1/work-orders
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { unstable_rethrow } from "next/navigation";
|
||||
import type { ActionState } from "@/lib/work-orders/action-state";
|
||||
import type { CompletionBlocker } from "@/lib/work-orders/status";
|
||||
import { wallTimeToUtc } from "@/lib/work-orders/time";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
import { ModuleDisabledError } from "@/server/modules";
|
||||
import { ForbiddenError } from "@/server/rbac";
|
||||
import { ServiceError } from "@/server/services/context";
|
||||
|
||||
/**
|
||||
* Internal helpers of the work_orders actions (file starts with "_": not an action module,
|
||||
* skipped by scripts/check-module-guards.ts). Maps thrown errors to a displayable ActionState
|
||||
* without leaking internals (F-16) and audits RBAC denials.
|
||||
*/
|
||||
export async function toErrorState(
|
||||
err: unknown,
|
||||
audit: { tenantId?: string; actorId?: string; entity: string; entityId?: string },
|
||||
): Promise<ActionState> {
|
||||
unstable_rethrow(err);
|
||||
const at = Date.now();
|
||||
if (err instanceof ServiceError) {
|
||||
return {
|
||||
status: "error",
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
blockers: err.code === "blocked" ? (err.details as CompletionBlocker[]) : undefined,
|
||||
at,
|
||||
};
|
||||
}
|
||||
if (err instanceof ForbiddenError || err instanceof ModuleDisabledError) {
|
||||
if (err instanceof ForbiddenError && audit.tenantId) {
|
||||
try {
|
||||
await writeAuditLog({ tenantId: audit.tenantId, actorId: audit.actorId, action: "denied", entity: audit.entity, entityId: audit.entityId, after: { reason: err.message } });
|
||||
} catch (auditErr) {
|
||||
console.error("[work_orders] audit-write-failed", auditErr);
|
||||
}
|
||||
}
|
||||
return { status: "error", code: "forbidden", message: "forbidden", at };
|
||||
}
|
||||
console.error(`[work_orders] ${audit.entity} action failed`, err);
|
||||
return { status: "error", code: "internal", message: "internal", at };
|
||||
}
|
||||
|
||||
export const ok = (): ActionState => ({ status: "ok", at: Date.now() });
|
||||
|
||||
export function str(fd: FormData, key: string): string | undefined {
|
||||
const v = fd.get(key);
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t ? t : undefined;
|
||||
}
|
||||
|
||||
/** Present field → string or null (empty); absent field → undefined. */
|
||||
export function nullable(fd: FormData, key: string): string | null | undefined {
|
||||
if (!fd.has(key)) return undefined;
|
||||
return str(fd, key) ?? null;
|
||||
}
|
||||
|
||||
export function bool(fd: FormData, key: string): boolean {
|
||||
const v = fd.get(key);
|
||||
return v === "on" || v === "true" || v === "1";
|
||||
}
|
||||
|
||||
/** "yes" | "no" | "" select → boolean | undefined */
|
||||
export function triState(fd: FormData, key: string): boolean | undefined {
|
||||
const v = str(fd, key);
|
||||
return v === "yes" ? true : v === "no" ? false : undefined;
|
||||
}
|
||||
|
||||
export function int(fd: FormData, key: string): number | undefined {
|
||||
const v = str(fd, key);
|
||||
if (v === undefined) return undefined;
|
||||
const n = Number.parseInt(v, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
/** datetime-local in tenant timezone → Date; empty → null; absent → undefined. */
|
||||
export function wallTime(fd: FormData, key: string, tz: string): Date | null | undefined {
|
||||
if (!fd.has(key)) return undefined;
|
||||
const v = str(fd, key);
|
||||
if (!v) return null;
|
||||
return wallTimeToUtc(v, tz) ?? null;
|
||||
}
|
||||
|
||||
/** Only internal detail links are allowed as return targets. */
|
||||
export function detailPath(workOrderId: string, tab?: string): string {
|
||||
const safeTab = tab && /^[a-z]{3,12}$/.test(tab) ? `?tab=${tab}` : "";
|
||||
return `/work-orders/${encodeURIComponent(workOrderId)}${safeTab}`;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { ActionState } from "@/lib/work-orders/action-state";
|
||||
import { slugKey } from "@/lib/work-orders/schemas";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { uniqueKey } from "@/server/services/work-orders/_shared";
|
||||
import {
|
||||
createChecklistTemplate,
|
||||
createOrderType,
|
||||
updateChecklistTemplate,
|
||||
updateNumberSequence,
|
||||
updateOrderType,
|
||||
} from "@/server/services/work-orders/settings";
|
||||
import { bool, int, ok, str, toErrorState } from "./_form";
|
||||
|
||||
/** Settings of the work order module (order types, checklist templates, numbering) — `settings:templates`. */
|
||||
const guard = moduleGuard("work_orders");
|
||||
|
||||
export async function createOrderTypeAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("settings:templates");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
const name = str(fd, "name") ?? "";
|
||||
await createOrderType(ctxFromGuard(g), {
|
||||
key: str(fd, "key") ?? slugKey(name),
|
||||
name,
|
||||
signatureRequired: bool(fd, "signatureRequired"),
|
||||
active: true,
|
||||
sortOrder: int(fd, "sortOrder") ?? 100,
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "order_type" });
|
||||
}
|
||||
revalidatePath("/settings/order-types");
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function updateOrderTypeAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "id") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("settings:templates");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await updateOrderType(ctxFromGuard(g), id, {
|
||||
name: str(fd, "name") ?? "",
|
||||
signatureRequired: bool(fd, "signatureRequired"),
|
||||
active: bool(fd, "active"),
|
||||
sortOrder: int(fd, "sortOrder") ?? 100,
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "order_type", entityId: id });
|
||||
}
|
||||
revalidatePath("/settings/order-types");
|
||||
return ok();
|
||||
}
|
||||
|
||||
/** Textarea lines → template items: "*" prefix = required, "[Foto]" suffix = requires photo. */
|
||||
function parseItems(text: string) {
|
||||
const keys = new Set<string>();
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 100)
|
||||
.map((line) => {
|
||||
let label = line;
|
||||
const required = label.startsWith("*");
|
||||
if (required) label = label.slice(1).trim();
|
||||
const requiresPhoto = /\[foto\]$/i.test(label);
|
||||
if (requiresPhoto) label = label.replace(/\[foto\]$/i, "").trim();
|
||||
return { key: uniqueKey(slugKey(label), keys), label: label.slice(0, 200), required, requiresPhoto };
|
||||
})
|
||||
.filter((i) => i.label.length > 0);
|
||||
}
|
||||
|
||||
function parsePhotos(text: string) {
|
||||
const keys = new Set<string>();
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 30)
|
||||
.map((label) => ({ key: uniqueKey(slugKey(label), keys), label: label.slice(0, 200) }));
|
||||
}
|
||||
|
||||
export async function saveChecklistTemplateAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "id");
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("settings:templates");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
const input = {
|
||||
name: str(fd, "name") ?? "",
|
||||
orderTypeId: str(fd, "orderTypeId") ?? null,
|
||||
active: bool(fd, "active"),
|
||||
items: parseItems(String(fd.get("items") ?? "")),
|
||||
requiredPhotos: parsePhotos(String(fd.get("requiredPhotos") ?? "")),
|
||||
};
|
||||
const ctx = ctxFromGuard(g);
|
||||
if (id) await updateChecklistTemplate(ctx, id, input);
|
||||
else await createChecklistTemplate(ctx, input);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "checklist_template", entityId: id });
|
||||
}
|
||||
revalidatePath("/settings/checklists");
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function updateNumberingAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("settings:templates");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await updateNumberSequence(ctxFromGuard(g), {
|
||||
key: str(fd, "key") ?? "",
|
||||
prefix: String(fd.get("prefix") ?? "").trim(),
|
||||
padding: str(fd, "padding") ?? "",
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "number_sequence" });
|
||||
}
|
||||
revalidatePath("/settings/numbering");
|
||||
return ok();
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { ActionState } from "@/lib/work-orders/action-state";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { tenantTimezone } from "@/server/services/work-orders/_shared";
|
||||
import { assignWorkOrder } from "@/server/services/work-orders/assign";
|
||||
import { addChecklistItem, addPhotoRequirement, applyChecklistTemplate, removeChecklistItem, removePhotoRequirement } from "@/server/services/work-orders/checklist";
|
||||
import { applySyncConflict, discardSyncConflict } from "@/server/services/work-orders/conflicts";
|
||||
import { createWorkOrder } from "@/server/services/work-orders/create";
|
||||
import { archiveWorkOrderDocument } from "@/server/services/work-orders/documents";
|
||||
import { addMaterialPlan, removeMaterialPlan } from "@/server/services/work-orders/materials";
|
||||
import { transitionWorkOrder } from "@/server/services/work-orders/transition";
|
||||
import { updateWorkOrder } from "@/server/services/work-orders/update";
|
||||
import { bool, detailPath, int, nullable, ok, str, toErrorState, triState, wallTime } from "./_form";
|
||||
|
||||
/**
|
||||
* Backoffice server actions of module "work_orders" — thin adapters:
|
||||
* moduleGuard → FormData → service (permission, scope, Zod, audit, event) → revalidate/redirect.
|
||||
*/
|
||||
const guard = moduleGuard("work_orders");
|
||||
|
||||
export async function createWorkOrderAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let id: string;
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
const ctx = ctxFromGuard(g);
|
||||
const tz = await tenantTimezone(ctx);
|
||||
const created = await createWorkOrder(ctx, {
|
||||
title: str(fd, "title") ?? "",
|
||||
customerId: str(fd, "customerId") ?? "",
|
||||
siteId: str(fd, "siteId") ?? null,
|
||||
contactId: str(fd, "contactId") ?? null,
|
||||
orderTypeId: str(fd, "orderTypeId") ?? null,
|
||||
priority: (str(fd, "priority") as never) ?? "normal",
|
||||
status: str(fd, "status") === "planned" ? "planned" : "draft",
|
||||
description: str(fd, "description"),
|
||||
scope: str(fd, "scope"),
|
||||
plannedStart: wallTime(fd, "plannedStart", tz) ?? null,
|
||||
plannedEnd: wallTime(fd, "plannedEnd", tz) ?? null,
|
||||
signatureRequired: triState(fd, "signatureRequired"),
|
||||
billingType: (str(fd, "billingType") as never) ?? null,
|
||||
internalNotes: str(fd, "internalNotes"),
|
||||
technicianNotes: str(fd, "technicianNotes"),
|
||||
externalOrderNumber: str(fd, "externalOrderNumber"),
|
||||
offerNumber: str(fd, "offerNumber"),
|
||||
});
|
||||
id = created.id;
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order" });
|
||||
}
|
||||
revalidatePath("/work-orders");
|
||||
redirect(detailPath(id));
|
||||
}
|
||||
|
||||
export async function updateWorkOrderAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
const ctx = ctxFromGuard(g);
|
||||
const tz = await tenantTimezone(ctx);
|
||||
const patch: Record<string, unknown> = {};
|
||||
for (const k of ["title", "priority", "billingType"]) if (fd.has(k)) patch[k] = str(fd, k) ?? null;
|
||||
for (const k of ["siteId", "contactId", "orderTypeId", "description", "scope", "internalNotes", "technicianNotes", "externalOrderNumber", "offerNumber"]) {
|
||||
const v = nullable(fd, k);
|
||||
if (v !== undefined) patch[k] = v;
|
||||
}
|
||||
for (const k of ["plannedStart", "plannedEnd"]) {
|
||||
const v = wallTime(fd, k, tz);
|
||||
if (v !== undefined) patch[k] = v;
|
||||
}
|
||||
const signature = triState(fd, "signatureRequired");
|
||||
if (signature !== undefined) patch.signatureRequired = signature;
|
||||
if (patch.title === null) patch.title = "";
|
||||
await updateWorkOrder(ctx, workOrderId, patch, int(fd, "baseVersion"));
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
redirect(detailPath(workOrderId));
|
||||
}
|
||||
|
||||
export async function transitionAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
// Permission depends on the concrete transition → decided by the service (mayTransition).
|
||||
const g = await guard();
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await transitionWorkOrder(ctxFromGuard(g), {
|
||||
workOrderId,
|
||||
to: (str(fd, "to") ?? "") as never,
|
||||
reason: str(fd, "reason") ?? null,
|
||||
baseVersion: int(fd, "baseVersion"),
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath("/work-orders");
|
||||
redirect(detailPath(workOrderId, str(fd, "returnTab")));
|
||||
}
|
||||
|
||||
export async function assignAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:assign");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await assignWorkOrder(ctxFromGuard(g), {
|
||||
workOrderId,
|
||||
teamId: str(fd, "teamId") ?? "",
|
||||
userIds: fd.getAll("userIds").filter((v): v is string => typeof v === "string" && v.length > 0),
|
||||
teamLeadUserId: str(fd, "teamLeadUserId") ?? null,
|
||||
baseVersion: int(fd, "baseVersion"),
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath("/work-orders");
|
||||
redirect(detailPath(workOrderId));
|
||||
}
|
||||
|
||||
export async function addMaterialPlanAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await addMaterialPlan(ctxFromGuard(g), workOrderId, {
|
||||
name: str(fd, "name") ?? "",
|
||||
articleNumber: str(fd, "articleNumber"),
|
||||
plannedQuantity: (str(fd, "plannedQuantity") ?? "").replace(",", ".") as never,
|
||||
unit: str(fd, "unit") ?? "",
|
||||
notes: str(fd, "notes"),
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function removeMaterialPlanAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await removeMaterialPlan(ctxFromGuard(g), str(fd, "planId") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function addChecklistItemAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await addChecklistItem(ctxFromGuard(g), workOrderId, {
|
||||
label: str(fd, "label") ?? "",
|
||||
required: bool(fd, "required"),
|
||||
requiresPhoto: bool(fd, "requiresPhoto"),
|
||||
});
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function removeChecklistItemAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await removeChecklistItem(ctxFromGuard(g), str(fd, "itemId") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function addPhotoRequirementAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await addPhotoRequirement(ctxFromGuard(g), workOrderId, { label: str(fd, "label") ?? "" });
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function removePhotoRequirementAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await removePhotoRequirement(ctxFromGuard(g), str(fd, "requirementId") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function applyChecklistTemplateAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await applyChecklistTemplate(ctxFromGuard(g), workOrderId, str(fd, "templateId") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order", entityId: workOrderId });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function archiveDocumentAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const workOrderId = str(fd, "workOrderId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("document:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await archiveWorkOrderDocument(ctxFromGuard(g), str(fd, "documentId") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "document", entityId: str(fd, "documentId") });
|
||||
}
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function applySyncConflictAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const opId = str(fd, "opId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await applySyncConflict(ctxFromGuard(g), opId);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "sync_operation", entityId: opId });
|
||||
}
|
||||
revalidatePath("/work-orders/conflicts");
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function discardSyncConflictAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const opId = str(fd, "opId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("work_order:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await discardSyncConflict(ctxFromGuard(g), opId);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "sync_operation", entityId: opId });
|
||||
}
|
||||
revalidatePath("/work-orders/conflicts");
|
||||
return ok();
|
||||
}
|
||||
Reference in New Issue
Block a user