diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index c694bac..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__chrome-devtools__navigate_page", - "mcp__chrome-devtools__list_pages", - "mcp__chrome-devtools__new_page", - "mcp__chrome-devtools__take_snapshot", - "mcp__chrome-devtools__fill_form", - "mcp__chrome-devtools__click", - "mcp__chrome-devtools__wait_for", - "mcp__chrome-devtools__take_screenshot", - "mcp__chrome-devtools__emulate", - "mcp__chrome-devtools__fill", - "mcp__chrome-devtools__list_console_messages", - "mcp__chrome-devtools__get_console_message", - "mcp__chrome-devtools__list_network_requests", - "mcp__chrome-devtools__get_network_request" - ] - } -} diff --git a/CLAUDE.md b/CLAUDE.md index 986975d..c8b250e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,11 +3,13 @@ Next.js 15, TS, Tailwind CSS, shadcn/ui. ## Architecture (MVVM) + - Structure: Every page needs `.model.ts` (logic), `.view.tsx` (pure JSX), `.types.ts` (interfaces), `.schema.ts` (Zod), and `page.tsx` (ViewModel). - Responsibilities: NO JSX in models. NO state/hooks/logic in views. - Forms: Validate with Zod schemas and `react-hook-form`. ## Code Style + - Types: Explicit everywhere. No `any`. - Endpoints: Always check `docs/endpoints/` before making API calls. - Functions: 4-20 lines. Split if longer. @@ -26,12 +28,14 @@ Next.js 15, TS, Tailwind CSS, shadcn/ui. - Wrap third-party libs behind a thin interface owned by this project. ## Components & UI + - Base: `/components/ui` using Tailwind + lucide-react (strokeWidth 2 or 2.5). - Breadcrumbs: Use `useBreadcrumb` hook ONLY in child routes (e.g., `/products/[id]/edit`). - Composites: Use standard wrappers ``, ``, ``. - Feedback: Always handle loading (``), errors (``), and no-data (``), and format forms with ``. ## Responsive & Design System + - Layout: Mobile First -> Tablet (`md:`) -> Desktop (`max-w-7xl`). - Sidebar Fix: Any `fixed` full-width element MUST include `md:ml-[240px]` to clear the sidebar. - Theme: Dark-only brutalism. Background `#0A0A0A`, surfaces `#171717`, borders `neutral-800`. @@ -39,10 +43,12 @@ Next.js 15, TS, Tailwind CSS, shadcn/ui. - Geometry & Interaction: 4px border radius everywhere. No shadows. No `rounded-full`. No animations (instant hover changes). Bold titles. ## Data Fetching + - Libraries: `swr` for caching + `ky` for HTTP requests. - Encapsulation: All `useSWR` and `ky.get/.post` calls must be inside standard hooks in `.model.ts`. ## Tests + - Scope: Unit test `.model.ts` files ONLY. - Workflow: Always ask to create model tests immediately after finishing a page implementation. - Runner: Vitest (`pnpm test`). @@ -53,5 +59,8 @@ Next.js 15, TS, Tailwind CSS, shadcn/ui. self-validating, timely. ## Workflow & Commands -- Commands: `pnpm dev` (Dev), `pnpm test` (Test), `pnpm build` (Build). + +- `pnpm test` (Test), `pnpm build` (Build). - Automation defaults: email `pass@pass.com` / pw `test123`. +- Só faça a build (`pnpm build`) quando for solicitado. Não quebre essa regra +- Só rode o `pnpm dev` quando for solicitado. Não quebre essa regra diff --git a/app/(pages)/batches/[id]/batches-detail.model.test.ts b/app/(pages)/batches/[id]/batches-detail.model.test.ts index 5b9f292..258d9d1 100644 --- a/app/(pages)/batches/[id]/batches-detail.model.test.ts +++ b/app/(pages)/batches/[id]/batches-detail.model.test.ts @@ -193,6 +193,10 @@ describe("formatCentsTotal", () => { expect(formatCentsTotal(1000, 5)).toMatch(/R\$\s?50,00/); }); + it("does not prefix the formatted total value", () => { + expect(formatCentsTotal(2000, 2)).not.toContain("Total:"); + }); + it("returns dash when price is null", () => { expect(formatCentsTotal(null, 10)).toBe("-"); }); diff --git a/app/(pages)/batches/[id]/batches-detail.model.ts b/app/(pages)/batches/[id]/batches-detail.model.ts index e381825..f27211c 100644 --- a/app/(pages)/batches/[id]/batches-detail.model.ts +++ b/app/(pages)/batches/[id]/batches-detail.model.ts @@ -106,7 +106,7 @@ export const formatCentsTotal = ( quantity: number, ): string => { if (unitCents === null || unitCents === undefined) return "-"; - return `Total: ${formatCentsToBRL(unitCents * quantity)}`; + return formatCentsToBRL(unitCents * quantity); }; export const computeMarginLabel = ( diff --git a/app/(pages)/batches/[id]/edit/batches-edit.model.ts b/app/(pages)/batches/[id]/edit/batches-edit.model.ts index 5c0ec9c..b27bf41 100644 --- a/app/(pages)/batches/[id]/edit/batches-edit.model.ts +++ b/app/(pages)/batches/[id]/edit/batches-edit.model.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useMemo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; @@ -9,6 +9,18 @@ import type { BatchEditResponse } from "./batches-edit.types"; import type { Batch } from "../../batches.types"; import { useBreadcrumb } from "@/components/breadcrumb"; +const DEFAULT_BATCH_EDIT_FORM_VALUES: BatchEditFormData = { + productId: "", + warehouseId: "", + quantity: 1, + batchCode: "", + manufacturedDate: "", + expirationDate: "", + costPrice: undefined, + sellingPrice: undefined, + notes: "", +}; + export const mapBatchToFormValues = (batch: Batch): BatchEditFormData => ({ productId: batch.productId, warehouseId: batch.warehouseId, @@ -23,21 +35,6 @@ export const mapBatchToFormValues = (batch: Batch): BatchEditFormData => ({ export const useBatchEditModel = (batchId: string) => { const router = useRouter(); - const form = useForm({ - resolver: zodResolver(batchEditSchema), - defaultValues: { - productId: "", - warehouseId: "", - quantity: 1, - batchCode: "", - manufacturedDate: "", - expirationDate: "", - costPrice: undefined, - sellingPrice: undefined, - notes: "", - }, - }); - const { data, isLoading } = useSWR( batchId ? `batches/${batchId}` : null, async (url: string) => { @@ -47,6 +44,15 @@ export const useBatchEditModel = (batchId: string) => { ); const batch = data?.data || null; + const formValues = useMemo( + () => (batch ? mapBatchToFormValues(batch) : DEFAULT_BATCH_EDIT_FORM_VALUES), + [batch], + ); + const form = useForm({ + resolver: zodResolver(batchEditSchema), + defaultValues: DEFAULT_BATCH_EDIT_FORM_VALUES, + values: formValues, + }); useBreadcrumb({ title: batch?.batchNumber || batch?.batchCode || "Carregando...", @@ -55,12 +61,6 @@ export const useBatchEditModel = (batchId: string) => { subsection: "Edição", }); - useEffect(() => { - if (batch) { - form.reset(mapBatchToFormValues(batch)); - } - }, [batch, form]); - const onSubmit = async (values: BatchEditFormData) => { try { const { api } = await import("@/lib/api"); diff --git a/app/(pages)/batches/[id]/edit/batches-edit.view.tsx b/app/(pages)/batches/[id]/edit/batches-edit.view.tsx index cdc43b1..334c6d7 100644 --- a/app/(pages)/batches/[id]/edit/batches-edit.view.tsx +++ b/app/(pages)/batches/[id]/edit/batches-edit.view.tsx @@ -12,6 +12,7 @@ import { Plus, Save, } from "lucide-react"; +import type { ReactNode } from "react"; import type { UseFormReturn } from "react-hook-form"; import type { BatchEditFormData } from "./batches-edit.schema"; import type { Batch } from "../../batches.types"; @@ -40,12 +41,20 @@ interface BatchEditViewProps { batch?: Batch | null; } -export const BatchEditView = ({ - form, - onSubmit, - isLoading, - batch, -}: BatchEditViewProps) => { +interface BatchEditViewState extends BatchEditViewProps { + batchCode: string | undefined; + handleQuantityDecrement: () => void; + handleQuantityIncrement: () => void; + isProfitable: boolean; + isSubmitting: boolean; + margin: number; + productLabel: string; + profit: number; + warehouseLabel: string; +} + +export const BatchEditView = (props: BatchEditViewProps) => { + const { form, onSubmit, isLoading, batch } = props; const { isSubmitting } = form.formState; const costPrice = form.watch("costPrice") || 0; @@ -76,6 +85,18 @@ export const BatchEditView = ({ const handleQuantityDecrement = () => { updateQuantity((form.getValues("quantity") || 1) - 1); }; + const viewState: BatchEditViewState = { + ...props, + batchCode, + handleQuantityDecrement, + handleQuantityIncrement, + isProfitable, + isSubmitting, + margin, + productLabel, + profit, + warehouseLabel, + }; if (isLoading) { return ( @@ -92,341 +113,406 @@ export const BatchEditView = ({
- - -
- - - Identificação e Origem - -
-
- -
- ( - - - Produto - - - - - - )} - /> - - ( - - - Warehouse - - - - - - )} - /> -
- - {batchCode && ( -
-
- Código gerado -
-
- {batchCode} -
-
- )} -
-
- - - -
- - - Financeiro e Estoque - -
-
- -
- { - const { onChange, value, ...rest } = field; - return ( - - - Quantidade - -
- - - - - -
- -
- ); - }} - /> - - { - const { onChange, value, ...rest } = field; - return ( - - - Custo Unitário - - - - - - - ); - }} - /> - - { - const { onChange, value, ...rest } = field; - return ( - - - Preço de Venda - - - - - - - ); - }} - /> -
- -
- - Lucro Estimado - -
- - {(profit / 100).toLocaleString("pt-BR", { - style: "currency", - currency: "BRL", - })} - -
- {margin.toFixed(1)}% -
-
-
-
-
+ +
- - -
- - - Vigência - -
-
- - ( - - - Data de Fabricação - - - - - - - )} - /> - - ( - - - Data de Validade - - - - - - - Mantenha vazio quando o produto não tiver validade. - - - - )} - /> - -
- - - -
- - - Observações - -
-
- - ( - - -