143 lines
5.1 KiB
TypeScript
143 lines
5.1 KiB
TypeScript
import type { ApprovalRequest, ApprovalStatus } from "@/types/appops";
|
|
|
|
export interface ApprovalFollowupLink {
|
|
label: string;
|
|
href: string;
|
|
}
|
|
|
|
export interface ApprovalFollowupView {
|
|
status: ApprovalStatus;
|
|
statusLabel: string;
|
|
expiryLabel: string;
|
|
auditReference: string | null;
|
|
jobReference: string | null;
|
|
executionCopy: string;
|
|
canDecide: boolean;
|
|
links: ApprovalFollowupLink[];
|
|
}
|
|
|
|
export interface ApprovalEmptyState {
|
|
title: string;
|
|
message: string;
|
|
actions: ApprovalFollowupLink[];
|
|
}
|
|
|
|
const statusLabels: Record<ApprovalStatus, string> = {
|
|
pending: "Pending",
|
|
approved: "Approved",
|
|
rejected: "Rejected",
|
|
expired: "Expired",
|
|
executed: "Executed",
|
|
};
|
|
|
|
const normalizeStatus = (status?: string): ApprovalStatus => (
|
|
status === "approved" ||
|
|
status === "rejected" ||
|
|
status === "expired" ||
|
|
status === "executed"
|
|
? status
|
|
: "pending"
|
|
);
|
|
|
|
const formatRelativeExpiry = (expiresAt?: string, now = new Date()): { label: string; expired: boolean } => {
|
|
if (!expiresAt) return { label: "Geen vervaldatum", expired: false };
|
|
const timestamp = Date.parse(expiresAt);
|
|
if (!Number.isFinite(timestamp)) return { label: "Vervaldatum onbekend", expired: false };
|
|
const diffMs = timestamp - now.getTime();
|
|
const absMinutes = Math.max(1, Math.round(Math.abs(diffMs) / 60_000));
|
|
const hours = Math.floor(absMinutes / 60);
|
|
const minutes = absMinutes % 60;
|
|
const duration = hours > 0 ? `${hours}u ${minutes}m` : `${absMinutes}m`;
|
|
return diffMs < 0
|
|
? { label: `Verlopen ${duration} geleden`, expired: true }
|
|
: { label: `Verloopt over ${duration}`, expired: false };
|
|
};
|
|
|
|
export const buildApprovalFollowupView = (approval: ApprovalRequest, now = new Date()): ApprovalFollowupView => {
|
|
const status = normalizeStatus(approval.status);
|
|
const expiry = formatRelativeExpiry(approval.expiresAt, now);
|
|
const effectiveStatus: ApprovalStatus = status === "pending" && expiry.expired ? "expired" : status;
|
|
const auditReference = approval.auditId || null;
|
|
const jobReference = approval.jobId || null;
|
|
const decided = effectiveStatus !== "pending";
|
|
const canDecide = effectiveStatus === "pending";
|
|
|
|
const links: ApprovalFollowupLink[] = decided
|
|
? [
|
|
{ label: "Open Audit", href: auditReference ? `/audit?query=${encodeURIComponent(auditReference)}` : "/audit" },
|
|
{ label: "Jobs", href: jobReference ? `/jobs?query=${encodeURIComponent(jobReference)}` : "/jobs" },
|
|
]
|
|
: [
|
|
{ label: "Open Audit", href: "/audit" },
|
|
{ label: "Command Center", href: "/command-center" },
|
|
{ label: "Jobs", href: "/jobs" },
|
|
];
|
|
|
|
return {
|
|
status: effectiveStatus,
|
|
statusLabel: statusLabels[effectiveStatus],
|
|
expiryLabel: effectiveStatus === "expired" ? expiry.label : expiry.label,
|
|
auditReference,
|
|
jobReference,
|
|
executionCopy: decided
|
|
? effectiveStatus === "expired"
|
|
? "Approval is verlopen. Er wordt geen actie uitgevoerd; maak indien nodig een nieuwe expliciete aanvraag."
|
|
: "Beslissing opgeslagen. Controleer Audit en Jobs voor echte vervolgstatussen wanneer die beschikbaar zijn."
|
|
: "Nog niet uitgevoerd. Goedkeuren of afwijzen schrijft audit en kan daarna een job opleveren als de actie een executor heeft.",
|
|
canDecide,
|
|
links,
|
|
};
|
|
};
|
|
|
|
export const filterApprovals = (approvals: ApprovalRequest[], query: string): ApprovalRequest[] => {
|
|
const normalizedQuery = query.trim().toLowerCase();
|
|
if (!normalizedQuery) return approvals;
|
|
|
|
return approvals.filter((approval) => [
|
|
approval.id,
|
|
approval.action,
|
|
approval.target,
|
|
approval.requester,
|
|
approval.context,
|
|
approval.reason || "",
|
|
approval.auditId || "",
|
|
approval.jobId || "",
|
|
].some((value) => value.toLowerCase().includes(normalizedQuery)));
|
|
};
|
|
|
|
export const buildApprovalEmptyState = (
|
|
options: { totalApprovals: number; visibleApprovals: number; query: string },
|
|
): ApprovalEmptyState => {
|
|
const query = options.query.trim();
|
|
|
|
if (options.totalApprovals === 0) {
|
|
return {
|
|
title: "Geen openstaande approvals",
|
|
message: "Er staan geen approvals klaar. Nieuwe approval-first acties verschijnen hier zodra Command Center, connectors of remediation flows er een aanvragen.",
|
|
actions: [{ label: "Command Center", href: "/command-center" }],
|
|
};
|
|
}
|
|
|
|
if (query && options.visibleApprovals === 0) {
|
|
return {
|
|
title: "Geen approval voor deze handoff",
|
|
message: `Er is geen approval gevonden voor "${query}". Controleer Audit of Jobs om te zien of de aanvraag al verwerkt, verlopen of als job doorgezet is.`,
|
|
actions: [
|
|
{ label: "Toon alle approvals", href: "/approvals" },
|
|
{ label: "Zoek in Audit", href: `/audit?query=${encodeURIComponent(query)}` },
|
|
{ label: "Zoek in Jobs", href: `/jobs?query=${encodeURIComponent(query)}` },
|
|
{ label: "Command Center", href: "/command-center" },
|
|
],
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: "Geen approvals zichtbaar",
|
|
message: "De huidige selectie bevat geen approvals. Toon alles of start een nieuwe approval-first actie via Command Center.",
|
|
actions: [
|
|
{ label: "Toon alle approvals", href: "/approvals" },
|
|
{ label: "Command Center", href: "/command-center" },
|
|
],
|
|
};
|
|
};
|