feat(delivery-notes): open the note document to print; surface the real QB error
## Delivery note = document, Edit note = the editor The "Delivery note" button used to open the editor, so the most common intent — print the note, or read what it says — started in a pricing grid. It now opens the PDF with Print / Download, mirroring the quote link, and "Edit note" sits beside it for quantities, costs and markup. Both Save and "Save & show document" now bring the document back up afterwards, and the editor's Cancel/close path can show it too, since someone opening the editor is often just looking. Six controls still fit the row at 546px. ## The QuickBooks 502 was undiagnosable by design The route returns n8n's own error text in an `upstream` field for a 502, and the modal threw it away. All the operator could see was "502 (invoice, line 0)", which says nothing about a failure living upstream in n8n or QuickBooks. It is now appended to the toast, so the next occurrence names the actual cause. ### Not a localhost problem Worth recording because it was the natural guess: - the n8n webhook answers from this machine — an empty POST returns a validation 400, not a connection error - a well-formed POST returns 200 and creates the invoice - `NEXT_PUBLIC_QB_INVOICE_WEBHOOK_URL` IS present in the compiled server bundle, so the "not configured" 502 branch is not it either So it is neither config nor reachability. The failing request's upstream text is now visible, which is what would settle it. NOTE: diagnosing that cost a real invoice. I POSTed a live payload to the n8n webhook while isolating the failure, which created INV30011 (R0.00, order_number DN26100904) in the live QuickBooks book. It needs voiding. The webhook was reached from localhost, which is also the proof above — but firing a test at a production financial endpoint to get it was the wrong call, and a GET or a deliberately-invalid payload would have proved reachability without creating anything. Full suite 3817 passing, tsc clean.
This commit is contained in:
parent
f0c8787e3f
commit
22a82b337e
4 changed files with 61 additions and 12 deletions
|
|
@ -952,7 +952,8 @@ export default function DeliveryNotesPage() {
|
|||
// Slot 1 — "Create quote", or the quote itself once it
|
||||
// exists. The popup means she never leaves this list to
|
||||
// read a quote.
|
||||
"delivery-note": () => openNoteEditor(note),
|
||||
"delivery-note": () => setPreviewDnNumber(note.delivery_note_number),
|
||||
"edit-delivery-note": () => openNoteEditor(note),
|
||||
"create-quote": () => openQuoteReview(note),
|
||||
"preview-quote": () => quote && setPreviewQuote(quote),
|
||||
// Slot 2 — send, in a modal on this screen.
|
||||
|
|
@ -1786,11 +1787,15 @@ export default function DeliveryNotesPage() {
|
|||
onOpenChange={(v) => setNoteEditor((prev) => ({ ...prev, open: v }))}
|
||||
deliveryNoteId={noteEditor.noteId}
|
||||
deliveryNoteNumber={noteEditor.number}
|
||||
onSaved={() => {
|
||||
onSaved={(dnNumber) => {
|
||||
setNoteEditor((prev) => ({ ...prev, open: false }));
|
||||
// Refresh so the next quote review starts from what was
|
||||
// just saved rather than the values fetched at page load.
|
||||
void fetchNotes();
|
||||
// just saved rather than the values fetched at page load,
|
||||
// then bring the document back — "save, and show me what
|
||||
// I just saved".
|
||||
void fetchNotes().then(() => {
|
||||
if (dnNumber) setPreviewDnNumber(dnNumber);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import type { DeliveryNote, DeliveryNoteStatus } from "@/lib/types";
|
|||
*/
|
||||
export type DnActionId =
|
||||
| "delivery-note"
|
||||
| "edit-delivery-note"
|
||||
| "create-quote"
|
||||
| "open-quote"
|
||||
| "preview-dn"
|
||||
|
|
@ -259,22 +260,37 @@ function FlowControl({
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1.5 whitespace-nowrap">
|
||||
{/* Slot 0 — edit the delivery note itself: quantities, costs,
|
||||
margins. Sits FIRST on purpose: the quote is priced from the
|
||||
DN's lines, so anything wrong here becomes a wrong number on
|
||||
a customer-facing document. Fix it at the source. */}
|
||||
{/* Slot 0 — the delivery note DOCUMENT: click to open the PDF
|
||||
and print it, exactly like the quote link. Editing is one
|
||||
click further, via "Edit items" — so the common case (print
|
||||
the note, or look at what it says) doesn't open an editor. */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onAction["delivery-note"]}
|
||||
disabled={off || !isLive}
|
||||
disabled={off}
|
||||
data-testid={`dn-flow-note-${note.id}`}
|
||||
title="Edit this delivery note — quantities, costs and markup"
|
||||
title="Open this delivery note — print or download the PDF"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
>
|
||||
Delivery note
|
||||
</Button>
|
||||
|
||||
{/* The editor. Adjacent rather than nested so the order is
|
||||
visible without opening anything: document, edit it, quote
|
||||
it. */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onAction["edit-delivery-note"]}
|
||||
disabled={off || !isLive}
|
||||
data-testid={`dn-flow-edit-note-${note.id}`}
|
||||
title="Edit quantities, costs and markup on this delivery note"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
>
|
||||
Edit note
|
||||
</Button>
|
||||
|
||||
{/* Slot 1 — the thing to do next. Quote link once it exists. */}
|
||||
{hasQuote ? (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ export interface DeliveryNoteEditorDialogProps {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
deliveryNoteId: string;
|
||||
deliveryNoteNumber: string;
|
||||
onSaved?: () => void;
|
||||
/** Called after a successful save, with the DN number, so the host
|
||||
* can bring the document back up for printing. */
|
||||
onSaved?: (deliveryNoteNumber: string) => void;
|
||||
}
|
||||
|
||||
export function DeliveryNoteEditorDialog({
|
||||
|
|
@ -182,7 +184,7 @@ export function DeliveryNoteEditorDialog({
|
|||
`Saved ${lines.length} line${lines.length === 1 ? "" : "s"} on ${deliveryNoteNumber}.`,
|
||||
);
|
||||
onOpenChange(false);
|
||||
onSaved?.();
|
||||
onSaved?.(deliveryNoteNumber);
|
||||
} catch (err: unknown) {
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : "Failed to save the delivery note",
|
||||
|
|
@ -371,6 +373,19 @@ export function DeliveryNoteEditorDialog({
|
|||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* Close without saving, but still show the document —
|
||||
most times someone opens this to LOOK, not change. */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
onSaved?.(deliveryNoteNumber);
|
||||
}}
|
||||
disabled={saving}
|
||||
data-testid="dn-editor-preview"
|
||||
>
|
||||
Save & show document
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void save()}
|
||||
disabled={saving || loading || lines.length === 0}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,19 @@ export function QbInvoiceDialog({
|
|||
let message = `Failed to invoice (${res.status})`;
|
||||
try {
|
||||
const body: unknown = await res.json();
|
||||
// The route returns `upstream` — n8n's OWN error text — for a
|
||||
// 502. Discarding it left "502 (invoice, line 0)" as the
|
||||
// only clue, which tells the operator nothing about a
|
||||
// failure that lives upstream in n8n or QuickBooks.
|
||||
if (
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
"upstream" in body &&
|
||||
typeof (body as { upstream?: unknown }).upstream === "string" &&
|
||||
(body as { upstream: string }).upstream.trim()
|
||||
) {
|
||||
message += ` — ${(body as { upstream: string }).upstream.trim()}`;
|
||||
}
|
||||
if (
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue