feat(collection-pool): add MarkReadyDialog component

This commit is contained in:
SJCoetz 2026-07-10 00:08:18 +02:00
parent 00fb9a38f0
commit 58d7c90e77

View file

@ -0,0 +1,128 @@
"use client";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
/**
* Modal that asks the operator for a free-text note before marking a
* collection-pool row ready. The note is required (non-empty after
* trim) and is saved into `OrderItem.notes` by the server route. We
* also surface a small "Notes" chip on the row whenever the row has
* any notes, so the operator can see at a glance whether they've
* already written something.
*
* The submit button is disabled until the textarea is non-empty.
* Whitespace-only content is treated as empty for the disabled
* state and the server validation.
*/
export interface MarkReadyDialogProps {
open: boolean;
/** PO number + item description, formatted for the title. */
poLabel: string;
/** Item description, formatted for the title. */
itemLabel: string;
/** Any pre-existing note (not currently used; reserved for edit flows). */
initialNote?: string;
busy?: boolean;
serverError?: string | null;
onOpenChange: (open: boolean) => void;
onSubmit: (note: string) => void;
}
export function MarkReadyDialog({
open,
poLabel,
itemLabel,
initialNote = "",
busy = false,
serverError = null,
onOpenChange,
onSubmit,
}: MarkReadyDialogProps) {
const [note, setNote] = useState(initialNote);
const trimmed = note.trim();
const canSubmit = trimmed.length > 0 && !busy;
return (
<Dialog
open={open}
onOpenChange={(o) => {
if (!busy) {
if (!o) setNote("");
onOpenChange(o);
}
}}
>
<DialogContent className="sm:max-w-md max-h-[90vh] grid-rows-[auto_1fr_auto] gap-0 p-0 overflow-hidden">
<DialogHeader className="px-6 pt-6 pb-4 border-b">
<DialogTitle>
Mark ready: {poLabel} · {itemLabel}
</DialogTitle>
<DialogDescription>
Add a note for the audit log. Required.
</DialogDescription>
</DialogHeader>
<div className="px-6 py-4 space-y-2 overflow-y-auto">
<Textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="e.g. Spoke to David — confirmed pickup tomorrow 09:00"
rows={4}
maxLength={500}
autoFocus
disabled={busy}
className="min-h-24"
onKeyDown={(e) => {
// Cmd/Ctrl+Enter submits, like a comment box.
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && canSubmit) {
e.preventDefault();
onSubmit(trimmed);
}
}}
/>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{note.length}/500</span>
<span className="hidden sm:inline">⌘/Ctrl + Enter to save</span>
</div>
{serverError && (
<p
role="alert"
className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
>
{serverError}
</p>
)}
</div>
<DialogFooter className="px-6 py-4 border-t">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={busy}
>
Cancel
</Button>
<Button
type="button"
onClick={() => onSubmit(trimmed)}
disabled={!canSubmit}
>
{busy && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
Save &amp; Mark Ready
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}