mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
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 1–5 · 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;
|