f30acef101
Deploy / deploy (push) Successful in 5m17s
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>
89 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|