Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ruleEngine/PriorityRuleApprovalsSection.tsx
2026-07-15 13:29:01 +00:00

126 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
import type { UseMutationResult } from "@tanstack/react-query";
import { CheckCircle2, XCircle } from "lucide-react";
import type { PriorityRuleChangeRequest } from "@/services/ruleEngine/ruleEngine.service";
const ACTION_COLOR: Record<PriorityRuleChangeRequest["action"], string> = {
CREATE: "teal",
UPDATE: "blue",
DELETE: "red",
};
const fmtDateTime = (iso: string) =>
new Date(iso).toLocaleString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
/** "WAGON 15 · 30 pts" from a change payload / target rule. */
const ruleSummary = (
r: PriorityRuleChangeRequest,
): string => {
const source = (r.payload ?? r.priorityConfig ?? {}) as Record<string, unknown>;
const base = (r.priorityConfig ?? {}) as Record<string, unknown>;
const pick = (key: string) => source[key] ?? base[key];
const type = pick("type");
const currency = pick("currency");
const min = pick("minWagonCount");
const max = pick("maxWagonCount");
const pts = pick("scorePoints");
const parts = [
type ? String(type) : null,
currency ? String(currency) : null,
min != null && max != null ? `${min}${max} wagons` : null,
pts != null ? `${pts} pts` : null,
].filter(Boolean);
return parts.join(" · ") || "—";
};
type Decide = UseMutationResult<
PriorityRuleChangeRequest,
unknown,
{ id: string; decisionNote?: string }
>;
interface PriorityRuleApprovalsSectionProps {
requests: PriorityRuleChangeRequest[];
canDecide: boolean;
approve: Decide;
reject: Decide;
}
/**
* Pending priority-rule change requests awaiting approval. Rendered above the
* rules table on the priority-configs page; every rule change lands here first
* and only an approval applies it.
*/
const PriorityRuleApprovalsSection = ({
requests,
canDecide,
approve,
reject,
}: PriorityRuleApprovalsSectionProps) => {
if (requests.length === 0) return null;
return (
<Card withBorder radius="md" padding="md" mb="md">
<Group gap={8} mb="sm">
<Text fw={700}>Pending approvals</Text>
<Badge variant="light" color="yellow">
{requests.length}
</Badge>
</Group>
<Stack gap={8}>
{requests.map((r) => (
<Card key={r.id} withBorder radius="md" padding="sm">
<Group justify="space-between" wrap="nowrap" align="flex-start">
<Stack gap={4} style={{ minWidth: 0 }}>
<Group gap={8} wrap="nowrap">
<Badge variant="light" color={ACTION_COLOR[r.action]} radius="sm">
{r.action.toLowerCase()}
</Badge>
<Text size="sm" fw={600} truncate>
{ruleSummary(r)}
</Text>
</Group>
<Text size="xs" c="dimmed">
Submitted {fmtDateTime(r.createdAt)}
</Text>
</Stack>
{canDecide ? (
<Group gap={8} wrap="nowrap">
<Button
size="compact-sm"
variant="subtle"
color="red"
leftSection={<XCircle size={14} />}
loading={reject.isPending}
onClick={() => reject.mutate({ id: r.id })}
>
Reject
</Button>
<Button
size="compact-sm"
color="edr-green"
leftSection={<CheckCircle2 size={14} />}
loading={approve.isPending}
onClick={() => approve.mutate({ id: r.id })}
>
Approve & apply
</Button>
</Group>
) : null}
</Group>
</Card>
))}
</Stack>
</Card>
);
};
export default PriorityRuleApprovalsSection;