L14 Abrechnungsübersicht: Backoffice-Übersicht, Detail, Druckansicht, Meilensteine im Auftrag und mobil, API
/billing mit Tabs Offen/Abgerechnet/Storniert, Filtern und Mehrfachdruck; /billing/[id] mit Abrechnungsblatt und Aktionen (abgerechnet mit Rechnungsnummer, PDF, Rechnungsnummer ändern, stornieren); /billing/print; Auftrags-Tab „Meilensteine & Abrechnung“; mobiler Abschnitt „Erreicht melden“ (offline); Dashboard-Kachel „Bereit zur Abrechnung“; /api/v1/billing*, Meilenstein-Endpunkte und OpenAPI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { ActionState } from "@/lib/work-orders/action-state";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ok, str, toErrorState } from "@/server/actions/work_orders/_form";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { confirmMilestone, createMilestone, deleteMilestone, markMilestoneReached, moveMilestone, rejectMilestone } from "@/server/services/billing/milestones";
|
||||
|
||||
/**
|
||||
* L14 milestones in the back office work order detail (tab "Meilensteine & Abrechnung").
|
||||
* Define/sort/delete: `work_order:write`; confirm/reject: `billing:write`; report as reached on
|
||||
* behalf of the field: `work_order:write` (the field uses the sync op `milestone.reach`).
|
||||
*/
|
||||
const guard = moduleGuard("billing");
|
||||
|
||||
function revalidate(fd: FormData) {
|
||||
const workOrderId = str(fd, "workOrderId");
|
||||
if (workOrderId) revalidatePath(`/work-orders/${workOrderId}`);
|
||||
revalidatePath("/billing");
|
||||
}
|
||||
|
||||
export async function createMilestoneAction(_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;
|
||||
await createMilestone(ctxFromGuard(g), { workOrderId: str(fd, "workOrderId") ?? "", title: str(fd, "title") ?? "", description: str(fd, "description") ?? null });
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone" });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function moveMilestoneAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "milestoneId") ?? "";
|
||||
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 moveMilestone(ctxFromGuard(g), { milestoneId: id, direction: str(fd, "direction") === "up" ? "up" : "down" });
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone", entityId: id });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function deleteMilestoneAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "milestoneId") ?? "";
|
||||
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 deleteMilestone(ctxFromGuard(g), id);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone", entityId: id });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function reachMilestoneAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "milestoneId") ?? "";
|
||||
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 markMilestoneReached(ctxFromGuard(g), { milestoneId: id, note: str(fd, "note") ?? null });
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone", entityId: id });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function confirmMilestoneAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "milestoneId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("billing:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await confirmMilestone(ctxFromGuard(g), id);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone", entityId: id });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function rejectMilestoneAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "milestoneId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("billing:write");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await rejectMilestone(ctxFromGuard(g), { milestoneId: id, reason: str(fd, "reason") ?? "" });
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "work_order_milestone", entityId: id });
|
||||
}
|
||||
revalidate(fd);
|
||||
return ok();
|
||||
}
|
||||
Reference in New Issue
Block a user