L12 Zeiterfassung: Mobile Uhr-Leiste, Meine Zeiten, Freigaben und Backoffice-Liste
Laufende-Uhr-Leiste auf allen /m-Seiten, Start/Pause direkt auf den Auftragskarten, Auto-Wechsel-Dialog, Auftragsdetail mit Pause / Für heute beenden / Abschließen-Link und Segmentwechseln, /m/time (Tagesübersicht, Nachtragen, Korrektur vorschlagen), /m/approvals für Teamleiter, /work-orders/time-approvals mit Sammelfreigabe, Zeiten-Tab mit Badges und Inline-Freigabe, Dashboard-Kachel, Texte de/en. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { ActionState } from "@/lib/work-orders/action-state";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { approveTimeEntries, approveTimeEntry, rejectTimeEntry } from "@/server/services/field/time-entries";
|
||||
import { ok, str, toErrorState } from "./_form";
|
||||
|
||||
/**
|
||||
* L12 back office: approve / reject manual time entries and correction proposals (`time:approve`).
|
||||
* Thin adapters — permission, team scope, "never own entries", audit and events live in
|
||||
* services/field/time-entries.ts.
|
||||
*/
|
||||
const guard = moduleGuard("work_orders");
|
||||
|
||||
function revalidate(workOrderId?: string) {
|
||||
revalidatePath("/work-orders/time-approvals");
|
||||
if (workOrderId) revalidatePath(`/work-orders/${workOrderId}`);
|
||||
}
|
||||
|
||||
export async function approveTimeEntryAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "timeEntryId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("time:approve");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await approveTimeEntry(ctxFromGuard(g), id);
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "time_entry", entityId: id });
|
||||
}
|
||||
revalidate(str(fd, "workOrderId"));
|
||||
return ok();
|
||||
}
|
||||
|
||||
export async function rejectTimeEntryAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const id = str(fd, "timeEntryId") ?? "";
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("time:approve");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
await rejectTimeEntry(ctxFromGuard(g), id, str(fd, "reason") ?? "");
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "time_entry", entityId: id });
|
||||
}
|
||||
revalidate(str(fd, "workOrderId"));
|
||||
return ok();
|
||||
}
|
||||
|
||||
/** Bulk approval of the selected entries (checkboxes `ids`); entries that cannot be approved stay open. */
|
||||
export async function approveTimeEntriesAction(_prev: ActionState, fd: FormData): Promise<ActionState> {
|
||||
const ids = fd.getAll("ids").filter((v): v is string => typeof v === "string" && v.length > 0);
|
||||
let tenantId: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
try {
|
||||
const g = await guard("time:approve");
|
||||
tenantId = g.session.user.tenantId;
|
||||
actorId = g.session.user.id;
|
||||
const results = await approveTimeEntries(ctxFromGuard(g), ids);
|
||||
const failed = results.filter((r) => !r.ok).length;
|
||||
revalidate();
|
||||
if (ids.length === 0 || failed === results.length) return { status: "error", code: "invalid", message: "time_bulk_none", at: Date.now() };
|
||||
return ok();
|
||||
} catch (err) {
|
||||
return toErrorState(err, { tenantId, actorId, entity: "time_entry" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user