L8 Notdienst: mobile Erfassung /m/emergency in drei Schritten
Kunde suchen oder vorläufig anlegen, Einsatzort mit Ansprechpartner, Grund mit optionaler Sprachnotiz, Beginn und Team; Start über die Sync-Op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"use server";
|
||||
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { toActionError } from "@/server/api/action-state";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import {
|
||||
listSitesForEmergency,
|
||||
searchCustomersForEmergency,
|
||||
type EmergencyCustomerHit,
|
||||
type EmergencySiteHit,
|
||||
} from "@/server/services/emergency/lookup";
|
||||
|
||||
/**
|
||||
* Read lookups of the mobile emergency capture. They are server actions (not page reads) because
|
||||
* the client wizard calls them interactively; both are audited in the service. The create itself
|
||||
* goes through the sync op `emergency.create` (offline-capable, POST /api/v1/sync).
|
||||
*/
|
||||
const guard = moduleGuard("emergency");
|
||||
|
||||
export type LookupResult<T> = { ok: true; items: T[] } | { ok: false; code: string };
|
||||
|
||||
export async function searchEmergencyCustomersAction(query: string): Promise<LookupResult<EmergencyCustomerHit>> {
|
||||
try {
|
||||
const ctx = ctxFromGuard(await guard("emergency:create"));
|
||||
return { ok: true, items: await searchCustomersForEmergency(ctx, String(query ?? "")) };
|
||||
} catch (err) {
|
||||
return { ok: false, code: toActionError(err).code };
|
||||
}
|
||||
}
|
||||
|
||||
export async function listEmergencySitesAction(customerId: string): Promise<LookupResult<EmergencySiteHit>> {
|
||||
try {
|
||||
const ctx = ctxFromGuard(await guard("emergency:create"));
|
||||
return { ok: true, items: await listSitesForEmergency(ctx, String(customerId ?? "")) };
|
||||
} catch (err) {
|
||||
return { ok: false, code: toActionError(err).code };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { formObject, toActionError, type ActionState } from "@/server/api/action-state";
|
||||
import { ctxFromGuard, type ServiceCtx } from "@/server/services/context";
|
||||
import {
|
||||
assignEmergencySite,
|
||||
assignEmergencyToCustomer,
|
||||
completeEmergencyOrderData,
|
||||
confirmEmergencyCustomer,
|
||||
correctEmergencySite,
|
||||
mergeEmergencyCustomer,
|
||||
releaseEmergencyForBilling,
|
||||
} from "@/server/services/emergency/review";
|
||||
|
||||
/** Backoffice review of emergency call-outs (/work-orders/emergency-review/[id]). */
|
||||
const guard = moduleGuard("emergency");
|
||||
|
||||
function done(workOrderId: string): ActionState {
|
||||
revalidatePath("/work-orders/emergency-review");
|
||||
revalidatePath(`/work-orders/emergency-review/${workOrderId}`);
|
||||
revalidatePath(`/work-orders/${workOrderId}`);
|
||||
return { status: "ok" };
|
||||
}
|
||||
|
||||
async function run(workOrderId: string, fn: () => Promise<unknown>): Promise<ActionState> {
|
||||
try {
|
||||
await fn();
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
return done(workOrderId);
|
||||
}
|
||||
|
||||
const version = (fd: FormData) => {
|
||||
const v = Number(fd.get("baseVersion"));
|
||||
return Number.isInteger(v) && v > 0 ? v : undefined;
|
||||
};
|
||||
|
||||
export async function confirmCustomerAction(workOrderId: string): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "customer:write"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
return run(workOrderId, () => confirmEmergencyCustomer(ctx, workOrderId));
|
||||
}
|
||||
|
||||
export async function assignCustomerAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "customer:write", "work_order:write"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
return run(workOrderId, async () => assignEmergencyToCustomer(ctx, { workOrderId, targetCustomerId: await targetCustomer(ctx, fd) }));
|
||||
}
|
||||
|
||||
/** Target from the candidate select or, as a fallback, from an entered customer number (tenant-bound lookup). */
|
||||
async function targetCustomer(ctx: ServiceCtx, fd: FormData): Promise<string> {
|
||||
const id = String(fd.get("targetCustomerId") ?? "").trim();
|
||||
if (id) return id;
|
||||
const customerNumber = String(fd.get("targetCustomerNumber") ?? "").trim();
|
||||
if (!customerNumber) return "";
|
||||
const c = await ctx.db.customer.findFirst({ where: { customerNumber, deletedAt: null }, select: { id: true } });
|
||||
return c?.id ?? "__unknown__";
|
||||
}
|
||||
|
||||
export async function mergeCustomerAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "customer:merge"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
const confirm = fd.get("confirm") === "on" || fd.get("confirm") === "1";
|
||||
return run(workOrderId, async () => mergeEmergencyCustomer(ctx, { workOrderId, targetCustomerId: await targetCustomer(ctx, fd), confirm }));
|
||||
}
|
||||
|
||||
export async function correctSiteAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "site:write"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
const values = formObject(fd, ["name", "street", "houseNumber", "postalCode", "city", "onSiteContact", "phone", "accessNotes"]);
|
||||
const patch = { ...values, name: values.name ?? undefined };
|
||||
const confirm = fd.get("confirm") === "on" || fd.get("confirm") === "1";
|
||||
return run(workOrderId, () => correctEmergencySite(ctx, { workOrderId, patch, confirm }));
|
||||
}
|
||||
|
||||
export async function assignSiteAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "work_order:write"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
const siteId = String(fd.get("siteId") ?? "");
|
||||
return run(workOrderId, () => assignEmergencySite(ctx, { workOrderId, siteId }));
|
||||
}
|
||||
|
||||
export async function completeOrderAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "work_order:write"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
const values = formObject(fd, ["title", "description", "orderTypeId", "billingType"]);
|
||||
return run(workOrderId, () => completeEmergencyOrderData(ctx, { workOrderId, ...values, baseVersion: version(fd) }));
|
||||
}
|
||||
|
||||
export async function releaseBillingAction(workOrderId: string, _prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
let ctx: ServiceCtx;
|
||||
try {
|
||||
ctx = ctxFromGuard(await guard("emergency:review", "work_order:release_billing"));
|
||||
} catch (err) {
|
||||
return toActionError(err);
|
||||
}
|
||||
return run(workOrderId, () => releaseEmergencyForBilling(ctx, { workOrderId, baseVersion: version(fd) }));
|
||||
}
|
||||
Reference in New Issue
Block a user