1
0
Fork 0
polaris-task-force/src/app/(frontend)/logistics/supply-boxes/[id]/page.tsx
Z8MB1E 5fb619a5cf fix(operations): move page data helpers to surface modules
Satisfies the Next 16 rule that page files only export page fields:
getSupplyBoxDetail/getSupplyBoxList and getReservationList/getReservationDetail
move to reservations/surface.ts and pages import from it.
2026-09-21 20:47:57 -04:00

281 lines
9.7 KiB
TypeScript

import config from "@payload-config";
import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import Link from "next/link";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
import {
LedgerRow,
NoticeState,
PageShell,
ProvenanceChip,
STATUS_TONE_CLASSES,
type ReadoutCell,
type StatusTone,
} from "@/components/frontend/operations";
import { SupplyBoxActions } from "@/components/frontend/logistics/SupplyBoxActions";
import type { ArmaCommand, SupplyBox } from "@/payload-types";
import type { SurfaceUser } from "@/lib/operations/surface";
type PayloadType = Awaited<ReturnType<typeof import("payload").getPayload>>;
export const metadata = {
title: "Supply Box Detail: Polaris Task Force",
};
function relationId(value: unknown): number | null {
if (typeof value === "number") return value;
if (value !== null && typeof value === "object" && "id" in (value as { id: unknown })) {
const id = (value as { id: unknown }).id;
return typeof id === "number" ? id : null;
}
return null;
}
function locationFromJson(value: unknown): { x: number; y: number; z: number } | null {
if (typeof value !== "object" || value === null) return null;
const { x, y, z } = value as { x?: unknown; y?: unknown; z?: unknown };
if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") return null;
return { x, y, z };
}
function locationLabel(value: unknown): string {
const loc = locationFromJson(value);
return loc === null ? "n/a" : `${loc.x}, ${loc.y}, ${loc.z}`;
}
function commandTone(status: string): StatusTone {
if (status === "succeeded") return "success";
if (status === "failed") return "danger";
if (status === "delivered") return "info";
return "neutral";
}
export interface SupplyBoxDetailData {
box: SupplyBox | null;
capability: boolean;
commands: ArmaCommand[];
}
/**
* Full detail for a single supply box plus its recent queued commands, fetched
* through collection access control (default access + the explicit session
* user, never overrideAccess). Returns null for an unknown id so the page can
* render a not-found notice.
*/
async function getSupplyBoxDetail(
payload: PayloadType,
user: SurfaceUser,
id: string,
): Promise<SupplyBoxDetailData> {
const qualified = await hasLogisticsQualification(payload, user);
if (!qualified) return { box: null, capability: false, commands: [] };
let box: SupplyBox | null = null;
try {
box = await payload.findByID({
collection: "supply-boxes",
id: Number(id),
depth: 2,
user: user ?? undefined,
});
} catch {
box = null;
}
if (box === null) return { box: null, capability: true, commands: [] };
const serverId = relationId(box.server);
const commands =
serverId === null
? []
: (
await payload.find({
collection: "arma-commands",
where: { server: { equals: serverId } },
sort: "-createdAt",
limit: 50,
depth: 0,
user: user ?? undefined,
})
).docs.filter((cmd) => Array.isArray(cmd.payload) && cmd.payload[0] === box.boxId);
return { box, capability: true, commands };
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section data-slot="supply-box-section" className="flex flex-col gap-2">
<h2 className="font-mono text-[9px] uppercase tracking-widest text-white/60">{title}</h2>
{children}
</section>
);
}
function kvRow(label: string, value: React.ReactNode) {
return (
<div className="flex items-start justify-between gap-4 border border-white/10 px-2 py-1.5">
<span className="shrink-0 font-mono text-[9px] uppercase tracking-widest text-white/50">
{label}
</span>
<span className="min-w-0 truncate text-right font-mono text-xs text-white/80">{value}</span>
</div>
);
}
export default async function SupplyBoxDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const payload = await getPayload({ config });
const { user: authUser } = await payload.auth({
headers: await nextHeaders(),
canSetHeaders: false,
});
const user = isPayloadUser(authUser) ? authUser : null;
const surfaceUser: SurfaceUser = user ? { id: user.id } : null;
const { box, capability, commands } = await getSupplyBoxDetail(payload, surfaceUser, id);
if (capability === false) {
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6">
<PageShell title="Supply Box" meta="LOGISTICS // CRATE SYNC">
<NoticeState
variant="error"
message="You do not have the Logistics qualification to manage supply boxes."
hint="Request the Logistics qualification from unit command."
/>
</PageShell>
</div>
);
}
if (box === null) {
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6">
<PageShell title="Supply Box" meta="LOGISTICS // CRATE SYNC">
<NoticeState
variant="error"
message="Supply box not found."
hint="The supply box id is unknown or you lack permission to read it."
action={
<Link
href="/logistics/supply-boxes"
className="mt-2 border border-white/15 px-3 py-1 font-mono text-[10px] uppercase tracking-widest text-white/80 hover:border-white/30 hover:text-white"
>
Back to supply boxes
</Link>
}
/>
</PageShell>
</div>
);
}
const contents = Array.isArray(box.contents) ? box.contents : [];
const itemCount = contents.reduce<number>((sum, line) => {
if (typeof line !== "object" || line === null) return sum;
const count = (line as { count?: unknown }).count;
return sum + (typeof count === "number" ? count : 0);
}, 0);
const sourceEventId = relationId(box.sourceEvent);
const readoutCells: ReadoutCell[] = [
{ label: "Class", value: box.boxClass ?? "n/a" },
{ label: "Status", value: box.status },
{ label: "Items", value: String(itemCount) },
{ label: "Last Sync", value: box.lastSyncedAt },
];
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6">
<PageShell title={box.boxId} meta="SUPPLY BOX // DETAIL">
<Link
href="/logistics/supply-boxes"
className="font-mono text-[10px] uppercase tracking-widest text-white/60 underline-offset-4 hover:underline"
>
Back to supply boxes
</Link>
<LedgerRow cells={readoutCells} />
{sourceEventId !== null ? (
<ProvenanceChip
label={`RAW EVENT #${sourceEventId}`}
href={`/admin/collections/arma-sync-events/${sourceEventId}`}
/>
) : null}
<Section title="Contents">
{contents.length === 0 ? (
<NoticeState variant="empty" message="This crate is empty." />
) : (
<ul className="flex flex-col gap-1">
{contents.map((line, index) => (
<li
key={index}
className="flex items-center justify-between gap-2 border border-white/10 px-2 py-1.5"
>
<span className="min-w-0 truncate font-mono text-xs text-white/80">
{(line as { class?: unknown }).class as string}
</span>
<span className="font-mono text-xs tabular-nums text-white/70">
{(line as { count?: unknown }).count as number}
</span>
</li>
))}
</ul>
)}
</Section>
<Section title="Location">
<div className="flex flex-col gap-1">
{kvRow("Position", locationLabel(box.location))}
{kvRow("Session", box.sessionId)}
</div>
</Section>
<Section title="Command Queue">
<SupplyBoxActions
boxId={box.boxId}
boxClass={box.boxClass ?? null}
location={locationFromJson(box.location)}
contents={contents.map((line) => ({
class: (line as { class?: unknown }).class as string,
count: (line as { count?: unknown }).count as number,
}))}
/>
</Section>
<Section title="Recent Commands">
{commands.length === 0 ? (
<NoticeState variant="empty" message="No commands have been queued for this crate." />
) : (
<ul className="flex flex-col gap-1">
{commands.map((cmd) => (
<li
key={cmd.id}
className="flex items-center justify-between gap-2 border border-white/10 px-2 py-1.5"
>
<span className="min-w-0 truncate font-mono text-xs text-white/80">
{cmd.type}
</span>
<span className="flex shrink-0 items-center gap-2">
<span
className={`border px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-widest ${STATUS_TONE_CLASSES[commandTone(cmd.status)]}`}
>
{cmd.status}
</span>
{cmd.error ? (
<span className="max-w-40 truncate font-mono text-[10px] text-rose-300">
{cmd.error}
</span>
) : null}
</span>
</li>
))}
</ul>
)}
</Section>
</PageShell>
</div>
);
}