feat(collection-pool): require note on Mark Ready
This commit is contained in:
parent
1698e3d05b
commit
32194e8bb3
1 changed files with 82 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue