Files
vfxreview/components/dashboard/AwaitingDelivery.tsx
T
twotalesanimation f30acef101
Deploy / deploy (push) Successful in 5m17s
feat(deliveries): delivery requests and upload/share evidence
A built package used to become DELIVERED on one click with nothing behind
it, and nothing recorded what a client had asked for.

- Marking delivered is now two recorded steps: Record upload (the link on
  the client's portal, contents confirmed) then Share link (who it was sent
  to). The second is refused until the first is on record.
- Delivery requests log a client's ask by episode, pasted shot codes or a
  Shot Status selection. A line closes only when its shot goes out with
  evidence after the request was logged; a request can otherwise only be
  cancelled with a reason.
- Delivered Outside Pipeline takes an upload link and recipient, required
  for shots on an open request.
- The dashboard gains an Awaiting delivery section listing open requests
  and built packages the client does not have yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:21:24 +02:00

89 lines
3.7 KiB
TypeScript

import Link from "next/link";
import { format, formatDistanceToNowStrict } from "date-fns";
import { cn } from "@/lib/utils";
import type { AwaitingDelivery as AwaitingDeliveryRow } from "@/lib/dashboard/ops";
import type { DeliveryRequestView } from "@/lib/render-pipeline/delivery-requests";
import { DELIVERABLE_LABEL, type Deliverable } from "@/lib/render-pipeline/delivery-request-state";
const STAGE_LABEL: Record<AwaitingDeliveryRow["stage"], string> = {
AWAITING_UPLOAD: "Not uploaded",
AWAITING_SHARE: "Uploaded — link not shared",
};
const SEVERITY_STYLES: Record<AwaitingDeliveryRow["severity"], string> = {
normal: "bg-zinc-500/15 text-zinc-300 border-zinc-500/30",
warning: "bg-amber-500/15 text-amber-400 border-amber-500/30",
critical: "bg-red-500/15 text-red-400 border-red-500/30",
};
const ROW_CLASS =
"flex items-center gap-3 border-b border-zinc-800/60 px-4 py-2.5 last:border-b-0 hover:bg-zinc-900/40";
const PILL_CLASS = "shrink-0 rounded-full border px-2 py-0.5 text-xs whitespace-nowrap";
/**
* Everything a client is owed: what they asked for that has not fully gone
* out, and built packages they do not have yet. A row only leaves this list
* when the upload location and who the link went to are on record (or a
* request is cancelled with a reason) — never because time passed or someone
* remembers doing it.
*/
export function AwaitingDelivery({
requests,
packages,
}: {
requests: DeliveryRequestView[];
packages: AwaitingDeliveryRow[];
}) {
if (requests.length === 0 && packages.length === 0) {
return (
<div className="rounded-lg border border-zinc-800 bg-card px-4 py-4 text-sm text-zinc-500">
Nothing is owed to a client.
</div>
);
}
return (
<div className="rounded-lg border border-amber-500/30 bg-card overflow-hidden">
{requests.map((r) => (
<Link key={r.id} href="/pipeline/deliveries" className={ROW_CLASS}>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] text-white">
<span className="font-medium">Request #{r.requestNumber}</span>{" "}
<span className="text-zinc-300">
{DELIVERABLE_LABEL[r.deliverable as Deliverable] ?? r.deliverable}
{r.episode ? ` for ${r.episode}` : ""}
</span>{" "}
<span className="text-zinc-500">{r.projectCode}</span>
</div>
<div className="truncate text-xs text-zinc-500">
asked by {r.requestedBy} {formatDistanceToNowStrict(r.createdAt)} ago
{r.dueDate ? ` · due ${format(r.dueDate, "d MMM")}` : ""}
</div>
</div>
<span className={cn(PILL_CLASS, SEVERITY_STYLES[r.severity])}>
{r.deliveredCount} of {r.lineCount} delivered
</span>
</Link>
))}
{packages.map((p) => (
<Link key={p.id} href="/pipeline/deliveries" className={ROW_CLASS}>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] text-white">
<span className="font-mono font-medium">#{p.packageNumber}</span>{" "}
<span className="text-zinc-300">{p.name}</span>{" "}
<span className="text-zinc-500">
{p.projectCode}
{p.episode ? ` · ${p.episode}` : ""}
</span>
</div>
<div className="truncate text-xs text-zinc-500">
{p.itemCount} shot{p.itemCount === 1 ? "" : "s"} · built {formatDistanceToNowStrict(p.builtAt)} ago
</div>
</div>
<span className={cn(PILL_CLASS, SEVERITY_STYLES[p.severity])}>{STAGE_LABEL[p.stage]}</span>
</Link>
))}
</div>
);
}