From 2d4d0df6d181b7bbd0449d625d8f0e6720619ed2 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 30 Jul 2026 22:15:51 +0200 Subject: [PATCH] feat(stock-quick-draft): modal skeleton + Stock Order Reference header Co-authored-by: Orca --- .../stock-orders/quick-draft-modal.test.tsx | 55 ++++++++++++++++ .../stock-orders/quick-draft-modal.tsx | 66 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/components/stock-orders/quick-draft-modal.test.tsx create mode 100644 src/components/stock-orders/quick-draft-modal.tsx diff --git a/src/components/stock-orders/quick-draft-modal.test.tsx b/src/components/stock-orders/quick-draft-modal.test.tsx new file mode 100644 index 0000000..d0af3a1 --- /dev/null +++ b/src/components/stock-orders/quick-draft-modal.test.tsx @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { QuickDraftModal } from "./quick-draft-modal"; + +// The modal uses ModalShell.Dialog which depends on @base-ui/react. +// The component test environment stubs it via a passthrough wrapper. +vi.mock("@/components/ui/modal-shell", () => ({ + ModalShell: { + Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => + open ?
{children}
: null, + Header: ({ children }: { children: React.ReactNode }) =>
{children}
, + Body: ({ children }: { children: React.ReactNode }) =>
{children}
, + Footer: ({ children }: { children: React.ReactNode }) =>
{children}
, + }, +})); + +beforeEach(() => { + vi.restoreAllMocks(); +}); + +describe("QuickDraftModal — header", () => { + it("renders nothing when closed", () => { + render( + {}} + />, + ); + expect(screen.queryByTestId("modal")).toBeNull(); + }); + + it("renders the Stock Order Reference input with a default month label", () => { + render( + {}} />, + ); + const input = screen.getByTestId("stock-order-reference"); + expect(input).toBeInTheDocument(); + // Value defaults to "Stock order entry — " (e.g. "Stock order entry — Jul 2026"). + expect((input as HTMLInputElement).value).toMatch(/^Stock order entry — /); + }); + + it("lets the buyer rename the reference", async () => { + const user = userEvent.setup(); + render( + {}} />, + ); + const input = screen.getByTestId( + "stock-order-reference", + ) as HTMLInputElement; + await user.clear(input); + await user.type(input, "Jul 2026 run"); + expect(input.value).toBe("Jul 2026 run"); + }); +}); \ No newline at end of file diff --git a/src/components/stock-orders/quick-draft-modal.tsx b/src/components/stock-orders/quick-draft-modal.tsx new file mode 100644 index 0000000..4f97583 --- /dev/null +++ b/src/components/stock-orders/quick-draft-modal.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useMemo } from "react"; +import { Input } from "@/components/ui/input"; +import { ModalShell } from "@/components/ui/modal-shell"; + +/** + * Quick-Draft Modal — item-first stock-order entry on /stock-orders. + * + * See docs/superpowers/specs/2026-07-30-stock-quick-draft-design.md + * for the full design. This file is the modal in its entirety; the + * three regions (search, cards, footer) live in sub-components + * further down so the test surface stays manageable. + * + * Data flow: + * - search & history hit /api/v1/stock-quick-draft/{search,supplier-history} + * - "Add & next" hits /api/v1/stock-quick-draft/add-line + * - cards region deletes via /api/v1/stock-quick-draft/line/[lineId] + * + * ModalShell in this codebase uses a prop-based pattern (eyebrow / + * title / description / children / footer) rather than exposing + * Header / Body / Footer sub-components, so the input lives as a + * child at the top of the dialog. The eyebrow surfaces the + * "Stock Order Reference" label above the input in production. + */ + +export interface QuickDraftModalProps { + open: boolean; + onOpenChange: (next: boolean) => void; +} + +function formatMonthYear(date: Date): string { + return date.toLocaleDateString("en-US", { month: "short", year: "numeric" }); +} + +export function QuickDraftModal({ open, onOpenChange }: QuickDraftModalProps) { + const defaultReference = useMemo( + () => `Stock order entry — ${formatMonthYear(new Date())}`, + // intentionally only computed once per mount + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + return ( +