feat(collection-pool): require note on Mark Ready

This commit is contained in:
SJCoetz 2026-07-10 00:15:24 +02:00
parent 1698e3d05b
commit 32194e8bb3

View file

@ -15,6 +15,7 @@ import {
AssignDriverDialog,
type AssignDriverSubmitPayload,
} from "@/components/ui/assign-driver-dialog";
import { MarkReadyDialog } from "@/components/ui/mark-ready-dialog";
import { parseCoordinates } from "@/lib/geo";
import { getPocketBase } from "@/lib/pocketbase";
import { getMappableSuppliers } from "@/lib/pb-helpers";
@ -111,6 +112,8 @@ export default function CollectionPoolPage() {
const supFetchPromise = useRef<Promise<void> | null>(null);
const [driversById, setDriversById] = useState<Record<string, string>>({});
const [assignDialog, setAssignDialog] = useState<CollectionPoolAssignDialogState | null>(null);
const [readyDialogRow, setReadyDialogRow] = useState<CollectionPoolVisibleRow | null>(null);
const [readyDialogError, setReadyDialogError] = useState<string | null>(null);
const closeAssignDialog = useCallback(() => setAssignDialog(null), []);
@ -284,28 +287,71 @@ export default function CollectionPoolPage() {
);
const handleToggleReady = useCallback(
async (itemId: string, ready: boolean) => {
setBusyId(itemId);
async (row: CollectionPoolVisibleRow, ready: boolean) => {
// Unmarking is a single PATCH with no note. The modal only opens
// for the "mark ready" direction.
if (!ready) {
setBusyId(row.id);
try {
const res = await fetch(`/api/v1/collection-map/items/${row.id}/assign`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ready_for_collection: false }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `HTTP ${res.status}`);
}
toast.success("Marked awaiting supplier confirmation.");
await load();
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
} finally {
setBusyId(null);
}
return;
}
// Marking ready opens the modal. The actual PATCH is fired from
// `submitMarkReady` once the operator has typed a note.
setReadyDialogError(null);
setReadyDialogRow(row);
},
[load],
);
const submitMarkReady = useCallback(
async (note: string) => {
const row = readyDialogRow;
if (!row) return;
setBusyId(row.id);
setReadyDialogError(null);
try {
const res = await fetch(`/api/v1/collection-map/items/${itemId}/assign`, {
const res = await fetch(`/api/v1/collection-map/items/${row.id}/assign`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ready_for_collection: ready }),
body: JSON.stringify({
ready_for_collection: true,
ready_notes: note,
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `HTTP ${res.status}`);
const body = await res.json().catch(() => ({}));
const message = body?.error || `HTTP ${res.status}`;
// Keep the modal open so the operator can fix the error.
setReadyDialogError(message);
throw new Error(message);
}
toast.success(ready ? "Marked ready for collection." : "Marked awaiting supplier confirmation.");
toast.success("Marked ready — note saved.");
setReadyDialogRow(null);
await load();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
toast.error(message);
// Error already shown via readyDialogError / toast.
console.error("Mark ready failed:", err);
} finally {
setBusyId(null);
}
},
[load],
[load, readyDialogRow],
);
return (
@ -482,6 +528,22 @@ export default function CollectionPoolPage() {
) : undefined
}
/>
{readyDialogRow && (
<MarkReadyDialog
open
onOpenChange={(o) => {
if (!o) {
setReadyDialogRow(null);
setReadyDialogError(null);
}
}}
onSubmit={submitMarkReady}
poLabel={readyDialogRow.poNumber}
itemLabel={readyDialogRow.description}
busy={busyId === readyDialogRow.id}
serverError={readyDialogError}
/>
)}
</div>
);
}
@ -499,7 +561,7 @@ function PoolRow({
markerToken?: string;
todayYmd: string;
onOpenAssign: (row: CollectionPoolVisibleRow) => void;
onToggleReady: (id: string, ready: boolean) => void;
onToggleReady: (row: CollectionPoolVisibleRow, ready: boolean) => void;
}) {
const assignmentState = getAssignmentState(row, todayYmd);
const assignmentBadge = getAssignmentBadge(assignmentState);
@ -560,6 +622,14 @@ function PoolRow({
>
{readyLabel}
</span>
{row.notes && row.notes.trim().length > 0 && (
<span
className="inline-flex items-center rounded-md border bg-muted/30 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
title={row.notes.length > 120 ? `${row.notes.slice(0, 120)}…` : row.notes}
>
Notes
</span>
)}
</div>
<div className="flex flex-col items-stretch gap-2 lg:min-w-[11rem]">
@ -569,7 +639,7 @@ function PoolRow({
<Button
size="sm"
variant={row.ready_for_collection ? "outline" : "default"}
onClick={() => onToggleReady(row.id, !row.ready_for_collection)}
onClick={() => onToggleReady(row, !row.ready_for_collection)}
disabled={busy}
className={
row.ready_for_collection