From 4b4203c75290f16eb44865b9a039a436e28358f8 Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:32:51 -0300 Subject: [PATCH 01/19] fix(frontend): add page metadata wrappers --- app/(pages)/batches/[id]/edit/page.client.tsx | 20 +++++ app/(pages)/batches/[id]/edit/page.tsx | 25 ++---- app/(pages)/batches/[id]/page.client.tsx | 31 +++++++ app/(pages)/batches/[id]/page.tsx | 36 ++------- app/(pages)/batches/create/page.client.tsx | 9 +++ app/(pages)/batches/create/page.tsx | 14 ++-- app/(pages)/batches/page.client.tsx | 9 +++ app/(pages)/batches/page.tsx | 14 ++-- app/(pages)/brands/page.client.tsx | 10 +++ app/(pages)/brands/page.tsx | 15 ++-- app/(pages)/categories/page.client.tsx | 10 +++ app/(pages)/categories/page.tsx | 15 ++-- .../products/[id]/edit/page.client.tsx | 76 +++++++++++++++++ app/(pages)/products/[id]/edit/page.tsx | 81 ++----------------- app/(pages)/products/[id]/page.client.tsx | 24 ++++++ app/(pages)/products/[id]/page.tsx | 29 ++----- app/(pages)/products/create/page.client.tsx | 10 +++ app/(pages)/products/create/page.tsx | 15 ++-- app/(pages)/products/page.client.tsx | 10 +++ app/(pages)/products/page.tsx | 15 ++-- app/(pages)/sales/[id]/page.client.tsx | 11 +++ app/(pages)/sales/[id]/page.tsx | 16 ++-- .../infinitepay/callback/page.client.tsx | 9 +++ .../sales/infinitepay/callback/page.tsx | 19 +++-- .../sales/infinitepay/result/page.client.tsx | 9 +++ app/(pages)/sales/infinitepay/result/page.tsx | 19 +++-- app/(pages)/sales/page.client.tsx | 9 +++ app/(pages)/sales/page.tsx | 14 ++-- app/(pages)/sales/pdv/page.client.tsx | 26 ++++++ app/(pages)/sales/pdv/page.tsx | 36 +++------ .../stock-movements/[id]/page.client.tsx | 21 +++++ app/(pages)/stock-movements/[id]/page.tsx | 26 ++---- .../create/new-product/page.client.tsx | 24 ++++++ .../create/new-product/page.tsx | 24 ++---- .../stock-movements/create/page.client.tsx | 24 ++++++ app/(pages)/stock-movements/create/page.tsx | 24 ++---- app/(pages)/stock-movements/page.client.tsx | 10 +++ app/(pages)/stock-movements/page.tsx | 15 ++-- app/(pages)/system/company/page.client.tsx | 57 +++++++++++++ app/(pages)/system/company/page.tsx | 62 ++------------ app/(pages)/system/page.client.tsx | 9 +++ app/(pages)/system/page.tsx | 14 ++-- app/(pages)/system/roles/page.client.tsx | 22 +++++ app/(pages)/system/roles/page.tsx | 27 ++----- app/(pages)/system/users/page.client.tsx | 22 +++++ app/(pages)/system/users/page.tsx | 27 ++----- app/(pages)/transfers/[id]/page.client.tsx | 14 ++++ app/(pages)/transfers/[id]/page.tsx | 19 ++--- .../transfers/[id]/validate/page.client.tsx | 14 ++++ app/(pages)/transfers/[id]/validate/page.tsx | 19 ++--- app/(pages)/transfers/new/page.client.tsx | 9 +++ app/(pages)/transfers/new/page.tsx | 14 ++-- app/(pages)/transfers/page.client.tsx | 10 +++ app/(pages)/transfers/page.tsx | 15 ++-- app/change-password/page.client.tsx | 32 ++++++++ app/change-password/page.tsx | 37 ++------- app/login/page.client.tsx | 10 +++ app/login/page.tsx | 15 ++-- app/page.tsx | 6 ++ app/register/page.client.tsx | 10 +++ app/register/page.tsx | 15 ++-- app/warehouses/page.client.tsx | 23 ++++++ app/warehouses/page.tsx | 28 ++----- 63 files changed, 853 insertions(+), 481 deletions(-) create mode 100644 app/(pages)/batches/[id]/edit/page.client.tsx create mode 100644 app/(pages)/batches/[id]/page.client.tsx create mode 100644 app/(pages)/batches/create/page.client.tsx create mode 100644 app/(pages)/batches/page.client.tsx create mode 100644 app/(pages)/brands/page.client.tsx create mode 100644 app/(pages)/categories/page.client.tsx create mode 100644 app/(pages)/products/[id]/edit/page.client.tsx create mode 100644 app/(pages)/products/[id]/page.client.tsx create mode 100644 app/(pages)/products/create/page.client.tsx create mode 100644 app/(pages)/products/page.client.tsx create mode 100644 app/(pages)/sales/[id]/page.client.tsx create mode 100644 app/(pages)/sales/infinitepay/callback/page.client.tsx create mode 100644 app/(pages)/sales/infinitepay/result/page.client.tsx create mode 100644 app/(pages)/sales/page.client.tsx create mode 100644 app/(pages)/sales/pdv/page.client.tsx create mode 100644 app/(pages)/stock-movements/[id]/page.client.tsx create mode 100644 app/(pages)/stock-movements/create/new-product/page.client.tsx create mode 100644 app/(pages)/stock-movements/create/page.client.tsx create mode 100644 app/(pages)/stock-movements/page.client.tsx create mode 100644 app/(pages)/system/company/page.client.tsx create mode 100644 app/(pages)/system/page.client.tsx create mode 100644 app/(pages)/system/roles/page.client.tsx create mode 100644 app/(pages)/system/users/page.client.tsx create mode 100644 app/(pages)/transfers/[id]/page.client.tsx create mode 100644 app/(pages)/transfers/[id]/validate/page.client.tsx create mode 100644 app/(pages)/transfers/new/page.client.tsx create mode 100644 app/(pages)/transfers/page.client.tsx create mode 100644 app/change-password/page.client.tsx create mode 100644 app/login/page.client.tsx create mode 100644 app/register/page.client.tsx create mode 100644 app/warehouses/page.client.tsx diff --git a/app/(pages)/batches/[id]/edit/page.client.tsx b/app/(pages)/batches/[id]/edit/page.client.tsx new file mode 100644 index 0000000..9da83de --- /dev/null +++ b/app/(pages)/batches/[id]/edit/page.client.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useBatchEditModel } from "./batches-edit.model"; +import { BatchEditView } from "./batches-edit.view"; + +export function PageClient() { + const params = useParams(); + const batchId = params.id as string; + const { form, onSubmit, batch, isLoading } = useBatchEditModel(batchId); + + return ( + + ); +} diff --git a/app/(pages)/batches/[id]/edit/page.tsx b/app/(pages)/batches/[id]/edit/page.tsx index b981d99..4a23887 100644 --- a/app/(pages)/batches/[id]/edit/page.tsx +++ b/app/(pages)/batches/[id]/edit/page.tsx @@ -1,20 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { useBatchEditModel } from "./batches-edit.model"; -import { BatchEditView } from "./batches-edit.view"; +export const metadata: Metadata = { + title: "Editar lote | StockShift", + description: "Atualize os dados de um lote existente.", +}; -export default function BatchEditPage() { - const params = useParams(); - const batchId = params.id as string; - const { form, onSubmit, batch, isLoading } = useBatchEditModel(batchId); - - return ( - - ); +export default function Page() { + return ; } diff --git a/app/(pages)/batches/[id]/page.client.tsx b/app/(pages)/batches/[id]/page.client.tsx new file mode 100644 index 0000000..b04159b --- /dev/null +++ b/app/(pages)/batches/[id]/page.client.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useBatchDetailModel } from "./batches-detail.model"; +import { BatchesDetailView } from "./batches-detail.view"; + +export function PageClient() { + const params = useParams(); + const batchId = params.id as string; + const { + batch, + isLoading, + error, + onDelete, + isDeleting, + isDeleteOpen, + onDeleteOpenChange, + } = useBatchDetailModel(batchId); + + return ( + + ); +} diff --git a/app/(pages)/batches/[id]/page.tsx b/app/(pages)/batches/[id]/page.tsx index 550e44b..2abe2ad 100644 --- a/app/(pages)/batches/[id]/page.tsx +++ b/app/(pages)/batches/[id]/page.tsx @@ -1,31 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { useBatchDetailModel } from "./batches-detail.model"; -import { BatchesDetailView } from "./batches-detail.view"; +export const metadata: Metadata = { + title: "Detalhe do lote | StockShift", + description: "Visualize dados completos de um lote.", +}; -export default function BatchDetailPage() { - const params = useParams(); - const batchId = params.id as string; - const { - batch, - isLoading, - error, - onDelete, - isDeleting, - isDeleteOpen, - onDeleteOpenChange, - } = useBatchDetailModel(batchId); - - return ( - - ); +export default function Page() { + return ; } diff --git a/app/(pages)/batches/create/page.client.tsx b/app/(pages)/batches/create/page.client.tsx new file mode 100644 index 0000000..627ac76 --- /dev/null +++ b/app/(pages)/batches/create/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useBatchCreateModel } from "./batches-create.model"; +import { BatchCreateView } from "./batches-create.view"; + +export function PageClient() { + const model = useBatchCreateModel(); + return ; +} diff --git a/app/(pages)/batches/create/page.tsx b/app/(pages)/batches/create/page.tsx index 9ade183..551aa64 100644 --- a/app/(pages)/batches/create/page.tsx +++ b/app/(pages)/batches/create/page.tsx @@ -1,9 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useBatchCreateModel } from "./batches-create.model"; -import { BatchCreateView } from "./batches-create.view"; +export const metadata: Metadata = { + title: "Novo lote | StockShift", + description: "Cadastre um novo lote de produto.", +}; -export default function BatchCreatePage() { - const model = useBatchCreateModel(); - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/batches/page.client.tsx b/app/(pages)/batches/page.client.tsx new file mode 100644 index 0000000..1454133 --- /dev/null +++ b/app/(pages)/batches/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useBatchesModel } from "./batches.model"; +import { BatchesView } from "./batches.view"; + +export function PageClient() { + const model = useBatchesModel(); + return ; +} diff --git a/app/(pages)/batches/page.tsx b/app/(pages)/batches/page.tsx index 1a2ff8b..7e4c37f 100644 --- a/app/(pages)/batches/page.tsx +++ b/app/(pages)/batches/page.tsx @@ -1,9 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useBatchesModel } from "./batches.model"; -import { BatchesView } from "./batches.view"; +export const metadata: Metadata = { + title: "Lotes | StockShift", + description: "Consulte e filtre lotes por produto, validade e estoque.", +}; -export default function BatchesPage() { - const model = useBatchesModel(); - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/brands/page.client.tsx b/app/(pages)/brands/page.client.tsx new file mode 100644 index 0000000..fcd4275 --- /dev/null +++ b/app/(pages)/brands/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useBrandsModel } from "./brands.model"; +import { BrandsView } from "./brands.view"; + +export function PageClient() { + const model = useBrandsModel(); + + return ; +} diff --git a/app/(pages)/brands/page.tsx b/app/(pages)/brands/page.tsx index ef81081..c1b6066 100644 --- a/app/(pages)/brands/page.tsx +++ b/app/(pages)/brands/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useBrandsModel } from "./brands.model"; -import { BrandsView } from "./brands.view"; +export const metadata: Metadata = { + title: "Marcas | StockShift", + description: "Gerencie marcas de produtos.", +}; -export default function BrandsPage() { - const model = useBrandsModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/categories/page.client.tsx b/app/(pages)/categories/page.client.tsx new file mode 100644 index 0000000..d522e44 --- /dev/null +++ b/app/(pages)/categories/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useCategoriesModel } from "./categories.model"; +import { CategoriesView } from "./categories.view"; + +export function PageClient() { + const model = useCategoriesModel(); + + return ; +} diff --git a/app/(pages)/categories/page.tsx b/app/(pages)/categories/page.tsx index 56e8765..6606931 100644 --- a/app/(pages)/categories/page.tsx +++ b/app/(pages)/categories/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useCategoriesModel } from "./categories.model"; -import { CategoriesView } from "./categories.view"; +export const metadata: Metadata = { + title: "Categorias | StockShift", + description: "Gerencie categorias de produtos.", +}; -export default function CategoriesPage() { - const model = useCategoriesModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/products/[id]/edit/page.client.tsx b/app/(pages)/products/[id]/edit/page.client.tsx new file mode 100644 index 0000000..fba8607 --- /dev/null +++ b/app/(pages)/products/[id]/edit/page.client.tsx @@ -0,0 +1,76 @@ +// app/products/[id]/edit/page.tsx +"use client"; + +import { useParams } from "next/navigation"; +import { useProductEditModel } from "./products-edit.model"; +import { ProductForm } from "../../components/product-form.view"; +import { Loader2, AlertCircle } from "lucide-react"; +import Link from "next/link"; + +export function PageClient() { + const params = useParams(); + const productId = params.id as string; + + const { isLoadingProduct, product, ...modelProps } = + useProductEditModel(productId); + + if (isLoadingProduct) { + return ( +
+
+
+
+ + Voltar + +
+
+
+
+
+
+ +

+ Carregando produto... +

+
+
+
+
+ ); + } + + if (!product) { + return ( +
+
+
+
+ + Voltar + +
+
+
+
+
+
+ +

+ Produto não encontrado +

+
+
+
+
+ ); + } + + return ; +} diff --git a/app/(pages)/products/[id]/edit/page.tsx b/app/(pages)/products/[id]/edit/page.tsx index b64d4cd..efbd195 100644 --- a/app/(pages)/products/[id]/edit/page.tsx +++ b/app/(pages)/products/[id]/edit/page.tsx @@ -1,76 +1,11 @@ -// app/products/[id]/edit/page.tsx -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { useProductEditModel } from "./products-edit.model"; -import { ProductForm } from "../../components/product-form.view"; -import { Loader2, AlertCircle } from "lucide-react"; -import Link from "next/link"; +export const metadata: Metadata = { + title: "Editar produto | StockShift", + description: "Atualize os dados cadastrais de um produto.", +}; -export default function ProductEditPage() { - const params = useParams(); - const productId = params.id as string; - - const { isLoadingProduct, product, ...modelProps } = - useProductEditModel(productId); - - if (isLoadingProduct) { - return ( -
-
-
-
- - Voltar - -
-
-
-
-
-
- -

- Carregando produto... -

-
-
-
-
- ); - } - - if (!product) { - return ( -
-
-
-
- - Voltar - -
-
-
-
-
-
- -

- Produto não encontrado -

-
-
-
-
- ); - } - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/products/[id]/page.client.tsx b/app/(pages)/products/[id]/page.client.tsx new file mode 100644 index 0000000..b5d9b7f --- /dev/null +++ b/app/(pages)/products/[id]/page.client.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useProductDetailModel } from "./products-detail.model"; +import { ProductDetailView } from "./products-detail.view"; + +export function PageClient() { + const params = useParams(); + const productId = params.id as string; + + const { product, batches, isLoading, isLoadingBatches, error, batchesError } = + useProductDetailModel(productId); + + return ( + + ); +} diff --git a/app/(pages)/products/[id]/page.tsx b/app/(pages)/products/[id]/page.tsx index b042fe9..be94cbb 100644 --- a/app/(pages)/products/[id]/page.tsx +++ b/app/(pages)/products/[id]/page.tsx @@ -1,24 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { useProductDetailModel } from "./products-detail.model"; -import { ProductDetailView } from "./products-detail.view"; +export const metadata: Metadata = { + title: "Detalhe do produto | StockShift", + description: "Visualize estoque, lotes e dados do produto.", +}; -export default function ProductDetailPage() { - const params = useParams(); - const productId = params.id as string; - - const { product, batches, isLoading, isLoadingBatches, error, batchesError } = - useProductDetailModel(productId); - - return ( - - ); +export default function Page() { + return ; } diff --git a/app/(pages)/products/create/page.client.tsx b/app/(pages)/products/create/page.client.tsx new file mode 100644 index 0000000..4a40d5e --- /dev/null +++ b/app/(pages)/products/create/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useProductCreateModel } from "./products-create.model"; +import { ProductForm } from "../components/product-form.view"; + +export function PageClient() { + const modelProps = useProductCreateModel(); + + return ; +} diff --git a/app/(pages)/products/create/page.tsx b/app/(pages)/products/create/page.tsx index b81a743..a8b39d3 100644 --- a/app/(pages)/products/create/page.tsx +++ b/app/(pages)/products/create/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useProductCreateModel } from "./products-create.model"; -import { ProductForm } from "../components/product-form.view"; +export const metadata: Metadata = { + title: "Novo produto | StockShift", + description: "Cadastre um novo produto no estoque.", +}; -export default function ProductCreatePage() { - const modelProps = useProductCreateModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/products/page.client.tsx b/app/(pages)/products/page.client.tsx new file mode 100644 index 0000000..df7ed94 --- /dev/null +++ b/app/(pages)/products/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useProductsModel } from "./products.model"; +import { ProductsView } from "./products.view"; + +export function PageClient() { + const model = useProductsModel(); + + return ; +} diff --git a/app/(pages)/products/page.tsx b/app/(pages)/products/page.tsx index 6b79555..364b219 100644 --- a/app/(pages)/products/page.tsx +++ b/app/(pages)/products/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useProductsModel } from "./products.model"; -import { ProductsView } from "./products.view"; +export const metadata: Metadata = { + title: "Produtos | StockShift", + description: "Gerencie produtos, estoque e cadastro comercial.", +}; -export default function ProductsPage() { - const model = useProductsModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/sales/[id]/page.client.tsx b/app/(pages)/sales/[id]/page.client.tsx new file mode 100644 index 0000000..78285d8 --- /dev/null +++ b/app/(pages)/sales/[id]/page.client.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { use } from "react"; +import { useSaleDetailModel } from "./sales-detail.model"; +import { SaleDetailView } from "./sales-detail.view"; + +export function PageClient({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const model = useSaleDetailModel(id); + return ; +} diff --git a/app/(pages)/sales/[id]/page.tsx b/app/(pages)/sales/[id]/page.tsx index 1576484..8580234 100644 --- a/app/(pages)/sales/[id]/page.tsx +++ b/app/(pages)/sales/[id]/page.tsx @@ -1,11 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { use } from "react"; -import { useSaleDetailModel } from "./sales-detail.model"; -import { SaleDetailView } from "./sales-detail.view"; +export const metadata: Metadata = { + title: "Detalhe da venda | StockShift", + description: "Visualize itens e pagamentos de uma venda.", +}; -export default function SaleDetailPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = use(params); - const model = useSaleDetailModel(id); - return ; +export default function Page(props: { params: Promise<{ id: string }> }) { + return ; } diff --git a/app/(pages)/sales/infinitepay/callback/page.client.tsx b/app/(pages)/sales/infinitepay/callback/page.client.tsx new file mode 100644 index 0000000..e1ad19c --- /dev/null +++ b/app/(pages)/sales/infinitepay/callback/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useInfinitePayCallbackModel } from "./infinitepay-callback.model"; +import { InfinitePayCallbackView } from "./infinitepay-callback.view"; + +export function PageClient() { + const model = useInfinitePayCallbackModel(); + return ; +} diff --git a/app/(pages)/sales/infinitepay/callback/page.tsx b/app/(pages)/sales/infinitepay/callback/page.tsx index d5461f4..3f82358 100644 --- a/app/(pages)/sales/infinitepay/callback/page.tsx +++ b/app/(pages)/sales/infinitepay/callback/page.tsx @@ -1,9 +1,16 @@ -"use client"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { PageClient } from "./page.client"; -import { useInfinitePayCallbackModel } from "./infinitepay-callback.model"; -import { InfinitePayCallbackView } from "./infinitepay-callback.view"; +export const metadata: Metadata = { + title: "Callback InfinitePay | StockShift", + description: "Processa o retorno de pagamento InfinitePay.", +}; -export default function InfinitePayCallbackPage() { - const model = useInfinitePayCallbackModel(); - return ; +export default function Page() { + return ( + + + + ); } diff --git a/app/(pages)/sales/infinitepay/result/page.client.tsx b/app/(pages)/sales/infinitepay/result/page.client.tsx new file mode 100644 index 0000000..23dd096 --- /dev/null +++ b/app/(pages)/sales/infinitepay/result/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useInfinitePayResultModel } from "./infinitepay-result.model"; +import { InfinitePayResultView } from "./infinitepay-result.view"; + +export function PageClient() { + const model = useInfinitePayResultModel(); + return ; +} diff --git a/app/(pages)/sales/infinitepay/result/page.tsx b/app/(pages)/sales/infinitepay/result/page.tsx index ee54396..db54b70 100644 --- a/app/(pages)/sales/infinitepay/result/page.tsx +++ b/app/(pages)/sales/infinitepay/result/page.tsx @@ -1,9 +1,16 @@ -"use client"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { PageClient } from "./page.client"; -import { useInfinitePayResultModel } from "./infinitepay-result.model"; -import { InfinitePayResultView } from "./infinitepay-result.view"; +export const metadata: Metadata = { + title: "Resultado InfinitePay | StockShift", + description: "Confira o resultado do pagamento InfinitePay.", +}; -export default function InfinitePayResultPage() { - const model = useInfinitePayResultModel(); - return ; +export default function Page() { + return ( + + + + ); } diff --git a/app/(pages)/sales/page.client.tsx b/app/(pages)/sales/page.client.tsx new file mode 100644 index 0000000..0d25076 --- /dev/null +++ b/app/(pages)/sales/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useSalesModel } from "./sales.model"; +import { SalesView } from "./sales.view"; + +export function PageClient() { + const model = useSalesModel(); + return ; +} diff --git a/app/(pages)/sales/page.tsx b/app/(pages)/sales/page.tsx index df562f0..d205c00 100644 --- a/app/(pages)/sales/page.tsx +++ b/app/(pages)/sales/page.tsx @@ -1,9 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useSalesModel } from "./sales.model"; -import { SalesView } from "./sales.view"; +export const metadata: Metadata = { + title: "Vendas | StockShift", + description: "Acompanhe vendas e indicadores comerciais.", +}; -export default function SalesPage() { - const model = useSalesModel(); - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/sales/pdv/page.client.tsx b/app/(pages)/sales/pdv/page.client.tsx new file mode 100644 index 0000000..69e4d6f --- /dev/null +++ b/app/(pages)/sales/pdv/page.client.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import { toast } from "sonner"; +import { usePdvModel } from "./pdv.model"; +import { PdvView } from "./pdv.view"; + +export function PageClient() { + const searchParams = useSearchParams(); + + useEffect(() => { + const infinitepay = searchParams.get("infinitepay"); + if (infinitepay === "success") { + toast.success("Pagamento aprovado! Venda registrada com sucesso."); + window.history.replaceState({}, "", "/sales/pdv"); + } else if (infinitepay === "error") { + const message = searchParams.get("message") || "Pagamento não concluído."; + toast.error("Pagamento falhou: " + decodeURIComponent(message)); + window.history.replaceState({}, "", "/sales/pdv"); + } + }, [searchParams]); + + const model = usePdvModel(); + return ; +} diff --git a/app/(pages)/sales/pdv/page.tsx b/app/(pages)/sales/pdv/page.tsx index 7b9845d..a58784c 100644 --- a/app/(pages)/sales/pdv/page.tsx +++ b/app/(pages)/sales/pdv/page.tsx @@ -1,26 +1,16 @@ -"use client"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { PageClient } from "./page.client"; -import { useSearchParams } from "next/navigation"; -import { useEffect } from "react"; -import { toast } from "sonner"; -import { usePdvModel } from "./pdv.model"; -import { PdvView } from "./pdv.view"; +export const metadata: Metadata = { + title: "PDV | StockShift", + description: "Registre vendas no ponto de venda.", +}; -export default function PdvPage() { - const searchParams = useSearchParams(); - - useEffect(() => { - const infinitepay = searchParams.get("infinitepay"); - if (infinitepay === "success") { - toast.success("Pagamento aprovado! Venda registrada com sucesso."); - window.history.replaceState({}, "", "/sales/pdv"); - } else if (infinitepay === "error") { - const message = searchParams.get("message") || "Pagamento não concluído."; - toast.error("Pagamento falhou: " + decodeURIComponent(message)); - window.history.replaceState({}, "", "/sales/pdv"); - } - }, [searchParams]); - - const model = usePdvModel(); - return ; +export default function Page() { + return ( + + + + ); } diff --git a/app/(pages)/stock-movements/[id]/page.client.tsx b/app/(pages)/stock-movements/[id]/page.client.tsx new file mode 100644 index 0000000..7392e3a --- /dev/null +++ b/app/(pages)/stock-movements/[id]/page.client.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useStockMovementDetailModel } from "./stock-movements-detail.model"; +import { StockMovementDetailView } from "./stock-movements-detail.view"; + +export function PageClient() { + const params = useParams(); + const movementId = params.id as string; + const { movement, batchPrices, isLoading, error } = + useStockMovementDetailModel(movementId); + + return ( + + ); +} diff --git a/app/(pages)/stock-movements/[id]/page.tsx b/app/(pages)/stock-movements/[id]/page.tsx index ee9d9e1..ccf53d4 100644 --- a/app/(pages)/stock-movements/[id]/page.tsx +++ b/app/(pages)/stock-movements/[id]/page.tsx @@ -1,21 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { useStockMovementDetailModel } from "./stock-movements-detail.model"; -import { StockMovementDetailView } from "./stock-movements-detail.view"; +export const metadata: Metadata = { + title: "Detalhe da movimentação | StockShift", + description: "Visualize detalhes de uma movimentação de estoque.", +}; -export default function StockMovementDetailPage() { - const params = useParams(); - const movementId = params.id as string; - const { movement, batchPrices, isLoading, error } = - useStockMovementDetailModel(movementId); - - return ( - - ); +export default function Page() { + return ; } diff --git a/app/(pages)/stock-movements/create/new-product/page.client.tsx b/app/(pages)/stock-movements/create/new-product/page.client.tsx new file mode 100644 index 0000000..fa32e3b --- /dev/null +++ b/app/(pages)/stock-movements/create/new-product/page.client.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Suspense } from "react"; +import { ProductForm } from "../../../products/components/product-form.view"; +import { useNewProductInlineModel } from "./new-product-inline.model"; +import { StockMovementReloadGuard } from "../stock-movement-reload-guard"; + +function NewProductInlineContent() { + const model = useNewProductInlineModel(); + return ( + <> + + + + ); +} + +export function PageClient() { + return ( + + + + ); +} diff --git a/app/(pages)/stock-movements/create/new-product/page.tsx b/app/(pages)/stock-movements/create/new-product/page.tsx index f6a9107..57c42ee 100644 --- a/app/(pages)/stock-movements/create/new-product/page.tsx +++ b/app/(pages)/stock-movements/create/new-product/page.tsx @@ -1,24 +1,16 @@ -"use client"; - +import type { Metadata } from "next"; import { Suspense } from "react"; -import { ProductForm } from "../../../products/components/product-form.view"; -import { useNewProductInlineModel } from "./new-product-inline.model"; -import { StockMovementReloadGuard } from "../stock-movement-reload-guard"; +import { PageClient } from "./page.client"; -function NewProductInlineContent() { - const model = useNewProductInlineModel(); - return ( - <> - - - - ); -} +export const metadata: Metadata = { + title: "Novo produto da movimentação | StockShift", + description: "Cadastre produto durante uma movimentação de estoque.", +}; -export default function NewProductInlinePage() { +export default function Page() { return ( - + ); } diff --git a/app/(pages)/stock-movements/create/page.client.tsx b/app/(pages)/stock-movements/create/page.client.tsx new file mode 100644 index 0000000..f973bab --- /dev/null +++ b/app/(pages)/stock-movements/create/page.client.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Suspense } from "react"; +import { CreateStockMovementView } from "./create-stock-movement.view"; +import { useCreateStockMovementModel } from "./create-stock-movement.model"; +import { StockMovementReloadGuard } from "./stock-movement-reload-guard"; + +function CreateStockMovementContent() { + const model = useCreateStockMovementModel(); + return ( + <> + + + + ); +} + +export function PageClient() { + return ( + + + + ); +} diff --git a/app/(pages)/stock-movements/create/page.tsx b/app/(pages)/stock-movements/create/page.tsx index 89785d6..1c2f1f6 100644 --- a/app/(pages)/stock-movements/create/page.tsx +++ b/app/(pages)/stock-movements/create/page.tsx @@ -1,24 +1,16 @@ -"use client"; - +import type { Metadata } from "next"; import { Suspense } from "react"; -import { CreateStockMovementView } from "./create-stock-movement.view"; -import { useCreateStockMovementModel } from "./create-stock-movement.model"; -import { StockMovementReloadGuard } from "./stock-movement-reload-guard"; +import { PageClient } from "./page.client"; -function CreateStockMovementContent() { - const model = useCreateStockMovementModel(); - return ( - <> - - - - ); -} +export const metadata: Metadata = { + title: "Nova movimentação | StockShift", + description: "Registre uma entrada ou saída manual de estoque.", +}; -export default function CreateStockMovementPage() { +export default function Page() { return ( - + ); } diff --git a/app/(pages)/stock-movements/page.client.tsx b/app/(pages)/stock-movements/page.client.tsx new file mode 100644 index 0000000..a028173 --- /dev/null +++ b/app/(pages)/stock-movements/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useStockMovementsModel } from "./stock-movements.model"; +import { StockMovementsView } from "./stock-movements.view"; + +export function PageClient() { + const model = useStockMovementsModel(); + + return ; +} diff --git a/app/(pages)/stock-movements/page.tsx b/app/(pages)/stock-movements/page.tsx index b6d3fc6..2db6e58 100644 --- a/app/(pages)/stock-movements/page.tsx +++ b/app/(pages)/stock-movements/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useStockMovementsModel } from "./stock-movements.model"; -import { StockMovementsView } from "./stock-movements.view"; +export const metadata: Metadata = { + title: "Movimentações | StockShift", + description: "Consulte entradas e saídas de estoque.", +}; -export default function StockMovementsPage() { - const model = useStockMovementsModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/system/company/page.client.tsx b/app/(pages)/system/company/page.client.tsx new file mode 100644 index 0000000..066dc45 --- /dev/null +++ b/app/(pages)/system/company/page.client.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useState } from "react"; +import { useCompanyModel } from "./company.model"; +import { CompanyView } from "./company.view"; +import type { UpdateCompanyData, UpdateInfinitePayData } from "./company.types"; + +export function PageClient() { + const [isUpdatingCompany, setIsUpdatingCompany] = useState(false); + const [isUpdatingInfinitePay, setIsUpdatingInfinitePay] = useState(false); + const [isEditingInfinitePay, setIsEditingInfinitePay] = useState(false); + + const { + companyConfig, + infinitePayConfig, + isLoadingCompany, + isLoadingInfinitePay, + error, + updateCompany, + updateInfinitePay, + } = useCompanyModel(); + + const handleUpdateCompany = async (data: UpdateCompanyData) => { + try { + setIsUpdatingCompany(true); + await updateCompany(data); + } finally { + setIsUpdatingCompany(false); + } + }; + + const handleUpdateInfinitePay = async (data: UpdateInfinitePayData) => { + try { + setIsUpdatingInfinitePay(true); + await updateInfinitePay(data); + setIsEditingInfinitePay(false); + } finally { + setIsUpdatingInfinitePay(false); + } + }; + + return ( + setIsEditingInfinitePay(true)} + /> + ); +} diff --git a/app/(pages)/system/company/page.tsx b/app/(pages)/system/company/page.tsx index 614951c..23f62b7 100644 --- a/app/(pages)/system/company/page.tsx +++ b/app/(pages)/system/company/page.tsx @@ -1,57 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useState } from "react"; -import { useCompanyModel } from "./company.model"; -import { CompanyView } from "./company.view"; -import type { UpdateCompanyData, UpdateInfinitePayData } from "./company.types"; +export const metadata: Metadata = { + title: "Empresa | StockShift", + description: "Configure dados da empresa e integrações de pagamento.", +}; -export default function CompanyConfigPage() { - const [isUpdatingCompany, setIsUpdatingCompany] = useState(false); - const [isUpdatingInfinitePay, setIsUpdatingInfinitePay] = useState(false); - const [isEditingInfinitePay, setIsEditingInfinitePay] = useState(false); - - const { - companyConfig, - infinitePayConfig, - isLoadingCompany, - isLoadingInfinitePay, - error, - updateCompany, - updateInfinitePay, - } = useCompanyModel(); - - const handleUpdateCompany = async (data: UpdateCompanyData) => { - try { - setIsUpdatingCompany(true); - await updateCompany(data); - } finally { - setIsUpdatingCompany(false); - } - }; - - const handleUpdateInfinitePay = async (data: UpdateInfinitePayData) => { - try { - setIsUpdatingInfinitePay(true); - await updateInfinitePay(data); - setIsEditingInfinitePay(false); - } finally { - setIsUpdatingInfinitePay(false); - } - }; - - return ( - setIsEditingInfinitePay(true)} - /> - ); +export default function Page() { + return ; } diff --git a/app/(pages)/system/page.client.tsx b/app/(pages)/system/page.client.tsx new file mode 100644 index 0000000..01bead7 --- /dev/null +++ b/app/(pages)/system/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useSystemModel } from "./system.model"; +import { SystemView } from "./system.view"; + +export function PageClient() { + const model = useSystemModel(); + return ; +} diff --git a/app/(pages)/system/page.tsx b/app/(pages)/system/page.tsx index c224cce..0f7100c 100644 --- a/app/(pages)/system/page.tsx +++ b/app/(pages)/system/page.tsx @@ -1,9 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useSystemModel } from "./system.model"; -import { SystemView } from "./system.view"; +export const metadata: Metadata = { + title: "Sistema | StockShift", + description: "Acesse configurações administrativas do sistema.", +}; -export default function SystemPage() { - const model = useSystemModel(); - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/system/roles/page.client.tsx b/app/(pages)/system/roles/page.client.tsx new file mode 100644 index 0000000..93d5bd4 --- /dev/null +++ b/app/(pages)/system/roles/page.client.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useEffect } from "react"; +import { useBreadcrumbContext } from "@/components/breadcrumb/breadcrumb-context"; +import { useRolesModel } from "./roles.model"; +import { RolesView } from "./roles.view"; + +export function PageClient() { + const { setBreadcrumb } = useBreadcrumbContext(); + const model = useRolesModel(); + + useEffect(() => { + setBreadcrumb({ + title: "Roles", + section: "Sistema", + subsection: "Permissões", + backUrl: "/system", + }); + }, [setBreadcrumb]); + + return ; +} diff --git a/app/(pages)/system/roles/page.tsx b/app/(pages)/system/roles/page.tsx index 9c355a1..d65cb41 100644 --- a/app/(pages)/system/roles/page.tsx +++ b/app/(pages)/system/roles/page.tsx @@ -1,22 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useEffect } from "react"; -import { useBreadcrumbContext } from "@/components/breadcrumb/breadcrumb-context"; -import { useRolesModel } from "./roles.model"; -import { RolesView } from "./roles.view"; +export const metadata: Metadata = { + title: "Perfis de acesso | StockShift", + description: "Gerencie perfis e permissões do sistema.", +}; -export default function RolesPage() { - const { setBreadcrumb } = useBreadcrumbContext(); - const model = useRolesModel(); - - useEffect(() => { - setBreadcrumb({ - title: "Roles", - section: "Sistema", - subsection: "Permissões", - backUrl: "/system", - }); - }, [setBreadcrumb]); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/system/users/page.client.tsx b/app/(pages)/system/users/page.client.tsx new file mode 100644 index 0000000..eeb9a74 --- /dev/null +++ b/app/(pages)/system/users/page.client.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useEffect } from "react"; +import { useBreadcrumbContext } from "@/components/breadcrumb/breadcrumb-context"; +import { useUsersModel } from "./users.model"; +import { UsersView } from "./users.view"; + +export function PageClient() { + const { setBreadcrumb } = useBreadcrumbContext(); + const model = useUsersModel(); + + useEffect(() => { + setBreadcrumb({ + title: "Usuários", + section: "Sistema", + subsection: "Gerenciamento", + backUrl: "/system", + }); + }, [setBreadcrumb]); + + return ; +} diff --git a/app/(pages)/system/users/page.tsx b/app/(pages)/system/users/page.tsx index 85c4d63..523ac71 100644 --- a/app/(pages)/system/users/page.tsx +++ b/app/(pages)/system/users/page.tsx @@ -1,22 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useEffect } from "react"; -import { useBreadcrumbContext } from "@/components/breadcrumb/breadcrumb-context"; -import { useUsersModel } from "./users.model"; -import { UsersView } from "./users.view"; +export const metadata: Metadata = { + title: "Usuários | StockShift", + description: "Gerencie usuários e permissões de acesso.", +}; -export default function UsersPage() { - const { setBreadcrumb } = useBreadcrumbContext(); - const model = useUsersModel(); - - useEffect(() => { - setBreadcrumb({ - title: "Usuários", - section: "Sistema", - subsection: "Gerenciamento", - backUrl: "/system", - }); - }, [setBreadcrumb]); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/transfers/[id]/page.client.tsx b/app/(pages)/transfers/[id]/page.client.tsx new file mode 100644 index 0000000..5a816fc --- /dev/null +++ b/app/(pages)/transfers/[id]/page.client.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { TransferDetailView } from "./transfer-detail.view"; +import { useTransferDetailModel } from "./transfer-detail.model"; + +export function PageClient() { + const params = useParams(); + const id = params?.id as string; + + const model = useTransferDetailModel(id); + + return ; +} diff --git a/app/(pages)/transfers/[id]/page.tsx b/app/(pages)/transfers/[id]/page.tsx index 4e6d71a..c082dd7 100644 --- a/app/(pages)/transfers/[id]/page.tsx +++ b/app/(pages)/transfers/[id]/page.tsx @@ -1,14 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { TransferDetailView } from "./transfer-detail.view"; -import { useTransferDetailModel } from "./transfer-detail.model"; +export const metadata: Metadata = { + title: "Detalhe da transferência | StockShift", + description: "Visualize dados e status de uma transferência.", +}; -export default function TransferDetailPage() { - const params = useParams(); - const id = params?.id as string; - - const model = useTransferDetailModel(id); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/transfers/[id]/validate/page.client.tsx b/app/(pages)/transfers/[id]/validate/page.client.tsx new file mode 100644 index 0000000..bf99daa --- /dev/null +++ b/app/(pages)/transfers/[id]/validate/page.client.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ValidateTransferView } from "./validate-transfer.view"; +import { useValidateTransferModel } from "./validate-transfer.model"; + +export function PageClient() { + const params = useParams(); + const id = params?.id as string; + + const model = useValidateTransferModel(id); + + return ; +} diff --git a/app/(pages)/transfers/[id]/validate/page.tsx b/app/(pages)/transfers/[id]/validate/page.tsx index 64353c5..d7c5969 100644 --- a/app/(pages)/transfers/[id]/validate/page.tsx +++ b/app/(pages)/transfers/[id]/validate/page.tsx @@ -1,14 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useParams } from "next/navigation"; -import { ValidateTransferView } from "./validate-transfer.view"; -import { useValidateTransferModel } from "./validate-transfer.model"; +export const metadata: Metadata = { + title: "Validar transferência | StockShift", + description: "Valide itens recebidos em uma transferência.", +}; -export default function ValidateTransferPage() { - const params = useParams(); - const id = params?.id as string; - - const model = useValidateTransferModel(id); - - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/transfers/new/page.client.tsx b/app/(pages)/transfers/new/page.client.tsx new file mode 100644 index 0000000..f4be7d4 --- /dev/null +++ b/app/(pages)/transfers/new/page.client.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { NewTransferView } from "./new-transfer.view"; +import { useNewTransferModel } from "./new-transfer.model"; + +export function PageClient() { + const model = useNewTransferModel(); + return ; +} diff --git a/app/(pages)/transfers/new/page.tsx b/app/(pages)/transfers/new/page.tsx index 1f450d8..0ea781b 100644 --- a/app/(pages)/transfers/new/page.tsx +++ b/app/(pages)/transfers/new/page.tsx @@ -1,9 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { NewTransferView } from "./new-transfer.view"; -import { useNewTransferModel } from "./new-transfer.model"; +export const metadata: Metadata = { + title: "Nova transferência | StockShift", + description: "Crie uma transferência entre depósitos.", +}; -export default function NewTransferPage() { - const model = useNewTransferModel(); - return ; +export default function Page() { + return ; } diff --git a/app/(pages)/transfers/page.client.tsx b/app/(pages)/transfers/page.client.tsx new file mode 100644 index 0000000..98edd9c --- /dev/null +++ b/app/(pages)/transfers/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useTransfersModel } from "./transfers.model"; +import { TransfersView } from "./transfers.view"; + +export function PageClient() { + const model = useTransfersModel(); + + return ; +} diff --git a/app/(pages)/transfers/page.tsx b/app/(pages)/transfers/page.tsx index d29fc73..eb62ca0 100644 --- a/app/(pages)/transfers/page.tsx +++ b/app/(pages)/transfers/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useTransfersModel } from "./transfers.model"; -import { TransfersView } from "./transfers.view"; +export const metadata: Metadata = { + title: "Transferências | StockShift", + description: "Consulte transferências entre depósitos.", +}; -export default function TransfersPage() { - const model = useTransfersModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/change-password/page.client.tsx b/app/change-password/page.client.tsx new file mode 100644 index 0000000..dd80eb7 --- /dev/null +++ b/app/change-password/page.client.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useChangePasswordModel } from "./change-password.model"; +import { ChangePasswordView } from "./change-password.view"; +import { useAuth } from "@/lib/contexts/auth-context"; + +export function PageClient() { + const router = useRouter(); + const { user, isLoading } = useAuth(); + const methods = useChangePasswordModel(); + + useEffect(() => { + if (isLoading) return; + + if (!user) { + router.push("/login"); + return; + } + + if (!user.mustChangePassword) { + router.push("/warehouses"); + } + }, [user, isLoading, router]); + + if (isLoading || !user || !user.mustChangePassword) { + return null; + } + + return ; +} diff --git a/app/change-password/page.tsx b/app/change-password/page.tsx index c980438..4b603c3 100644 --- a/app/change-password/page.tsx +++ b/app/change-password/page.tsx @@ -1,32 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useChangePasswordModel } from "./change-password.model"; -import { ChangePasswordView } from "./change-password.view"; -import { useAuth } from "@/lib/contexts/auth-context"; +export const metadata: Metadata = { + title: "Alterar senha | StockShift", + description: "Atualize a senha da sua conta.", +}; -export default function ChangePasswordPage() { - const router = useRouter(); - const { user, isLoading } = useAuth(); - const methods = useChangePasswordModel(); - - useEffect(() => { - if (isLoading) return; - - if (!user) { - router.push("/login"); - return; - } - - if (!user.mustChangePassword) { - router.push("/warehouses"); - } - }, [user, isLoading, router]); - - if (isLoading || !user || !user.mustChangePassword) { - return null; - } - - return ; +export default function Page() { + return ; } diff --git a/app/login/page.client.tsx b/app/login/page.client.tsx new file mode 100644 index 0000000..77e80c7 --- /dev/null +++ b/app/login/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useLoginModel } from "./login.model"; +import { LoginView } from "./login.view"; + +export function PageClient() { + const methods = useLoginModel(); + + return ; +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 02333d2..12ad901 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useLoginModel } from "./login.model"; -import { LoginView } from "./login.view"; +export const metadata: Metadata = { + title: "Login | StockShift", + description: "Entre no StockShift com suas credenciais.", +}; -export default function LoginPage() { - const methods = useLoginModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/page.tsx b/app/page.tsx index 9848e2f..4876ba1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,11 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "StockShift", + description: "Gestão de estoque, vendas e transferências.", +}; + export default function HomePage(): never { redirect("/dashboard"); } diff --git a/app/register/page.client.tsx b/app/register/page.client.tsx new file mode 100644 index 0000000..a0d1ca0 --- /dev/null +++ b/app/register/page.client.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useRegisterModel } from "./register.model"; +import { RegisterView } from "./register.view"; + +export function PageClient() { + const methods = useRegisterModel(); + + return ; +} diff --git a/app/register/page.tsx b/app/register/page.tsx index f9e4ac5..bc42d27 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,10 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useRegisterModel } from "./register.model"; -import { RegisterView } from "./register.view"; +export const metadata: Metadata = { + title: "Cadastro | StockShift", + description: "Crie uma conta para acessar o StockShift.", +}; -export default function RegisterPage() { - const methods = useRegisterModel(); - - return ; +export default function Page() { + return ; } diff --git a/app/warehouses/page.client.tsx b/app/warehouses/page.client.tsx new file mode 100644 index 0000000..252914d --- /dev/null +++ b/app/warehouses/page.client.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useWarehousesModel } from "./warehouses.model"; +import { WarehousesView } from "./warehouses.view"; +import { useAuth } from "@/lib/contexts/auth-context"; + +export function PageClient() { + const model = useWarehousesModel(); + const { user } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (user?.mustChangePassword) { + router.replace("/change-password"); + } + }, [user, router]); + + if (user?.mustChangePassword) return null; + + return ; +} diff --git a/app/warehouses/page.tsx b/app/warehouses/page.tsx index db92088..dd39542 100644 --- a/app/warehouses/page.tsx +++ b/app/warehouses/page.tsx @@ -1,23 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { PageClient } from "./page.client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useWarehousesModel } from "./warehouses.model"; -import { WarehousesView } from "./warehouses.view"; -import { useAuth } from "@/lib/contexts/auth-context"; +export const metadata: Metadata = { + title: "Depósitos | StockShift", + description: "Gerencie depósitos e locais de estoque.", +}; -export default function WarehousesPage() { - const model = useWarehousesModel(); - const { user } = useAuth(); - const router = useRouter(); - - useEffect(() => { - if (user?.mustChangePassword) { - router.replace("/change-password"); - } - }, [user, router]); - - if (user?.mustChangePassword) return null; - - return ; +export default function Page() { + return ; } From 1a0a8d1facb7833f660a447ed12920cfed024413 Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:34:11 -0300 Subject: [PATCH 02/19] fix(frontend): address react doctor correctness issues --- app/(pages)/batches/batches.view.tsx | 29 +- app/(pages)/brands/brands.view.tsx | 27 +- app/(pages)/dashboard/dashboard.view.tsx | 8 +- app/(pages)/products/products.view.tsx | 333 ++++++++------- app/(pages)/sales/sales.view.tsx | 114 ++--- .../stock-movements/stock-movements.view.tsx | 258 ++++++------ app/(pages)/system/roles/roles.view.tsx | 393 ++++++++++-------- app/(pages)/system/users/users.view.tsx | 388 +++++++++-------- .../[id]/validate/validate-transfer.view.tsx | 4 +- app/change-password/change-password.view.tsx | 2 +- app/login/login.view.tsx | 6 +- app/register/register.view.tsx | 2 +- .../product/scanner-drawer/scanner-drawer.tsx | 16 +- components/ui/field.tsx | 4 +- components/ui/slider.tsx | 4 +- 15 files changed, 849 insertions(+), 739 deletions(-) diff --git a/app/(pages)/batches/batches.view.tsx b/app/(pages)/batches/batches.view.tsx index 5dc8523..d6e508e 100644 --- a/app/(pages)/batches/batches.view.tsx +++ b/app/(pages)/batches/batches.view.tsx @@ -241,6 +241,21 @@ const FilterToken = ({ ); +const SortIcon = ({ + field, + sortConfig, +}: { + field: SortConfig["key"]; + sortConfig: SortConfig; +}) => { + if (sortConfig.key !== field) return
; + return sortConfig.direction === "asc" ? ( + + ) : ( + + ); +}; + export const BatchesView = ({ batches, groupedByProduct, @@ -269,14 +284,6 @@ export const BatchesView = ({ isGroupedByProduct, ); - const SortIcon = ({ field }: { field: SortConfig["key"] }) => { - if (sortConfig.key !== field) return
; - return sortConfig.direction === "asc" ? ( - - ) : ( - - ); - }; const statusToneMap: Record = { all: { activeBorder: "border-neutral-500/50", activeBg: "bg-neutral-500/10", activeText: "text-neutral-100" }, @@ -907,7 +914,7 @@ export const BatchesView = ({ onClick={() => onSortChange("product")} >
- Produto + Produto
@@ -921,7 +928,7 @@ export const BatchesView = ({ onClick={() => onSortChange("quantity")} >
- Qtd. + Qtd.
onSortChange("expiration")} >
- Validade + Validade
diff --git a/app/(pages)/brands/brands.view.tsx b/app/(pages)/brands/brands.view.tsx index 1a5ff76..ce48a9a 100644 --- a/app/(pages)/brands/brands.view.tsx +++ b/app/(pages)/brands/brands.view.tsx @@ -78,6 +78,21 @@ interface BrandsViewProps { isDeleting: boolean; } +const SortIcon = ({ + field, + sortConfig, +}: { + field: SortConfig["key"]; + sortConfig: SortConfig; +}) => { + if (sortConfig.key !== field) return
; + return sortConfig.direction === "asc" ? ( + + ) : ( + + ); +}; + export const BrandsView = ({ brands, isLoading, @@ -114,14 +129,6 @@ export const BrandsView = ({ } }, [logoUrl]); - const SortIcon = ({ field }: { field: SortConfig["key"] }) => { - if (sortConfig.key !== field) return
; - return sortConfig.direction === "asc" ? ( - - ) : ( - - ); - }; return (
@@ -298,7 +305,7 @@ export const BrandsView = ({ onClick={() => handleSort("name")} >
- Nome + Nome
handleSort("createdAt")} >
- Data de Registro + Data de Registro
diff --git a/app/(pages)/dashboard/dashboard.view.tsx b/app/(pages)/dashboard/dashboard.view.tsx index ab6acda..532b14e 100644 --- a/app/(pages)/dashboard/dashboard.view.tsx +++ b/app/(pages)/dashboard/dashboard.view.tsx @@ -232,8 +232,8 @@ function WarehouseChart({ } /> - {data.map((_, i) => ( - + {data.map((item, i) => ( + ))} @@ -287,9 +287,9 @@ function CategoryChart({ paddingAngle={2} strokeWidth={0} > - {data.map((_, i) => ( + {data.map((item, i) => ( ))} diff --git a/app/(pages)/products/products.view.tsx b/app/(pages)/products/products.view.tsx index fa7ca9a..55f7388 100644 --- a/app/(pages)/products/products.view.tsx +++ b/app/(pages)/products/products.view.tsx @@ -241,6 +241,172 @@ const activeToneMap: Record { + if (sortBy !== field) return
; + return sortOrder === "asc" ? ( + + ) : ( + + ); +}; + +const ProductActions = ({ + product, + onOpenDeleteDialog, +}: { + product: Product; + onOpenDeleteDialog: (product: Product) => void; +}) => ( + + + + + + + Ações do Produto + + + + + Detalhes + + + + + + Editar + + + + + onOpenDeleteDialog(product)} + className="cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400" + > + Excluir + + + + +); + +const InsightCards = ({ + totalElements, + lowStockCount, + outOfStockCount, + topCategory, +}: { + totalElements: number; + lowStockCount: number; + outOfStockCount: number; + topCategory: string; +}) => ( + <> + {/* Total Items */} +
+
+
+ +
+ + Total Geral + +
+
+ + {totalElements} + + + itens + +
+
+ + {/* Low Stock */} +
+
+
+ +
+ + Baixo Estoque + +
+
+ + {lowStockCount} + + + alertas + +
+
+ + {/* Out of Stock */} +
+
+
+ +
+ + Sem Estoque + +
+
+ + {outOfStockCount} + + + itens + +
+
+ + {/* Top Category */} +
+
+
+ +
+ + Top Categoria + +
+
+ + {topCategory || "—"} + +
+
+ +); + export const ProductsView = ({ filteredProducts, isLoading, @@ -305,147 +471,8 @@ export const ProductsView = ({ onSortChange(field, newOrder); }; - const SortIcon = ({ field }: { field: SortField }) => { - if (filters.sortBy !== field) return
; - return filters.sortOrder === "asc" ? ( - - ) : ( - - ); - }; - const ProductActions = ({ product }: { product: Product }) => ( - - - - - - - Ações do Produto - - - - - Detalhes - - - - - - Editar - - - - - onOpenDeleteDialog(product)} - className="cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400" - > - Excluir - - - - - ); - const InsightCards = () => ( - <> - {/* Total Items */} -
-
-
- -
- - Total Geral - -
-
- - {pagination.totalElements} - - - itens - -
-
- - {/* Low Stock */} -
-
-
- -
- - Baixo Estoque - -
-
- - {lowStockCount} - - - alertas - -
-
- - {/* Out of Stock */} -
-
-
- -
- - Sem Estoque - -
-
- - {outOfStockCount} - - - itens - -
-
- - {/* Top Category */} -
-
-
- -
- - Top Categoria - -
-
- - {topCategory || "—"} - -
-
- - ); // ── Mobile filter drawer ── const renderMobileFiltersPanel = () => ( @@ -643,7 +670,7 @@ export const ProductsView = ({ {/* Row 1: Insight Cards */}
- +
{/* Mobile Insight Cards */} @@ -651,7 +678,7 @@ export const ProductsView = ({ data-slot="mobile-product-kpis" className="grid grid-cols-2 gap-3 md:hidden" > - +
{/* Row 2: Search & Filters */} @@ -875,7 +902,7 @@ export const ProductsView = ({ onClick={() => handleSort("name")} >
- Nome + Nome
handleSort("sku")} >
- SKU + SKU
@@ -1034,7 +1061,7 @@ export const ProductsView = ({ > {product.totalQuantity} un - +
@@ -1093,8 +1120,8 @@ export const ProductsView = ({ onOpenChange={(open) => { if (!open) onCloseDeleteDialog(); }} - title="Confirmar remoção" - description={`Tem certeza que deseja remover o produto ${deleteProduct?.name} deste armazém? Esta ação removerá todos os lotes associados.`} + title="Confirmar exclusão" + description={`Tem certeza que deseja excluir o produto ${deleteProduct?.name}? Esta ação removerá o produto e todos os lotes associados.`} maxWidth="sm:max-w-[450px]" footer={ <> @@ -1117,7 +1144,7 @@ export const ProductsView = ({ Removendo... ) : ( - "Remover" + "Excluir" )} @@ -1138,8 +1165,8 @@ export const ProductsView = ({ Estoque Existente

- Ainda existe estoque deste produto neste armazém. A remoção irá - apagar todos os lotes. + Ainda existe estoque deste produto. A exclusão irá apagar todos + os lotes associados.

{deleteBatches.map((batch) => ( @@ -1163,7 +1190,7 @@ export const ProductsView = ({ if (!open) onCloseSecondConfirm(); }} title="Confirmação Final" - description={`Tem certeza que deseja remover? O produto ${deleteProduct?.name} será desvinculado deste armazém.`} + description={`Tem certeza que deseja excluir? O produto ${deleteProduct?.name} será removido do sistema.`} maxWidth="sm:max-w-[400px]" footer={ <> @@ -1185,7 +1212,7 @@ export const ProductsView = ({ Removendo... ) : ( - "Confirmar Remoção" + "Confirmar Exclusão" )} @@ -1193,8 +1220,8 @@ export const ProductsView = ({ >

- Esta é a última confirmação antes da remoção definitiva dos lotes - deste produto neste armazém. + Esta é a última confirmação antes de remover o produto e seus lotes + do sistema.

diff --git a/app/(pages)/sales/sales.view.tsx b/app/(pages)/sales/sales.view.tsx index 8801f0f..9cc2300 100644 --- a/app/(pages)/sales/sales.view.tsx +++ b/app/(pages)/sales/sales.view.tsx @@ -197,6 +197,64 @@ const preventDrawerDismissFromSelectPortal = (event: Event) => { } }; +const SaleActions = ({ sale }: { sale: SaleSummary }) => ( + + + + + + + Ações + + + + + Ver Detalhes + + + + +); + +const FilterToken = ({ + icon, + label, + badge, + onClick, +}: { + icon: ReactNode; + label: string; + badge?: number; + onClick: () => void; +}) => ( + +); + export const SalesView = ({ sales, isLoading, @@ -218,63 +276,7 @@ export const SalesView = ({ onMobileDateInputChange, onMobileFilterDraftChange, }: SalesViewProps) => { - const SaleActions = ({ sale }: { sale: SaleSummary }) => ( - - - - - - - Ações - - - - - Ver Detalhes - - - - - ); - const FilterToken = ({ - icon, - label, - badge, - onClick, - }: { - icon: ReactNode; - label: string; - badge?: number; - onClick: () => void; - }) => ( - - ); const renderMobileFiltersPanel = (draft: SaleFilterDraft) => { return ( diff --git a/app/(pages)/stock-movements/stock-movements.view.tsx b/app/(pages)/stock-movements/stock-movements.view.tsx index 730392c..7cfd4aa 100644 --- a/app/(pages)/stock-movements/stock-movements.view.tsx +++ b/app/(pages)/stock-movements/stock-movements.view.tsx @@ -154,6 +154,137 @@ const preventDrawerDismissFromSelectPortal = (event: Event) => { } }; +const MovementActions = ({ movement }: { movement: StockMovement }) => ( + + + + + + + Ações + + + + + Ver Detalhes + + + + +); + +const CreateMovementDropdown = ({ + label, + direction, + types, +}: { + label: string; + direction: "IN" | "OUT"; + types: readonly (keyof typeof MANUAL_MOVEMENT_TYPE_LABELS)[]; +}) => { + const isIn = direction === "IN"; + + return ( + + + + + + + Tipo de movimentação + + + {types.map((type) => ( + + + {isIn ? ( + + ) : ( + + )} + {MANUAL_MOVEMENT_TYPE_LABELS[type]} + + + ))} + + + ); +}; + +const CreateMovementActions = () => ( + + + + +); + +const FilterToken = ({ + icon, + label, + badge, + onClick, +}: { + icon: ReactNode; + label: string; + badge?: number; + onClick: () => void; +}) => ( + +); + export const StockMovementsView = ({ movements, isLoading, @@ -193,136 +324,9 @@ export const StockMovementsView = ({ }; }; - const MovementActions = ({ movement }: { movement: StockMovement }) => ( - - - - - - - Ações - - - - - Ver Detalhes - - - - - ); - - const CreateMovementDropdown = ({ - label, - direction, - types, - }: { - label: string; - direction: "IN" | "OUT"; - types: readonly (keyof typeof MANUAL_MOVEMENT_TYPE_LABELS)[]; - }) => { - const isIn = direction === "IN"; - return ( - - - - - - - Tipo de movimentação - - - {types.map((type) => ( - - - {isIn ? ( - - ) : ( - - )} - {MANUAL_MOVEMENT_TYPE_LABELS[type]} - - - ))} - - - ); - }; - const CreateMovementActions = () => ( - - - - - ); - const FilterToken = ({ - icon, - label, - badge, - onClick, - }: { - icon: ReactNode; - label: string; - badge?: number; - onClick: () => void; - }) => ( - - ); const renderMobileFiltersPanel = (draft: StockMovementFilterDraft) => { return ( diff --git a/app/(pages)/system/roles/roles.view.tsx b/app/(pages)/system/roles/roles.view.tsx index 7385a40..e9ab224 100644 --- a/app/(pages)/system/roles/roles.view.tsx +++ b/app/(pages)/system/roles/roles.view.tsx @@ -55,9 +55,218 @@ import { AlertTriangle, } from "lucide-react"; import { RolesViewProps, Role } from "./roles.types"; + +type RoleForm = RolesViewProps["createForm"] | RolesViewProps["editForm"]; +type RolePermission = RolesViewProps["permissions"][number]; +type GroupedRolePermissions = RolesViewProps["groupedPermissions"]; import { RolePermissionsModal } from "./roles-permissions-modal.view"; import { cn } from "@/lib/utils"; +const DesktopActions = ({ + role, + openPermissionsModal, + openEditModal, + openDeleteModal, +}: { + role: Role; + openPermissionsModal: (role: Role) => void; + openEditModal: (role: Role) => void; + openDeleteModal: (role: Role) => void; +}) => ( +
+ + + +
+); + +const MobileActions = ({ + role, + openPermissionsModal, + openEditModal, + openDeleteModal, +}: { + role: Role; + openPermissionsModal: (role: Role) => void; + openEditModal: (role: Role) => void; + openDeleteModal: (role: Role) => void; +}) => ( + + + + + + + Ações da Role + + + openPermissionsModal(role)} + className="cursor-pointer" + > + + Ver permissões + + openEditModal(role)} + disabled={role.isSystemRole} + className={cn( + "cursor-pointer", + role.isSystemRole && "cursor-not-allowed opacity-50" + )} + > + + Editar + + openDeleteModal(role)} + disabled={role.isSystemRole} + className={cn( + "cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400", + role.isSystemRole && "cursor-not-allowed opacity-50" + )} + > + + Deletar + + + +); + +const PermissionSelector = ({ + form, + isLoadingPermissions, + permissions, + groupedPermissions, +}: { + form: RoleForm; + isLoadingPermissions: boolean; + permissions: RolePermission[]; + groupedPermissions: GroupedRolePermissions; +}) => ( + ( + + + Permissões + + {isLoadingPermissions ? ( +
+ + Carregando permissões... +
+ ) : permissions.length === 0 ? ( +
+ Nenhuma permissão disponível +
+ ) : ( + + {Array.from(groupedPermissions.entries()).map(([resource, resourcePermissions]) => ( + + +
+ + {resource} + + {resourcePermissions.length} + +
+
+ +
+ {resourcePermissions.map((permission) => ( + ( + + + { + const current = field.value || []; + if (checked) { + field.onChange([...current, permission.id]); + } else { + field.onChange( + current.filter((id) => id !== permission.id) + ); + } + }} + className="rounded-[2px] border-neutral-700 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600" + /> + + + {permission.actionDisplayName || permission.action} + ({permission.scopeDisplayName || permission.scope}) + {permission.description && ( + + {permission.description} + + )} + + + )} + /> + ))} +
+
+
+ ))} +
+ )} + +
+ )} + /> +); + export const RolesView = ({ roles, isLoading, @@ -94,185 +303,9 @@ export const RolesView = ({ isLoadingAdmin }: RolesViewProps) => { // Desktop actions - visible buttons - const DesktopActions = ({ role }: { role: Role }) => ( -
- - - -
- ); // Mobile actions - dropdown menu - const MobileActions = ({ role }: { role: Role }) => ( - - - - - - - Ações da Role - - - openPermissionsModal(role)} - className="cursor-pointer" - > - - Ver permissões - - openEditModal(role)} - disabled={role.isSystemRole} - className={cn( - "cursor-pointer", - role.isSystemRole && "cursor-not-allowed opacity-50" - )} - > - - Editar - - openDeleteModal(role)} - disabled={role.isSystemRole} - className={cn( - "cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400", - role.isSystemRole && "cursor-not-allowed opacity-50" - )} - > - - Deletar - - - - ); - const PermissionSelector = ({ - form, - }: { - form: typeof createForm | typeof editForm; - }) => ( - ( - - - Permissões - - {isLoadingPermissions ? ( -
- - Carregando permissões... -
- ) : permissions.length === 0 ? ( -
- Nenhuma permissão disponível -
- ) : ( - - {Array.from(groupedPermissions.entries()).map(([resource, resourcePermissions]) => ( - - -
- - {resource} - - {resourcePermissions.length} - -
-
- -
- {resourcePermissions.map((permission) => ( - ( - - - { - const current = field.value || []; - if (checked) { - field.onChange([...current, permission.id]); - } else { - field.onChange( - current.filter((id) => id !== permission.id) - ); - } - }} - className="rounded-[2px] border-neutral-700 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600" - /> - - - {permission.actionDisplayName || permission.action} - ({permission.scopeDisplayName || permission.scope}) - {permission.description && ( - - {permission.description} - - )} - - - )} - /> - ))} -
-
-
- ))} -
- )} - -
- )} - /> - ); // Access denied state if (!isLoadingAdmin && !isAdmin) { @@ -430,7 +463,7 @@ export const RolesView = ({ )} - + ))} @@ -471,7 +504,7 @@ export const RolesView = ({ )} - +
@@ -582,7 +615,7 @@ export const RolesView = ({ />
- +
@@ -672,7 +705,7 @@ export const RolesView = ({ />
- +
diff --git a/app/(pages)/system/users/users.view.tsx b/app/(pages)/system/users/users.view.tsx index 5c37427..728d713 100644 --- a/app/(pages)/system/users/users.view.tsx +++ b/app/(pages)/system/users/users.view.tsx @@ -53,8 +53,214 @@ import { } from "lucide-react"; import { useState } from "react"; import { UsersViewProps, User } from "./users.types"; + +const isCurrentUser = (user: User, currentUserId: UsersViewProps["currentUserId"]) => + user.id === currentUserId; import { cn } from "@/lib/utils"; +const DesktopActions = ({ + user, + currentUserId, + openEditModal, + toggleUserStatus, +}: { + user: User; + currentUserId: UsersViewProps["currentUserId"]; + openEditModal: (user: User) => void; + toggleUserStatus: (user: User) => void; +}) => ( +
+ + +
+); + +const MobileActions = ({ + user, + currentUserId, + openEditModal, + toggleUserStatus, +}: { + user: User; + currentUserId: UsersViewProps["currentUserId"]; + openEditModal: (user: User) => void; + toggleUserStatus: (user: User) => void; +}) => ( + + + + + + + Ações do Usuário + + + openEditModal(user)} + className="cursor-pointer focus:bg-neutral-800 focus:text-white" + > + + Editar + + toggleUserStatus(user)} + disabled={isCurrentUser(user, currentUserId)} + className={cn( + "cursor-pointer", + isCurrentUser(user, currentUserId) + ? "cursor-not-allowed opacity-50" + : user.isActive + ? "text-amber-500 focus:bg-amber-950/20 focus:text-amber-400" + : "text-emerald-500 focus:bg-emerald-950/20 focus:text-emerald-400" + )} + > + {user.isActive ? ( + <> + + Desativar + + ) : ( + <> + + Ativar + + )} + + + +); + +const RoleBadges = ({ userRoles }: { userRoles: string[] }) => { + const maxVisible = 2; + const visible = userRoles.slice(0, maxVisible); + const remaining = userRoles.length - maxVisible; + + return ( +
+ {visible.map((role) => ( + + {role} + + ))} + {remaining > 0 && ( + + +{remaining} + + )} +
+ ); +}; + +const WarehouseBadges = ({ userWarehouses }: { userWarehouses: string[] }) => { + const maxVisible = 2; + const visible = userWarehouses.slice(0, maxVisible); + const remaining = userWarehouses.length - maxVisible; + + return ( +
+ {visible.map((warehouse) => ( + + {warehouse} + + ))} + {remaining > 0 && ( + + +{remaining} + + )} +
+ ); +}; + +const StatusBadge = ({ user }: { user: User }) => { + if (!user.isActive) { + return ( + + INATIVO + + ); + } + + if (user.mustChangePassword) { + return ( + + SENHA TEMP. + + ); + } + + return ( + + ATIVO + + ); +}; + export const UsersView = ({ users, isLoading, @@ -110,189 +316,11 @@ export const UsersView = ({ const isCurrentUser = (user: User) => user.id === currentUserId; // Desktop actions - visible buttons - const DesktopActions = ({ user }: { user: User }) => ( -
- - -
- ); // Mobile actions - dropdown menu - const MobileActions = ({ user }: { user: User }) => ( - - - - - - - Ações do Usuário - - - openEditModal(user)} - className="cursor-pointer focus:bg-neutral-800 focus:text-white" - > - - Editar - - toggleUserStatus(user)} - disabled={isCurrentUser(user)} - className={cn( - "cursor-pointer", - isCurrentUser(user) - ? "cursor-not-allowed opacity-50" - : user.isActive - ? "text-amber-500 focus:bg-amber-950/20 focus:text-amber-400" - : "text-emerald-500 focus:bg-emerald-950/20 focus:text-emerald-400" - )} - > - {user.isActive ? ( - <> - - Desativar - - ) : ( - <> - - Ativar - - )} - - - - ); - - const RoleBadges = ({ userRoles }: { userRoles: string[] }) => { - const maxVisible = 2; - const visible = userRoles.slice(0, maxVisible); - const remaining = userRoles.length - maxVisible; - - return ( -
- {visible.map((role) => ( - - {role} - - ))} - {remaining > 0 && ( - - +{remaining} - - )} -
- ); - }; - - const WarehouseBadges = ({ userWarehouses }: { userWarehouses: string[] }) => { - const maxVisible = 2; - const visible = userWarehouses.slice(0, maxVisible); - const remaining = userWarehouses.length - maxVisible; - - return ( -
- {visible.map((warehouse) => ( - - {warehouse} - - ))} - {remaining > 0 && ( - - +{remaining} - - )} -
- ); - }; - const StatusBadge = ({ user }: { user: User }) => { - if (!user.isActive) { - return ( - - INATIVO - - ); - } - if (user.mustChangePassword) { - return ( - - SENHA TEMP. - - ); - } - return ( - - ATIVO - - ); - }; // Access denied state if (!isLoadingAdmin && !isAdmin) { @@ -439,7 +467,7 @@ export const UsersView = ({
- + ))} @@ -469,7 +497,7 @@ export const UsersView = ({
- +
diff --git a/app/(pages)/transfers/[id]/validate/validate-transfer.view.tsx b/app/(pages)/transfers/[id]/validate/validate-transfer.view.tsx index 54213c2..cc110ce 100644 --- a/app/(pages)/transfers/[id]/validate/validate-transfer.view.tsx +++ b/app/(pages)/transfers/[id]/validate/validate-transfer.view.tsx @@ -546,9 +546,9 @@ export function ValidateTransferView({ {discrepancies.length > 1 ? "s" : ""}
- {discrepancies.map((disc, idx) => ( + {discrepancies.map((disc) => (

- © {new Date().getFullYear()} StockShift Inc. v1.0.0 + © 2026 StockShift Inc. v1.0.0

diff --git a/app/login/login.view.tsx b/app/login/login.view.tsx index 67608a5..3fb96ab 100644 --- a/app/login/login.view.tsx +++ b/app/login/login.view.tsx @@ -179,7 +179,7 @@ export const LoginView = ({

- © {new Date().getFullYear()} StockShift Inc. v1.0.0 + © 2026 StockShift Inc. v1.0.0

@@ -190,9 +190,9 @@ export const LoginView = ({ Logs do Sistema
- {debugMessages.map((msg, idx) => ( + {debugMessages.map((msg) => (

- © {new Date().getFullYear()} StockShift Inc. v1.0.0 + © 2026 StockShift Inc. v1.0.0

diff --git a/components/product/scanner-drawer/scanner-drawer.tsx b/components/product/scanner-drawer/scanner-drawer.tsx index c25c8b4..09cc8b9 100644 --- a/components/product/scanner-drawer/scanner-drawer.tsx +++ b/components/product/scanner-drawer/scanner-drawer.tsx @@ -62,13 +62,15 @@ export const ScannerDrawer = ({ open, onOpenChange }: ScannerDrawerProps) => { // Reset on drawer close useEffect(() => { - if (!open) { - setTimeout(() => { - onReset(); - form.reset(); - setBatchCode(""); - }, 300); - } + if (open) return; + + const resetTimeout = window.setTimeout(() => { + onReset(); + form.reset(); + setBatchCode(""); + }, 300); + + return () => window.clearTimeout(resetTimeout); }, [open, onReset, form]); const handleScan = (detectedCodes: { rawValue: string }[]) => { diff --git a/components/ui/field.tsx b/components/ui/field.tsx index 373d9b2..7b05916 100644 --- a/components/ui/field.tsx +++ b/components/ui/field.tsx @@ -207,8 +207,8 @@ function FieldError({ return (
    {errors.map( - (error, index) => - error?.message &&
  • {error.message}
  • + (error) => + error?.message &&
  • {error.message}
  • )}
) diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx index 1a1bd73..aa0b462 100644 --- a/components/ui/slider.tsx +++ b/components/ui/slider.tsx @@ -49,10 +49,10 @@ function Slider({ )} /> - {Array.from({ length: _values.length }, (_, index) => ( + {_values.map((value) => ( ))} From d9ead401255d04333183a1cedc81c6cd85bd6d0d Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:35:56 -0300 Subject: [PATCH 03/19] chore(frontend): update local AGENTS instructions --- AGENTS.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c97e336..44964e8 100644 --- a/AGENTS.md +++ b/AGENTS.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,6 +59,6 @@ Next.js 15, TS, Tailwind CSS, shadcn/ui. self-validating, timely. ## Workflow & Commands + - Commands: `pnpm dev` (Dev), `pnpm test` (Test), `pnpm build` (Build). - Automation defaults: email `pass@pass.com` / pw `test123`. -- If you use `pnpm build`, go to the `stockshift-frontend` GNU screen and restart `pnpm dev` \ No newline at end of file From 1dda8015e8e2e3d2fb6af52796bdcf7d6d808913 Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:35:56 -0300 Subject: [PATCH 04/19] feat(frontend): implement stock movement create flow and tests --- .../create-stock-movement.model.test.ts | 144 ++++++++- .../create/create-stock-movement.model.ts | 230 ++++++++++++-- .../create/create-stock-movement.schema.ts | 4 + .../create/create-stock-movement.types.ts | 30 ++ .../create/create-stock-movement.view.tsx | 189 ++--------- ...ck-movement-batch-data-modal.view.test.tsx | 71 +++++ .../stock-movement-batch-data-modal.view.tsx | 146 +++++++++ .../stock-movement-items-list.view.test.tsx | 69 ++++ .../create/stock-movement-items-list.view.tsx | 298 ++++++++++++++++++ docs/endpoints/stock-movements.md | 12 +- 10 files changed, 1001 insertions(+), 192 deletions(-) create mode 100644 app/(pages)/stock-movements/create/stock-movement-batch-data-modal.view.test.tsx create mode 100644 app/(pages)/stock-movements/create/stock-movement-batch-data-modal.view.tsx create mode 100644 app/(pages)/stock-movements/create/stock-movement-items-list.view.test.tsx create mode 100644 app/(pages)/stock-movements/create/stock-movement-items-list.view.tsx diff --git a/app/(pages)/stock-movements/create/create-stock-movement.model.test.ts b/app/(pages)/stock-movements/create/create-stock-movement.model.test.ts index c9a8653..9c8a0a1 100644 --- a/app/(pages)/stock-movements/create/create-stock-movement.model.test.ts +++ b/app/(pages)/stock-movements/create/create-stock-movement.model.test.ts @@ -380,6 +380,34 @@ describe("helpers de produto", () => { expect(payload.items[0].newProduct?.hasExpiration).toBe(true); }); + it("envia dados de lote em item de produto existente quando preenchidos", () => { + const payload = buildMovementPayload( + "PURCHASE_IN", + createSubmitPayload({ + items: [ + { + productId: validExistingProductUuid, + quantity: 2, + productName: "Café Torrado", + manufacturedDate: "2026-04-01", + expirationDate: "2026-12-31", + costPrice: 1290, + sellingPrice: 2490, + }, + ], + }), + ); + + expect(payload.items[0]).toEqual({ + productId: validExistingProductUuid, + quantity: 2, + manufacturedDate: "2026-04-01", + expirationDate: "2026-12-31", + costPrice: 1290, + sellingPrice: 2490, + }); + }); + it("exibe footer no fim da pagina ou quando usuario rola para cima", () => { expect( shouldShowStockMovementFooter({ @@ -531,7 +559,8 @@ describe("useCreateStockMovementModel", () => { expect(result.current.isProductOptionsOpen).toBe(false); }); - it("adiciona item válido e valida quantidade/produto", () => { + it("adiciona item válido em saída e valida quantidade/produto", () => { + fakeSearchParams.setType("USAGE"); const { result } = renderHook(() => useCreateStockMovementModel()); act(() => { @@ -574,6 +603,99 @@ describe("useCreateStockMovementModel", () => { expect(result.current.addItemError).toBe( "Este produto já foi adicionado. Remova-o para alterar a quantidade.", ); + expect(result.current.existingProductBatchForm.isOpen).toBe(false); + }); + + it("abre dados de lote antes de adicionar produto existente em compra", () => { + const { result } = renderHook(() => useCreateStockMovementModel()); + + act(() => { + result.current.onProductSelect(movementProducts[0]); + }); + act(() => { + result.current.onQuantityChange("2"); + }); + act(() => { + result.current.onAddItem(); + }); + + expect(result.current.items).toHaveLength(0); + expect(result.current.existingProductBatchForm).toMatchObject({ + isOpen: true, + productId: "p-1", + productName: "Café Torrado", + quantity: "2", + editingIndex: null, + }); + }); + + it("confirma dados de lote e adiciona item com datas e preços", () => { + const { result } = renderHook(() => useCreateStockMovementModel()); + + act(() => { + result.current.onProductSelect(movementProducts[0]); + }); + act(() => { + result.current.onQuantityChange("2"); + }); + act(() => { + result.current.onAddItem(); + }); + act(() => { + result.current.onExistingProductBatchManufacturedDateChange("2026-04-01"); + result.current.onExistingProductBatchExpirationDateChange("2026-12-31"); + result.current.onExistingProductBatchCostPriceChange(1290); + result.current.onExistingProductBatchSellingPriceChange(2490); + }); + act(() => { + result.current.onConfirmExistingProductBatchData(); + }); + + expect(result.current.items[0]).toMatchObject({ + productId: "p-1", + productName: "Café Torrado", + quantity: 2, + manufacturedDate: "2026-04-01", + expirationDate: "2026-12-31", + costPrice: 1290, + sellingPrice: 2490, + }); + expect(result.current.existingProductBatchForm.isOpen).toBe(false); + expect(result.current.selectedProductId).toBe(""); + }); + + it("edita dados de lote de produto existente de entrada", () => { + const { result } = renderHook(() => useCreateStockMovementModel()); + + act(() => { + result.current.form.setValue("items", [ + { + productId: validExistingProductUuid, + productName: "Café Torrado", + quantity: 2, + expirationDate: "2026-12-31", + }, + ]); + }); + act(() => { + result.current.onEditExistingProductBatchData(0); + }); + act(() => { + result.current.onExistingProductBatchQuantityChange("3"); + result.current.onExistingProductBatchManufacturedDateChange("2026-04-01"); + result.current.onExistingProductBatchCostPriceChange(1290); + }); + act(() => { + result.current.onConfirmExistingProductBatchData(); + }); + + expect(result.current.form.getValues("items")[0]).toMatchObject({ + productId: validExistingProductUuid, + quantity: 3, + manufacturedDate: "2026-04-01", + expirationDate: "2026-12-31", + costPrice: 1290, + }); }); it("bloqueia criação de novo produto fora dos tipos de entrada", () => { @@ -610,7 +732,8 @@ describe("useCreateStockMovementModel", () => { ); }); - it("adiciona item por código de barras e impede duplicado imediato", async () => { + it("adiciona item por código de barras em saída e impede duplicado imediato", async () => { + fakeSearchParams.setType("USAGE"); const { result } = renderHook(() => useCreateStockMovementModel()); fakeApi.get.mockImplementation((url: string) => { @@ -655,6 +778,23 @@ describe("useCreateStockMovementModel", () => { expect(result.current.form.getValues("items")).toHaveLength(2); }); + it("abre dados de lote por código de barras em ajuste de entrada", async () => { + fakeSearchParams.setType("ADJUSTMENT_IN"); + const { result } = renderHook(() => useCreateStockMovementModel()); + + await act(async () => { + await result.current.onBarcodeScan("7891000000004"); + }); + + expect(result.current.form.getValues("items")).toHaveLength(0); + expect(result.current.existingProductBatchForm).toMatchObject({ + isOpen: true, + productId: "p-4", + productName: "Copo Térmico", + quantity: "1", + }); + }); + it("mostra erro com ação para criação quando produto não existe", async () => { fakeApi.get.mockRejectedValue(new Error("não encontrado")); diff --git a/app/(pages)/stock-movements/create/create-stock-movement.model.ts b/app/(pages)/stock-movements/create/create-stock-movement.model.ts index a9c8181..8c46284 100644 --- a/app/(pages)/stock-movements/create/create-stock-movement.model.ts +++ b/app/(pages)/stock-movements/create/create-stock-movement.model.ts @@ -12,6 +12,7 @@ import { } from "./create-stock-movement.schema"; import { CreateStockMovementViewProps, + ExistingProductBatchFormState, StockMovementProductOption, } from "./create-stock-movement.types"; import { useBreadcrumb } from "@/components/breadcrumb"; @@ -26,13 +27,36 @@ import { writeStockMovementDraft, } from "./create-stock-movement.storage"; -const buildMovementItemPayload = ( +const getOptionalText = (value: string | undefined): string | undefined => { + const trimmedValue = value?.trim(); + return trimmedValue || undefined; +}; + +const buildExistingProductItemPayload = ( item: CreateStockMovementSchema["items"][number], ) => { - if (!item.newProductData) { - return { productId: item.productId, quantity: item.quantity }; - } + const payload: { + productId: string | undefined; + quantity: number; + manufacturedDate?: string; + expirationDate?: string; + costPrice?: number; + sellingPrice?: number; + } = { productId: item.productId, quantity: item.quantity }; + + const manufacturedDate = getOptionalText(item.manufacturedDate); + const expirationDate = getOptionalText(item.expirationDate); + if (manufacturedDate) payload.manufacturedDate = manufacturedDate; + if (expirationDate) payload.expirationDate = expirationDate; + if (item.costPrice !== undefined) payload.costPrice = item.costPrice; + if (item.sellingPrice !== undefined) payload.sellingPrice = item.sellingPrice; + return payload; +}; +const buildMovementItemPayload = ( + item: CreateStockMovementSchema["items"][number], +) => { + if (!item.newProductData) return buildExistingProductItemPayload(item); const newProduct = { name: item.newProductData.name, description: item.newProductData.description, @@ -47,8 +71,8 @@ const buildMovementItemPayload = ( return { quantity: item.quantity, newProduct, - manufacturedDate: item.newProductData.manufacturedDate, - expirationDate: item.newProductData.expirationDate, + manufacturedDate: getOptionalText(item.newProductData.manufacturedDate), + expirationDate: getOptionalText(item.newProductData.expirationDate), costPrice: item.newProductData.costPrice, sellingPrice: item.newProductData.sellingPrice, }; @@ -102,6 +126,17 @@ interface ProductByBarcodeResponse { const PRODUCT_SEARCH_LIMIT = 5; +const EMPTY_EXISTING_BATCH_FORM: ExistingProductBatchFormState = { + isOpen: false, + productId: "", + productName: "", + quantity: "", + manufacturedDate: "", + expirationDate: "", + editingIndex: null, + error: null, +}; + interface FooterVisibilityParams { currentScrollY: number; lastScrollY: number; @@ -159,6 +194,8 @@ export function useCreateStockMovementModel(): CreateStockMovementViewProps { const [addItemError, setAddItemError] = useState(null); const [isScannerOpen, setIsScannerOpen] = useState(false); const [isFooterVisible, setIsFooterVisible] = useState(false); + const [existingProductBatchForm, setExistingProductBatchForm] = + useState(EMPTY_EXISTING_BATCH_FORM); const lastScannedBarcodeRef = useRef(null); const lastScrollYRef = useRef(0); const productSearchBlurTimeoutRef = @@ -194,7 +231,7 @@ export function useCreateStockMovementModel(): CreateStockMovementViewProps { form.setValue("type", selectedMovementType, { shouldValidate: true }); }, [form, router, selectedMovementType]); - const { fields, append, remove } = useFieldArray({ + const { fields, append, remove, update } = useFieldArray({ control: form.control, name: "items", }); @@ -317,41 +354,137 @@ export function useCreateStockMovementModel(): CreateStockMovementViewProps { setAddItemError(null); }; + const isSelectedInMovement = (): boolean => { + if (!selectedMovementType) return false; + return MANUAL_IN_MOVEMENT_TYPES.includes( + selectedMovementType as (typeof MANUAL_IN_MOVEMENT_TYPES)[number], + ); + }; + + const resetProductBuilder = (): void => { + setSelectedProductId(""); + setSelectedProduct(null); + setProductSearchQuery(""); + setItemQuantity(""); + }; + + const closeExistingProductBatchForm = (): void => { + setExistingProductBatchForm(EMPTY_EXISTING_BATCH_FORM); + }; + + const openExistingProductBatchForm = ( + params: Omit, + ): void => { + setExistingProductBatchForm({ + ...params, + isOpen: true, + error: null, + }); + }; + + const appendExistingProductItem = ( + productId: string, + productName: string, + quantity: number, + ): void => { + append({ productId, quantity, productName }); + resetProductBuilder(); + }; + + const hasExistingProductAlreadyAdded = (productId: string): boolean => { + return form.getValues("items").some((item) => item.productId === productId); + }; + + const getSelectedProductQuantity = (): number | null => { + const quantity = Number(itemQuantity); + return quantity > 0 ? quantity : null; + }; + const handleAddItem = () => { setAddItemError(null); - if (!selectedProductId) { setAddItemError("Selecione um produto."); return; } - const qty = Number(itemQuantity); - if (!qty || qty <= 0) { + const quantity = getSelectedProductQuantity(); + if (!quantity) { setAddItemError("Informe uma quantidade válida."); return; } - const alreadyAdded = fields.find((f) => f.productId === selectedProductId); - if (alreadyAdded) { + if (hasExistingProductAlreadyAdded(selectedProductId)) { setAddItemError( "Este produto já foi adicionado. Remova-o para alterar a quantidade.", ); return; } - const product = selectedProduct || products.find((p) => p.id === selectedProductId); + const productName = product?.name || "Produto"; + + if (isSelectedInMovement()) { + openExistingProductBatchForm({ + productId: selectedProductId, + productName, + quantity: String(quantity), + manufacturedDate: "", + expirationDate: "", + costPrice: undefined, + sellingPrice: undefined, + editingIndex: null, + }); + return; + } - append({ - productId: selectedProductId, - quantity: qty, - productName: product?.name || "Produto", - }); + appendExistingProductItem(selectedProductId, productName, quantity); + }; - setSelectedProductId(""); - setSelectedProduct(null); - setProductSearchQuery(""); - setItemQuantity(""); + const handleExistingProductBatchOpenChange = (open: boolean): void => { + if (open) { + setExistingProductBatchForm((current) => ({ ...current, isOpen: true })); + return; + } + closeExistingProductBatchForm(); + }; + + const updateExistingProductBatchForm = ( + patch: Partial, + ): void => { + setExistingProductBatchForm((current) => ({ + ...current, + ...patch, + error: patch.error ?? null, + })); + }; + + const buildExistingProductBatchItem = (): CreateStockMovementSchema["items"][number] => ({ + productId: existingProductBatchForm.productId, + productName: existingProductBatchForm.productName, + quantity: Number(existingProductBatchForm.quantity), + manufacturedDate: getOptionalText(existingProductBatchForm.manufacturedDate), + expirationDate: getOptionalText(existingProductBatchForm.expirationDate), + costPrice: existingProductBatchForm.costPrice, + sellingPrice: existingProductBatchForm.sellingPrice, + }); + + const handleConfirmExistingProductBatchData = (): void => { + const quantity = Number(existingProductBatchForm.quantity); + if (!quantity || quantity <= 0) { + updateExistingProductBatchForm({ + error: "Informe uma quantidade válida para o lote.", + }); + return; + } + + const item = buildExistingProductBatchItem(); + if (existingProductBatchForm.editingIndex !== null) { + update(existingProductBatchForm.editingIndex, item); + } else { + append(item); + resetProductBuilder(); + } + closeExistingProductBatchForm(); }; const handleCreateNewProduct = () => { @@ -417,20 +550,49 @@ export function useCreateStockMovementModel(): CreateStockMovementViewProps { ); }; - const resolveScannerQuantity = () => { + const handleEditExistingProductBatchData = (index: number): void => { + if (!isSelectedInMovement()) return; + const item = form.getValues("items")[index]; + if (!item?.productId || item.newProductData) return; + + openExistingProductBatchForm({ + productId: item.productId, + productName: item.productName || "Produto", + quantity: String(item.quantity), + manufacturedDate: item.manufacturedDate || "", + expirationDate: item.expirationDate || "", + costPrice: item.costPrice, + sellingPrice: item.sellingPrice, + editingIndex: index, + }); + }; + + const resolveScannerQuantity = (): number => { const qty = Number(itemQuantity); return qty > 0 ? qty : 1; }; const appendScannedProduct = (product: StockMovementProductOption) => { - const alreadyAdded = form.getValues("items").some((item) => { - return item.productId === product.id; - }); - if (alreadyAdded) { + if (hasExistingProductAlreadyAdded(product.id)) { toast.error(`${product.name} já está na movimentação.`); return; } + if (isSelectedInMovement()) { + setIsScannerOpen(false); + openExistingProductBatchForm({ + productId: product.id, + productName: product.name, + quantity: String(resolveScannerQuantity()), + manufacturedDate: "", + expirationDate: "", + costPrice: undefined, + sellingPrice: undefined, + editingIndex: null, + }); + return; + } + append({ productId: product.id, productName: product.name, @@ -528,9 +690,23 @@ export function useCreateStockMovementModel(): CreateStockMovementViewProps { onAddItem: handleAddItem, onCreateNewProduct: handleCreateNewProduct, onEditNewProductItem: handleEditNewProductItem, + onEditExistingProductBatchData: handleEditExistingProductBatchData, onScannerOpenChange: setIsScannerOpen, onBarcodeScan: handleBarcodeScan, onRemoveItem: remove, + existingProductBatchForm, + onExistingProductBatchOpenChange: handleExistingProductBatchOpenChange, + onExistingProductBatchQuantityChange: (quantity: string) => + updateExistingProductBatchForm({ quantity }), + onExistingProductBatchManufacturedDateChange: (manufacturedDate: string) => + updateExistingProductBatchForm({ manufacturedDate }), + onExistingProductBatchExpirationDateChange: (expirationDate: string) => + updateExistingProductBatchForm({ expirationDate }), + onExistingProductBatchCostPriceChange: (costPrice?: number) => + updateExistingProductBatchForm({ costPrice }), + onExistingProductBatchSellingPriceChange: (sellingPrice?: number) => + updateExistingProductBatchForm({ sellingPrice }), + onConfirmExistingProductBatchData: handleConfirmExistingProductBatchData, items: fields, }; } diff --git a/app/(pages)/stock-movements/create/create-stock-movement.schema.ts b/app/(pages)/stock-movements/create/create-stock-movement.schema.ts index dc6e191..056b45a 100644 --- a/app/(pages)/stock-movements/create/create-stock-movement.schema.ts +++ b/app/(pages)/stock-movements/create/create-stock-movement.schema.ts @@ -28,6 +28,10 @@ export const movementItemSchema = z.object({ productId: z.string().uuid("Produto inválido").optional(), quantity: z.number().positive("Quantidade deve ser maior que zero"), productName: z.string().optional(), // UI helper + manufacturedDate: z.string().optional(), + expirationDate: z.string().optional(), + costPrice: z.number().int().min(0).optional(), + sellingPrice: z.number().int().min(0).optional(), newProductData: inlineProductSchema.optional(), }).refine((item) => Boolean(item.productId) !== Boolean(item.newProductData), { message: "Informe um produto existente ou um novo produto", diff --git a/app/(pages)/stock-movements/create/create-stock-movement.types.ts b/app/(pages)/stock-movements/create/create-stock-movement.types.ts index 9d01e8e..d411168 100644 --- a/app/(pages)/stock-movements/create/create-stock-movement.types.ts +++ b/app/(pages)/stock-movements/create/create-stock-movement.types.ts @@ -28,9 +28,26 @@ export interface StockMovementDraftItem { productId?: string; quantity: number; productName?: string; + manufacturedDate?: string; + expirationDate?: string; + costPrice?: number; + sellingPrice?: number; newProductData?: InlineProductData; } +export interface ExistingProductBatchFormState { + isOpen: boolean; + productId: string; + productName: string; + quantity: string; + manufacturedDate: string; + expirationDate: string; + costPrice?: number; + sellingPrice?: number; + editingIndex: number | null; + error: string | null; +} + export interface StockMovementProductOption { id: string; name: string; @@ -65,14 +82,27 @@ export interface CreateStockMovementViewProps { onAddItem: () => void; onCreateNewProduct: () => void; onEditNewProductItem: (index: number) => void; + onEditExistingProductBatchData: (index: number) => void; onScannerOpenChange: (open: boolean) => void; onBarcodeScan: (barcode: string) => void; onRemoveItem: (index: number) => void; + existingProductBatchForm: ExistingProductBatchFormState; + onExistingProductBatchOpenChange: (open: boolean) => void; + onExistingProductBatchQuantityChange: (quantity: string) => void; + onExistingProductBatchManufacturedDateChange: (date: string) => void; + onExistingProductBatchExpirationDateChange: (date: string) => void; + onExistingProductBatchCostPriceChange: (price?: number) => void; + onExistingProductBatchSellingPriceChange: (price?: number) => void; + onConfirmExistingProductBatchData: () => void; items: Array<{ id: string; productId?: string; quantity: number; productName?: string; + manufacturedDate?: string; + expirationDate?: string; + costPrice?: number; + sellingPrice?: number; newProductData?: InlineProductData; }>; } diff --git a/app/(pages)/stock-movements/create/create-stock-movement.view.tsx b/app/(pages)/stock-movements/create/create-stock-movement.view.tsx index 2d836e8..32a4582 100644 --- a/app/(pages)/stock-movements/create/create-stock-movement.view.tsx +++ b/app/(pages)/stock-movements/create/create-stock-movement.view.tsx @@ -2,16 +2,13 @@ import { Plus, - Trash2, Package, AlertCircle, FileText, Save, - Hash, TrendingDown, TrendingUp, ScanLine, - Pencil, Search, X, Loader2, @@ -31,13 +28,13 @@ import { Textarea } from "@/components/ui/textarea"; import { PageContainer } from "@/components/ui/page-container"; import { FormSection } from "@/components/ui/form-section"; import { FixedBottomBar } from "@/components/ui/fixed-bottom-bar"; -import { EmptyState } from "@/components/ui/empty-state"; -import { SectionLabel } from "@/components/ui/section-label"; import { ResponsiveModal } from "@/components/ui/responsive-modal"; import { PermissionGate } from "@/components/permission-gate"; import { cn } from "@/lib/utils"; import { CreateStockMovementViewProps } from "./create-stock-movement.types"; import { StockMovementScanner } from "./stock-movement-scanner.view"; +import { StockMovementBatchDataModal } from "./stock-movement-batch-data-modal.view"; +import { StockMovementItemsList } from "./stock-movement-items-list.view"; import { MANUAL_MOVEMENT_TYPE_LABELS, MANUAL_OUT_MOVEMENT_TYPES, @@ -65,9 +62,18 @@ export function CreateStockMovementView({ onAddItem, onCreateNewProduct, onEditNewProductItem, + onEditExistingProductBatchData, onScannerOpenChange, onBarcodeScan, onRemoveItem, + existingProductBatchForm, + onExistingProductBatchOpenChange, + onExistingProductBatchQuantityChange, + onExistingProductBatchManufacturedDateChange, + onExistingProductBatchExpirationDateChange, + onExistingProductBatchCostPriceChange, + onExistingProductBatchSellingPriceChange, + onConfirmExistingProductBatchData, items, }: CreateStockMovementViewProps) { const [isNotesOpen, setIsNotesOpen] = useState(false); @@ -80,6 +86,7 @@ export function CreateStockMovementView({ selectedType as (typeof MANUAL_OUT_MOVEMENT_TYPES)[number], ) : false; + const isInMovement = Boolean(selectedType) && !isOutMovement; return ( @@ -88,6 +95,16 @@ export function CreateStockMovementView({ onOpenChange={onScannerOpenChange} onScan={onBarcodeScan} /> +
- )} - -
- - ))} - - - {/* Desktop table */} -
-
- - - - - - - - - {items.map((item, index) => ( - - - - - - ))} - - - - - - - -
- Produto - - Quantidade - -
-
- {item.productName || "Produto"} - {item.newProductData && ( - - Novo - - )} -
-
- {item.quantity} - -
- {item.newProductData && ( - - )} - -
-
- Total - - {totalQuantity} - -
-
-
- - )} - - {form.formState.errors.items && ( -

- {form.formState.errors.items.message} -

- )} - + {/* ── Fixed Bottom Bar ── */} ({ + ResponsiveModal: ({ + children, + footer, + open, + title, + }: { + children: React.ReactNode; + footer: React.ReactNode; + open: boolean; + title: string; + }) => + open ? ( +
+

{title}

+ {children} + {footer} +
+ ) : null, +})); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const openBatchForm: ExistingProductBatchFormState = { + isOpen: true, + productId: "p-1", + productName: "Café Torrado", + quantity: "2", + manufacturedDate: "2026-04-01", + expirationDate: "2026-12-31", + costPrice: 1290, + sellingPrice: 2490, + editingIndex: null, + error: null, +}; + +describe("StockMovementBatchDataModal", () => { + it("renderiza campos e ações de dados do lote", () => { + render( + , + ); + + expect(screen.getByText("Dados do lote")).toBeTruthy(); + expect(screen.getByText("Café Torrado")).toBeTruthy(); + expect(screen.getByText("Quantidade")).toBeTruthy(); + expect(screen.getByText("Fabricação")).toBeTruthy(); + expect(screen.getByText("Validade")).toBeTruthy(); + expect(screen.getByText("Preço de custo")).toBeTruthy(); + expect(screen.getByText("Preço de venda")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Confirmar" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "Cancelar" })).toBeTruthy(); + }); +}); diff --git a/app/(pages)/stock-movements/create/stock-movement-batch-data-modal.view.tsx b/app/(pages)/stock-movements/create/stock-movement-batch-data-modal.view.tsx new file mode 100644 index 0000000..77f7481 --- /dev/null +++ b/app/(pages)/stock-movements/create/stock-movement-batch-data-modal.view.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { CalendarDays, DollarSign, PackageCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { CurrencyInput } from "@/components/ui/currency-input"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { ResponsiveModal } from "@/components/ui/responsive-modal"; +import type { ExistingProductBatchFormState } from "./create-stock-movement.types"; + +interface StockMovementBatchDataModalProps { + form: ExistingProductBatchFormState; + onOpenChange: (open: boolean) => void; + onQuantityChange: (quantity: string) => void; + onManufacturedDateChange: (date: string) => void; + onExpirationDateChange: (date: string) => void; + onCostPriceChange: (price?: number) => void; + onSellingPriceChange: (price?: number) => void; + onConfirm: () => void; +} + +export function StockMovementBatchDataModal({ + form, + onOpenChange, + onQuantityChange, + onManufacturedDateChange, + onExpirationDateChange, + onCostPriceChange, + onSellingPriceChange, + onConfirm, +}: StockMovementBatchDataModalProps) { + return ( + + + + + } + > +
+
+
+ +
+

+ {form.productName || "Produto"} +

+

+ {form.editingIndex === null ? "Novo item" : "Editando item"} +

+
+
+
+ +
+
+ + + onQuantityChange(value !== undefined ? String(value) : "") + } + className="h-10 rounded-[4px] border-neutral-800 bg-neutral-900 font-mono text-sm text-white focus:border-blue-600" + placeholder="0" + /> +
+
+ + onManufacturedDateChange(event.target.value)} + className="h-10 rounded-[4px] border-neutral-800 bg-neutral-900 text-sm text-white focus:border-blue-600" + /> +
+
+ + onExpirationDateChange(event.target.value)} + className="h-10 rounded-[4px] border-neutral-800 bg-neutral-900 text-sm text-white focus:border-blue-600" + /> +
+
+ + +
+
+ + +
+
+ + {form.error && ( +
+ {form.error} +
+ )} +
+
+ ); +} diff --git a/app/(pages)/stock-movements/create/stock-movement-items-list.view.test.tsx b/app/(pages)/stock-movements/create/stock-movement-items-list.view.test.tsx new file mode 100644 index 0000000..76339d3 --- /dev/null +++ b/app/(pages)/stock-movements/create/stock-movement-items-list.view.test.tsx @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { StockMovementItemsList } from "./stock-movement-items-list.view"; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const baseHandlers = { + onEditNewProductItem: vi.fn(), + onEditExistingProductBatchData: vi.fn(), + onRemoveItem: vi.fn(), +}; + +describe("StockMovementItemsList", () => { + it("mostra resumo de lote e permite editar produto existente de entrada", () => { + render( + , + ); + + expect(screen.getAllByText("Café Torrado")).toHaveLength(2); + expect(screen.getAllByText("Fab: 01/04/2026")[0]).toBeTruthy(); + expect(screen.getAllByText("Val: 31/12/2026")[0]).toBeTruthy(); + expect( + screen.getAllByText((text) => text.includes("Custo") && text.includes("12,90"))[0], + ).toBeTruthy(); + expect( + screen.getAllByText((text) => text.includes("Venda") && text.includes("24,90"))[0], + ).toBeTruthy(); + + fireEvent.click(screen.getAllByLabelText("Editar item")[0]); + expect(baseHandlers.onEditExistingProductBatchData).toHaveBeenCalledWith(0); + }); + + it("não mostra edição de lote para produto existente em saída", () => { + render( + , + ); + + expect(screen.queryByLabelText("Editar item")).toBeNull(); + }); +}); diff --git a/app/(pages)/stock-movements/create/stock-movement-items-list.view.tsx b/app/(pages)/stock-movements/create/stock-movement-items-list.view.tsx new file mode 100644 index 0000000..24937dd --- /dev/null +++ b/app/(pages)/stock-movements/create/stock-movement-items-list.view.tsx @@ -0,0 +1,298 @@ +"use client"; + +import { Hash, Package, Pencil, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { EmptyState } from "@/components/ui/empty-state"; +import { SectionLabel } from "@/components/ui/section-label"; +import type { StockMovementDraftItem } from "./create-stock-movement.types"; + +type StockMovementListItem = StockMovementDraftItem & { id: string }; + +interface StockMovementItemsListProps { + items: StockMovementListItem[]; + isInMovement: boolean; + itemsErrorMessage?: string; + onEditNewProductItem: (index: number) => void; + onEditExistingProductBatchData: (index: number) => void; + onRemoveItem: (index: number) => void; +} + +const currencyFormatter = new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", +}); + +const formatBatchDate = (value: string): string => { + const [year, month, day] = value.split("-"); + if (!year || !month || !day) return value; + return `${day}/${month}/${year}`; +}; + +const formatBatchPrice = (value: number): string => { + return currencyFormatter.format(value / 100); +}; + +const getBatchSummaryLines = (item: StockMovementListItem): string[] => { + const batchSource = item.newProductData || item; + const lines: string[] = []; + if (batchSource.manufacturedDate) { + lines.push(`Fab: ${formatBatchDate(batchSource.manufacturedDate)}`); + } + if (batchSource.expirationDate) { + lines.push(`Val: ${formatBatchDate(batchSource.expirationDate)}`); + } + if (batchSource.costPrice !== undefined) { + lines.push(`Custo: ${formatBatchPrice(batchSource.costPrice)}`); + } + if (batchSource.sellingPrice !== undefined) { + lines.push(`Venda: ${formatBatchPrice(batchSource.sellingPrice)}`); + } + return lines; +}; + +const canEditExistingBatchData = ( + item: StockMovementListItem, + isInMovement: boolean, +): boolean => { + return isInMovement && Boolean(item.productId) && !item.newProductData; +}; + +const renderNewProductBadge = (item: StockMovementListItem) => { + if (!item.newProductData) return null; + return ( + + Novo + + ); +}; + +const BatchSummary = ({ item }: { item: StockMovementListItem }) => { + const lines = getBatchSummaryLines(item); + if (lines.length === 0) return null; + return ( +
+ {lines.map((line) => ( + + {line} + + ))} +
+ ); +}; + +export function StockMovementItemsList({ + items, + isInMovement, + itemsErrorMessage, + onEditNewProductItem, + onEditExistingProductBatchData, + onRemoveItem, +}: StockMovementItemsListProps) { + const totalQuantity = items.reduce((acc, item) => acc + item.quantity, 0); + + return ( +
+ + Itens da Movimentação ({items.length}) + + + {items.length === 0 ? ( + + ) : ( + <> +
+ {items.map((item, index) => ( + + ))} +
+ +
+ +
+ + )} + + {itemsErrorMessage && ( +

+ {itemsErrorMessage} +

+ )} +
+ ); +} + +interface MovementItemActionProps { + item: StockMovementListItem; + index: number; + isInMovement: boolean; + onEditNewProductItem: (index: number) => void; + onEditExistingProductBatchData: (index: number) => void; + onRemoveItem: (index: number) => void; + buttonSizeClass: string; +} + +function MovementItemActions({ + item, + index, + isInMovement, + onEditNewProductItem, + onEditExistingProductBatchData, + onRemoveItem, + buttonSizeClass, +}: MovementItemActionProps) { + const canEditBatchData = canEditExistingBatchData(item, isInMovement); + const canEditItem = Boolean(item.newProductData) || canEditBatchData; + const editItem = (): void => { + if (item.newProductData) { + onEditNewProductItem(index); + return; + } + onEditExistingProductBatchData(index); + }; + + return ( +
+ {canEditItem && ( + + )} + +
+ ); +} + +function MobileMovementItem(props: MovementItemActionProps) { + const { item } = props; + return ( +
+
+
+

+ {item.productName || "Produto"} +

+ {renderNewProductBadge(item)} +
+

+ Qtd:{" "} + + {item.quantity} + +

+ +
+ +
+ ); +} + +interface DesktopMovementItemsTableProps + extends Omit { + items: StockMovementListItem[]; + totalQuantity: number; +} + +function DesktopMovementItemsTable({ + items, + totalQuantity, + ...actionProps +}: DesktopMovementItemsTableProps) { + return ( +
+ + + + + + + + + {items.map((item, index) => ( + + + + + + ))} + + + + + + + +
+ Produto + + Quantidade + +
+
+
+ + {item.productName || "Produto"} + + {renderNewProductBadge(item)} +
+ +
+
+ {item.quantity} + + +
+ Total + + {totalQuantity} + +
+
+ ); +} diff --git a/docs/endpoints/stock-movements.md b/docs/endpoints/stock-movements.md index e4e2b11..810048a 100644 --- a/docs/endpoints/stock-movements.md +++ b/docs/endpoints/stock-movements.md @@ -52,7 +52,11 @@ Request: }, { "productId": "660e8400-e29b-41d4-a716-446655440001", - "quantity": 2.5 + "quantity": 2.5, + "costPrice": 500, + "sellingPrice": 800, + "manufacturedDate": "2026-04-01", + "expirationDate": "2026-12-31" }, { "quantity": 4, @@ -87,8 +91,8 @@ Regras: - Produto existente: `productId` (UUID). - Produto novo: `newProduct` com o mesmo formato JSON de `POST /api/products`. - Produtos em `newProduct` sao persistidos dentro da mesma transacao da movimentacao. -- `costPrice` e `sellingPrice` sao opcionais, enviados em centavos no item da movimentacao, e aplicados ao batch criado para o produto novo. -- `manufacturedDate` e `expirationDate` sao opcionais, enviados no item da movimentacao, e aplicados ao batch criado em movimentos de entrada. +- `costPrice` e `sellingPrice` sao opcionais, enviados em centavos no item da movimentacao, e aplicados ao batch criado em movimentos de entrada para produto existente ou produto novo. +- `manufacturedDate` e `expirationDate` sao opcionais, enviados no item da movimentacao, e aplicados ao batch criado em movimentos de entrada para produto existente ou produto novo. - Para imagem de produto novo, envie multipart com: - `movement`: Blob JSON do request acima (`application/json`). - `inlineProductImages`: uma parte de arquivo para cada produto novo, na mesma ordem em que esses produtos aparecem em `items`; quando um produto novo nao tiver imagem, envie uma parte vazia para preservar o pareamento. @@ -96,7 +100,7 @@ Regras: - Produtos novos inline sao aceitos somente para movimentacoes de entrada (`PURCHASE_IN`, `ADJUSTMENT_IN`). - Para movimentos `OUT`, o sistema deduz automaticamente dos batches usando FIFO (batch mais antigo primeiro). - Se a quantidade total disponivel no warehouse for insuficiente, retorna `400` com mensagem de estoque insuficiente. -- Para movimentos `IN`, o sistema cria um novo batch ou adiciona ao batch existente do produto. +- Para movimentos `IN` com `productId`, o sistema cria um novo batch quando qualquer data/preco de lote for informado. Sem esses campos, adiciona a quantidade ao primeiro batch existente do produto, ou cria o primeiro batch se ainda nao houver lote. - O `warehouseId` e determinado automaticamente pelo warehouse do usuario logado. - Um codigo unico e gerado automaticamente (ex: `MOV-2026-0001`). From b14f2b83f858d606a2536d6a403a97d4121545da Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:35:56 -0300 Subject: [PATCH 05/19] test(frontend): update products model and view tests --- app/(pages)/products/products.model.test.ts | 20 +++++++++++------- app/(pages)/products/products.model.ts | 23 ++++++++------------- app/(pages)/products/products.view.test.tsx | 2 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/(pages)/products/products.model.test.ts b/app/(pages)/products/products.model.test.ts index 56f66b0..55aa624 100644 --- a/app/(pages)/products/products.model.test.ts +++ b/app/(pages)/products/products.model.test.ts @@ -4,6 +4,7 @@ import { useProductsModel } from "./products.model"; import { toast } from "sonner"; const mockMutate = vi.fn(); +const mockGlobalMutate = vi.fn(); const mockGet = vi.fn(); const mockDelete = vi.fn(); @@ -53,6 +54,7 @@ vi.mock("swr", () => ({ isLoading: false, mutate: mockMutate, })), + mutate: (...args: unknown[]) => mockGlobalMutate(...args), })); vi.mock("@/hooks/use-selected-warehouse", () => ({ @@ -100,7 +102,7 @@ describe("useProductsModel - delete flow", () => { }; }); - it("loads and filters batches for the current warehouse when opening delete dialog", async () => { + it("loads batches with positive stock when opening delete dialog", async () => { mockGet.mockReturnValue({ json: vi.fn(async () => ({ success: true, @@ -144,7 +146,7 @@ describe("useProductsModel - delete flow", () => { expect(result.current.deleteDialogOpen).toBe(true); expect(result.current.deleteProduct?.id).toBe("prod-1"); - expect(result.current.deleteBatches).toHaveLength(1); + expect(result.current.deleteBatches).toHaveLength(2); expect(result.current.deleteBatches[0].warehouseId).toBe("wh-1"); expect(result.current.isCheckingDeleteBatches).toBe(false); }); @@ -196,7 +198,9 @@ describe("useProductsModel - delete flow", () => { await result.current.onSecondConfirmDelete(); }); - expect(mockDelete).toHaveBeenCalledWith("batches/warehouses/wh-1/products/prod-1/batches"); + expect(mockDelete).toHaveBeenCalledWith("products/prod-1"); + expect(mockMutate).toHaveBeenCalled(); + expect(mockGlobalMutate).toHaveBeenCalledWith(expect.any(Function)); expect(result.current.secondConfirmOpen).toBe(false); }); @@ -209,8 +213,8 @@ describe("useProductsModel - delete flow", () => { id: "b1", productId: "prod-1", productName: "Produto Teste", - warehouseId: "wh-2", - quantity: 4, + warehouseId: "wh-1", + quantity: 0, batchNumber: "L2", expirationDate: null, }, @@ -235,7 +239,7 @@ describe("useProductsModel - delete flow", () => { await result.current.onConfirmDelete(); }); - expect(mockDelete).toHaveBeenCalledWith("batches/warehouses/wh-1/products/prod-1/batches"); + expect(mockDelete).toHaveBeenCalledWith("products/prod-1"); expect(result.current.secondConfirmOpen).toBe(false); expect(result.current.deleteDialogOpen).toBe(false); }); @@ -350,8 +354,8 @@ describe("useProductsModel - delete flow", () => { await result.current.onSecondConfirmDelete(); }); - expect(mockDelete).toHaveBeenCalledWith("batches/warehouses/wh-1/products/prod-1/batches"); - expect(toast.error).toHaveBeenCalledWith("Erro ao remover produto do armazém"); + expect(mockDelete).toHaveBeenCalledWith("products/prod-1"); + expect(toast.error).toHaveBeenCalledWith("Erro ao excluir produto"); expect(result.current.isDeletingProduct).toBe(false); }); }); diff --git a/app/(pages)/products/products.model.ts b/app/(pages)/products/products.model.ts index c5b9019..0ec3389 100644 --- a/app/(pages)/products/products.model.ts +++ b/app/(pages)/products/products.model.ts @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import useSWR from "swr"; +import useSWR, { mutate as mutateGlobal } from "swr"; import { api } from "@/lib/api"; import { toast } from "sonner"; import { useSelectedWarehouse } from "@/hooks/use-selected-warehouse"; @@ -20,13 +20,6 @@ interface BatchesResponse { data: Batch[]; } -interface DeleteBatchesResponse { - message: string; - deletedCount: number; - productId: string; - warehouseId: string; -} - const DEFAULT_FILTERS: Omit = { sortBy: "name", sortOrder: "asc", @@ -226,7 +219,7 @@ export const useProductsModel = () => { if (response.success) { const filtered = response.data.filter( - (batch) => batch.warehouseId === warehouseId && batch.quantity > 0 + (batch) => batch.quantity > 0 ); setDeleteBatches(filtered); } @@ -257,17 +250,19 @@ export const useProductsModel = () => { setIsDeletingProduct(true); try { - const response = await api - .delete(`batches/warehouses/${warehouseId}/products/${deleteProduct.id}/batches`) - .json(); + await api.delete(`products/${deleteProduct.id}`).json(); - toast.success(response.message || "Produto removido do armazém com sucesso"); + toast.success("Produto excluído com sucesso"); mutate(); + mutateGlobal((key) => + typeof key === "string" && + (key.includes("products") || key.includes("batches")) + ); onCloseDeleteDialog(); } catch (err) { const error = err as { response?: { data?: { message?: string } } }; const errorMessage = - error?.response?.data?.message || "Erro ao remover produto do armazém"; + error?.response?.data?.message || "Erro ao excluir produto"; toast.error(errorMessage); } finally { setIsDeletingProduct(false); diff --git a/app/(pages)/products/products.view.test.tsx b/app/(pages)/products/products.view.test.tsx index 9f9bfa0..0200778 100644 --- a/app/(pages)/products/products.view.test.tsx +++ b/app/(pages)/products/products.view.test.tsx @@ -151,7 +151,7 @@ describe("ProductsView - delete action", () => { /> ); - expect(screen.getByText(/confirmar remoção/i)).toBeTruthy(); + expect(screen.getByText(/confirmar exclusão/i)).toBeTruthy(); }); it("shows warning block with batches when delete dialog is open", () => { From 7289a785453df24b53c5a97f492a241bdcef16ed Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 17:47:18 -0300 Subject: [PATCH 06/19] refactor: migrate product detail view to use standardized UI components and improved layout --- .../products/[id]/products-detail.view.tsx | 746 ++++++++++-------- 1 file changed, 402 insertions(+), 344 deletions(-) diff --git a/app/(pages)/products/[id]/products-detail.view.tsx b/app/(pages)/products/[id]/products-detail.view.tsx index 4b17463..7c40e2e 100644 --- a/app/(pages)/products/[id]/products-detail.view.tsx +++ b/app/(pages)/products/[id]/products-detail.view.tsx @@ -3,13 +3,11 @@ import Image from "next/image"; import { Product, ProductBatch } from "./products-detail.types"; import { - ArrowLeft, Package, Tag, Barcode, Calendar, Layers, - AlertCircle, Building2, Hash, Pencil, @@ -17,6 +15,10 @@ import { QrCode, Box, Copy, + Warehouse, + ChevronRight, + ShieldCheck, + ShieldOff, } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; @@ -26,6 +28,10 @@ import { ptBR } from "date-fns/locale"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { PermissionGate } from "@/components/permission-gate"; +import { PageContainer } from "@/components/ui/page-container"; +import { PageHeader } from "@/components/ui/page-header"; +import { LoadingState } from "@/components/ui/loading-state"; +import { ErrorState } from "@/components/ui/error-state"; interface ProductDetailViewProps { product: Product | null; @@ -44,7 +50,6 @@ export const ProductDetailView = ({ error, batchesError, }: ProductDetailViewProps) => { - // Formatters const formatDateTime = (dateString: string) => { try { return format(new Date(dateString), "dd MMM yyyy • HH:mm", { @@ -64,7 +69,7 @@ export const ProductDetailView = ({ }; const formatDate = (dateString?: string | null) => { - if (!dateString) return "Sem validade"; + if (!dateString) return "—"; try { return format(new Date(dateString), "dd/MM/yyyy", { locale: ptBR }); @@ -73,392 +78,445 @@ export const ProductDetailView = ({ } }; - // Loading State + const formatCurrency = (value?: number | null) => { + if (value === null || value === undefined) return "—"; + return new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(value); + }; + if (isLoading) { return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + + ); } - // Error State if (error || !product) { return ( -
-
- -
-

- Produto não encontrado -

-

- O produto que você está procurando não existe ou foi removido do - sistema. -

- - - -
+ + + ); } + const totalStock = batches.reduce((sum, batch) => sum + batch.quantity, 0); + return ( -
-
- -
- - -
-
-
- {/* Left Column: Image & Quick Stats (4 cols) */} -
- {/* Product Image Card */} -
-
- - {product.active ? "Ativo" : "Inativo"} - -
+ + } + /> -
- {product.imageUrl ? ( -
- {product.name} -
+ {/* Hero: Image + Identity */} +
+ {/* Product Image */} +
+
+
+ + {product.active ? ( + ) : ( -
- - - Sem Imagem - -
+ )} -
+ {product.active ? "Ativo" : "Inativo"} + +
- {/* Image Footer Stats */} -
-
- - Tipo - -
- {product.isKit ? ( - - ) : ( - - )} - {product.isKit ? "KIT / COMBO" : "UNITÁRIO"} -
+
+ {product.imageUrl ? ( +
+ {product.name}
-
- - Validade + ) : ( +
+ + + Sem Imagem -
- - {product.hasExpiration ? "CONTROLADA" : "LIVRE"} -
-
+ )}
- {/* System Info Card */} -
-

- - Metadados do Sistema -

-
-
- Criado em - - {formatDateTime(product.createdAt)} - -
-
- - Atualizado em - - - {formatDateTime(product.updatedAt)} - + {/* Quick Stats Footer */} +
+
+ + Tipo + +
+ {product.isKit ? ( + + ) : ( + + )} + {product.isKit ? "Kit" : "Un."}
-
- - UUID do Produto - - +
+
+ + Validade + +
+ + {product.hasExpiration ? "Ctrl." : "Livre"}
+
+ + Estoque + + + {totalStock} + +
+
- {/* Right Column: Details (8 cols) */} -
- {/* Main Header Card */} -
-
- -
-
-
-

- {product.name} -

-

- {product.description || - "Nenhuma descrição fornecida para este produto."} -

-
-
+ {/* Identity + Codes */} +
+ {/* Description Card */} +
+

+ Descrição +

+

+ {product.description || + "Nenhuma descrição fornecida para este produto."} +

+
-
- {/* SKU Box */} -
-
- -
-
-

- SKU (Stock Keeping Unit) -

-

- {product.sku || "NÃO DEFINIDO"} -

-
-
+ {/* Codes Grid */} +
+ {/* SKU */} + } + label="SKU" + value={product.sku || "NÃO DEFINIDO"} + mono + onCopy={ + product.sku + ? () => copyToClipboard(product.sku!, "SKU") + : undefined + } + /> - {/* Barcode Box */} -
-
- -
-
-
-

- Código de Barras -

- {product.barcodeType && ( - - {product.barcodeType} - - )} -
-

- {product.barcode || "NÃO CADASTRADO"} -

-
-
-
+ {/* Barcode */} + } + label="Código de Barras" + badge={product.barcodeType || undefined} + value={product.barcode || "NÃO CADASTRADO"} + mono + onCopy={ + product.barcode + ? () => copyToClipboard(product.barcode!, "Código de Barras") + : undefined + } + /> + + {/* Category */} + } + label="Categoria" + value={product.categoryName || "Sem Categoria"} + /> + + {/* Brand */} + } + label="Marca / Fabricante" + value={product.brand?.name || "Genérico"} + /> +
+ + {/* System Metadata */} +
+

+ Metadados +

+
+
+ + Criado em + + + {formatDateTime(product.createdAt)} + +
+
+ + Atualizado em + + + {formatDateTime(product.updatedAt)} + +
+
+ + UUID + +
+
+
+
- {/* Categorization Grid */} -
- {/* Category */} -
-
-
- -
-

- Categoria -

-
-
-

- {product.categoryName || "Sem Categoria"} -

-

- Classificação principal do item no estoque -

+ {/* Technical Specs */} + {product.attributes && Object.keys(product.attributes).length > 0 && ( +
+
+ +

+ Especificações Técnicas +

+ + {Object.keys(product.attributes).length} + +
+
+
+ {Object.entries(product.attributes).map(([key, value]) => ( +
+ + {key} + + + {value} +
-
+ ))} +
+
+
+ )} - {/* Brand */} -
-
-
- -
-

- Marca / Fabricante -

-
-
-

- {product.brand?.name || "Genérico"} -

-

- Fabricante registrado do produto -

-
-
+ {/* Batches Section */} +
+
+
+ +

+ Lotes do Produto +

+
+ + {batches.length} + +
+ + {isLoadingBatches && ( +
+ +
+ )} + + {!isLoadingBatches && batchesError && ( +
+ +
+ )} + + {!isLoadingBatches && !batchesError && batches.length === 0 && ( +
+ +

+ Nenhum lote cadastrado +

+
+ )} + + {!isLoadingBatches && !batchesError && batches.length > 0 && ( + <> + {/* Table Header — Desktop */} +
+
Lote
+
Armazém
+
Quantidade
+
Validade
+
Preço Venda
- {/* Product Batches */} -
-
-
- -

- Lotes do Produto -

-
- - {batches.length} - -
+ {/* Batch Rows */} +
+ {batches.map((batch) => ( + + {/* Batch Code */} +
+ + {batch.batchNumber || batch.batchCode || "SEM LOTE"} + +
-
- {isLoadingBatches && ( -
-
-
+ {/* Warehouse */} +
+ + + {batch.warehouseName} +
- )} - {!isLoadingBatches && batchesError && ( -

- Não foi possível carregar os lotes deste produto. -

- )} + {/* Quantity */} +
+ + {batch.quantity} + + + un + +
- {!isLoadingBatches && !batchesError && batches.length === 0 && ( -

- Nenhum lote cadastrado para este produto. -

- )} + {/* Expiration Date */} +
+ + + {formatDate(batch.expirationDate)} + +
- {!isLoadingBatches && !batchesError && batches.length > 0 && ( -
- {batches.map((batch) => ( - -
-

- {batch.batchNumber || batch.batchCode || "SEM LOTE"} -

-

- {batch.warehouseName} -

-
-
-

- {batch.quantity} un -

-
- - {formatDate(batch.expirationDate)} -
-
- - ))} + {/* Selling Price */} +
+ + {formatCurrency(batch.sellingPrice)} + +
- )} -
+ + ))}
- {/* Dynamic Attributes */} - {product.attributes && - Object.keys(product.attributes).length > 0 && ( -
-
- -

- Especificações Técnicas -

-
-
-
- {Object.entries(product.attributes).map( - ([key, value]) => ( -
- - {key} - - - {value} - -
- ), - )} -
-
-
- )} -
+ {/* Table Footer */} +
+ + Total em Estoque + + + {totalStock} + + un + + +
+ + )} +
+ + ); +}; + +/* ─── Helper Component: DataField ─── */ +function DataField({ + icon, + label, + badge, + value, + mono, + onCopy, +}: { + icon: React.ReactNode; + label: string; + badge?: string; + value: string; + mono?: boolean; + onCopy?: () => void; +}) { + return ( +
+
+ {icon} +
+
+
+ + {label} + + {badge && ( + + {badge} + + )} +
+
+ + {value} + + {onCopy && ( + + )}
-
+
); -}; +} From 4a534fd679075cc59c8b8ecee986c135d85bfc38 Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 18:10:33 -0300 Subject: [PATCH 07/19] feat: implement interactive KPI period selector and modularize sales charts in dashboard --- .../[id]/batches-detail.components.tsx | 134 ++---- .../batches/[id]/batches-detail.view.tsx | 445 +++++++++++------- app/(pages)/sales/sales-chart.view.tsx | 166 +++++++ app/(pages)/sales/sales.model.ts | 10 +- app/(pages)/sales/sales.types.ts | 2 + app/(pages)/sales/sales.view.tsx | 214 +++------ docs/endpoints/batches.md | 203 ++++++-- 7 files changed, 724 insertions(+), 450 deletions(-) create mode 100644 app/(pages)/sales/sales-chart.view.tsx diff --git a/app/(pages)/batches/[id]/batches-detail.components.tsx b/app/(pages)/batches/[id]/batches-detail.components.tsx index d0f52da..872a066 100644 --- a/app/(pages)/batches/[id]/batches-detail.components.tsx +++ b/app/(pages)/batches/[id]/batches-detail.components.tsx @@ -4,7 +4,6 @@ import Link from "next/link"; import { AlertTriangle, Archive, - Box, CalendarDays, CheckCircle2, Package, @@ -38,6 +37,8 @@ import { PermissionGate } from "@/components/permission-gate"; import { cn } from "@/lib/utils"; import type { Batch } from "../batches.types"; +/* ─── State Definitions ─── */ + export interface BatchStateView { label: string; description: string; @@ -48,36 +49,6 @@ export interface BatchStateView { meterClass: string; } -interface MetricTileProps { - label: string; - value: string; - detail: string; - icon: LucideIcon; - toneClass: string; - valueClass?: string; -} - -interface LedgerItemProps { - label: string; - value: string; - icon: LucideIcon; -} - -interface TimelineStepProps { - label: string; - value: string; - detail: string; - toneClass: string; -} - -interface BatchActionsProps { - batch: Batch; - isDeleteOpen: boolean; - onDeleteOpenChange: (open: boolean) => void; - isDeleting: boolean; - onDelete: () => void; -} - export const LOW_STOCK_THRESHOLD = 10; const activeState: BatchStateView = { @@ -130,6 +101,8 @@ const emptyState: BatchStateView = { meterClass: "bg-neutral-600", }; +/* ─── Utilities ─── */ + const parseBatchDate = (value?: string | null): Date | null => { if (!value) return null; const parsedDate = parseISO(value); @@ -224,40 +197,16 @@ export const getBatchState = ( return activeState; }; -export const LoadingBatchDetail = () => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-); +/* ─── Sub-Components ─── */ -export const MissingBatchDetail = () => ( -
-
-
- -

Batch não encontrado

-

- O lote que você procura não existe ou foi removido. -

- - - -
-
-
-); +interface MetricTileProps { + label: string; + value: string; + detail: string; + icon: LucideIcon; + toneClass: string; + valueClass?: string; +} export const MetricTile = ({ label, @@ -267,19 +216,19 @@ export const MetricTile = ({ toneClass, valueClass, }: MetricTileProps) => ( -
+
-
-

+

+

{label}

-

+

{value}

@@ -289,19 +238,32 @@ export const MetricTile = ({

{detail}

-
+ ); +interface LedgerItemProps { + label: string; + value: string; + icon: LucideIcon; +} + export const LedgerItem = ({ label, value, icon: Icon }: LedgerItemProps) => ( -
-
+
+
- {label} + {label}
-

{value}

+

{value}

); +interface TimelineStepProps { + label: string; + value: string; + detail: string; + toneClass: string; +} + export const TimelineStep = ({ label, value, @@ -311,13 +273,13 @@ export const TimelineStep = ({
-

{label}

+

{label}

{value}

-

{detail}

+

{detail}

); @@ -327,7 +289,7 @@ export const StatusBadge = ({ state }: { state: BatchStateView }) => { @@ -337,6 +299,14 @@ export const StatusBadge = ({ state }: { state: BatchStateView }) => { ); }; +interface BatchActionsProps { + batch: Batch; + isDeleteOpen: boolean; + onDeleteOpenChange: (open: boolean) => void; + isDeleting: boolean; + onDelete: () => void; +} + export const BatchActions = ({ batch, isDeleteOpen, @@ -344,12 +314,12 @@ export const BatchActions = ({ isDeleting, onDelete, }: BatchActionsProps) => ( -
+
- +
+
-
- - - - + {/* Row 2: Financial Metrics */} +
+ + + + +
+ + {/* Row 3: Location · Lifecycle · Notes */} +
+ {/* Location */} +
+
+ +

+ Localização +

+
+ +

+ Warehouse +

+

+ {batch.warehouseName} +

+ +
+ + COD: {formatOptionalText(batch.warehouseCode)} + +
-
-
-
- -

Localização

-
-

- Warehouse -

-

- {batch.warehouseName} -

-
- - COD: {formatOptionalText(batch.warehouseCode)} - -
-
+ {/* Lifecycle / Timeline */} +
+
+ +

+ Ciclo de vida +

+
-
-
- -

Ciclo de vida

-
-
- - - - -
-
+
+ + + + +
+
-
-
- -

Observações

-
-
-

- {formatOptionalText(batch.notes)} -

-
-
-
-

- ID -

-

- {batch.id} -

-
-
-

- Produto -

-

- {batch.productId} -

-
-
-
+ {/* Notes + IDs */} +
+
+ +

+ Observações +

+
+ +
+

+ {formatOptionalText(batch.notes)} +

+
+ +
+ copyToClipboard(batch.id, "ID do Lote")} + /> + copyToClipboard(batch.productId, "ID do Produto")} + /> +
- -
+
+ ); }; + +/* ─── Helper: ID Row ─── */ + +function IdField({ + label, + value, + onCopy, +}: { + label: string; + value: string; + onCopy: () => void; +}) { + return ( +
+

+ {label} +

+ +
+ ); +} diff --git a/app/(pages)/sales/sales-chart.view.tsx b/app/(pages)/sales/sales-chart.view.tsx new file mode 100644 index 0000000..5bc56b0 --- /dev/null +++ b/app/(pages)/sales/sales-chart.view.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend, +} from "recharts"; +import type { DailyChartEntry } from "./sales.types"; +import { formatCents } from "./sales.types"; + +interface SalesChartProps { + data: DailyChartEntry[]; +} + +const useIsMobile = (breakpoint = 768): boolean => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < breakpoint); + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, [breakpoint]); + + return isMobile; +}; + +const formatXAxisLabel = (value: string, isMobile: boolean): string => { + const parts = value.split("-"); + const label = `${parts[1]}/${parts[2]}`; + return isMobile ? label : `${parts[2]}/${parts[1]}`; +}; + +const CustomTooltip = ({ active, payload, label }: { + active?: boolean; + payload?: Array<{ value: number; name: string; color: string }>; + label?: string; +}) => { + if (!active || !payload?.length) return null; + + return ( +
+
+ {label?.split("-").reverse().join("/")} +
+ {payload.map((entry) => ( +
+
+ + {entry.name === "count" ? "Vendas" : "Faturamento"} + + + {entry.name === "count" + ? entry.value + : formatCents(entry.value)} + +
+ ))} +
+ ); +}; + +export const SalesChart = ({ data }: SalesChartProps) => { + const isMobile = useIsMobile(); + + const xTickFormatter = useCallback( + (value: string) => formatXAxisLabel(value, isMobile), + [isMobile], + ); + + return ( +
+ + + + + + `R$${(v / 100).toFixed(0)}`} + axisLine={!isMobile} + stroke="#262626" + width={isMobile ? 0 : 55} + /> + } /> + + value === "count" ? "Vendas" : "Faturamento" + } + wrapperStyle={{ fontSize: 10, color: "#737373" }} + verticalAlign={isMobile ? "top" : "bottom"} + align={isMobile ? "right" : "center"} + iconSize={isMobile ? 8 : 10} + /> + + + + +
+ ); +}; diff --git a/app/(pages)/sales/sales.model.ts b/app/(pages)/sales/sales.model.ts index 69e84db..a485cc1 100644 --- a/app/(pages)/sales/sales.model.ts +++ b/app/(pages)/sales/sales.model.ts @@ -1,10 +1,11 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useCallback } from "react"; import useSWR from "swr"; import { api } from "@/lib/api"; import { toast } from "sonner"; import { useSelectedWarehouse } from "@/hooks/use-selected-warehouse"; import { DateFilterPreset, + KpiPeriodKey, SaleFilterDraft, SalesResponse, SaleFilters, @@ -98,6 +99,7 @@ export const useSalesModel = () => { const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false); const [mobileFiltersDraft, setMobileFiltersDraft] = useState(buildDefaultFilterDraft); + const [kpiPeriod, setKpiPeriod] = useState("today"); const url = useMemo(() => { if (!warehouseId) return null; @@ -228,6 +230,10 @@ export const useSalesModel = () => { setMobileFiltersDraft((prev) => ({ ...prev, [key]: value })); }; + const onKpiPeriodChange = useCallback((period: KpiPeriodKey) => { + setKpiPeriod(period); + }, []); + return { sales, isLoading, @@ -236,6 +242,8 @@ export const useSalesModel = () => { mobileFiltersDraft, isMobileFiltersOpen, pagination, + kpiPeriod, + onKpiPeriodChange, onPageChange, onFilterChange, onDatePresetChange, diff --git a/app/(pages)/sales/sales.types.ts b/app/(pages)/sales/sales.types.ts index 7a58a5c..ef35072 100644 --- a/app/(pages)/sales/sales.types.ts +++ b/app/(pages)/sales/sales.types.ts @@ -63,6 +63,8 @@ export interface KpiPeriod { avgTicket: number; } +export type KpiPeriodKey = "today" | "week" | "month"; + export interface DailyChartEntry { date: string; count: number; diff --git a/app/(pages)/sales/sales.view.tsx b/app/(pages)/sales/sales.view.tsx index 9cc2300..6ec6743 100644 --- a/app/(pages)/sales/sales.view.tsx +++ b/app/(pages)/sales/sales.view.tsx @@ -44,16 +44,7 @@ import { import Link from "next/link"; import { PermissionGate } from "@/components/permission-gate"; import { InsightCard } from "@/components/ui/insight-card"; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - Legend, -} from "recharts"; +import { SalesChart } from "./sales-chart.view"; import { SaleSummary, SaleStatus, @@ -62,6 +53,7 @@ import { SaleFilterDraft, PaymentMethod, DateFilterPreset, + KpiPeriodKey, PAYMENT_METHOD_LABELS, SALE_STATUS_LABELS, formatCents, @@ -92,6 +84,8 @@ interface SalesViewProps { }; dashboardData: SalesDashboardData | null; dashboardLoading: boolean; + kpiPeriod: KpiPeriodKey; + onKpiPeriodChange: (period: KpiPeriodKey) => void; onPageChange: (page: number) => void; onFilterChange: ( key: K, @@ -264,6 +258,8 @@ export const SalesView = ({ pagination, dashboardData, dashboardLoading, + kpiPeriod, + onKpiPeriodChange, onPageChange, onFilterChange, onDatePresetChange, @@ -632,74 +628,62 @@ export const SalesView = ({
{/* KPI Cards */} -
- {dashboardLoading ? ( -
-
-
- ) : ( - dashboardData && ( - <> - - - - - - - - - - - ) - )} +
+ {/* Period Toggle */} +
+ {( + [ + { key: "today", label: "Hoje" }, + { key: "week", label: "Semana" }, + { key: "month", label: "Mês" }, + ] as const + ).map(({ key, label }) => ( + + ))} +
+ +
+ {dashboardLoading ? ( +
+
+
+ ) : ( + dashboardData && ( + <> + + + + + ) + )} +
{/* Monthly Chart */} @@ -708,79 +692,7 @@ export const SalesView = ({

Vendas do Mês

-
- - - - - v.split("-").slice(1).join("/") - } - stroke="#262626" - /> - - `R$${(v / 100).toFixed(0)}`} - stroke="#262626" - width={70} - /> - - v.split("-").reverse().join("/") - } - formatter={(value: number, name: string) => { - if (name === "revenue") - return [formatCents(value), "Faturamento"]; - return [value, "Vendas"]; - }} - /> - - value === "count" ? "Vendas" : "Faturamento" - } - wrapperStyle={{ fontSize: 10, color: "#737373" }} - /> - - - - -
+
)} diff --git a/docs/endpoints/batches.md b/docs/endpoints/batches.md index 2f62314..1a1108e 100644 --- a/docs/endpoints/batches.md +++ b/docs/endpoints/batches.md @@ -1,6 +1,7 @@ # Batch Endpoints ## Overview + Batches represent specific quantities of products stored in warehouses. Each batch tracks quantity, expiration date, and location. **Base URL**: `/api/batches` @@ -9,39 +10,45 @@ Batches represent specific quantities of products stored in warehouses. Each bat --- ## POST /api/batches + **Summary**: Create a new batch ### Authorization -**Required Permissions**: `BATCH_CREATE` or `ROLE_ADMIN` + +**Required Permissions**: `batches:create` ### Request + **Method**: `POST` **Content-Type**: `application/json` #### Request Body + ```json { "productId": "550e8400-e29b-41d4-a716-446655440000", "warehouseId": "660e8400-e29b-41d4-a716-446655440001", - "quantity": 100, + "quantity": 100.0, "batchCode": "BATCH-2025-001", "expirationDate": "2026-12-31", "costPrice": 1050, - "notes": "Initial stock from supplier" + "sellingPrice": 2000 } ``` **Field Details**: + - `productId`: Required, UUID of the product - `warehouseId`: Required, UUID of the warehouse -- `quantity`: Required, positive integer +- `quantity`: Required, positive number (BigDecimal format) - `batchCode`: Optional, unique batch identifier. If not provided, will be auto-generated in format `BATCH-YYYYMMDD-XXX` - `manufacturedDate`: Optional, ISO date string for manufacturing date - `expirationDate`: Optional, ISO date string. When present, the product is treated as using expiration tracking. - `costPrice`: Optional, cost per unit in cents (e.g., 1050 = R$10,50) -- `notes`: Optional, additional notes +- `sellingPrice`: Optional, selling price per unit in cents (e.g., 2000 = R$20,00) ### Response + **Status Code**: `201 CREATED` ```json @@ -52,24 +59,25 @@ Batches represent specific quantities of products stored in warehouses. Each bat "id": "770e8400-e29b-41d4-a716-446655440002", "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", "warehouseId": "660e8400-e29b-41d4-a716-446655440001", "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 100, + "originStockMovementItemId": null, + "originStockMovementId": null, + "originStockMovementCode": null, "batchCode": "BATCH-2025-001", + "quantity": 100.0, "manufacturedDate": "2026-01-01", "expirationDate": "2026-12-31", "costPrice": 1050, - "notes": "Initial stock from supplier", + "sellingPrice": 2000, "createdAt": "2025-12-28T10:00:00Z", "updatedAt": "2025-12-28T10:00:00Z" } } -} ``` ### Frontend Implementation Guide + 1. **Product Selector**: Autocomplete/dropdown with search 2. **Warehouse Selector**: Dropdown of active warehouses 3. **Quantity Input**: Numeric input with validation (positive numbers) @@ -82,23 +90,29 @@ Batches represent specific quantities of products stored in warehouses. Each bat --- ## POST /api/batches/with-product + **Summary**: Create a new product with initial stock in warehouse ### Authorization -**Required Permissions**: `BATCH_CREATE` and `PRODUCT_CREATE` or `ROLE_ADMIN` + +**Required Permissions**: `batches:create` and `products:create` ### Description + This endpoint atomically creates a new product and its first batch in a single transaction. Use this when receiving a new product that needs to be registered and stocked immediately. If the product already exists, use `POST /api/batches` instead. ### Request + **Method**: `POST` **Content-Type**: `multipart/form-data` #### Request Parts + - `product`: JSON object (see below) - `image`: Optional, image file (PNG, JPG, JPEG, WEBP) #### Product JSON Structure + ```json { "name": "New Product Name", @@ -125,6 +139,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` **Product Fields**: + - `name`: Required, product name - `description`: Optional, detailed product description - `categoryId`: Optional, UUID of the category @@ -137,15 +152,17 @@ This endpoint atomically creates a new product and its first batch in a single t - `attributes`: Optional, custom product attributes as key-value pairs **Batch Fields**: + - `warehouseId`: Required, UUID of the warehouse - `batchCode`: Optional, unique batch identifier. If not provided, will be auto-generated in format `BATCH-YYYYMMDD-XXX` -- `quantity`: Required, positive integer or zero +- `quantity`: Required, positive number or zero (BigDecimal format) - `manufacturedDate`: Optional, ISO date string - `expirationDate`: Optional, ISO date string (required if `hasExpiration: true`) - `costPrice`: Optional, cost per unit in cents (e.g., 1050 = R$10,50) - `sellingPrice`: Optional, selling price per unit in cents (e.g., 2000 = R$20,00) ### Response + **Status Code**: `201 CREATED` ```json @@ -184,8 +201,11 @@ This endpoint atomically creates a new product and its first batch in a single t "productName": "New Product Name", "warehouseId": "660e8400-e29b-41d4-a716-446655440001", "warehouseName": "Main Warehouse", + "originStockMovementItemId": null, + "originStockMovementId": null, + "originStockMovementCode": null, "batchCode": "BATCH-2026-001", - "quantity": 100, + "quantity": 100.0, "manufacturedDate": "2026-01-01", "expirationDate": "2026-12-31", "costPrice": 1050, @@ -200,6 +220,7 @@ This endpoint atomically creates a new product and its first batch in a single t ### Error Responses #### 400 Bad Request - SKU Already Exists + ```json { "status": 400, @@ -210,6 +231,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 400 Bad Request - Barcode Already Exists + ```json { "status": 400, @@ -220,6 +242,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 400 Bad Request - Warehouse Inactive + ```json { "status": 400, @@ -230,6 +253,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 400 Bad Request - Batch Code Already Exists + ```json { "status": 400, @@ -240,6 +264,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 400 Bad Request - Invalid Date Range + ```json { "status": 400, @@ -250,6 +275,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 404 Not Found - Warehouse Not Found + ```json { "status": 404, @@ -260,6 +286,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` #### 404 Not Found - Category Not Found + ```json { "status": 404, @@ -270,6 +297,7 @@ This endpoint atomically creates a new product and its first batch in a single t ``` ### Frontend Implementation Guide + 1. **Form Handling**: Use `FormData` to handle file upload and JSON data (`product` part) 2. **Image Preview**: Implement client-side image preview before upload 3. **Use Case**: Product registration + stock entry workflow @@ -288,6 +316,7 @@ This endpoint atomically creates a new product and its first batch in a single t 9. **UX Tip**: Combine product and batch forms in a single wizard or tabbed interface ### When to Use This Endpoint + - ✅ Registering a brand new product with initial stock and optional image - ✅ Receiving new products from suppliers - ✅ Quick product + stock entry workflow @@ -297,15 +326,19 @@ This endpoint atomically creates a new product and its first batch in a single t --- ## GET /api/batches + **Summary**: Get all batches ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` ### Response + **Status Code**: `200 OK` ```json @@ -317,19 +350,17 @@ This endpoint atomically creates a new product and its first batch in a single t "id": "770e8400-e29b-41d4-a716-446655440002", "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", "warehouseId": "660e8400-e29b-41d4-a716-446655440001", "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "originStockMovementItemId": "990e8400-e29b-41d4-a716-446655440000", - "originStockMovementId": "880e8400-e29b-41d4-a716-446655440000", - "originStockMovementCode": "MOV-2026-0001", - "quantity": 100, + "originStockMovementItemId": null, + "originStockMovementId": null, + "originStockMovementCode": null, "batchCode": "BATCH-2025-001", + "quantity": 100.0, "manufacturedDate": "2026-01-01", "expirationDate": "2026-12-31", "costPrice": 1050, - "notes": "Initial stock from supplier", + "sellingPrice": 2000, "createdAt": "2025-12-28T10:00:00Z", "updatedAt": "2025-12-28T10:00:00Z" } @@ -337,9 +368,8 @@ This endpoint atomically creates a new product and its first batch in a single t } ``` -Origin fields are nullable. They are filled when the batch was created by a stock movement entry and point to the movement item that created the batch. - ### Frontend Implementation Guide + 1. **Table View**: Display batches in data table 2. **Columns**: Product, Warehouse, Quantity, Batch #, Expiration, Actions 3. **Expiration Warning**: Highlight batches near expiration @@ -352,16 +382,20 @@ Origin fields are nullable. They are filled when the batch was created by a stoc --- ## GET /api/batches/{id} + **Summary**: Get batch by ID ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` **URL Parameters**: `id` (UUID) - Batch identifier ### Response + **Status Code**: `200 OK` ```json @@ -372,16 +406,17 @@ Origin fields are nullable. They are filled when the batch was created by a stoc "id": "770e8400-e29b-41d4-a716-446655440002", "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", "warehouseId": "660e8400-e29b-41d4-a716-446655440001", "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 100, + "originStockMovementItemId": null, + "originStockMovementId": null, + "originStockMovementCode": null, "batchCode": "BATCH-2025-001", + "quantity": 100.0, "manufacturedDate": "2026-01-01", "expirationDate": "2026-12-31", "costPrice": 1050, - "notes": "Initial stock from supplier", + "sellingPrice": 2000, "createdAt": "2025-12-28T10:00:00Z", "updatedAt": "2025-12-28T10:00:00Z" } @@ -389,6 +424,7 @@ Origin fields are nullable. They are filled when the batch was created by a stoc ``` ### Frontend Implementation Guide + 1. **Detail View**: Show all batch information 2. **Product Link**: Link to product detail page 3. **Warehouse Link**: Link to warehouse detail page @@ -400,19 +436,24 @@ Origin fields are nullable. They are filled when the batch was created by a stoc --- ## GET /api/batches/warehouse/{warehouseId} + **Summary**: Get batches by warehouse ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` **URL Parameters**: `warehouseId` (UUID) - Warehouse identifier ### Response + Same format as GET /api/batches (returns array of batches) ### Frontend Implementation Guide + 1. **Warehouse View**: Use in warehouse detail page 2. **Stock Overview**: Show all products in warehouse 3. **Filtering**: Additional filters within warehouse @@ -422,19 +463,24 @@ Same format as GET /api/batches (returns array of batches) --- ## GET /api/batches/product/{productId} + **Summary**: Get batches by product ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` **URL Parameters**: `productId` (UUID) - Product identifier ### Response + Same format as GET /api/batches (returns array of batches) ### Frontend Implementation Guide + 1. **Product View**: Use in product detail page 2. **Stock Locations**: Show where product is stored 3. **Total Quantity**: Calculate total across all batches @@ -443,22 +489,57 @@ Same format as GET /api/batches (returns array of batches) --- +## GET /api/batches/warehouses/{warehouseId}/products/{productId}/batches + +**Summary**: Get batches by warehouse and product + +### Authorization + +**Required Permissions**: `batches:read` + +### Request + +**Method**: `GET` +**URL Parameters**: + +- `warehouseId` (UUID) - Warehouse identifier +- `productId` (UUID) - Product identifier + +### Response + +Same format as GET /api/batches (returns array of batches) + +### Frontend Implementation Guide + +1. **Product-in-Warehouse View**: Use in warehouse product detail pages +2. **Precise Stock Query**: Fetch only batches for a specific product in a specific warehouse +3. **Batch Selection**: Support FEFO/FIFO batch picking in transfer and picking flows +4. **Scoped Inventory Actions**: Load only relevant batches before stock adjustments +5. **Performance**: Prefer this endpoint over client-side filtering when both IDs are known + +--- + ## GET /api/batches/expiring/{daysAhead} + **Summary**: Get batches expiring in next N days ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` **URL Parameters**: `daysAhead` (Integer) - Number of days to look ahead **Example**: `/api/batches/expiring/30` - Get batches expiring in next 30 days ### Response + Same format as GET /api/batches (returns array of expiring batches) ### Frontend Implementation Guide + 1. **Dashboard Widget**: Show on dashboard as alert widget 2. **Color Coding**: Red for <7 days, yellow for 7-30 days 3. **Urgency Sorting**: Sort by expiration date (soonest first) @@ -469,21 +550,26 @@ Same format as GET /api/batches (returns array of expiring batches) --- ## GET /api/batches/low-stock/{threshold} + **Summary**: Get batches with quantity below threshold ### Authorization -**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` + +**Required Permissions**: `batches:read` ### Request + **Method**: `GET` **URL Parameters**: `threshold` (Integer) - Quantity threshold **Example**: `/api/batches/low-stock/10` - Get batches with quantity < 10 ### Response + Same format as GET /api/batches (returns array of low-stock batches) ### Frontend Implementation Guide + 1. **Dashboard Widget**: Low stock alert on dashboard 2. **Reorder List**: Use for creating purchase orders 3. **Threshold Settings**: Allow setting per-product thresholds @@ -494,22 +580,27 @@ Same format as GET /api/batches (returns array of low-stock batches) --- ## PUT /api/batches/{id} + **Summary**: Update batch ### Authorization -**Required Permissions**: `BATCH_UPDATE` or `ROLE_ADMIN` + +**Required Permissions**: `batches:update` ### Request + **Method**: `PUT` **URL Parameters**: `id` (UUID) - Batch identifier **Content-Type**: `application/json` #### Request Body + Same structure as POST /api/batches **Note**: Quantity should typically be updated via stock movements, not direct updates. ### Response + **Status Code**: `200 OK` ```json @@ -523,6 +614,7 @@ Same structure as POST /api/batches ``` ### Frontend Implementation Guide + 1. **Edit Form**: Pre-populate with current values 2. **Restricted Fields**: Disable product and warehouse fields 3. **Quantity Warning**: Warn about direct quantity changes @@ -533,16 +625,20 @@ Same structure as POST /api/batches --- ## DELETE /api/batches/{id} + **Summary**: Delete batch ### Authorization -**Required Permissions**: `BATCH_DELETE` or `ROLE_ADMIN` + +**Required Permissions**: `batches:delete` ### Request + **Method**: `DELETE` **URL Parameters**: `id` (UUID) - Batch identifier ### Response + **Status Code**: `200 OK` ```json @@ -554,6 +650,7 @@ Same structure as POST /api/batches ``` ### Frontend Implementation Guide + 1. **Confirmation**: Require strong confirmation 2. **Quantity Check**: Warn if batch still has quantity 3. **Movement Check**: Warn if batch has movements @@ -563,24 +660,30 @@ Same structure as POST /api/batches --- -## DELETE /api/warehouses/{warehouseId}/products/{productId}/batches +## DELETE /api/batches/warehouses/{warehouseId}/products/{productId}/batches + **Summary**: Delete all batches of a product in a warehouse ### Authorization -**Required Permissions**: `BATCH_DELETE` or `ROLE_ADMIN` + +**Required Permissions**: `batches:delete` ### Description + This endpoint performs a bulk soft-delete operation, removing all batches that match the specified product and warehouse combination. The batches are soft-deleted (marked with a deletion timestamp) rather than permanently removed, preserving data for audit purposes. The operation is scoped to the current tenant and validates that both the warehouse and product exist before proceeding. ### Request + **Method**: `DELETE` **URL Parameters**: + - `warehouseId` (UUID) - Warehouse identifier - `productId` (UUID) - Product identifier -**Example**: `/api/warehouses/660e8400-e29b-41d4-a716-446655440001/products/550e8400-e29b-41d4-a716-446655440000/batches` +**Example**: `/api/batches/warehouses/660e8400-e29b-41d4-a716-446655440001/products/550e8400-e29b-41d4-a716-446655440000/batches` ### Response + **Status Code**: `200 OK` ```json @@ -593,6 +696,7 @@ This endpoint performs a bulk soft-delete operation, removing all batches that m ``` **Response Fields**: + - `message`: Descriptive message indicating the number of batches deleted - `deletedCount`: Integer count of batches that were soft-deleted - `productId`: UUID of the product (confirmation) @@ -601,7 +705,9 @@ This endpoint performs a bulk soft-delete operation, removing all batches that m ### Success Scenarios #### Batches Deleted + Returns 200 with the count of deleted batches: + ```json { "message": "Successfully deleted 3 batches", @@ -612,7 +718,9 @@ Returns 200 with the count of deleted batches: ``` #### No Batches to Delete + Returns 200 with zero count (idempotent operation): + ```json { "message": "Successfully deleted 0 batches", @@ -625,6 +733,7 @@ Returns 200 with zero count (idempotent operation): ### Error Responses #### 404 Not Found - Warehouse Not Found + ```json { "status": 404, @@ -635,6 +744,7 @@ Returns 200 with zero count (idempotent operation): ``` #### 404 Not Found - Product Not Found + ```json { "status": 404, @@ -645,6 +755,7 @@ Returns 200 with zero count (idempotent operation): ``` #### 403 Forbidden - Insufficient Permissions + ```json { "status": 403, @@ -664,7 +775,7 @@ Returns 200 with zero count (idempotent operation): 2. **Validation**: - Verify warehouse and product IDs are valid UUIDs - - Check user has BATCH_DELETE permission before showing delete option + - Check user has `batches:delete` permission before showing delete option - Consider checking if batches have recent movements before allowing deletion 3. **Success Feedback**: @@ -725,6 +836,7 @@ Returns 200 with zero count (idempotent operation): ## Frontend Component Examples ### Batch Table + ```typescript interface BatchTableProps { batches: Batch[]; @@ -753,6 +865,7 @@ interface BatchTableProps { ``` ### Batch Status Badges + ```typescript interface BatchStatusProps { batch: Batch; @@ -769,10 +882,11 @@ interface BatchStatusProps { ``` ### Stock Level Visualization + ```typescript interface StockLevelProps { batches: Batch[]; - groupBy: 'warehouse' | 'product'; + groupBy: "warehouse" | "product"; } // Visualizations: @@ -788,6 +902,7 @@ interface StockLevelProps { ## Frontend Best Practices ### Expiration Management + 1. **Color System**: Consistent color coding for expiration status 2. **FEFO Display**: Show First-Expired-First-Out order 3. **Alerts**: Proactive alerts before expiration @@ -795,6 +910,7 @@ interface StockLevelProps { 5. **Reports**: Expiration reports and forecasts ### Stock Monitoring + 1. **Real-time Updates**: Update stock levels in real-time 2. **Threshold Alerts**: Configurable low-stock thresholds 3. **Multi-level Alerts**: Different alert levels (critical, warning, info) @@ -802,6 +918,7 @@ interface StockLevelProps { 5. **Dashboard Widgets**: Key metrics on dashboard ### Data Entry + 1. **Smart Defaults**: Pre-fill common values 2. **Batch Number Generation**: Auto-generate with pattern 3. **Barcode Integration**: Scan products for quick entry @@ -809,6 +926,7 @@ interface StockLevelProps { 5. **Error Prevention**: Prevent common mistakes ### Performance + 1. **Pagination**: Essential for large batch lists 2. **Lazy Loading**: Load details on demand 3. **Caching**: Cache batch data with smart invalidation @@ -820,6 +938,7 @@ interface StockLevelProps { ## Common Error Responses ### 400 Bad Request - Duplicate Batch Number + ```json { "success": false, @@ -829,6 +948,7 @@ interface StockLevelProps { ``` ### 400 Bad Request - Invalid Quantity + ```json { "success": false, @@ -838,6 +958,7 @@ interface StockLevelProps { ``` ### 409 Conflict - Batch in Use + ```json { "success": false, @@ -853,18 +974,22 @@ interface StockLevelProps { ## Integration Points ### With Products + - Validate product has expiration if expiration date provided - Display product details in batch views ### With Warehouses + - Validate warehouse is active - Show warehouse capacity and utilization ### With Stock Movements + - Batches are affected by stock movements - Movement history shows batch transactions ### With Reports + - Stock reports aggregate batch data - Expiration reports use batch expiration dates - Cost analysis uses batch cost prices From 0053ad2c44ff741b2a0f9e58334386d977ef498d Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 18:31:41 -0300 Subject: [PATCH 08/19] docs: standardize error response formats, update security policies, and refactor API endpoint documentation. --- docs/endpoints/README.md | 419 ++------------- docs/endpoints/audit.md | 87 +++ docs/endpoints/auth.md | 60 ++- docs/endpoints/batches.md | 29 +- docs/endpoints/brands.md | 73 ++- docs/endpoints/categories.md | 109 ++-- docs/endpoints/permissions.md | 200 +------ docs/endpoints/products.md | 116 ++-- docs/endpoints/reports copy.md | 849 ------------------------------ docs/endpoints/reports.md | 535 +++++++++++++++---- docs/endpoints/roles.md | 348 ++---------- docs/endpoints/sales.md | 330 +++--------- docs/endpoints/stock-movements.md | 192 +++---- docs/endpoints/tenants.md | 86 +++ docs/endpoints/transfer.md | 360 +++---------- docs/endpoints/users.md | 165 ++++-- docs/endpoints/warehouses.md | 585 +++----------------- 17 files changed, 1437 insertions(+), 3106 deletions(-) create mode 100644 docs/endpoints/audit.md delete mode 100644 docs/endpoints/reports copy.md create mode 100644 docs/endpoints/tenants.md diff --git a/docs/endpoints/README.md b/docs/endpoints/README.md index 5c5ce3a..b198c04 100644 --- a/docs/endpoints/README.md +++ b/docs/endpoints/README.md @@ -1,142 +1,27 @@ # StockShift API Endpoint Documentation -## Overview -This directory contains comprehensive endpoint documentation for the StockShift inventory management API. Each file provides detailed instructions for AI agents developing a frontend application. +This directory documents the frontend-facing HTTP API. The backend is the source of truth; when there is a conflict, use the backend controller and DTO definitions. -## Purpose -These documents are designed to guide AI agents in building a complete, production-ready frontend application that integrates with the StockShift backend API. +## Base URL -## Document Structure - -Each endpoint documentation file follows this structure: -1. **Overview**: General description and base URL -2. **Endpoint Details**: For each endpoint: - - Summary and purpose - - Authorization requirements - - Request format with examples - - Response format with examples - - Frontend Implementation Guide -3. **Component Examples**: Reusable component suggestions -4. **Best Practices**: Frontend development best practices -5. **Error Handling**: Common errors and how to handle them -6. **Integration Points**: How endpoints relate to each other - -## Files - -### [auth.md](auth.md) -**Authentication & Authorization** -- User login and registration -- Token management (access & refresh tokens) -- Session handling -- Tenant registration - -**Key Concepts**: -- JWT Bearer token authentication -- Token refresh mechanism -- Multi-tenant architecture - ---- - -### [products.md](products.md) -**Product Management** -- CRUD operations for products -- Product search and filtering -- Barcode and SKU lookup -- Category assignment - -**Key Concepts**: -- Product attributes (dynamic JSON) -- Barcode types (EAN13, UPC, etc.) -- Product kits -- Expiration tracking - ---- - -### [categories.md](categories.md) -**Category Management** -- Hierarchical category structure -- CRUD operations -- Parent-child relationships - -**Key Concepts**: -- Tree structure management -- Recursive parent-child relationships -- Category attributes - ---- - -### [warehouses.md](warehouses.md) -**Warehouse Management** -- Physical location management -- CRUD operations -- Active/inactive status - -**Key Concepts**: -- Multi-warehouse support -- Warehouse codes -- Contact information - ---- - -### [batches.md](batches.md) -**Batch/Stock Management** -- Stock batch tracking -- Quantity management -- Expiration date tracking -- Cost price tracking - -**Key Concepts**: -- Batch numbers -- FEFO (First Expired First Out) -- Low stock alerts -- Expiring product alerts - ---- - -### [reports.md](reports.md) -**Reporting & Analytics** -- Dashboard metrics -- Stock reports -- Low stock alerts -- Expiring products - -**Key Concepts**: -- Real-time metrics -- Aggregated data -- Alert thresholds -- Data visualization - -## API Base URL - -``` +```text Development: http://localhost:8080 -Production: [To be configured] -``` - -## Authentication - -All endpoints except authentication endpoints require a Bearer token in the Authorization header: - -``` -Authorization: Bearer {accessToken} ``` ## Response Format -All API responses follow this standard format: +Most authenticated endpoints return `ApiResponse`: -### Success Response ```json { "success": true, - "message": "Optional success message", - "data": { - // Response data - } + "message": "Optional message", + "data": {} } ``` -### Error Response +Errors generally return: + ```json { "success": false, @@ -145,265 +30,39 @@ All API responses follow this standard format: } ``` -## Permissions - -The API uses a permission-based authorization system. Each endpoint specifies required permissions. - -### Permission Format -- `{RESOURCE}_{ACTION}` (e.g., `PRODUCT_CREATE`, `WAREHOUSE_READ`) -- `ROLE_ADMIN` - Full access to all resources - -### Common Permissions -- **Products**: `PRODUCT_CREATE`, `PRODUCT_READ`, `PRODUCT_UPDATE`, `PRODUCT_DELETE` -- **Categories**: `CATEGORY_CREATE`, `CATEGORY_READ`, `CATEGORY_UPDATE`, `CATEGORY_DELETE` -- **Warehouses**: `WAREHOUSE_CREATE`, `WAREHOUSE_READ`, `WAREHOUSE_UPDATE`, `WAREHOUSE_DELETE` -- **Batches**: `BATCH_CREATE`, `BATCH_READ`, `BATCH_UPDATE`, `BATCH_DELETE` -- **Reports**: `REPORT_READ` - -## Frontend Development Guidelines - -### Technology Recommendations -- **Framework**: React, Vue, or Angular -- **State Management**: Redux, Vuex, or NgRx -- **HTTP Client**: Axios or Fetch API -- **UI Components**: Material-UI, Ant Design, or Chakra UI -- **Charts**: Chart.js, Recharts, or D3.js -- **Forms**: React Hook Form, Formik, or VeeValidate - -### Architecture Patterns -1. **Modular Structure**: Organize by feature/domain -2. **Service Layer**: Separate API calls from components -3. **State Management**: Centralized state with clear actions -4. **Error Handling**: Global error handler with local overrides -5. **Authentication**: Token storage and automatic refresh -6. **Routing**: Protected routes with permission checks +Some endpoints intentionally return non-JSON responses, such as redirects, CSV/XLSX downloads, or empty webhook responses. Those exceptions are documented in the endpoint-specific files. -### Code Organization Example -``` -src/ -├── components/ -│ ├── common/ -│ ├── products/ -│ ├── warehouses/ -│ └── ... -├── services/ -│ ├── api/ -│ │ ├── auth.service.ts -│ │ ├── products.service.ts -│ │ └── ... -│ └── ... -├── store/ -│ ├── auth/ -│ ├── products/ -│ └── ... -├── hooks/ -├── utils/ -├── types/ -└── pages/ -``` - -### API Service Layer Example - -```typescript -// services/api/products.service.ts -import { apiClient } from './client'; -import { Product, ProductRequest } from '@/types'; - -export const productsService = { - getAll: async (): Promise => { - const response = await apiClient.get('/api/products'); - return response.data.data; - }, - - getById: async (id: string): Promise => { - const response = await apiClient.get(`/api/products/${id}`); - return response.data.data; - }, - - create: async (data: ProductRequest): Promise => { - const response = await apiClient.post('/api/products', data); - return response.data.data; - }, - - update: async (id: string, data: ProductRequest): Promise => { - const response = await apiClient.put(`/api/products/${id}`, data); - return response.data.data; - }, - - delete: async (id: string): Promise => { - await apiClient.delete(`/api/products/${id}`); - }, - - search: async (query: string): Promise => { - const response = await apiClient.get(`/api/products/search?q=${query}`); - return response.data.data; - } -}; -``` - -### Authentication Setup Example - -```typescript -// services/api/client.ts -import axios from 'axios'; -import { authService } from './auth.service'; - -export const apiClient = axios.create({ - baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8080', - headers: { - 'Content-Type': 'application/json' - } -}); +## Authentication -// Request interceptor: Add auth token -apiClient.interceptors.request.use((config) => { - const token = localStorage.getItem('accessToken'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); +Most endpoints require a JWT Bearer token: -// Response interceptor: Handle token refresh -apiClient.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - try { - const refreshToken = localStorage.getItem('refreshToken'); - const response = await authService.refresh(refreshToken); - - localStorage.setItem('accessToken', response.accessToken); - originalRequest.headers.Authorization = `Bearer ${response.accessToken}`; - - return apiClient(originalRequest); - } catch (refreshError) { - // Refresh failed, redirect to login - localStorage.clear(); - window.location.href = '/login'; - return Promise.reject(refreshError); - } - } - - return Promise.reject(error); - } -); +```text +Authorization: Bearer {accessToken} ``` -## Testing Recommendations - -### Unit Tests -- Test service functions -- Test utility functions -- Test state management actions/reducers - -### Integration Tests -- Test API integration -- Test authentication flow -- Test error handling - -### E2E Tests -- Test critical user flows -- Test permission-based access - -## Accessibility - -Ensure the frontend application follows accessibility standards: -- WCAG 2.1 Level AA compliance -- Keyboard navigation support -- Screen reader compatibility -- Proper ARIA labels -- Color contrast ratios -- Focus indicators - -## Internationalization (i18n) - -Consider implementing multi-language support: -- Use i18n library (react-i18next, vue-i18n) -- Externalize all user-facing text -- Support date/time localization -- Support number/currency formatting - -## Performance Optimization - -### Best Practices -1. **Code Splitting**: Split code by route -2. **Lazy Loading**: Load components on demand -3. **Memoization**: Use React.memo, useMemo, useCallback -4. **Virtualization**: Virtual scrolling for large lists -5. **Debouncing**: Debounce search and filter inputs -6. **Caching**: Cache API responses with TTL -7. **Optimistic Updates**: Update UI before API confirmation -8. **Image Optimization**: Optimize and lazy load images -9. **Bundle Size**: Minimize bundle size -10. **CDN**: Serve static assets from CDN - -## Security Considerations - -### Frontend Security -1. **XSS Protection**: Sanitize user input -2. **CSRF Protection**: Include CSRF tokens if required -3. **Secure Storage**: Use HttpOnly cookies or secure storage for tokens -4. **Input Validation**: Validate all user input -5. **Permission Checks**: Hide/disable unauthorized actions -6. **HTTPS Only**: Enforce HTTPS in production -7. **Content Security Policy**: Implement CSP headers -8. **Dependency Security**: Regularly update dependencies - -## Mobile Considerations - -### Responsive Design -- Mobile-first approach -- Breakpoints for different screen sizes -- Touch-friendly UI elements -- Optimized navigation for mobile - -### Progressive Web App (PWA) -Consider implementing PWA features: -- Service workers for offline support -- App manifest -- Push notifications -- Install prompts - -## Browser Support - -Target modern browsers: -- Chrome (last 2 versions) -- Firefox (last 2 versions) -- Safari (last 2 versions) -- Edge (last 2 versions) - -## Development Workflow - -### Recommended Steps -1. **Setup**: Initialize project with chosen framework -2. **Authentication**: Implement auth flow first -3. **API Services**: Create service layer for all endpoints -4. **Core Features**: Build core features (products, warehouses, batches) -5. **Reports**: Build dashboard and reports -6. **Testing**: Write tests for critical paths -7. **Optimization**: Optimize performance -8. **Accessibility**: Ensure accessibility compliance -9. **Documentation**: Document components and flows - -## Support and Questions - -For questions or clarifications about the API: -1. Refer to the specific endpoint documentation -2. Check the backend source code in `/src/main/java/br/com/stockshift/` -3. Review the database migrations in `/src/main/resources/db/migration/` -4. Check the implementation plan in `/docs/plans/` - -## Version - -**API Version**: 1.0 -**Documentation Version**: 1.0 -**Last Updated**: December 28, 2025 - ---- - -**Note for AI Agents**: These documents provide comprehensive guidance for building a production-ready frontend. Follow the patterns and best practices outlined in each file. Pay special attention to error handling, permission checks, and user experience guidelines. +Auth endpoints and selected public callbacks/webhooks document their own behavior. + +## Documents + +- [audit.md](audit.md): audit log queries and exports. +- [auth.md](auth.md): login, refresh, logout, registration, current user, warehouse switching. +- [batches.md](batches.md): stock batches and product batch operations. +- [brands.md](brands.md): product brand CRUD. +- [categories.md](categories.md): product category CRUD and parent filtering. +- [permissions.md](permissions.md): permission catalog. +- [products.md](products.md): product CRUD, lookup, search, and image analysis. +- [reports.md](reports.md): dashboard and stock reports. +- [roles.md](roles.md): role CRUD and role-permission assignment. +- [sales.md](sales.md): sales, sales dashboard, cancellation, and InfinitePay integration. +- [stock-movements.md](stock-movements.md): stock movement creation, listing, detail, and summary. +- [tenants.md](tenants.md): company configuration and InfinitePay tenant settings. +- [transfer.md](transfer.md): warehouse transfer lifecycle and validation. +- [users.md](users.md): tenant user management. +- [warehouses.md](warehouses.md): warehouse CRUD and warehouse stock views. + +## Frontend Rules + +- Check these documents before adding or changing API calls. +- Keep all `ky` and `useSWR` calls inside standard `.model.ts` hooks. +- Use backend permission strings exactly as documented. +- Preserve Spring pageable response handling for endpoints returning `Page`. diff --git a/docs/endpoints/audit.md b/docs/endpoints/audit.md new file mode 100644 index 0000000..d95596c --- /dev/null +++ b/docs/endpoints/audit.md @@ -0,0 +1,87 @@ +# Audit Endpoints + +Base path: `/api/audit` + +All endpoints require Bearer authentication and `audit:read`. + +## Query Filters + +Audit list and export endpoints accept these optional filters: + +- `actorUserId`: UUID +- `resourceType`: string +- `resourceId`: string +- `operation`: string +- `action`: string +- `outcome`: string +- `dateFrom`: ISO date-time +- `dateTo`: ISO date-time + +List endpoints use Spring pageable parameters: `page`, `size`, and `sort`. + +## Data Shape + +### AuditEventResponse + +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "tenantId": "00000000-0000-0000-0000-000000000001", + "occurredAt": "2026-05-09T10:00:00", + "actorUserId": "00000000-0000-0000-0000-000000000002", + "actorEmail": "user@example.com", + "warehouseId": "00000000-0000-0000-0000-000000000003", + "operation": "UPDATE", + "action": "products:update", + "outcome": "SUCCESS", + "resourceType": "PRODUCT", + "resourceId": "00000000-0000-0000-0000-000000000004", + "reason": null, + "requestId": "request-id", + "httpMethod": "PUT", + "httpPath": "/api/products/00000000-0000-0000-0000-000000000004", + "httpStatus": 200, + "ipAddress": "127.0.0.1", + "userAgent": "Mozilla/5.0", + "beforeState": {}, + "afterState": {}, + "changedFields": ["name"], + "metadata": {} +} +``` + +## Endpoints + +### GET `/api/audit/events` + +Lists audit events. + +- Query: filters plus pageable params +- Default sort: `occurredAt,DESC` +- Success: `200 OK`, `ApiResponse>` + +### GET `/api/audit/resources/{resourceType}/{resourceId}` + +Lists audit events for one resource. + +- Path: `resourceType`, `resourceId` +- Query: pageable params +- Default sort: `occurredAt,DESC` +- Success: `200 OK`, `ApiResponse>` + +### GET `/api/audit/events/export.csv` + +Exports audit events as CSV. + +- Query: filters plus optional `limit`, default `10000` +- Success: `200 OK`, `text/csv; charset=UTF-8` +- Response header: `Content-Disposition: attachment; filename="audit-events-{dateFrom}-{dateTo}.csv"` + +### GET `/api/audit/events/export.xlsx` + +Exports audit events as XLSX. + +- Query: filters plus optional `limit`, default `10000` +- Success: `200 OK`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` +- Response header: `Content-Disposition: attachment; filename="audit-events-{dateFrom}-{dateTo}.xlsx"` + diff --git a/docs/endpoints/auth.md b/docs/endpoints/auth.md index e668f10..f450490 100644 --- a/docs/endpoints/auth.md +++ b/docs/endpoints/auth.md @@ -38,7 +38,7 @@ These endpoints handle user authentication, registration, and session management "message": null, "data": { "tokenType": "Bearer", - "expiresIn": 3600000, + "expiresIn": 900000, "userId": "550e8400-e29b-41d4-a716-446655440000", "email": "user@example.com", "fullName": "John Doe", @@ -68,9 +68,9 @@ These endpoints handle user authentication, registration, and session management ### Rate Limiting & Captcha Logic The system tracks login attempts per IP address using a token bucket algorithm: -- **Default capacity**: 5 attempts -- **Refill rate**: 5 tokens every 15 minutes -- **Captcha threshold**: When remaining tokens ≤ 50% of capacity (e.g., after 3 attempts with capacity=5) +- **Default capacity**: 10 attempts +- **Refill rate**: 10 tokens every 15 minutes +- **Captcha threshold**: When remaining tokens ≤ 50% of capacity (e.g., after 5 attempts with capacity=10) When `requiresCaptcha: true` is returned, the frontend should display a captcha challenge before allowing the next login attempt. @@ -125,7 +125,7 @@ When `requiresCaptcha: true` is returned, the frontend should display a captcha ### Request **Method**: `POST` **Content-Type**: `application/json` -**Authentication**: Not required +**Authentication**: Required (Bearer token via HTTP-only cookie) **Request Body**: None @@ -241,18 +241,22 @@ When `requiresCaptcha: true` is returned, the frontend should display a captcha **400 Bad Request** (Current password incorrect): ```json { - "success": false, + "timestamp": "2025-01-22T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Current password is incorrect", - "data": null + "path": "/api/auth/change-password" } ``` **401 Unauthorized** (Not authenticated): ```json { - "success": false, + "timestamp": "2025-01-22T10:30:00", + "status": 401, + "error": "Unauthorized", "message": "Unauthorized", - "data": null + "path": "/api/auth/change-password" } ``` @@ -315,6 +319,7 @@ When `requiresCaptcha: true` is returned, the frontend should display a captcha ### Roles Roles are tenant-specific groups that bundle permissions. Common roles include: - `ADMIN`: System administrator with full access (receives `["*"]` permission) +- `SUPER_ADMIN`: Platform-level administrator with full access across tenants (receives `["*"]` permission) - Custom roles created by the tenant (e.g., `VENDEDOR`, `GERENTE`, `ESTOQUISTA`) ### Permission Format @@ -372,7 +377,7 @@ Permissions follow the format `resource:action`: } ``` -> **Note**: A new access token containing the `warehouseId` claim is set as an HTTP-only cookie. The refresh token remains unchanged. +> **Note**: New access and refresh tokens are set as HTTP-only cookies. The refresh token is rotated with the new `warehouseId` embedded in it. ### Error Responses @@ -385,12 +390,14 @@ Permissions follow the format `resource:action`: } ``` -**401 Unauthorized** (No access to warehouse): +**403 Forbidden** (No access to warehouse): ```json { - "success": false, + "timestamp": "2025-01-22T10:30:00", + "status": 403, + "error": "Forbidden", "message": "User does not have access to this warehouse", - "data": null + "path": "/api/auth/switch-warehouse" } ``` @@ -434,6 +441,13 @@ The access token is a JWT (JSON Web Token) signed with HS256. Below is the decod "stock_movements:read", "stock_movements:create" ], + "authorities": [ + "products:read", + "products:update", + "warehouses:read", + "stock_movements:read", + "stock_movements:create" + ], "iat": 1706097600, "exp": 1706101200 } @@ -450,11 +464,13 @@ The access token is a JWT (JSON Web Token) signed with HS256. Below is the decod | `email` | string | User's email address | | `roles` | array of strings | List of role names assigned to the user | | `permissions` | array of strings | List of permission codes granted to the user | +| `authorities` | array of strings | Duplicate of `permissions` for backward compatibility with legacy consumers | +| `warehouseId` | string (UUID) or null | Currently selected warehouse ID, if any | | `iat` | number | Issued at timestamp (Unix epoch in seconds) | | `exp` | number | Expiration timestamp (Unix epoch in seconds) | ### Token Expiration -- **Access Token**: 1 hour (3600000 ms) +- **Access Token**: 15 minutes (900000 ms) - **Refresh Token**: 7 days ### Frontend Notes @@ -465,12 +481,24 @@ The access token is a JWT (JSON Web Token) signed with HS256. Below is the decod --- ## Error Response Format -All endpoints return errors in the following format: +Most endpoints return errors in the following format (`ErrorResponse`): ```json { - "success": false, + "timestamp": "2025-01-22T10:30:00", + "status": 401, + "error": "Unauthorized", "message": "Error description", + "path": "/api/auth/login" +} +``` + +Rate limit (429) errors use a different format (`ApiResponse`): + +```json +{ + "success": false, + "message": "Muitas tentativas de login. Tente novamente em 15 minutos.", "data": null } ``` diff --git a/docs/endpoints/batches.md b/docs/endpoints/batches.md index 1a1108e..c1e707f 100644 --- a/docs/endpoints/batches.md +++ b/docs/endpoints/batches.md @@ -15,7 +15,7 @@ Batches represent specific quantities of products stored in warehouses. Each bat ### Authorization -**Required Permissions**: `batches:create` +**Required Permissions**: `BATCH_CREATE` or `ROLE_ADMIN` ### Request @@ -74,6 +74,7 @@ Batches represent specific quantities of products stored in warehouses. Each bat "updatedAt": "2025-12-28T10:00:00Z" } } +} ``` ### Frontend Implementation Guide @@ -95,7 +96,7 @@ Batches represent specific quantities of products stored in warehouses. Each bat ### Authorization -**Required Permissions**: `batches:create` and `products:create` +**Required Permissions**: `BATCH_CREATE` and `PRODUCT_CREATE` or `ROLE_ADMIN` ### Description @@ -155,7 +156,7 @@ This endpoint atomically creates a new product and its first batch in a single t - `warehouseId`: Required, UUID of the warehouse - `batchCode`: Optional, unique batch identifier. If not provided, will be auto-generated in format `BATCH-YYYYMMDD-XXX` -- `quantity`: Required, positive number or zero (BigDecimal format) +- `quantity`: Required, positive integer or zero - `manufacturedDate`: Optional, ISO date string - `expirationDate`: Optional, ISO date string (required if `hasExpiration: true`) - `costPrice`: Optional, cost per unit in cents (e.g., 1050 = R$10,50) @@ -331,7 +332,7 @@ This endpoint atomically creates a new product and its first batch in a single t ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -387,7 +388,7 @@ This endpoint atomically creates a new product and its first batch in a single t ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -441,7 +442,7 @@ This endpoint atomically creates a new product and its first batch in a single t ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -468,7 +469,7 @@ Same format as GET /api/batches (returns array of batches) ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -495,7 +496,7 @@ Same format as GET /api/batches (returns array of batches) ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -525,7 +526,7 @@ Same format as GET /api/batches (returns array of batches) ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -555,7 +556,7 @@ Same format as GET /api/batches (returns array of expiring batches) ### Authorization -**Required Permissions**: `batches:read` +**Required Permissions**: `BATCH_READ` or `ROLE_ADMIN` ### Request @@ -585,7 +586,7 @@ Same format as GET /api/batches (returns array of low-stock batches) ### Authorization -**Required Permissions**: `batches:update` +**Required Permissions**: `BATCH_UPDATE` or `ROLE_ADMIN` ### Request @@ -630,7 +631,7 @@ Same structure as POST /api/batches ### Authorization -**Required Permissions**: `batches:delete` +**Required Permissions**: `BATCH_DELETE` or `ROLE_ADMIN` ### Request @@ -666,7 +667,7 @@ Same structure as POST /api/batches ### Authorization -**Required Permissions**: `batches:delete` +**Required Permissions**: `BATCH_DELETE` or `ROLE_ADMIN` ### Description @@ -775,7 +776,7 @@ Returns 200 with zero count (idempotent operation): 2. **Validation**: - Verify warehouse and product IDs are valid UUIDs - - Check user has `batches:delete` permission before showing delete option + - Check user has BATCH_DELETE permission before showing delete option - Consider checking if batches have recent movements before allowing deletion 3. **Success Feedback**: diff --git a/docs/endpoints/brands.md b/docs/endpoints/brands.md index e0c72c1..34952af 100644 --- a/docs/endpoints/brands.md +++ b/docs/endpoints/brands.md @@ -1,6 +1,7 @@ # Brand Endpoints ## Overview + These endpoints manage brands in the StockShift system. Brands can be associated with products to organize inventory by manufacturer or brand name. All endpoints require authentication with appropriate permissions. **Base URL**: `/api/brands` @@ -9,16 +10,20 @@ These endpoints manage brands in the StockShift system. Brands can be associated --- ## POST /api/brands + **Summary**: Create a new brand ### Authorization -**Required Permissions**: `BRAND_CREATE` or `ROLE_ADMIN` + +**Required Permissions**: `brands:create` ### Request + **Method**: `POST` **Content-Type**: `application/json` #### Request Body + ```json { "name": "Natura", @@ -27,10 +32,12 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` **Field Details**: -- `name`: Required, brand name (unique per tenant, max 255 characters) -- `logoUrl`: Optional, URL for brand logo (max 500 characters) + +- `name`: Required, brand name (unique per tenant, max 255 characters). Only letters, numbers, spaces and symbols `-`, `.`, `&`, `'`, `(`, `)` are allowed. +- `logoUrl`: Optional, URL for brand logo (max 500 characters). Must be a valid `http` or `https` URL. ### Response + **Status Code**: `201 CREATED` ```json @@ -48,6 +55,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Frontend Implementation Guide + 1. **Form Fields**: Simple form with name (required) and logo URL (optional) 2. **Logo Upload**: Consider implementing file upload for logo (store URL after upload) 3. **Logo Preview**: Show logo preview when URL is provided @@ -56,7 +64,9 @@ These endpoints manage brands in the StockShift system. Brands can be associated 6. **Error Handling**: Display validation errors (duplicate name, invalid URL format) ### Error Responses + **400 Bad Request** - Duplicate brand name: + ```json { "success": false, @@ -68,15 +78,19 @@ These endpoints manage brands in the StockShift system. Brands can be associated --- ## GET /api/brands + **Summary**: Get all brands for the current tenant ### Authorization -**Required Permissions**: `BRAND_READ` or `ROLE_ADMIN` + +**Required Permissions**: `brands:read` ### Request + **Method**: `GET` ### Response + **Status Code**: `200 OK` ```json @@ -103,6 +117,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Frontend Implementation Guide + 1. **List View**: Display brands in table or card grid 2. **Columns**: Show logo (thumbnail), name, creation date 3. **Logo Display**: Show logo thumbnail if available, placeholder if not @@ -116,16 +131,20 @@ These endpoints manage brands in the StockShift system. Brands can be associated --- ## GET /api/brands/{id} + **Summary**: Get brand by ID ### Authorization -**Required Permissions**: `BRAND_READ` or `ROLE_ADMIN` + +**Required Permissions**: `brands:read` ### Request + **Method**: `GET` **URL Parameters**: `id` (UUID) - Brand identifier ### Response + **Status Code**: `200 OK` ```json @@ -143,6 +162,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Frontend Implementation Guide + 1. **Detail View**: Display brand information with logo 2. **Logo Display**: Show full-size logo if available 3. **Product Count**: Optionally show count of products using this brand @@ -153,17 +173,21 @@ These endpoints manage brands in the StockShift system. Brands can be associated --- ## PUT /api/brands/{id} + **Summary**: Update brand ### Authorization -**Required Permissions**: `BRAND_UPDATE` or `ROLE_ADMIN` + +**Required Permissions**: `brands:update` ### Request + **Method**: `PUT` **URL Parameters**: `id` (UUID) - Brand identifier **Content-Type**: `application/json` #### Request Body + ```json { "name": "Natura Cosméticos", @@ -172,6 +196,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Response + **Status Code**: `200 OK` ```json @@ -189,6 +214,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Frontend Implementation Guide + 1. **Edit Form**: Pre-populate form with current brand data 2. **Logo Update**: Allow changing logo URL or uploading new logo 3. **Logo Preview**: Show preview of new logo before saving @@ -198,7 +224,9 @@ These endpoints manage brands in the StockShift system. Brands can be associated 7. **Duplicate Check**: Handle error if new name conflicts with another brand ### Error Responses + **400 Bad Request** - Duplicate name with another brand: + ```json { "success": false, @@ -210,16 +238,20 @@ These endpoints manage brands in the StockShift system. Brands can be associated --- ## DELETE /api/brands/{id} + **Summary**: Delete brand (soft delete) ### Authorization -**Required Permissions**: `BRAND_DELETE` or `ROLE_ADMIN` + +**Required Permissions**: `brands:delete` ### Request + **Method**: `DELETE` **URL Parameters**: `id` (UUID) - Brand identifier ### Response + **Status Code**: `200 OK` ```json @@ -231,6 +263,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` ### Frontend Implementation Guide + 1. **Confirmation Modal**: Always confirm before deletion 2. **Product Check**: Warn if brand has associated products 3. **Soft Delete Explanation**: Explain brand is deactivated, not permanently deleted @@ -239,7 +272,9 @@ These endpoints manage brands in the StockShift system. Brands can be associated 6. **Alternative Action**: Suggest removing brand from products first ### Error Responses + **400 Bad Request** - Brand has associated products: + ```json { "success": false, @@ -249,10 +284,11 @@ These endpoints manage brands in the StockShift system. Brands can be associated ``` **404 Not Found** - Brand not found: + ```json { "success": false, - "message": "Brand not found", + "message": "Marca não encontrada.", "data": null } ``` @@ -262,33 +298,37 @@ These endpoints manage brands in the StockShift system. Brands can be associated ## Common Error Responses ### 401 Unauthorized + ```json { "success": false, - "message": "Authentication required", + "message": "Token JWT ausente ou inválido.", "data": null } ``` ### 403 Forbidden + ```json { "success": false, - "message": "Access denied. Required permission: BRAND_CREATE", + "message": "Acesso negado. Esta funcionalidade requer permissão em 'Brands'.", "data": null } ``` ### 404 Not Found + ```json { "success": false, - "message": "Brand not found", + "message": "Marca não encontrada.", "data": null } ``` ### 400 Bad Request + ```json { "success": false, @@ -302,11 +342,13 @@ These endpoints manage brands in the StockShift system. Brands can be associated ## Business Rules ### Brand Name Uniqueness + - Brand names must be unique per tenant - Validation occurs at both database and application level - Case-sensitive comparison (e.g., "Natura" and "natura" are different) ### Brand Deletion + - Brands can only be deleted if they have no associated products - If products exist with the brand, deletion is blocked - To delete a brand: @@ -315,12 +357,14 @@ These endpoints manage brands in the StockShift system. Brands can be associated 3. Wait until all products are naturally removed ### Soft Delete + - Deleted brands are not permanently removed from database - They are marked with `deletedAt` timestamp - Deleted brands do not appear in listings - Soft-deleted brands cannot be restored via API (requires database operation) ### Multi-Tenancy + - Brands are isolated by tenant - Users can only see and manage brands within their tenant - Brand name uniqueness is enforced per tenant (different tenants can have brands with same name) @@ -352,6 +396,7 @@ These endpoints manage brands in the StockShift system. Brands can be associated ## Integration with Products ### Assigning Brand to Product + When creating or updating a product, include `brandId`: ```json @@ -364,6 +409,7 @@ POST /api/products ``` ### Product Response includes Brand + Product responses include full brand object: ```json @@ -381,6 +427,7 @@ Product responses include full brand object: ``` ### Filtering Products by Brand + To get all products for a specific brand, use product search/filter endpoints with brand criteria. --- @@ -388,6 +435,7 @@ To get all products for a specific brand, use product search/filter endpoints wi ## Example Frontend Workflows ### Creating a Brand + 1. User clicks "Add Brand" button 2. Modal/page opens with form (name required, logo optional) 3. User fills name "Natura" @@ -399,6 +447,7 @@ To get all products for a specific brand, use product search/filter endpoints wi 9. On error: display error message (e.g., duplicate name) ### Editing a Brand + 1. User clicks "Edit" on brand in list 2. Modal/page opens with pre-filled form 3. User modifies name or logo @@ -409,6 +458,7 @@ To get all products for a specific brand, use product search/filter endpoints wi 8. On error: display error message ### Deleting a Brand + 1. User clicks "Delete" on brand in list 2. Confirmation modal appears: "Are you sure you want to delete 'Natura'?" 3. User confirms deletion @@ -417,6 +467,7 @@ To get all products for a specific brand, use product search/filter endpoints wi 6. On error (brand has products): show error "Cannot delete brand with associated products. Remove brand from products first." ### Using Brand in Product Form + 1. Product create/edit form includes brand selector 2. Brand selector dropdown populated with GET `/api/brands` 3. User selects brand from dropdown (optional field) diff --git a/docs/endpoints/categories.md b/docs/endpoints/categories.md index 59332f1..874f722 100644 --- a/docs/endpoints/categories.md +++ b/docs/endpoints/categories.md @@ -1,6 +1,7 @@ # Category Endpoints ## Overview + These endpoints manage product categories in the StockShift system. Categories can be hierarchical (have parent categories). **Base URL**: `/api/categories` @@ -9,22 +10,26 @@ These endpoints manage product categories in the StockShift system. Categories c --- ## POST /api/categories + **Summary**: Create a new category ### Authorization -**Required Permissions**: `CATEGORY_CREATE` or `ROLE_ADMIN` + +**Required Permissions**: `categories:create` ### Request + **Method**: `POST` **Content-Type**: `application/json` #### Request Body + ```json { "name": "Electronics", "description": "Electronic products", - "parentId": null, - "attributes": { + "parentCategoryId": null, + "attributesSchema": { "color": "#FF5733", "icon": "electronics" } @@ -32,12 +37,14 @@ These endpoints manage product categories in the StockShift system. Categories c ``` **Field Details**: -- `name`: Required, category name (2-100 characters) -- `description`: Optional, category description -- `parentId`: Optional, UUID of parent category (null for root categories) -- `attributes`: Optional, JSON object with custom attributes + +- `name`: Required, category name (cannot exceed 255 characters, allowed chars: letters, numbers, spaces, - . & ' ( )) +- `description`: Optional, category description (cannot exceed 1000 characters) +- `parentCategoryId`: Optional, UUID of parent category (null for root categories) +- `attributesSchema`: Optional, JSON object representing the configuration schema for product attributes within this category ### Response + **Status Code**: `201 CREATED` ```json @@ -48,9 +55,9 @@ These endpoints manage product categories in the StockShift system. Categories c "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Electronics", "description": "Electronic products", - "parentId": null, - "parentName": null, - "attributes": { + "parentCategoryId": null, + "parentCategoryName": null, + "attributesSchema": { "color": "#FF5733", "icon": "electronics" }, @@ -61,6 +68,7 @@ These endpoints manage product categories in the StockShift system. Categories c ``` ### Frontend Implementation Guide + 1. **Category Form**: Create modal or page with form fields 2. **Parent Selector**: Implement tree selector or dropdown for parent category 3. **Hierarchy Preview**: Show category hierarchy in real-time @@ -72,15 +80,19 @@ These endpoints manage product categories in the StockShift system. Categories c --- ## GET /api/categories + **Summary**: Get all categories ### Authorization -**Required Permissions**: `CATEGORY_READ` or `ROLE_ADMIN` + +**Required Permissions**: `categories:read` ### Request + **Method**: `GET` ### Response + **Status Code**: `200 OK` ```json @@ -92,9 +104,9 @@ These endpoints manage product categories in the StockShift system. Categories c "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Electronics", "description": "Electronic products", - "parentId": null, - "parentName": null, - "attributes": { + "parentCategoryId": null, + "parentCategoryName": null, + "attributesSchema": { "color": "#FF5733", "icon": "electronics" }, @@ -105,9 +117,9 @@ These endpoints manage product categories in the StockShift system. Categories c "id": "660e8400-e29b-41d4-a716-446655440001", "name": "Smartphones", "description": "Mobile phones", - "parentId": "550e8400-e29b-41d4-a716-446655440000", - "parentName": "Electronics", - "attributes": {}, + "parentCategoryId": "550e8400-e29b-41d4-a716-446655440000", + "parentCategoryName": "Electronics", + "attributesSchema": {}, "createdAt": "2025-12-28T10:00:00Z", "updatedAt": "2025-12-28T10:00:00Z" } @@ -116,6 +128,7 @@ These endpoints manage product categories in the StockShift system. Categories c ``` ### Frontend Implementation Guide + 1. **Tree View**: Display categories in hierarchical tree structure 2. **Flat List**: Alternative view as flat list with indentation 3. **Expand/Collapse**: Allow expanding/collapsing category branches @@ -128,16 +141,20 @@ These endpoints manage product categories in the StockShift system. Categories c --- ## GET /api/categories/{id} + **Summary**: Get category by ID ### Authorization -**Required Permissions**: `CATEGORY_READ` or `ROLE_ADMIN` + +**Required Permissions**: `categories:read` ### Request + **Method**: `GET` **URL Parameters**: `id` (UUID) - Category identifier ### Response + **Status Code**: `200 OK` ```json @@ -148,9 +165,9 @@ These endpoints manage product categories in the StockShift system. Categories c "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Electronics", "description": "Electronic products", - "parentId": null, - "parentName": null, - "attributes": { + "parentCategoryId": null, + "parentCategoryName": null, + "attributesSchema": { "color": "#FF5733", "icon": "electronics" }, @@ -161,6 +178,7 @@ These endpoints manage product categories in the StockShift system. Categories c ``` ### Frontend Implementation Guide + 1. **Detail View**: Display full category information 2. **Breadcrumb**: Show category path from root 3. **Products List**: Show products in this category @@ -171,21 +189,26 @@ These endpoints manage product categories in the StockShift system. Categories c --- ## GET /api/categories/parent/{parentId} + **Summary**: Get categories by parent ID ### Authorization -**Required Permissions**: `CATEGORY_READ` or `ROLE_ADMIN` + +**Required Permissions**: `categories:read` ### Request + **Method**: `GET` **URL Parameters**: `parentId` (UUID) - Parent category identifier **Special Case**: To get root categories (no parent), use a special endpoint or filter ### Response + Same format as GET /api/categories (returns array of child categories) ### Frontend Implementation Guide + 1. **Lazy Loading**: Load child categories on demand when expanding tree 2. **Breadcrumb Navigation**: Use for navigating category hierarchy 3. **Subcategory List**: Display immediate children in category detail view @@ -194,20 +217,25 @@ Same format as GET /api/categories (returns array of child categories) --- ## PUT /api/categories/{id} + **Summary**: Update category ### Authorization -**Required Permissions**: `CATEGORY_UPDATE` or `ROLE_ADMIN` + +**Required Permissions**: `categories:update` ### Request + **Method**: `PUT` **URL Parameters**: `id` (UUID) - Category identifier **Content-Type**: `application/json` #### Request Body + Same structure as POST /api/categories ### Response + **Status Code**: `200 OK` ```json @@ -221,6 +249,7 @@ Same structure as POST /api/categories ``` ### Frontend Implementation Guide + 1. **Edit Modal**: Open modal/drawer with pre-populated form 2. **Inline Editing**: Allow inline name editing in tree view 3. **Parent Change**: Allow changing parent (with validation) @@ -231,16 +260,20 @@ Same structure as POST /api/categories --- ## DELETE /api/categories/{id} + **Summary**: Delete category (soft delete) ### Authorization -**Required Permissions**: `CATEGORY_DELETE` or `ROLE_ADMIN` + +**Required Permissions**: `categories:delete` ### Request + **Method**: `DELETE` **URL Parameters**: `id` (UUID) - Category identifier ### Response + **Status Code**: `200 OK` ```json @@ -252,6 +285,7 @@ Same structure as POST /api/categories ``` ### Frontend Implementation Guide + 1. **Confirmation Modal**: Require confirmation before deletion 2. **Impact Check**: Show number of products and subcategories affected 3. **Subcategory Handling**: Explain what happens to subcategories @@ -265,46 +299,48 @@ Same structure as POST /api/categories ## Frontend Best Practices ### Tree Structure Management + ```typescript // Example category tree structure interface CategoryTree { id: string; name: string; description: string; - parentId: string | null; + parentCategoryId: string | null; children: CategoryTree[]; productCount: number; - attributes: Record; + attributesSchema: Record; } // Build tree from flat array function buildCategoryTree(categories: Category[]): CategoryTree[] { const map = new Map(); const roots: CategoryTree[] = []; - + // Create map entries - categories.forEach(cat => { + categories.forEach((cat) => { map.set(cat.id, { ...cat, children: [], productCount: 0 }); }); - + // Build hierarchy - categories.forEach(cat => { + categories.forEach((cat) => { const node = map.get(cat.id)!; - if (cat.parentId === null) { + if (cat.parentCategoryId === null) { roots.push(node); } else { - const parent = map.get(cat.parentId); + const parent = map.get(cat.parentCategoryId); if (parent) { parent.children.push(node); } } }); - + return roots; } ``` ### Category Selector Component + 1. **Tree Dropdown**: Expandable tree in dropdown 2. **Breadcrumb Path**: Show full path in display 3. **Search**: Filter categories by name @@ -312,6 +348,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { 5. **Create New**: Allow creating new category inline ### Visual Design + 1. **Colors**: Use category colors for visual coding 2. **Icons**: Display category icons consistently 3. **Indentation**: Clear visual hierarchy in lists @@ -319,6 +356,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { 5. **Depth Limit**: Limit visual depth (e.g., 3-4 levels) ### State Management + 1. **Cache Tree**: Cache category tree structure 2. **Invalidation**: Refresh on create/update/delete 3. **Optimistic Updates**: Update tree immediately @@ -326,6 +364,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { 5. **Selected State**: Track selected category across navigation ### Performance + 1. **Lazy Loading**: Load subcategories on demand 2. **Virtualization**: Use virtual scrolling for large trees 3. **Debounce Search**: Debounce category search @@ -337,6 +376,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { ## Common Error Responses ### 400 Bad Request - Circular Reference + ```json { "success": false, @@ -346,6 +386,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { ``` ### 400 Bad Request - Category Has Products + ```json { "success": false, @@ -357,6 +398,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { ``` ### 404 Not Found + ```json { "success": false, @@ -366,6 +408,7 @@ function buildCategoryTree(categories: Category[]): CategoryTree[] { ``` ### 409 Conflict - Duplicate Name + ```json { "success": false, diff --git a/docs/endpoints/permissions.md b/docs/endpoints/permissions.md index db30c5a..6ee69a1 100644 --- a/docs/endpoints/permissions.md +++ b/docs/endpoints/permissions.md @@ -1,199 +1,33 @@ -# Permissions +# Permissions Endpoints -## Overview -The StockShift system uses a role-based permission system. Permissions follow the format `RESOURCE:ACTION:SCOPE` and are assigned to roles, which are then assigned to users. +Base path: `/api/permissions` ---- +All endpoints require Bearer authentication. Responses use `ApiResponse`. -## Endpoints +## Permissions -### List All Permissions +- `permissions:read` for listing permissions. -``` -GET /stockshift/api/permissions -``` +## Data Shapes -**Authorization:** Requires `ROLE_ADMIN` +### PermissionResponse -**Response:** ```json { - "success": true, - "data": [ - { - "id": "uuid", - "resource": "PRODUCT", - "resourceDisplayName": "Produto", - "action": "CREATE", - "actionDisplayName": "Criar", - "scope": "ALL", - "scopeDisplayName": "Todos", - "description": "Create products" - } - ] + "id": "00000000-0000-0000-0000-000000000000", + "code": "products:read", + "description": "Read products", + "resource": "products", + "action": "read", + "scope": "tenant" } ``` ---- - -## Permission Format - -``` -RESOURCE:ACTION:SCOPE -``` - -- **RESOURCE**: The entity type being accessed -- **ACTION**: The operation being performed -- **SCOPE**: The access level - ---- - -## Resources - -| Resource | Description | -|----------|-------------| -| `PRODUCT` | Product management | -| `STOCK` | Stock and inventory management | -| `SALE` | Sales transactions | -| `USER` | User management | -| `WAREHOUSE` | Warehouse management | -| `REPORT` | Reports and analytics | - ---- - -## Actions - -| Action | Description | -|--------|-------------| -| `CREATE` | Create new resources | -| `READ` | View/read resources | -| `UPDATE` | Modify existing resources | -| `DELETE` | Remove resources | -| `APPROVE` | Approve pending operations (e.g., stock transfers) | - ---- - -## Scopes - -| Scope | Description | -|-------|-------------| -| `ALL` | Access to all resources of this type | -| `OWN_WAREHOUSE` | Access only to resources in user's assigned warehouse(s) | -| `OWN` | Access only to resources created by the user | - ---- - -## Special Permissions - -| Permission | Description | -|------------|-------------| -| `*` | Wildcard - grants full access to all resources. Only assigned to ADMIN role. | - ---- - -## Default Permissions - -### Product Permissions -| Permission | Description | -|------------|-------------| -| `PRODUCT:CREATE:ALL` | Create products | -| `PRODUCT:READ:ALL` | View all products | -| `PRODUCT:UPDATE:ALL` | Update products | -| `PRODUCT:DELETE:ALL` | Delete products | - -### Stock Permissions -| Permission | Description | -|------------|-------------| -| `STOCK:CREATE:ALL` | Create stock movements | -| `STOCK:READ:ALL` | View stock | -| `STOCK:UPDATE:ALL` | Update stock | -| `STOCK:APPROVE:ALL` | Approve stock transfers (all warehouses) | -| `STOCK:APPROVE:OWN_WAREHOUSE` | Approve transfers for own warehouse only | - -### Sale Permissions -| Permission | Description | -|------------|-------------| -| `SALE:CREATE:ALL` | Create sales | -| `SALE:READ:ALL` | View sales | - -### User Permissions -| Permission | Description | -|------------|-------------| -| `USER:CREATE:ALL` | Create users | -| `USER:READ:ALL` | View users | -| `USER:UPDATE:ALL` | Update users | -| `USER:DELETE:ALL` | Delete users | - -### Warehouse Permissions -| Permission | Description | -|------------|-------------| -| `WAREHOUSE:CREATE:ALL` | Create warehouses | -| `WAREHOUSE:READ:ALL` | View warehouses | -| `WAREHOUSE:UPDATE:ALL` | Update warehouses | -| `WAREHOUSE:DELETE:ALL` | Delete warehouses | - -### Report Permissions -| Permission | Description | -|------------|-------------| -| `REPORT:READ:ALL` | View all reports | - ---- - -## System Roles - -### ADMIN -- Automatically created when a tenant registers -- Receives the wildcard permission `*` granting full access -- Users with ADMIN role bypass individual permission checks - ---- - -## Frontend Implementation - -### Checking Permissions - -```typescript -// Check if user has a specific permission -function hasPermission(userPermissions: string[], required: string): boolean { - // Admin has all permissions - if (userPermissions.includes('*')) { - return true; - } - return userPermissions.includes(required); -} - -// Example usage -const canCreateProduct = hasPermission(user.permissions, 'PRODUCT:CREATE:ALL'); -const canApproveTransfers = hasPermission(user.permissions, 'STOCK:APPROVE:ALL'); -``` - -### Checking Multiple Permissions - -```typescript -// Check if user has any of the required permissions -function hasAnyPermission(userPermissions: string[], required: string[]): boolean { - if (userPermissions.includes('*')) { - return true; - } - return required.some(perm => userPermissions.includes(perm)); -} - -// Check if user has all required permissions -function hasAllPermissions(userPermissions: string[], required: string[]): boolean { - if (userPermissions.includes('*')) { - return true; - } - return required.every(perm => userPermissions.includes(perm)); -} -``` +## Endpoints ---- +### GET `/api/permissions` -## API Authorization +Lists all permissions available to assign to roles. -The backend validates permissions on each request: +- Success: `200 OK`, `ApiResponse` -1. Extract permissions from JWT token -2. Check if `*` (admin wildcard) is present - grants access -3. Otherwise, verify required permission exists in user's permission list -4. Return `403 Forbidden` if permission check fails diff --git a/docs/endpoints/products.md b/docs/endpoints/products.md index 2bf15c2..6623bc7 100644 --- a/docs/endpoints/products.md +++ b/docs/endpoints/products.md @@ -6,13 +6,20 @@ These endpoints manage products in the StockShift system. All endpoints require **Base URL**: `/api/products` **Authentication**: Required (Bearer token) +### Current Permission Codes +- Create product: `products:create` +- Read products: `products:read` +- Update product: `products:update` +- Delete product: `products:delete` +- Analyze product image: `products:analyze_image` + --- ## POST /api/products/analyze-image **Summary**: Analyze a product image using AI to extract details ### Authorization -**Required Permissions**: `PRODUCT_CREATE` or `ROLE_ADMIN` +**Required Permission**: `products:analyze_image` ### Request **Method**: `POST` @@ -27,7 +34,6 @@ These endpoints manage products in the StockShift system. All endpoints require ```json { "success": true, - "message": null, "data": { "name": "Make Me Fever Gold", "brandId": "660e8400-e29b-41d4-a716-446655440002", @@ -66,7 +72,7 @@ These endpoints manage products in the StockShift system. All endpoints require **Summary**: Create a new product ### Authorization -**Required Permissions**: `PRODUCT_CREATE` or `ROLE_ADMIN` +**Required Permission**: `products:create` ### Request **Method**: `POST` @@ -84,7 +90,7 @@ These endpoints manage products in the StockShift system. All endpoints require "categoryId": "550e8400-e29b-41d4-a716-446655440000", "brandId": "660e8400-e29b-41d4-a716-446655440002", "barcode": "1234567890123", - "barcodeType": "EAN13", + "barcodeType": "EXTERNAL", "sku": "PROD-001", "isKit": false, "attributes": { @@ -103,8 +109,8 @@ These endpoints manage products in the StockShift system. All endpoints require - `categoryId`: Optional, UUID of the category - `brandId`: Optional, UUID of the brand - `barcode`: Optional, alphanumeric with hyphens only -- `barcodeType`: Optional, enum values: `EAN13`, `EAN8`, `UPC`, `CODE128`, `CODE39` -- `sku`: Optional, alphanumeric with hyphens only +- `barcodeType`: Optional, enum values: `EXTERNAL`, `GENERATED` +- `sku`: Optional, alphanumeric with hyphens only. On create, if omitted or blank, the backend generates a unique SKU in the format `PRD-{timestamp}-{random}` - `isKit`: Optional, default `false`, indicates if product is a kit - `attributes`: Optional, JSON object with custom key-value pairs - `hasExpiration`: Optional, default `false`, indicates if product has expiration date @@ -131,11 +137,11 @@ These endpoints manage products in the StockShift system. All endpoints require "id": "660e8400-e29b-41d4-a716-446655440002", "name": "Brand Name", "logoUrl": "https://example.com/brand-logo.png", - "createdAt": "2025-12-28T09:00:00Z", - "updatedAt": "2025-12-28T09:00:00Z" + "createdAt": "2025-12-28T09:00:00", + "updatedAt": "2025-12-28T09:00:00" }, "barcode": "1234567890123", - "barcodeType": "EAN13", + "barcodeType": "EXTERNAL", "sku": "PROD-001", "isKit": false, "attributes": { @@ -144,8 +150,8 @@ These endpoints manage products in the StockShift system. All endpoints require }, "hasExpiration": true, "active": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" + "createdAt": "2025-12-28T10:00:00", + "updatedAt": "2025-12-28T10:00:00" } } ``` @@ -157,7 +163,7 @@ These endpoints manage products in the StockShift system. All endpoints require 4. **Category Selector**: Implement dropdown/autocomplete for category selection 5. **Brand Selector**: Implement dropdown/autocomplete for brand selection (optional field) 6. **Barcode Scanner**: Consider integrating barcode scanner library -7. **Barcode Type**: Provide dropdown with barcode types +7. **Barcode Type**: Provide dropdown with `EXTERNAL` and `GENERATED` 8. **Attributes Builder**: Allow dynamic key-value pairs for custom attributes 9. **Validation**: Validate required fields and image file type/size before submission 10. **Success Feedback**: Show success message and optionally redirect to product list @@ -168,7 +174,7 @@ These endpoints manage products in the StockShift system. All endpoints require **Summary**: Get all products ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -179,7 +185,6 @@ These endpoints manage products in the StockShift system. All endpoints require ```json { "success": true, - "message": null, "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", @@ -192,18 +197,18 @@ These endpoints manage products in the StockShift system. All endpoints require "id": "660e8400-e29b-41d4-a716-446655440002", "name": "Brand Name", "logoUrl": "https://example.com/brand-logo.png", - "createdAt": "2025-12-28T09:00:00Z", - "updatedAt": "2025-12-28T09:00:00Z" + "createdAt": "2025-12-28T09:00:00", + "updatedAt": "2025-12-28T09:00:00" }, "barcode": "1234567890123", - "barcodeType": "EAN13", + "barcodeType": "EXTERNAL", "sku": "PROD-001", "isKit": false, "attributes": {}, "hasExpiration": true, "active": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" + "createdAt": "2025-12-28T10:00:00", + "updatedAt": "2025-12-28T10:00:00" } ] } @@ -224,7 +229,7 @@ These endpoints manage products in the StockShift system. All endpoints require **Summary**: Get product by ID ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -236,7 +241,6 @@ These endpoints manage products in the StockShift system. All endpoints require ```json { "success": true, - "message": null, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Product Name", @@ -248,18 +252,18 @@ These endpoints manage products in the StockShift system. All endpoints require "id": "660e8400-e29b-41d4-a716-446655440002", "name": "Brand Name", "logoUrl": "https://example.com/brand-logo.png", - "createdAt": "2025-12-28T09:00:00Z", - "updatedAt": "2025-12-28T09:00:00Z" + "createdAt": "2025-12-28T09:00:00", + "updatedAt": "2025-12-28T09:00:00" }, "barcode": "1234567890123", - "barcodeType": "EAN13", + "barcodeType": "EXTERNAL", "sku": "PROD-001", "isKit": false, "attributes": {}, "hasExpiration": true, "active": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" + "createdAt": "2025-12-28T10:00:00", + "updatedAt": "2025-12-28T10:00:00" } } ``` @@ -278,7 +282,7 @@ These endpoints manage products in the StockShift system. All endpoints require **Summary**: Get products by category ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -298,7 +302,7 @@ Same format as GET /api/products (returns array of products with imageUrl) **Summary**: Get products by active status ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -318,7 +322,7 @@ Same format as GET /api/products (returns array of products with imageUrl) **Summary**: Search products by name, SKU or barcode ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -344,7 +348,7 @@ Same format as GET /api/products (returns array of matching products with imageU **Summary**: Get product by barcode ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -364,7 +368,7 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) **Summary**: Get product by SKU ### Authorization -**Required Permissions**: `PRODUCT_READ` or `ROLE_ADMIN` +**Required Permission**: `products:read` ### Request **Method**: `GET` @@ -384,7 +388,7 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) **Summary**: Update product ### Authorization -**Required Permissions**: `PRODUCT_UPDATE` or `ROLE_ADMIN` +**Required Permission**: `products:update` ### Request **Method**: `PUT` @@ -395,6 +399,8 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) - `product`: JSON object (same structure as POST /api/products) - `image`: Optional, new image file to replace current one +> **Important**: Update uses the submitted product payload as replacement values for mutable fields. Include the current `sku`, `categoryId`, and `brandId` when they should be preserved; omitting nullable fields clears them. + ### Response **Status Code**: `200 OK` @@ -405,8 +411,20 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Updated Product Name", + "description": "Updated description", "imageUrl": "https://example.com/storage/products/new-uuid.png", - // ... rest of product object + "categoryId": "550e8400-e29b-41d4-a716-446655440000", + "categoryName": "Category Name", + "brand": null, + "barcode": "1234567890123", + "barcodeType": "EXTERNAL", + "sku": "PROD-001", + "isKit": false, + "attributes": {}, + "hasExpiration": true, + "active": true, + "createdAt": "2025-12-28T10:00:00", + "updatedAt": "2025-12-28T11:00:00" } } ``` @@ -425,7 +443,7 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) **Summary**: Delete product (soft delete) ### Authorization -**Required Permissions**: `PRODUCT_DELETE` or `ROLE_ADMIN` +**Required Permission**: `products:delete` ### Request **Method**: `DELETE` @@ -437,8 +455,7 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) ```json { "success": true, - "message": "Product deleted successfully", - "data": null + "message": "Product deleted successfully" } ``` @@ -457,29 +474,36 @@ Same format as GET /api/products/{id} (returns single product with imageUrl) ### 403 Forbidden ```json { - "success": false, - "message": "Access denied. Required permission: PRODUCT_CREATE", - "data": null + "timestamp": "2026-01-25T10:00:00", + "status": 403, + "error": "Forbidden", + "message": "You don't have permission to access this resource", + "path": "/api/products" } ``` ### 404 Not Found ```json { - "success": false, - "message": "Product not found", - "data": null + "timestamp": "2026-01-25T10:00:00", + "status": 404, + "error": "Not Found", + "message": "Product not found with id: 550e8400-e29b-41d4-a716-446655440000", + "path": "/api/products/550e8400-e29b-41d4-a716-446655440000" } ``` ### 400 Bad Request ```json { - "success": false, - "message": "Validation failed", - "data": { + "timestamp": "2026-01-25T10:00:00", + "status": 400, + "error": "Validation Failed", + "message": "Invalid input", + "path": "/api/products", + "validationErrors": { "name": "Product name is required", - "barcode": "Invalid barcode format" + "barcode": "Barcode contains invalid characters" } } ``` diff --git a/docs/endpoints/reports copy.md b/docs/endpoints/reports copy.md deleted file mode 100644 index 731e9b9..0000000 --- a/docs/endpoints/reports copy.md +++ /dev/null @@ -1,849 +0,0 @@ -# Report Endpoints - -## Overview -These endpoints provide dashboard summaries, stock reports, and analytics for the StockShift system. - -**Base URL**: `/api/reports` -**Authentication**: Required (Bearer token) - ---- - -## GET /api/reports/dashboard -**Summary**: Get dashboard summary with key metrics - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "totalProducts": 150, - "activeProducts": 142, - "totalWarehouses": 3, - "activeWarehouses": 3, - "totalBatches": 287, - "totalStockValue": 125430.50, - "lowStockCount": 12, - "expiringCount": 8, - "recentMovements": [ - { - "id": "880e8400-e29b-41d4-a716-446655440003", - "movementType": "ENTRY", - "status": "COMPLETED", - "createdAt": "2025-12-28T10:00:00Z", - "productCount": 5, - "notes": "Purchase order #12345" - } - ], - "stockByWarehouse": [ - { - "warehouseId": "550e8400-e29b-41d4-a716-446655440000", - "warehouseName": "Main Warehouse", - "batchCount": 150, - "stockValue": 75200.00, - "productCount": 89 - } - ], - "stockByCategory": [ - { - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Electronics", - "batchCount": 45, - "stockValue": 35000.00, - "productCount": 25 - } - ], - "movementStats": { - "today": { - "entries": 5, - "exits": 3, - "transfers": 2, - "adjustments": 1 - }, - "thisWeek": { - "entries": 23, - "exits": 18, - "transfers": 7, - "adjustments": 4 - }, - "thisMonth": { - "entries": 95, - "exits": 78, - "transfers": 25, - "adjustments": 12 - } - } - } -} -``` - -### Frontend Implementation Guide -1. **KPI Cards**: Display key metrics in cards (total products, stock value, etc.) -2. **Warehouse Chart**: Pie or bar chart for stock by warehouse -3. **Category Chart**: Donut chart for stock by category -4. **Recent Movements**: Timeline or list of recent movements -5. **Alerts Widget**: Highlight low stock and expiring items -6. **Movement Stats**: Trend charts for movements over time -7. **Quick Actions**: Buttons for common tasks (new movement, view reports) -8. **Refresh**: Auto-refresh dashboard data periodically -9. **Responsive Layout**: Optimize for different screen sizes -10. **Drill-down**: Allow clicking metrics to see details - ---- - -## GET /api/reports/stock -**Summary**: Get complete stock report - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": [ - { - "productId": "550e8400-e29b-41d4-a716-446655440000", - "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Electronics", - "totalQuantity": 250, - "totalValue": 2625.00, - "averageCostPrice": 10.50, - "batchCount": 5, - "warehouseCount": 2, - "oldestExpirationDate": "2026-03-15", - "newestExpirationDate": "2027-12-31", - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 150, - "batchCount": 3 - }, - { - "warehouseId": "880e8400-e29b-41d4-a716-446655440003", - "warehouseName": "Secondary Warehouse", - "warehouseCode": "WH-002", - "quantity": 100, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 80, - "expirationDate": "2026-03-15", - "costPrice": 10.50 - } - ] - } - ] -} -``` - -### Frontend Implementation Guide -1. **Comprehensive Table**: Display all stock data in sortable table -2. **Expandable Rows**: Expand to show warehouse/batch details -3. **Export Options**: Export to CSV, Excel, PDF -4. **Print View**: Optimized print layout -5. **Filters**: Filter by category, warehouse, stock level -6. **Search**: Full-text search across products -7. **Aggregations**: Show totals at bottom (quantity, value) -8. **Visualization**: Charts for top products, categories -9. **Stock Levels**: Visual indicators (bars, gauges) -10. **Drill-down**: Link to product details - ---- - -## GET /api/reports/stock/low-stock -**Summary**: Get low stock report - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` -**Query Parameters**: -- `threshold` (Integer, default: 10) - Quantity threshold -- `limit` (Integer, optional) - Maximum results to return - -**Example**: `/api/reports/stock/low-stock?threshold=20&limit=50` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": [ - { - "productId": "550e8400-e29b-41d4-a716-446655440000", - "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Electronics", - "totalQuantity": 8, - "totalValue": 84.00, - "averageCostPrice": 10.50, - "batchCount": 2, - "warehouseCount": 1, - "oldestExpirationDate": "2026-03-15", - "newestExpirationDate": null, - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 8, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 5, - "expirationDate": "2026-03-15", - "costPrice": 10.50 - } - ] - } - ] -} -``` - -### Frontend Implementation Guide -1. **Alert Dashboard**: Prominent display of low stock items -2. **Urgency Levels**: Color-coded by severity (critical, warning) -3. **Reorder Actions**: Quick buttons to create purchase orders -4. **Threshold Settings**: Allow customizing threshold per product -5. **Notification**: Email/push notifications for low stock -6. **History**: Track when products went low and were restocked -7. **Supplier Info**: Show supplier info for quick reordering -8. **Trend Analysis**: Show historical stock levels -9. **Forecast**: Predict when stock will run out -10. **Export**: Export to CSV for procurement - ---- - -## GET /api/reports/stock/expiring -**Summary**: Get expiring products report - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` -**Query Parameters**: -- `daysAhead` (Integer, default: 30) - Days to look ahead -- `limit` (Integer, optional) - Maximum results to return - -**Example**: `/api/reports/stock/expiring?daysAhead=60&limit=100` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": [ - { - "productId": "550e8400-e29b-41d4-a716-446655440000", - "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Food Items", - "totalQuantity": 45, - "totalValue": 472.50, - "averageCostPrice": 10.50, - "batchCount": 3, - "warehouseCount": 2, - "oldestExpirationDate": "2026-01-15", - "newestExpirationDate": "2026-02-28", - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 30, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 15, - "expirationDate": "2026-01-15", - "costPrice": 10.50, - "daysUntilExpiration": 18 - }, - { - "batchId": "aa0e8400-e29b-41d4-a716-446655440005", - "batchNumber": "BATCH-2025-002", - "warehouseName": "Main Warehouse", - "quantity": 15, - "expirationDate": "2026-01-20", - "costPrice": 10.50, - "daysUntilExpiration": 23 - } - ] - } - ] -} -``` - -### Frontend Implementation Guide -1. **Expiration Dashboard**: Dedicated view for expiring products -2. **Urgency Indicators**: Color coding by days until expiration -3. **Time Filters**: Quick filters (7, 15, 30, 60, 90 days) -4. **Action Buttons**: - - Mark for discount/clearance - - Create transfer movement - - Mark as waste -5. **Notification System**: Alert users of soon-to-expire items -6. **Calendar View**: Calendar showing expiration dates -7. **FEFO Enforcement**: Highlight when wrong batches are being used -8. **Loss Prevention**: Calculate potential losses -9. **Disposal Tracking**: Track disposed expired items -10. **Trend Analysis**: Historical expiration patterns - ---- - -## GET /api/reports/dashboard/summary -**Summary**: Get dashboard quick summary with key operational metrics - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "totalProducts": 150, - "totalWarehouses": 3, - "totalActiveBatches": 280, - "totalStockQuantity": 15000.000, - "totalStockValue": 150000.000, - "totalTransitQuantity": 500.000, - "pendingTransfers": 3, - "todayMovements": 12, - "criticalAlerts": 5 - } -} -``` - -### Fields -| Field | Type | Description | -|---|---|---| -| `totalProducts` | Long | Number of distinct products with stock | -| `totalWarehouses` | Long | Number of warehouses (1 if warehouse-scoped) | -| `totalActiveBatches` | Long | Number of active (non-deleted) batches | -| `totalStockQuantity` | BigDecimal | Sum of all batch quantities | -| `totalStockValue` | BigDecimal | Sum of `costPrice * quantity` across all batches | -| `totalTransitQuantity` | BigDecimal | Sum of all `transitQuantity` across batches | -| `pendingTransfers` | Long | Transfers in DRAFT, IN_TRANSIT, or PENDING_VALIDATION status | -| `todayMovements` | Long | Stock movements created today | -| `criticalAlerts` | Long | Products with low stock (<=10) OR expiring within 7 days | - -### Frontend Implementation Guide -1. **KPI Cards**: Display each metric in a card with icon and label -2. **Critical Alerts Badge**: Highlight `criticalAlerts` with a red badge when > 0 -3. **Pending Transfers Link**: Make `pendingTransfers` clickable to navigate to transfers list -4. **Auto-refresh**: Refresh every 60 seconds for real-time monitoring - ---- - -## GET /api/reports/dashboard/kpis -**Summary**: Get financial KPIs with month-over-month comparison - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "currentMonth": { - "totalStockValue": 150000.00, - "totalPurchasesValue": 15000.00, - "totalLossesValue": 800.00, - "totalDamageValue": 200.00, - "totalGiftValue": 150.00, - "totalAdjustmentValue": 300.00, - "totalTransitValue": 5000.00, - "stockTurnoverRate": 2.50 - }, - "previousMonth": { - "totalStockValue": 143000.00, - "totalPurchasesValue": 12000.00, - "totalLossesValue": 600.00, - "totalDamageValue": 100.00, - "totalGiftValue": 300.00, - "totalAdjustmentValue": 0.00, - "totalTransitValue": 3000.00, - "stockTurnoverRate": 2.10 - }, - "variations": { - "totalStockValue": 4.90, - "totalPurchasesValue": 25.00, - "totalLossesValue": 33.33, - "totalDamageValue": 100.00, - "totalGiftValue": -50.00, - "totalAdjustmentValue": null, - "totalTransitValue": 66.67, - "stockTurnoverRate": 19.05 - } - } -} -``` - -### Fields -| Field | Type | Description | -|---|---|---| -| `currentMonth` | KpiPeriodData | KPIs for the current calendar month | -| `previousMonth` | KpiPeriodData | KPIs for the previous calendar month. `null` if no historical data | -| `variations` | KpiVariations | Percentage variation between months. `null` if `previousMonth` is null. Individual fields are `null` when previous value is zero | - -#### KpiPeriodData Fields -| Field | Type | Description | -|---|---|---| -| `totalStockValue` | BigDecimal | Current total stock value (`costPrice * quantity`) | -| `totalPurchasesValue` | BigDecimal | Total quantity from PURCHASE_IN movements in the period | -| `totalLossesValue` | BigDecimal | Total quantity from LOSS movements in the period | -| `totalDamageValue` | BigDecimal | Total quantity from DAMAGE movements in the period | -| `totalGiftValue` | BigDecimal | Total quantity from GIFT movements in the period | -| `totalAdjustmentValue` | BigDecimal | Total quantity from ADJUSTMENT_IN + ADJUSTMENT_OUT movements | -| `totalTransitValue` | BigDecimal | Current transit quantity value | -| `stockTurnoverRate` | BigDecimal | Total OUT quantity / average stock value | - -#### KpiVariations Fields -Each field represents the percentage change: `((current - previous) / previous) * 100`. All fields share the same names as `KpiPeriodData`. - -### Frontend Implementation Guide -1. **KPI Cards with Arrows**: Show each KPI with an up/down arrow and variation percentage -2. **Color Coding**: Green for positive variations (purchases up), red for negative (losses up) -3. **Tooltip**: Show both current and previous month values on hover -4. **Turnover Gauge**: Display `stockTurnoverRate` as a gauge or progress bar -5. **Null Handling**: Show "N/A" when `previousMonth` or individual variations are null - ---- - -## GET /api/reports/dashboard/alerts -**Summary**: Get operational alerts for the dashboard - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "lowStockProducts": [ - { - "productId": "550e8400-e29b-41d4-a716-446655440000", - "productName": "Product A", - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "totalQuantity": 5.000, - "totalValue": 52.500, - "nearestExpiration": "2026-06-15", - "batchCount": 1 - } - ], - "expiringProducts": [ - { - "productId": "660e8400-e29b-41d4-a716-446655440001", - "productName": "Product B", - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "totalQuantity": 45.000, - "totalValue": 472.500, - "nearestExpiration": "2026-04-20", - "batchCount": 2 - } - ], - "recentLosses": [ - { - "movementType": "LOSS", - "productName": "Product C", - "quantity": 10.000, - "value": 105.000, - "date": "2026-04-05" - } - ], - "pendingTransfers": 3, - "highTransitValue": 5000.000 - } -} -``` - -### Fields -| Field | Type | Description | -|---|---|---| -| `lowStockProducts` | List\ | Top 10 products with quantity <= 10 | -| `expiringProducts` | List\ | Top 10 products expiring within 30 days | -| `recentLosses` | List\ | Last 30 days of LOSS and DAMAGE movements (max 10) | -| `pendingTransfers` | Long | Transfers in DRAFT, IN_TRANSIT, or PENDING_VALIDATION status | -| `highTransitValue` | BigDecimal | Sum of `costPrice * transitQuantity` for batches in transit | - -#### RecentMovementAlert Fields -| Field | Type | Description | -|---|---|---| -| `movementType` | StockMovementType | `LOSS` or `DAMAGE` | -| `productName` | String | Name of the product | -| `quantity` | BigDecimal | Quantity affected | -| `value` | BigDecimal | Financial value (`costPrice * quantity`) | -| `date` | LocalDate | Date of the movement | - -### Frontend Implementation Guide -1. **Alert Panels**: Group alerts by type (low stock, expiring, losses) -2. **Severity Colors**: Red for critical, yellow for warning -3. **Loss History**: Show recent losses in a timeline or compact list -4. **Pending Transfers Counter**: Badge with count, clickable to transfers page -5. **Transit Value Warning**: Highlight when `highTransitValue` exceeds a threshold - ---- - -## GET /api/reports/dashboard/movement-trend -**Summary**: Get daily movement volume trend for charts - -### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` -**Query Parameters**: -- `days` (Integer, default: 30) - Number of days to look back - -**Example**: `/api/reports/dashboard/movement-trend?days=7` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "startDate": "2026-03-09", - "endDate": "2026-04-08", - "days": [ - { - "date": "2026-04-01", - "totalInQuantity": 150.000, - "totalInValue": 0.000, - "totalOutQuantity": 80.000, - "totalOutValue": 0.000, - "movementCount": 12 - }, - { - "date": "2026-04-02", - "totalInQuantity": 200.000, - "totalInValue": 0.000, - "totalOutQuantity": 0.000, - "totalOutValue": 0.000, - "movementCount": 5 - } - ], - "totals": { - "totalInQuantity": 4500.000, - "totalInValue": 0.000, - "totalOutQuantity": 2800.000, - "totalOutValue": 0.000, - "movementCount": 320 - } - } -} -``` - -### Fields -| Field | Type | Description | -|---|---|---| -| `startDate` | LocalDate | Start date of the period | -| `endDate` | LocalDate | End date of the period (today) | -| `days` | List\ | One entry per day, including days with zero movements | -| `totals` | MovementTotals | Aggregated totals for the entire period | - -#### DailyMovement Fields -| Field | Type | Description | -|---|---|---| -| `date` | LocalDate | The date | -| `totalInQuantity` | BigDecimal | Total quantity from IN-direction movements | -| `totalInValue` | BigDecimal | Reserved for future use (currently 0) | -| `totalOutQuantity` | BigDecimal | Total quantity from OUT-direction movements | -| `totalOutValue` | BigDecimal | Reserved for future use (currently 0) | -| `movementCount` | Long | Number of distinct movements on this date | - -#### MovementTotals Fields -Same structure as `DailyMovement` but aggregated across all days in the period. - -### Frontend Implementation Guide -1. **Bar Chart**: Stacked bar chart with IN (green) and OUT (red) quantities per day -2. **Line Chart**: Alternative view showing IN and OUT trend lines -3. **Period Selector**: Buttons for 7, 15, 30, 60, 90 days -4. **Tooltip**: Show exact values on hover -5. **Summary Stats**: Display `totals` below the chart -6. **Zero-fill**: API already returns zero-filled days, no frontend gap-filling needed - ---- - -## Frontend Component Examples - -### Dashboard Widget -```typescript -interface DashboardProps { - data: DashboardResponse; - onRefresh: () => void; - loading: boolean; -} - -// Sections: -// 1. KPI Cards (4-6 key metrics) -// 2. Charts (stock distribution, movement trends) -// 3. Alerts (low stock, expiring) -// 4. Recent Activity -// 5. Quick Actions -``` - -### Stock Report Table -```typescript -interface StockReportTableProps { - data: StockReportResponse[]; - loading: boolean; - onExport: (format: 'csv' | 'excel' | 'pdf') => void; -} - -// Features: -// - Multi-level sorting -// - Expandable rows for warehouse/batch details -// - Column visibility toggles -// - Inline filters -// - Aggregated totals -// - Export functionality -``` - -### Alert Widgets -```typescript -interface AlertWidgetProps { - type: 'low-stock' | 'expiring'; - threshold?: number; - daysAhead?: number; - maxItems?: number; -} - -// Display: -// - Count of alerts -// - Top N critical items -// - Quick action buttons -// - View all link -// - Auto-refresh -``` - -### Stock Level Gauge -```typescript -interface StockGaugeProps { - current: number; - minimum: number; - optimal: number; - maximum: number; - label: string; -} - -// Visualization: -// - Gauge or bar showing current level -// - Color zones (red, yellow, green) -// - Threshold indicators -// - Percentage display -``` - ---- - -## Frontend Best Practices - -### Dashboard Design -1. **Information Hierarchy**: Most important metrics at top -2. **Visual Design**: Use charts, colors, icons effectively -3. **Interactivity**: Make widgets clickable for details -4. **Responsiveness**: Adapt layout for different screens -5. **Performance**: Optimize for fast loading -6. **Refresh**: Auto-refresh at sensible intervals -7. **Customization**: Allow users to customize dashboard - -### Report Generation -1. **Loading States**: Show progress for long reports -2. **Pagination**: Paginate large reports -3. **Export**: Support multiple export formats -4. **Print**: Optimize for printing -5. **Save Reports**: Allow saving report configurations -6. **Schedule**: Consider scheduled report generation -7. **Filters**: Comprehensive filtering options - -### Data Visualization -1. **Chart Selection**: Choose appropriate chart types -2. **Color Scheme**: Consistent, accessible colors -3. **Legends**: Clear legends and labels -4. **Tooltips**: Informative hover tooltips -5. **Zoom/Pan**: Allow interaction with charts -6. **Responsive**: Charts adapt to screen size -7. **Accessibility**: Ensure charts are accessible - -### Performance -1. **Caching**: Cache dashboard data with TTL -2. **Lazy Loading**: Load heavy reports on demand -3. **Aggregation**: Pre-aggregate data server-side -4. **Virtualization**: Virtual scrolling for large tables -5. **Debouncing**: Debounce filter changes -6. **Web Workers**: Process large datasets in workers -7. **Progressive Loading**: Load critical data first - -### Alerting -1. **Thresholds**: Configurable alert thresholds -2. **Notifications**: Push/email notifications -3. **Acknowledgement**: Allow marking alerts as seen -4. **History**: Track alert history -5. **Escalation**: Escalate critical alerts -6. **Snooze**: Allow snoozing non-critical alerts -7. **Batch Actions**: Handle multiple alerts at once - ---- - -## Common Error Responses - -### 403 Forbidden -```json -{ - "success": false, - "message": "Access denied. Required permission: REPORT_READ", - "data": null -} -``` - -### 400 Bad Request - Invalid Parameters -```json -{ - "success": false, - "message": "Invalid parameter: daysAhead must be positive", - "data": null -} -``` - ---- - -## Integration Points - -### With Dashboard Tools -- Connect to BI tools (Power BI, Tableau) -- Export to analytics platforms -- API for custom reports - -### With Notification Systems -- Email alerts for low stock -- Push notifications for critical issues -- SMS alerts for urgent situations - -### With External Systems -- ERP integration -- Accounting system sync -- E-commerce platform data - -### With Mobile Apps -- Mobile-optimized dashboard -- Push notifications -- Quick actions from alerts - ---- - -## Performance Considerations - -### Caching Strategy -```typescript -// Dashboard data cache: 5 minutes -// Stock report cache: 15 minutes -// Low stock/expiring cache: 10 minutes - -interface CacheConfig { - dashboard: { ttl: 300 }, // 5 minutes - stockReport: { ttl: 900 }, // 15 minutes - lowStock: { ttl: 600 }, // 10 minutes - expiring: { ttl: 600 } // 10 minutes -} -``` - -### Optimization Tips -1. **Reduce API Calls**: Cache aggressively -2. **Pagination**: Always paginate large reports -3. **Debounce**: Debounce filter changes -4. **Progressive Enhancement**: Load critical data first -5. **Background Updates**: Update cache in background -6. **Service Workers**: Use for offline capability -7. **CDN**: Serve static chart assets from CDN - ---- - -## Future Enhancements - -### Advanced Reports -- Custom report builder -- Scheduled reports -- Report subscriptions -- Multi-tenant reporting -- Comparative analysis -- Trend forecasting - -### Advanced Visualizations -- Interactive charts -- Geographic maps -- Network graphs -- Heatmaps -- Real-time updates - -### Export Enhancements -- More formats (Word, PowerPoint) -- Custom templates -- Branded reports -- Automated distribution diff --git a/docs/endpoints/reports.md b/docs/endpoints/reports.md index 8e0f326..6b5f6cf 100644 --- a/docs/endpoints/reports.md +++ b/docs/endpoints/reports.md @@ -12,7 +12,7 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th **Summary**: Get dashboard summary with key metrics ### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` +**Required Permissions**: `reports:read` ### Request **Method**: `GET` @@ -54,7 +54,7 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th ], "stockByCategory": [ { - "categoryId": "660e8400-e29b-41d4-a716-446655440001", + "categoryId": "electronics", "categoryName": "Electronics", "batchCount": 45, "stockValue": 35000.00, @@ -103,7 +103,7 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th **Summary**: Get complete stock report ### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` +**Required Permissions**: `reports:read` ### Request **Method**: `GET` @@ -119,58 +119,39 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th { "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Electronics", - "totalQuantity": 250, + "warehouseId": "770e8400-e29b-41d4-a716-446655440002", + "warehouseName": "Main Warehouse", + "totalQuantity": 250.000, "totalValue": 2625.00, - "averageCostPrice": 10.50, - "batchCount": 5, - "warehouseCount": 2, - "oldestExpirationDate": "2026-03-15", - "newestExpirationDate": "2027-12-31", - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 150, - "batchCount": 3 - }, - { - "warehouseId": "880e8400-e29b-41d4-a716-446655440003", - "warehouseName": "Secondary Warehouse", - "warehouseCode": "WH-002", - "quantity": 100, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 80, - "expirationDate": "2026-03-15", - "costPrice": 10.50 - } - ] + "nearestExpiration": "2026-03-15", + "batchCount": 5 } ] } ``` +### Fields +| Field | Type | Description | +|---|---|---| +| `productId` | UUID | Product identifier | +| `productName` | String | Product name | +| `warehouseId` | UUID | Warehouse identifier | +| `warehouseName` | String | Warehouse name | +| `totalQuantity` | BigDecimal | Total quantity in stock | +| `totalValue` | BigDecimal | Total stock value (`costPrice * quantity`) | +| `nearestExpiration` | LocalDate | Nearest batch expiration date (nullable) | +| `batchCount` | Integer | Number of batches for this product/warehouse | + ### Frontend Implementation Guide 1. **Comprehensive Table**: Display all stock data in sortable table -2. **Expandable Rows**: Expand to show warehouse/batch details -3. **Export Options**: Export to CSV, Excel, PDF -4. **Print View**: Optimized print layout -5. **Filters**: Filter by category, warehouse, stock level -6. **Search**: Full-text search across products -7. **Aggregations**: Show totals at bottom (quantity, value) -8. **Visualization**: Charts for top products, categories -9. **Stock Levels**: Visual indicators (bars, gauges) -10. **Drill-down**: Link to product details +2. **Export Options**: Export to CSV, Excel, PDF +3. **Print View**: Optimized print layout +4. **Filters**: Filter by warehouse, stock level +5. **Search**: Full-text search across products +6. **Aggregations**: Show totals at bottom (quantity, value) +7. **Visualization**: Charts for top products +8. **Stock Levels**: Visual indicators (bars, gauges) +9. **Drill-down**: Link to product details --- @@ -178,7 +159,7 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th **Summary**: Get low stock report ### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` +**Required Permissions**: `reports:read` ### Request **Method**: `GET` @@ -199,35 +180,12 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th { "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Electronics", - "totalQuantity": 8, + "warehouseId": "770e8400-e29b-41d4-a716-446655440002", + "warehouseName": "Main Warehouse", + "totalQuantity": 8.000, "totalValue": 84.00, - "averageCostPrice": 10.50, - "batchCount": 2, - "warehouseCount": 1, - "oldestExpirationDate": "2026-03-15", - "newestExpirationDate": null, - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 8, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 5, - "expirationDate": "2026-03-15", - "costPrice": 10.50 - } - ] + "nearestExpiration": "2026-03-15", + "batchCount": 2 } ] } @@ -251,7 +209,7 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th **Summary**: Get expiring products report ### Authorization -**Required Permissions**: `REPORT_READ` or `ROLE_ADMIN` +**Required Permissions**: `reports:read` ### Request **Method**: `GET` @@ -272,45 +230,12 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th { "productId": "550e8400-e29b-41d4-a716-446655440000", "productName": "Product Name", - "productSku": "PROD-001", - "categoryId": "660e8400-e29b-41d4-a716-446655440001", - "categoryName": "Food Items", - "totalQuantity": 45, + "warehouseId": "770e8400-e29b-41d4-a716-446655440002", + "warehouseName": "Main Warehouse", + "totalQuantity": 45.000, "totalValue": 472.50, - "averageCostPrice": 10.50, - "batchCount": 3, - "warehouseCount": 2, - "oldestExpirationDate": "2026-01-15", - "newestExpirationDate": "2026-02-28", - "warehouses": [ - { - "warehouseId": "770e8400-e29b-41d4-a716-446655440002", - "warehouseName": "Main Warehouse", - "warehouseCode": "WH-001", - "quantity": 30, - "batchCount": 2 - } - ], - "batches": [ - { - "batchId": "990e8400-e29b-41d4-a716-446655440004", - "batchNumber": "BATCH-2025-001", - "warehouseName": "Main Warehouse", - "quantity": 15, - "expirationDate": "2026-01-15", - "costPrice": 10.50, - "daysUntilExpiration": 18 - }, - { - "batchId": "aa0e8400-e29b-41d4-a716-446655440005", - "batchNumber": "BATCH-2025-002", - "warehouseName": "Main Warehouse", - "quantity": 15, - "expirationDate": "2026-01-20", - "costPrice": 10.50, - "daysUntilExpiration": 23 - } - ] + "nearestExpiration": "2026-01-15", + "batchCount": 3 } ] } @@ -333,6 +258,386 @@ These endpoints provide dashboard summaries, stock reports, and analytics for th --- +## GET /api/reports/dashboard/summary +**Summary**: Get dashboard quick summary with key operational metrics + +### Authorization +**Required Permissions**: `reports:read` + +### Request +**Method**: `GET` + +### Response +**Status Code**: `200 OK` + +```json +{ + "success": true, + "message": null, + "data": { + "totalProducts": 150, + "totalWarehouses": 3, + "totalActiveBatches": 280, + "totalStockQuantity": 15000.000, + "totalStockValue": 150000.000, + "totalTransitQuantity": 500.000, + "pendingTransfers": 3, + "todayMovements": 12, + "criticalAlerts": 5 + } +} +``` + +### Fields +| Field | Type | Description | +|---|---|---| +| `totalProducts` | Long | Number of distinct products with stock | +| `totalWarehouses` | Long | Number of warehouses (1 if warehouse-scoped) | +| `totalActiveBatches` | Long | Number of active (non-deleted) batches | +| `totalStockQuantity` | BigDecimal | Sum of all batch quantities | +| `totalStockValue` | BigDecimal | Sum of `costPrice * quantity` across all batches | +| `totalTransitQuantity` | BigDecimal | Sum of all `transitQuantity` across batches | +| `pendingTransfers` | Long | Transfers in DRAFT, IN_TRANSIT, or PENDING_VALIDATION status | +| `todayMovements` | Long | Stock movements created today | +| `criticalAlerts` | Long | Products with low stock (<=10) OR expiring within 7 days | + +### Frontend Implementation Guide +1. **KPI Cards**: Display each metric in a card with icon and label +2. **Critical Alerts Badge**: Highlight `criticalAlerts` with a red badge when > 0 +3. **Pending Transfers Link**: Make `pendingTransfers` clickable to navigate to transfers list +4. **Auto-refresh**: Refresh every 60 seconds for real-time monitoring + +--- + +## GET /api/reports/dashboard/kpis +**Summary**: Get financial KPIs with month-over-month comparison + +### Authorization +**Required Permissions**: `reports:read` + +### Request +**Method**: `GET` + +### Response +**Status Code**: `200 OK` + +```json +{ + "success": true, + "message": null, + "data": { + "currentMonth": { + "totalStockValue": 150000.00, + "totalPurchasesValue": 15000.00, + "totalLossesValue": 800.00, + "totalDamageValue": 200.00, + "totalGiftValue": 150.00, + "totalAdjustmentValue": 300.00, + "totalTransitValue": 5000.00, + "stockTurnoverRate": 2.50 + }, + "previousMonth": { + "totalStockValue": 143000.00, + "totalPurchasesValue": 12000.00, + "totalLossesValue": 600.00, + "totalDamageValue": 100.00, + "totalGiftValue": 300.00, + "totalAdjustmentValue": 0.00, + "totalTransitValue": 3000.00, + "stockTurnoverRate": 2.10 + }, + "variations": { + "totalStockValue": 4.90, + "totalPurchasesValue": 25.00, + "totalLossesValue": 33.33, + "totalDamageValue": 100.00, + "totalGiftValue": -50.00, + "totalAdjustmentValue": null, + "totalTransitValue": 66.67, + "stockTurnoverRate": 19.05 + } + } +} +``` + +### Fields +| Field | Type | Description | +|---|---|---| +| `currentMonth` | KpiPeriodData | KPIs for the current calendar month | +| `previousMonth` | KpiPeriodData | KPIs for the previous calendar month. `null` if no historical data | +| `variations` | KpiVariations | Percentage variation between months. `null` if `previousMonth` is null. Individual fields are `null` when previous value is zero | + +#### KpiPeriodData Fields +| Field | Type | Description | +|---|---|---| +| `totalStockValue` | BigDecimal | Current total stock value (`costPrice * quantity`) | +| `totalPurchasesValue` | BigDecimal | Total quantity from PURCHASE_IN movements in the period | +| `totalLossesValue` | BigDecimal | Total quantity from LOSS movements in the period | +| `totalDamageValue` | BigDecimal | Total quantity from DAMAGE movements in the period | +| `totalGiftValue` | BigDecimal | Total quantity from GIFT movements in the period | +| `totalAdjustmentValue` | BigDecimal | Total quantity from ADJUSTMENT_IN + ADJUSTMENT_OUT movements | +| `totalTransitValue` | BigDecimal | Current transit quantity value | +| `stockTurnoverRate` | BigDecimal | Total OUT quantity / average stock value | + +#### KpiVariations Fields +Each field represents the percentage change: `((current - previous) / previous) * 100`. All fields share the same names as `KpiPeriodData`. + +### Frontend Implementation Guide +1. **KPI Cards with Arrows**: Show each KPI with an up/down arrow and variation percentage +2. **Color Coding**: Green for positive variations (purchases up), red for negative (losses up) +3. **Tooltip**: Show both current and previous month values on hover +4. **Turnover Gauge**: Display `stockTurnoverRate` as a gauge or progress bar +5. **Null Handling**: Show "N/A" when `previousMonth` or individual variations are null + +--- + +## GET /api/reports/dashboard/alerts +**Summary**: Get operational alerts for the dashboard + +### Authorization +**Required Permissions**: `reports:read` + +### Request +**Method**: `GET` + +### Response +**Status Code**: `200 OK` + +```json +{ + "success": true, + "message": null, + "data": { + "lowStockProducts": [ + { + "productId": "550e8400-e29b-41d4-a716-446655440000", + "productName": "Product A", + "warehouseId": "770e8400-e29b-41d4-a716-446655440002", + "warehouseName": "Main Warehouse", + "totalQuantity": 5.000, + "totalValue": 52.500, + "nearestExpiration": "2026-06-15", + "batchCount": 1 + } + ], + "expiringProducts": [ + { + "productId": "660e8400-e29b-41d4-a716-446655440001", + "productName": "Product B", + "warehouseId": "770e8400-e29b-41d4-a716-446655440002", + "warehouseName": "Main Warehouse", + "totalQuantity": 45.000, + "totalValue": 472.500, + "nearestExpiration": "2026-04-20", + "batchCount": 2 + } + ], + "recentLosses": [ + { + "movementType": "LOSS", + "productName": "Product C", + "quantity": 10.000, + "value": 105.000, + "date": "2026-04-05" + } + ], + "pendingTransfers": 3, + "highTransitValue": 5000.000 + } +} +``` + +### Fields +| Field | Type | Description | +|---|---|---| +| `lowStockProducts` | List\ | Top 10 products with quantity <= 10 | +| `expiringProducts` | List\ | Top 10 products expiring within 30 days | +| `recentLosses` | List\ | Last 30 days of LOSS and DAMAGE movements (max 10) | +| `pendingTransfers` | Long | Transfers in DRAFT, IN_TRANSIT, or PENDING_VALIDATION status | +| `highTransitValue` | BigDecimal | Sum of `costPrice * transitQuantity` for batches in transit | + +#### RecentMovementAlert Fields +| Field | Type | Description | +|---|---|---| +| `movementType` | StockMovementType | `LOSS` or `DAMAGE` | +| `productName` | String | Name of the product | +| `quantity` | BigDecimal | Quantity affected | +| `value` | BigDecimal | Financial value (`costPrice * quantity`) | +| `date` | LocalDate | Date of the movement | + +### Frontend Implementation Guide +1. **Alert Panels**: Group alerts by type (low stock, expiring, losses) +2. **Severity Colors**: Red for critical, yellow for warning +3. **Loss History**: Show recent losses in a timeline or compact list +4. **Pending Transfers Counter**: Badge with count, clickable to transfers page +5. **Transit Value Warning**: Highlight when `highTransitValue` exceeds a threshold + +--- + +## GET /api/reports/dashboard/movement-trend +**Summary**: Get daily movement volume trend for charts + +### Authorization +**Required Permissions**: `reports:read` + +### Request +**Method**: `GET` +**Query Parameters**: +- `days` (Integer, default: 30) - Number of days to look back + +**Example**: `/api/reports/dashboard/movement-trend?days=7` + +### Response +**Status Code**: `200 OK` + +```json +{ + "success": true, + "message": null, + "data": { + "startDate": "2026-03-09", + "endDate": "2026-04-08", + "days": [ + { + "date": "2026-04-01", + "totalInQuantity": 150.000, + "totalInValue": 0.000, + "totalOutQuantity": 80.000, + "totalOutValue": 0.000, + "movementCount": 12 + }, + { + "date": "2026-04-02", + "totalInQuantity": 200.000, + "totalInValue": 0.000, + "totalOutQuantity": 0.000, + "totalOutValue": 0.000, + "movementCount": 5 + } + ], + "totals": { + "totalInQuantity": 4500.000, + "totalInValue": 0.000, + "totalOutQuantity": 2800.000, + "totalOutValue": 0.000, + "movementCount": 320 + } + } +} +``` + +### Fields +| Field | Type | Description | +|---|---|---| +| `startDate` | LocalDate | Start date of the period | +| `endDate` | LocalDate | End date of the period (today) | +| `days` | List\ | One entry per day, including days with zero movements | +| `totals` | MovementTotals | Aggregated totals for the entire period | + +#### DailyMovement Fields +| Field | Type | Description | +|---|---|---| +| `date` | LocalDate | The date | +| `totalInQuantity` | BigDecimal | Total quantity from IN-direction movements | +| `totalInValue` | BigDecimal | Reserved for future use (currently 0) | +| `totalOutQuantity` | BigDecimal | Total quantity from OUT-direction movements | +| `totalOutValue` | BigDecimal | Reserved for future use (currently 0) | +| `movementCount` | Long | Number of distinct movements on this date | + +#### MovementTotals Fields +Same structure as `DailyMovement` but aggregated across all days in the period. + +### Frontend Implementation Guide +1. **Bar Chart**: Stacked bar chart with IN (green) and OUT (red) quantities per day +2. **Line Chart**: Alternative view showing IN and OUT trend lines +3. **Period Selector**: Buttons for 7, 15, 30, 60, 90 days +4. **Tooltip**: Show exact values on hover +5. **Summary Stats**: Display `totals` below the chart +6. **Zero-fill**: API already returns zero-filled days, no frontend gap-filling needed + +--- + +## GET /api/transfers/{id}/discrepancy-report +**Summary**: Get discrepancy report for a completed transfer + +### Authorization +**Required Permissions**: `transfers:read` + +### Request +**Method**: `GET` +**Path Parameters**: +- `id` (UUID, required) - Transfer ID + +**Example**: `/api/transfers/550e8400-e29b-41d4-a716-446655440000/discrepancy-report` + +### Response +**Status Code**: `200 OK` + +```json +{ + "success": true, + "message": null, + "data": { + "transferId": "550e8400-e29b-41d4-a716-446655440000", + "transferCode": "TRF-2026-001", + "sourceWarehouseName": "Main Warehouse", + "destinationWarehouseName": "Secondary Warehouse", + "completedAt": "2026-04-25T14:30:00Z", + "discrepancies": [ + { + "productName": "Product A", + "productBarcode": "1234567890128", + "quantitySent": 100.000, + "quantityReceived": 95.000, + "difference": -5.000, + "type": "SHORTAGE" + }, + { + "productName": "Product B", + "productBarcode": "9876543210987", + "quantitySent": 50.000, + "quantityReceived": 52.000, + "difference": 2.000, + "type": "OVERAGE" + } + ], + "totalShortage": -5.000, + "totalOverage": 2.000 + } +} +``` + +### Fields +| Field | Type | Description | +|---|---|---| +| `transferId` | UUID | Transfer identifier | +| `transferCode` | String | Human-readable transfer code | +| `sourceWarehouseName` | String | Origin warehouse name | +| `destinationWarehouseName` | String | Destination warehouse name | +| `completedAt` | Instant | Timestamp when transfer was completed | +| `discrepancies` | List\ | List of item-level discrepancies | +| `totalShortage` | BigDecimal | Sum of all SHORTAGE differences (negative) | +| `totalOverage` | BigDecimal | Sum of all OVERAGE differences (positive) | + +#### DiscrepancyItem Fields +| Field | Type | Description | +|---|---|---| +| `productName` | String | Product name | +| `productBarcode` | String | Product barcode | +| `quantitySent` | BigDecimal | Quantity sent from source | +| `quantityReceived` | BigDecimal | Quantity received at destination | +| `difference` | BigDecimal | `quantityReceived - quantitySent` | +| `type` | DiscrepancyType | `SHORTAGE` (received less) or `OVERAGE` (received more) | + +### Frontend Implementation Guide +1. **Discrepancy Table**: Show each item with sent vs received quantities +2. **Color Coding**: Red for SHORTAGE, green for OVERAGE +3. **Summary Cards**: Display totalShortage and totalOverage as KPIs +4. **Print**: Support printing the report for physical records +5. **Export**: Export to CSV or PDF for documentation + +--- + ## Frontend Component Examples ### Dashboard Widget diff --git a/docs/endpoints/roles.md b/docs/endpoints/roles.md index 82a0cf1..abeb75f 100644 --- a/docs/endpoints/roles.md +++ b/docs/endpoints/roles.md @@ -1,338 +1,82 @@ -# Role Endpoints +# Roles Endpoints -## Overview -These endpoints manage roles in the StockShift system. Roles are tenant-scoped and can have multiple permissions assigned. +Base path: `/api/roles` -**Base URL**: `/api/roles` -**Authentication**: Required (Bearer token) +All endpoints require Bearer authentication. Responses use `ApiResponse`. ---- +## Permissions -## POST /api/roles -**Summary**: Create a new role +- `roles:create` for create. +- `roles:read` for list and detail. +- `roles:update` for update. +- `roles:delete` for delete. -### Authorization -**Required Permissions**: `ROLE_ADMIN` +## Data Shapes -### Request -**Method**: `POST` -**Content-Type**: `application/json` +### RoleRequest -#### Request Body ```json { - "name": "Stock Manager", - "description": "Manages stock movements and batches", - "permissionIds": [ - "550e8400-e29b-41d4-a716-446655440000", - "550e8400-e29b-41d4-a716-446655440001" - ] + "name": "Operator", + "description": "Warehouse operator", + "permissionIds": ["00000000-0000-0000-0000-000000000000"] } ``` -**Field Details**: -- `name`: Required, role name (max 100 characters) -- `description`: Optional, role description (max 500 characters) -- `permissionIds`: Optional, array of permission UUIDs to assign - -### Response -**Status Code**: `201 CREATED` - -```json -{ - "success": true, - "message": "Role created successfully", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Stock Manager", - "description": "Manages stock movements and batches", - "isSystemRole": false, - "permissions": [ - { - "id": "660e8400-e29b-41d4-a716-446655440000", - "resource": "BATCH", - "action": "CREATE", - "scope": "TENANT", - "description": "Create batches" - }, - { - "id": "660e8400-e29b-41d4-a716-446655440001", - "resource": "STOCK_MOVEMENT", - "action": "CREATE", - "scope": "TENANT", - "description": "Create stock movements" - } - ], - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } -} -``` - -### Frontend Implementation Guide -1. **Role Form**: Create modal or page with form fields -2. **Permission Selector**: Multi-select or checkbox list for permissions -3. **Group Permissions**: Group permissions by resource (Product, Batch, etc.) -4. **Preview**: Show selected permissions summary -5. **Validation**: Validate name is required and unique - ---- - -## GET /api/roles -**Summary**: Get all roles - -### Authorization -**Required Permissions**: `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` +### RoleResponse ```json { - "success": true, - "message": null, - "data": [ + "id": "00000000-0000-0000-0000-000000000000", + "name": "Operator", + "description": "Warehouse operator", + "isSystemRole": false, + "permissions": [ { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Admin", - "description": "Full system access", - "isSystemRole": true, - "permissions": [...], - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - }, - { - "id": "660e8400-e29b-41d4-a716-446655440001", - "name": "Stock Manager", - "description": "Manages stock movements and batches", - "isSystemRole": false, - "permissions": [...], - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" + "id": "00000000-0000-0000-0000-000000000001", + "code": "products:read", + "description": "Read products" } - ] -} -``` - -### Frontend Implementation Guide -1. **Roles List**: Display roles in table or card layout -2. **System Role Badge**: Indicate system roles (non-editable) -3. **Permission Count**: Show number of permissions per role -4. **Actions**: Include edit, delete actions (disabled for system roles) -5. **Search/Filter**: Allow searching roles by name - ---- - -## GET /api/roles/{id} -**Summary**: Get role by ID - -### Authorization -**Required Permissions**: `ROLE_ADMIN` - -### Request -**Method**: `GET` -**URL Parameters**: `id` (UUID) - Role identifier - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Stock Manager", - "description": "Manages stock movements and batches", - "isSystemRole": false, - "permissions": [ - { - "id": "660e8400-e29b-41d4-a716-446655440000", - "resource": "BATCH", - "action": "CREATE", - "scope": "TENANT", - "description": "Create batches" - } - ], - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } -} -``` - -### Frontend Implementation Guide -1. **Detail View**: Display full role information -2. **Permissions List**: Show all assigned permissions grouped by resource -3. **Users with Role**: List users assigned to this role -4. **Edit Button**: Quick access to edit form (hidden for system roles) - ---- - -## PUT /api/roles/{id} -**Summary**: Update role - -### Authorization -**Required Permissions**: `ROLE_ADMIN` - -### Request -**Method**: `PUT` -**URL Parameters**: `id` (UUID) - Role identifier -**Content-Type**: `application/json` - -#### Request Body -Same structure as POST /api/roles - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": "Role updated successfully", - "data": { - // Updated role object - } + ], + "createdAt": "2026-05-09T10:00:00", + "updatedAt": "2026-05-09T10:00:00" } ``` -### Frontend Implementation Guide -1. **Edit Modal**: Open modal/drawer with pre-populated form -2. **System Role Check**: Prevent editing system roles -3. **Permission Diff**: Show added/removed permissions -4. **Impact Warning**: Warn about users affected by permission changes -5. **Optimistic Update**: Update UI immediately, rollback on error +## Endpoints ---- +### POST `/api/roles` -## DELETE /api/roles/{id} -**Summary**: Delete role +Creates a role. -### Authorization -**Required Permissions**: `ROLE_ADMIN` +- Body: `RoleRequest` +- Success: `201 Created`, `ApiResponse` -### Request -**Method**: `DELETE` -**URL Parameters**: `id` (UUID) - Role identifier +### GET `/api/roles` -### Response -**Status Code**: `200 OK` +Lists all roles for the current tenant. -```json -{ - "success": true, - "message": "Role deleted successfully", - "data": null -} -``` +- Success: `200 OK`, `ApiResponse` -### Frontend Implementation Guide -1. **Confirmation Modal**: Require confirmation before deletion -2. **System Role Check**: Prevent deleting system roles -3. **User Impact**: Show number of users assigned to this role -4. **Alternative Action**: Suggest reassigning users to another role -5. **Error Handling**: Handle constraint errors - ---- - -## Frontend Best Practices - -### Role Management Interface -```typescript -// Example role types -interface Role { - id: string; - name: string; - description: string; - isSystemRole: boolean; - permissions: Permission[]; - createdAt: string; - updatedAt: string; -} +### GET `/api/roles/{id}` -interface Permission { - id: string; - resource: string; - action: string; - scope: string; - description: string; -} +Gets one role by UUID. -// Group permissions by resource -function groupPermissionsByResource(permissions: Permission[]): Map { - return permissions.reduce((map, permission) => { - const existing = map.get(permission.resource) || []; - map.set(permission.resource, [...existing, permission]); - return map; - }, new Map()); -} -``` +- Path: `id` UUID +- Success: `200 OK`, `ApiResponse` -### Permission Selector Component -1. **Grouped View**: Group permissions by resource -2. **Select All**: Allow selecting all permissions for a resource -3. **Search**: Filter permissions by name/description -4. **Preview**: Show summary of selected permissions -5. **Conflict Detection**: Warn about conflicting permissions +### PUT `/api/roles/{id}` -### Visual Design -1. **System Role Badge**: Clearly mark system roles as non-editable -2. **Permission Icons**: Use icons for different resources -3. **Color Coding**: Use colors for different permission actions (read, create, update, delete) -4. **Count Badges**: Show permission count on role cards +Updates a role. -### State Management -1. **Cache Roles**: Cache role list -2. **Invalidation**: Refresh on create/update/delete -3. **Permission Cache**: Cache available permissions for selector +- Path: `id` UUID +- Body: `RoleRequest` +- Success: `200 OK`, `ApiResponse` ---- +### DELETE `/api/roles/{id}` -## Common Error Responses +Soft-deletes a role. -### 400 Bad Request - Duplicate Name -```json -{ - "success": false, - "message": "Role with name 'Stock Manager' already exists", - "data": null -} -``` +- Path: `id` UUID +- Success: `200 OK`, `ApiResponse` -### 400 Bad Request - System Role Modification -```json -{ - "success": false, - "message": "System roles cannot be modified", - "data": null -} -``` - -### 400 Bad Request - System Role Deletion -```json -{ - "success": false, - "message": "System roles cannot be deleted", - "data": null -} -``` - -### 404 Not Found -```json -{ - "success": false, - "message": "Role not found", - "data": null -} -``` - -### 404 Not Found - Permission Not Found -```json -{ - "success": false, - "message": "Permission not found with id: 550e8400-e29b-41d4-a716-446655440000", - "data": null -} -``` diff --git a/docs/endpoints/sales.md b/docs/endpoints/sales.md index 34feefe..217376a 100644 --- a/docs/endpoints/sales.md +++ b/docs/endpoints/sales.md @@ -1,299 +1,139 @@ -# Sales API Endpoints +# Sales Endpoints -## Overview -These endpoints manage sales transactions in the StockShift system. All endpoints require authentication with appropriate permissions. +Base path: `/api/sales` -**Base URL**: `/api/sales` -**Authentication**: Required (Bearer token) +Most endpoints require Bearer authentication and use `ApiResponse`. The InfinitePay webhook endpoint is public and authenticated by the `{token}` path segment. ---- +Money fields are integer cents unless the field is explicitly a percentage or quantity. ## Permissions -- `SALES:CREATE` - Create sales -- `SALES:READ` - View sales -- `SALES:CANCEL` - Cancel sales ---- +- `sales:create` for creating sales and confirming InfinitePay returns. +- `sales:read` for listing, detail, next code, and dashboard. +- `sales:cancel` for cancellation. -## POST /api/sales -**Summary**: Create a new sale and reduce stock automatically +## Enums -### Authorization -**Required Permissions**: `SALES:CREATE` +- `PaymentMethod`: `CASH`, `DEBIT_CARD`, `CREDIT_CARD`, `INSTALLMENT`, `PIX`, `BANK_TRANSFER`, `OTHER` +- `PaymentMode`: `DIRECT`, `TAP`, `LINK` +- `SaleStatus`: `PENDING`, `COMPLETED`, `CANCELLED` -### Request -**Method**: `POST` -**Content-Type**: `application/json` +## Data Shapes + +### CreateSaleRequest -#### Request Body ```json { - "warehouseId": 1, - "paymentMethod": "CASH", - "customerId": 10, - "customerName": "João Silva", - "discount": 10.50, - "notes": "Venda balcão", + "warehouseId": "00000000-0000-0000-0000-000000000000", + "paymentMethod": "PIX", + "installments": 1, + "discountPercentage": 0, "items": [ { - "productId": 5, - "batchId": 20, - "quantity": 2, - "unitPrice": 50.00 + "productId": "00000000-0000-0000-0000-000000000001", + "batchId": "00000000-0000-0000-0000-000000000002", + "quantity": 2 } - ] + ], + "useInfinitePay": false, + "paymentMode": "DIRECT" } ``` -**Field Details**: -- `warehouseId` (required): ID of warehouse where sale occurs -- `paymentMethod` (required): Payment method enum value -- `customerId` (optional): Customer ID if registered -- `customerName` (optional): Customer name for unregistered customers -- `discount` (optional): Discount amount (must be ≥ 0) -- `notes` (optional): Additional sale notes -- `items` (required): Array of sale items (must have at least 1) - - `productId` (required): Product being sold - - `batchId` (optional): Specific batch to use - - `quantity` (required): Quantity to sell (must be ≥ 1) - - `unitPrice` (required): Unit price (must be > 0) - -**Payment Methods**: -- `CASH` - Dinheiro -- `DEBIT_CARD` - Cartão de débito -- `CREDIT_CARD` - Cartão de crédito -- `INSTALLMENT` - Fiado/Crediário -- `PIX` - PIX -- `BANK_TRANSFER` - Transferência bancária -- `OTHER` - Outros - -### Response -**Status Code**: `201 Created` +### SaleResponse ```json { - "id": 100, - "warehouseId": 1, - "warehouseName": "Loja Principal", - "userId": 3, - "userName": "Maria Santos", - "customerId": 10, - "customerName": "João Silva", - "paymentMethod": "CASH", + "id": "00000000-0000-0000-0000-000000000000", + "code": "SALE-0001", + "warehouseId": "00000000-0000-0000-0000-000000000001", + "warehouseName": "Main Warehouse", + "paymentMethod": "PIX", + "installments": 1, + "discountPercentage": 0, + "subtotal": 10000, + "discountAmount": 0, + "total": 10000, "status": "COMPLETED", - "subtotal": 100.00, - "discount": 10.50, - "total": 89.50, - "notes": "Venda balcão", - "stockMovementId": null, - "createdAt": "2026-01-26T10:30:00", - "completedAt": "2026-01-26T10:30:00", + "cancelledByUserId": null, "cancelledAt": null, - "cancelledBy": null, - "cancelledByName": null, "cancellationReason": null, - "items": [ - { - "id": 150, - "productId": 5, - "productName": "Produto X", - "productSku": "SKU-001", - "batchId": 20, - "batchCode": "BATCH-2024-01", - "quantity": 2, - "unitPrice": 50.00, - "subtotal": 100.00 - } - ] + "createdByUserId": "00000000-0000-0000-0000-000000000003", + "createdAt": "2026-05-09T10:00:00Z", + "items": [], + "infinitepayNsu": null, + "infinitepayAut": null, + "infinitepayCardBrand": null, + "paymentMode": "DIRECT", + "paymentLink": null } ``` -### Error Responses -- `400 Bad Request` - Invalid request data, insufficient stock, or validation error -- `404 Not Found` - Warehouse or product not found +## Endpoints ---- +### POST `/api/sales` -## GET /api/sales -**Summary**: List all sales with pagination +Creates a sale. -### Authorization -**Required Permissions**: `SALES:READ` +- Body: `CreateSaleRequest` +- Success: `201 Created`, `ApiResponse` -### Request -**Method**: `GET` +### GET `/api/sales` -#### Query Parameters -- `page` (optional): Page number (default: 0) -- `size` (optional): Page size (default: 20) -- `sort` (optional): Sort field (default: createdAt,desc) +Lists sales using Spring pageable response format. -### Response -**Status Code**: `200 OK` +- Query: `warehouseId`, `paymentMethod`, `status`, `dateFrom`, `dateTo`, `page`, `size`, `sort` +- Date format: ISO date-time, for example `2026-05-09T10:00:00` +- Success: `200 OK`, `ApiResponse>` -```json -{ - "content": [ - { - "id": 100, - "warehouseId": 1, - "warehouseName": "Loja Principal", - "paymentMethod": "CASH", - "status": "COMPLETED", - "total": 89.50, - "createdAt": "2026-01-26T10:30:00" - } - ], - "pageable": { - "pageNumber": 0, - "pageSize": 20 - }, - "totalElements": 45, - "totalPages": 3 -} -``` +### GET `/api/sales/next-code` ---- +Returns the next sale code. -## GET /api/sales/{id} -**Summary**: Retrieve details of a specific sale +- Success: `200 OK`, `ApiResponse<{ "code": "SALE-0001" }>` -### Authorization -**Required Permissions**: `SALES:READ` +### GET `/api/sales/dashboard` -### Request -**Method**: `GET` +Returns sales dashboard KPIs and daily chart data. -#### Path Parameters -- `id` (required): Sale ID +- Query: `warehouseId` +- Success: `200 OK`, `ApiResponse` -### Response -**Status Code**: `200 OK` +### GET `/api/sales/{id}` -```json -{ - "id": 100, - "warehouseId": 1, - "warehouseName": "Loja Principal", - "userId": 3, - "userName": "Maria Santos", - "paymentMethod": "CASH", - "status": "COMPLETED", - "subtotal": 100.00, - "discount": 10.50, - "total": 89.50, - "items": [ - { - "id": 150, - "productId": 5, - "productName": "Produto X", - "quantity": 2, - "unitPrice": 50.00, - "subtotal": 100.00 - } - ] -} -``` +Gets sale details by UUID. -### Error Responses -- `404 Not Found` - Sale not found +- Path: `id` UUID +- Success: `200 OK`, `ApiResponse` ---- +### PUT `/api/sales/{id}/cancel` -## PUT /api/sales/{id}/cancel -**Summary**: Cancel a sale and return stock to warehouse +Cancels a sale. -### Authorization -**Required Permissions**: `SALES:CANCEL` +- Path: `id` UUID +- Body: `{ "cancellationReason": "Customer requested cancellation" }` +- Success: `200 OK`, `ApiResponse` -### Request -**Method**: `PUT` -**Content-Type**: `application/json` +### GET `/api/sales/infinitepay/callback` -#### Path Parameters -- `id` (required): Sale ID +Handles InfinitePay browser callback and redirects to `/sales/infinitepay/result`. -#### Request Body -```json -{ - "reason": "Cliente desistiu da compra" -} -``` +- Query: `order_id`, optional `warning` +- Success: `302 Found` -**Field Details**: -- `reason` (required): Cancellation reason (must not be blank) +### GET `/api/sales/infinitepay/confirm` -### Response -**Status Code**: `200 OK` +Confirms an InfinitePay return without redirect. -```json -{ - "id": 100, - "status": "CANCELLED", - "cancelledAt": "2026-01-26T11:00:00", - "cancelledBy": 3, - "cancelledByName": "Admin User", - "cancellationReason": "Cliente desistiu da compra", - "total": 89.50 -} -``` +- Query: `order_id`, optional `warning` +- Success: `200 OK`, `ApiResponse` -### Error Responses -- `400 Bad Request` - Sale already cancelled or cannot be cancelled -- `404 Not Found` - Sale not found - ---- - -## Business Rules - -### Stock Management -- Stock is reduced immediately when sale is created using FIFO strategy -- Products with expiration dates are prioritized (closest to expiry first) -- If a batch doesn't have enough quantity, multiple batches are used - -### Cancellation -- Only completed sales can be cancelled -- Stock is returned to the original batches -- Cancellation reason is mandatory -- Audit trail is maintained (who cancelled, when, why) - -### Validations -- All products must be active and available -- Sufficient stock must be available in the specified warehouse -- Unit prices must be positive -- Sale must have at least one item -- Discount cannot be negative - ---- - -## Examples - -### Create Sale with Multiple Items -```bash -curl -X POST http://localhost:8080/api/sales \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "warehouseId": 1, - "paymentMethod": "CREDIT_CARD", - "discount": 0, - "items": [ - {"productId": 1, "quantity": 5, "unitPrice": 10.00}, - {"productId": 2, "quantity": 3, "unitPrice": 25.00} - ] - }' -``` +### POST `/api/sales/infinitepay/webhook/{token}` -### List Sales with Pagination -```bash -curl -X GET "http://localhost:8080/api/sales?page=0&size=10&sort=createdAt,desc" \ - -H "Authorization: Bearer YOUR_TOKEN" -``` +Receives InfinitePay webhook notifications. This endpoint does not use Bearer auth. + +- Path: `token` +- Body fields include `invoice_slug`, `amount`, `paid_amount`, `installments`, `capture_method`, `transaction_nsu`, `order_nsu`, `receipt_url`, `items` +- Success: `200 OK` with empty body +- Invalid payload/token: `400 Bad Request` -### Cancel Sale -```bash -curl -X PUT http://localhost:8080/api/sales/100/cancel \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"reason": "Customer requested refund"}' -``` diff --git a/docs/endpoints/stock-movements.md b/docs/endpoints/stock-movements.md index 810048a..d1de482 100644 --- a/docs/endpoints/stock-movements.md +++ b/docs/endpoints/stock-movements.md @@ -6,17 +6,17 @@ Modulo de movimentacao de estoque para registrar entradas e saidas manuais (uso, Tipos de movimentacao e direcao: -| Tipo | Direcao | Descricao | -|------------------|---------|---------------------------------------------| -| `USAGE` | `OUT` | Produto consumido/utilizado | -| `GIFT` | `OUT` | Produto dado como presente | -| `LOSS` | `OUT` | Produto perdido/extraviado | -| `DAMAGE` | `OUT` | Produto danificado | -| `ADJUSTMENT_OUT` | `OUT` | Ajuste manual de saida | -| `PURCHASE_IN` | `IN` | Entrada por compra | -| `ADJUSTMENT_IN` | `IN` | Ajuste manual de entrada | -| `TRANSFER_IN` | `IN` | *(automatico)* Entrada por transferencia | -| `TRANSFER_OUT` | `OUT` | *(automatico)* Saida por transferencia | +| Tipo | Direcao | Descricao | +| ---------------- | ------- | ---------------------------------------- | +| `USAGE` | `OUT` | Produto consumido/utilizado | +| `GIFT` | `OUT` | Produto dado como presente | +| `LOSS` | `OUT` | Produto perdido/extraviado | +| `DAMAGE` | `OUT` | Produto danificado | +| `ADJUSTMENT_OUT` | `OUT` | Ajuste manual de saida | +| `PURCHASE_IN` | `IN` | Entrada por compra | +| `ADJUSTMENT_IN` | `IN` | Ajuste manual de entrada | +| `TRANSFER_IN` | `IN` | _(automatico)_ Entrada por transferencia | +| `TRANSFER_OUT` | `OUT` | _(automatico)_ Saida por transferencia | Base path de aplicacao: `/stockshift` @@ -28,16 +28,17 @@ Autenticacao: obrigatoria (JWT em cookie `accessToken` ou header `Authorization` ## Authorization Matrix -- `POST /api/stock-movements`: `stock_movements:create` +- `POST /api/stock-movements` (application/json): `stock_movements:create` +- `POST /api/stock-movements` (multipart/form-data): `stock_movements:create` - `GET /api/stock-movements`: `stock_movements:read` - `GET /api/stock-movements/{id}`: `stock_movements:read` - `GET /api/stock-movements/warehouse-summary`: `stock_movements:read` ## Endpoints -### POST /api/stock-movements +### POST /api/stock-movements (application/json) -Cria uma movimentacao de estoque manual. Use `application/json` quando nao houver imagem de produto inline. Use `multipart/form-data` quando algum produto inline tiver imagem. +Cria uma movimentacao de estoque manual. Request: @@ -51,32 +52,32 @@ Request: "quantity": 5 }, { - "productId": "660e8400-e29b-41d4-a716-446655440001", - "quantity": 2.5, - "costPrice": 500, - "sellingPrice": 800, - "manufacturedDate": "2026-04-01", - "expirationDate": "2026-12-31" + "newProduct": { + "name": "Novo Produto Exemplo", + "description": "Descricao do novo produto", + "sku": "NP-001", + "barcode": "1234567890123", + "categoryId": "440e8400-e29b-41d4-a716-446655440000", + "brandId": "330e8400-e29b-41d4-a716-446655440000", + "minStock": 10, + "maxStock": 100, + "taxPercentage": 5.5, + "unitOfMeasure": "UN", + "costPrice": 1050, + "sellingPrice": 2000, + "isEnabled": true + }, + "quantity": 10, + "costPrice": 1050, + "sellingPrice": 2000 }, { - "quantity": 4, - "costPrice": 1290, - "sellingPrice": 2490, + "productId": "660e8400-e29b-41d4-a716-446655440001", + "quantity": 2.5, "manufacturedDate": "2026-04-01", "expirationDate": "2026-12-31", - "newProduct": { - "name": "Produto configurado durante a compra", - "description": "Criado somente ao registrar a movimentacao", - "barcode": "ABC-123", - "categoryId": "550e8400-e29b-41d4-a716-446655440000", - "brandId": "660e8400-e29b-41d4-a716-446655440002", - "isKit": false, - "hasExpiration": false, - "active": true, - "attributes": { - "weight": "1kg" - } - } + "costPrice": 500, + "sellingPrice": 800 } ] } @@ -87,24 +88,30 @@ Regras: - `type` obrigatorio. Valores permitidos: `USAGE`, `GIFT`, `LOSS`, `DAMAGE`, `ADJUSTMENT_OUT`, `PURCHASE_IN`, `ADJUSTMENT_IN`. - `type` **nao** pode ser `TRANSFER_IN` ou `TRANSFER_OUT` (retorna `400`). - `items` obrigatorio e nao vazio. -- Cada item precisa de `quantity` positivo e deve conter **um** destes formatos: - - Produto existente: `productId` (UUID). - - Produto novo: `newProduct` com o mesmo formato JSON de `POST /api/products`. -- Produtos em `newProduct` sao persistidos dentro da mesma transacao da movimentacao. -- `costPrice` e `sellingPrice` sao opcionais, enviados em centavos no item da movimentacao, e aplicados ao batch criado em movimentos de entrada para produto existente ou produto novo. -- `manufacturedDate` e `expirationDate` sao opcionais, enviados no item da movimentacao, e aplicados ao batch criado em movimentos de entrada para produto existente ou produto novo. -- Para imagem de produto novo, envie multipart com: - - `movement`: Blob JSON do request acima (`application/json`). - - `inlineProductImages`: uma parte de arquivo para cada produto novo, na mesma ordem em que esses produtos aparecem em `items`; quando um produto novo nao tiver imagem, envie uma parte vazia para preservar o pareamento. -- Se a criacao de qualquer produto, batch, ledger ou item falhar, toda a movimentacao faz rollback. -- Produtos novos inline sao aceitos somente para movimentacoes de entrada (`PURCHASE_IN`, `ADJUSTMENT_IN`). +- Cada item precisa possuir **obrigatoriamente** `productId` (UUID) ou `newProduct` (objeto `ProductRequest` com os dados do novo produto a ser salvo), mas nao ambos simultaneamente. Retorna validação se colocar ambos ou nenhum. +- `quantity` (positivo) obrigatorio para todos os itens. +- Para movimentos `IN` (`PURCHASE_IN`, `ADJUSTMENT_IN`), valores opcionais `manufacturedDate`, `expirationDate`, `costPrice` e `sellingPrice` podem ser repassados no item, tanto para `productId` quanto para `newProduct`. - Para movimentos `OUT`, o sistema deduz automaticamente dos batches usando FIFO (batch mais antigo primeiro). - Se a quantidade total disponivel no warehouse for insuficiente, retorna `400` com mensagem de estoque insuficiente. -- Para movimentos `IN` com `productId`, o sistema cria um novo batch quando qualquer data/preco de lote for informado. Sem esses campos, adiciona a quantidade ao primeiro batch existente do produto, ou cria o primeiro batch se ainda nao houver lote. +- Para movimentos `IN` com `productId`, se qualquer data/preco de lote for informado, o sistema cria um novo batch com esses dados; se nenhum dado for informado, adiciona a quantidade ao primeiro batch existente do produto ou cria o primeiro batch se ainda nao houver lote. +- Se passado um `newProduct`, o sistema antes ira cadastrar tal produto no BD para em seguida criar o seu batch com as premissas deste estoque de entrada. - O `warehouseId` e determinado automaticamente pelo warehouse do usuario logado. - Um codigo unico e gerado automaticamente (ex: `MOV-2026-0001`). -Response (`201 Created`): +--- + +### POST /api/stock-movements (multipart/form-data) + +Cria uma movimentacao de estoque semelhante ao endpoint json, porem com suporte a envio de imagens do novo produto gerado inline, utilizando content-type `multipart/form-data`. + +Parts: + +- `movement` (Obrigatorio): Parte contendo o JSON com o body equivalente a `CreateStockMovementRequest`. Content-Type dessa part deve ser `application/json`. +- `inlineProductImages` (Opcional): Lista de arquivos de imagem (`MultipartFile`). Permite anexar a imagem do novo produto listado em `movement.items.newProduct`. Apos cadastrar o produto (que ira ser associado a essa mov), o sistema fara upload desta imagem associando-a ao novo produto. O suporte aceita o envio de varias fotos num unico vetor de dados, caso aja varios novos produtos com imagens enviadas na mesma transacao, preenchendo da forma programada. + +Response (`201 Created`): Retorna as mesmas informacoes geradas do modo padrao da aplicacao json. + +Response (`201 Created`) comum: ```json { @@ -157,16 +164,16 @@ Lista movimentacoes com filtros opcionais e paginacao. Query parameters: -| Parametro | Tipo | Obrigatorio | Descricao | -|---------------|-----------------|-------------|----------------------------------------------| -| `warehouseId` | UUID | Nao | Filtra por warehouse (padrao: warehouse do usuario) | -| `productId` | UUID | Nao | Filtra movimentacoes que contenham o produto | -| `type` | StockMovementType | Nao | Filtra por tipo (ex: `USAGE`, `GIFT`) | -| `dateFrom` | ISO DateTime | Nao | Data/hora inicial (ex: `2026-03-01T00:00:00`) | -| `dateTo` | ISO DateTime | Nao | Data/hora final (ex: `2026-03-31T23:59:59`) | -| `page` | int | Nao | Numero da pagina (0-indexed, padrao: 0) | -| `size` | int | Nao | Tamanho da pagina (padrao: 20) | -| `sort` | string | Nao | Campo e direcao (ex: `createdAt,desc`) | +| Parametro | Tipo | Obrigatorio | Descricao | +| ------------- | ----------------- | ----------- | --------------------------------------------------- | +| `warehouseId` | UUID | Nao | Filtra por warehouse (padrao: warehouse do usuario) | +| `productId` | UUID | Nao | Filtra movimentacoes que contenham o produto | +| `type` | StockMovementType | Nao | Filtra por tipo (ex: `USAGE`, `GIFT`) | +| `dateFrom` | ISO DateTime | Nao | Data/hora inicial (ex: `2026-03-01T00:00:00`) | +| `dateTo` | ISO DateTime | Nao | Data/hora final (ex: `2026-03-31T23:59:59`) | +| `page` | int | Nao | Numero da pagina (0-indexed, padrao: 0) | +| `size` | int | Nao | Tamanho da pagina (padrao: 20) | +| `sort` | string | Nao | Campo e direcao (ex: `createdAt,desc`) | Exemplo de chamada: @@ -225,10 +232,10 @@ Retorna relatorio resumido de movimentacoes agrupado por warehouse e tipo. Query parameters: -| Parametro | Tipo | Obrigatorio | Descricao | -|------------|--------------|-------------|-----------------------------------------------| -| `dateFrom` | ISO DateTime | Nao | Data/hora inicial do periodo | -| `dateTo` | ISO DateTime | Nao | Data/hora final do periodo | +| Parametro | Tipo | Obrigatorio | Descricao | +| ---------- | ------------ | ----------- | ---------------------------- | +| `dateFrom` | ISO DateTime | Nao | Data/hora inicial do periodo | +| `dateTo` | ISO DateTime | Nao | Data/hora final do periodo | Exemplo de chamada: @@ -251,24 +258,24 @@ Response (`200 OK`): { "type": "USAGE", "direction": "OUT", - "totalQuantity": 150.00, + "totalQuantity": 150.0, "count": 12 }, { "type": "GIFT", "direction": "OUT", - "totalQuantity": 25.00, + "totalQuantity": 25.0, "count": 3 }, { "type": "PURCHASE_IN", "direction": "IN", - "totalQuantity": 500.00, + "totalQuantity": 500.0, "count": 5 } ], - "totalIn": 500.00, - "totalOut": 175.00 + "totalIn": 500.0, + "totalOut": 175.0 }, { "warehouseId": "880e8400-e29b-41d4-a716-446655440000", @@ -284,33 +291,33 @@ Response (`200 OK`): ## Campos do response `StockMovementResponse` -| Campo | Tipo | Descricao | -|--------------------|----------|------------------------------------------------------------| -| `id` | UUID | ID da movimentacao | -| `code` | string | Codigo unico gerado (ex: `MOV-2026-0001`) | -| `warehouseId` | UUID | ID do warehouse | -| `warehouseName` | string | Nome do warehouse | -| `type` | string | Tipo da movimentacao (ver tabela acima) | -| `direction` | string | `IN` ou `OUT` | -| `notes` | string? | Observacoes opcionais | -| `createdByUserId` | UUID | ID do usuario que criou | -| `referenceType` | string? | Tipo da referencia (ex: `TRANSFER` para movimentos automaticos) | -| `referenceId` | UUID? | ID do recurso referenciado (ex: ID da transferencia) | -| `createdAt` | ISO 8601 | Data/hora de criacao | -| `updatedAt` | ISO 8601 | Data/hora da ultima atualizacao | -| `items` | array | Lista de items da movimentacao | +| Campo | Tipo | Descricao | +| ----------------- | -------- | --------------------------------------------------------------- | +| `id` | UUID | ID da movimentacao | +| `code` | string | Codigo unico gerado (ex: `MOV-2026-0001`) | +| `warehouseId` | UUID | ID do warehouse | +| `warehouseName` | string | Nome do warehouse | +| `type` | string | Tipo da movimentacao (ver tabela acima) | +| `direction` | string | `IN` ou `OUT` | +| `notes` | string? | Observacoes opcionais | +| `createdByUserId` | UUID | ID do usuario que criou | +| `referenceType` | string? | Tipo da referencia (ex: `TRANSFER` para movimentos automaticos) | +| `referenceId` | UUID? | ID do recurso referenciado (ex: ID da transferencia) | +| `createdAt` | ISO 8601 | Data/hora de criacao | +| `updatedAt` | ISO 8601 | Data/hora da ultima atualizacao | +| `items` | array | Lista de items da movimentacao | ## Campos do response `StockMovementItemResponse` -| Campo | Tipo | Descricao | -|---------------|------------|-------------------------------------| -| `id` | UUID | ID do item | -| `productId` | UUID | ID do produto | -| `productName` | string | Nome do produto (snapshot) | -| `productSku` | string? | SKU do produto (snapshot) | -| `batchId` | UUID | ID do batch afetado | -| `batchCode` | string | Codigo do batch afetado (snapshot) | -| `quantity` | BigDecimal | Quantidade movimentada neste batch | +| Campo | Tipo | Descricao | +| ------------- | ---------- | ---------------------------------- | +| `id` | UUID | ID do item | +| `productId` | UUID | ID do produto | +| `productName` | string | Nome do produto (snapshot) | +| `productSku` | string? | SKU do produto (snapshot) | +| `batchId` | UUID | ID do batch afetado | +| `batchCode` | string | Codigo do batch afetado (snapshot) | +| `quantity` | BigDecimal | Quantidade movimentada neste batch | ## Integracao com Transfer @@ -319,6 +326,7 @@ Quando uma transferencia e executada (`POST /api/transfers/{id}/execute`), o sis Quando uma transferencia e validada com sucesso (`POST /api/transfers/{id}/complete-validation`), o sistema cria automaticamente um `StockMovement` do tipo `TRANSFER_IN` no warehouse de destino. Esses movimentos possuem: + - `referenceType`: `"TRANSFER"` - `referenceId`: ID da transferencia diff --git a/docs/endpoints/tenants.md b/docs/endpoints/tenants.md new file mode 100644 index 0000000..4bce9da --- /dev/null +++ b/docs/endpoints/tenants.md @@ -0,0 +1,86 @@ +# Tenants Endpoints + +Base path: `/api/tenants` + +All endpoints require Bearer authentication. Responses use `ApiResponse`. + +## Permissions + +- `tenants:read` for reads. +- `tenants:update` for updates. + +## Data Shapes + +### CompanyConfigResponse + +```json +{ + "businessName": "StockShift Ltda", + "document": "12345678000190", + "email": "company@example.com", + "phone": "+5511999999999", + "logoUrl": "https://storage.example.com/logo.png", + "isActive": true +} +``` + +### UpdateCompanyRequest + +```json +{ + "businessName": "StockShift Ltda", + "document": "12345678000190", + "email": "company@example.com", + "phone": "+5511999999999" +} +``` + +### InfinitePayConfigResponse + +```json +{ + "handle": "stockshift", + "docNumber": "12345678000190", + "configured": true +} +``` + +## Endpoints + +### GET `/api/tenants/me` + +Returns company configuration for the current tenant. + +- Success: `200 OK`, `ApiResponse` + +### PUT `/api/tenants/me` + +Updates company configuration. The backend accepts both JSON and multipart form data. + +JSON request: + +- Content-Type: `application/json` +- Body: `UpdateCompanyRequest` + +Multipart request: + +- Content-Type: `multipart/form-data` +- Parts: + - `company`: `UpdateCompanyRequest` + - `logo`: optional file + +Success: `200 OK`, `ApiResponse` + +### GET `/api/tenants/me/infinitepay` + +Returns InfinitePay configuration for the current tenant. + +- Success: `200 OK`, `ApiResponse` + +### PUT `/api/tenants/me/infinitepay` + +Updates InfinitePay configuration. + +- Body: `{ "handle": "stockshift", "docNumber": "12345678000190" }` +- Success: `200 OK`, `ApiResponse` + diff --git a/docs/endpoints/transfer.md b/docs/endpoints/transfer.md index 1ae7664..1af9e1e 100644 --- a/docs/endpoints/transfer.md +++ b/docs/endpoints/transfer.md @@ -1,34 +1,47 @@ # Transfer Endpoints ## Overview -These endpoints manage inventory transfers between warehouses. -Transfer lifecycle: -- `DRAFT` -> `IN_TRANSIT` -> `PENDING_VALIDATION` -> `COMPLETED` -- `DRAFT` -> `IN_TRANSIT` -> `PENDING_VALIDATION` -> `COMPLETED_WITH_DISCREPANCY` -- `DRAFT` -> `CANCELLED` -- `IN_TRANSIT` -> `CANCELLED` +Fluxo de transferencia entre warehouses: -**Base URL**: `/stockshift/api/transfers` -**Authentication**: Required (Bearer token) +- `DRAFT -> IN_TRANSIT -> PENDING_VALIDATION -> COMPLETED` +- `DRAFT -> IN_TRANSIT -> PENDING_VALIDATION -> COMPLETED_WITH_DISCREPANCY` +- `DRAFT -> CANCELLED` +- `IN_TRANSIT -> CANCELLED` ---- +Base path de aplicacao: `/stockshift` -## POST /stockshift/api/transfers -**Summary**: Create a new inventory transfer in `DRAFT` status. +Base path do recurso: `/api/transfers` -### Authorization -**Required Permissions**: `TRANSFER_EXECUTE` or `ROLE_ADMIN` +Base URL efetiva: `/stockshift/api/transfers` -### Request -**Method**: `POST` -**Content-Type**: `application/json` +Autenticacao: obrigatoria (JWT em cookie `accessToken` ou header `Authorization`) + +## Authorization Matrix + +- `POST /api/transfers`: `transfers:create` +- `GET /api/transfers`: `transfers:read` +- `GET /api/transfers/{id}`: `transfers:read` +- `PATCH /api/transfers/{id}`: `transfers:update` +- `DELETE /api/transfers/{id}`: `transfers:delete` +- `POST /api/transfers/{id}/execute`: `transfers:execute` +- `POST /api/transfers/{id}/start-validation`: `transfers:validate` +- `POST /api/transfers/{id}/scan`: `transfers:validate` +- `POST /api/transfers/{id}/complete-validation`: `transfers:validate` +- `GET /api/transfers/{id}/discrepancy-report`: `transfers:read` +- `GET /api/transfers/{id}/validation-logs`: `transfers:read` + +## Endpoints + +### POST /api/transfers +Cria transferencia em `DRAFT`. + +Request: -#### Request Body ```json { "destinationWarehouseId": "550e8400-e29b-41d4-a716-446655440000", - "notes": "Transferência de reposição mensal", + "notes": "Reposicao mensal", "items": [ { "sourceBatchId": "660e8400-e29b-41d4-a716-446655440002", @@ -38,312 +51,99 @@ Transfer lifecycle: } ``` -### Response (201 Created) -```json -{ - "success": true, - "message": "Transfer created successfully", - "data": { - "id": "770e8400-e29b-41d4-a716-446655440003", - "code": "TRF-2026-0001", - "sourceWarehouseId": "...", - "sourceWarehouseName": "Warehouse A", - "destinationWarehouseId": "...", - "destinationWarehouseName": "Warehouse B", - "status": "DRAFT", - "notes": "Transferência de reposição mensal", - "items": [...], - "createdAt": "2026-02-04T10:00:00Z" - } -} -``` +Regras: ---- +- `destinationWarehouseId` obrigatorio. +- `items` obrigatorio e nao vazio. +- origem e destino devem ser diferentes. -## GET /stockshift/api/transfers -**Summary**: List transfers with optional filtering and pagination. +### GET /api/transfers +Lista transferencias com filtros opcionais: -### Request -**Method**: `GET` -**Query Parameters**: -- `status`: Filter by status (`DRAFT`, `IN_TRANSIT`, `PENDING_VALIDATION`, `COMPLETED`, `COMPLETED_WITH_DISCREPANCY`, `CANCELLED`) -- `sourceWarehouseId`: Filter by origin warehouse -- `destinationWarehouseId`: Filter by destination warehouse -- `page`, `size`, `sort`: Standard pagination parameters +- `status` +- `sourceWarehouseId` +- `destinationWarehouseId` +- `page`, `size`, `sort` -### Response (200 OK) -```json -{ - "success": true, - "message": "Transfers retrieved successfully", - "data": { - "content": [...], - "totalElements": 50, - "totalPages": 5, - "number": 0, - "size": 10 - } -} -``` +### GET /api/transfers/{id} +Retorna transferencia por ID. ---- +### PATCH /api/transfers/{id} +Atualiza transferencia (somente em `DRAFT`). -## GET /stockshift/api/transfers/{id} -**Summary**: Get a transfer by ID. +Request: -### Response (200 OK) ```json { - "success": true, - "message": "Transfer retrieved successfully", - "data": { - "id": "770e8400-e29b-41d4-a716-446655440003", - "code": "TRF-2026-0001", - "sourceWarehouseId": "...", - "sourceWarehouseName": "Warehouse A", - "destinationWarehouseId": "...", - "destinationWarehouseName": "Warehouse B", - "status": "DRAFT", - "notes": "...", - "items": [...] - } -} -``` - ---- - -## PATCH /stockshift/api/transfers/{id} -**Summary**: Update a transfer in `DRAFT` status. - -### Authorization -**Required Permissions**: `TRANSFER_EXECUTE` or `ROLE_ADMIN` - -### Description -Only transfers in `DRAFT` status can be updated. Can only be performed by users in the source warehouse. - -### Request Body -```json -{ - "notes": "Updated notes", + "notes": "Observacao atualizada", "items": [ { "sourceBatchId": "660e8400-e29b-41d4-a716-446655440002", - "quantity": 15.0 + "quantity": 15 } ] } ``` -### Response (200 OK) -```json -{ - "success": true, - "message": "Transfer updated successfully", - "data": { ... } -} -``` - ---- - -## POST /stockshift/api/transfers/{id}/execute -**Summary**: Transitions a transfer from `DRAFT` to `IN_TRANSIT`. +### DELETE /api/transfers/{id} +Cancela transferencia. -### Authorization -**Required Permissions**: `TRANSFER_EXECUTE` or `ROLE_ADMIN` +- Em `IN_TRANSIT`, `reason` e obrigatorio. -### Description -This operation deducts the items from the source warehouse inventory and marks them as "in transit". Once executed, the transfer cannot be edited, only cancelled or validated. +Request opcional: -### Response (200 OK) ```json { - "success": true, - "message": "Transfer executed successfully", - "data": { ... } + "reason": "Erro de separacao" } ``` ---- +### POST /api/transfers/{id}/execute +Move status para `IN_TRANSIT` e gera lancamentos de saida no ledger. -## POST /stockshift/api/transfers/{id}/start-validation -**Summary**: Initiates the receiving process at the destination warehouse. +### POST /api/transfers/{id}/start-validation +Move status para `PENDING_VALIDATION`. -### Authorization -**Required Permissions**: `TRANSFER_VALIDATE` or `ROLE_ADMIN` +### POST /api/transfers/{id}/scan +Registra leitura de barcode na validacao. -### Description -Moves the status to `PENDING_VALIDATION`. This step is required before scanning items at the destination. +Request: -### Response (200 OK) -```json -{ - "success": true, - "message": "Validation started successfully", - "data": { ... } -} -``` - ---- - -## POST /stockshift/api/transfers/{id}/scan -**Summary**: Validates an item during the receiving process via barcode. - -### Authorization -**Required Permissions**: `TRANSFER_VALIDATE` or `ROLE_ADMIN` - -### Request Body ```json { "barcode": "7891234567890" } ``` -### Response (200 OK) -```json -{ - "success": true, - "message": "Barcode processed", - "data": { - "valid": true, - "message": "Item validated successfully", - "warning": null, - "productName": "Perfume XYZ", - "productBarcode": "7891234567890", - "quantitySent": 10.0, - "quantityReceived": 1.0 - } -} -``` - ---- - -## POST /stockshift/api/transfers/{id}/complete-validation -**Summary**: Finalizes the transfer and updates destination inventory. - -### Authorization -**Required Permissions**: `TRANSFER_VALIDATE` or `ROLE_ADMIN` - -### Description -Finalizes the process, moving stock from "transit" to the destination warehouse. Returns a report identifying any `SHORTAGE` (faltas) or `OVERAGE` (sobras). - -### Response (200 OK) -```json -{ - "success": true, - "message": "Validation completed successfully", - "data": { - "transferId": "...", - "discrepancies": [...] - } -} -``` - ---- - -## GET /stockshift/api/transfers/{id}/discrepancy-report -**Summary**: Get the discrepancy report for a transfer. - -### Description -Returns a report showing differences between sent and received quantities. -This endpoint is only available when transfer status is `COMPLETED_WITH_DISCREPANCY`. - -### Response (200 OK) -```json -{ - "success": true, - "message": "Discrepancy report retrieved successfully", - "data": { - "transferId": "...", - "transferCode": "TRF-2026-0001", - "sourceWarehouseName": "Warehouse A", - "destinationWarehouseName": "Warehouse B", - "completedAt": "2026-02-06T01:20:00Z", - "discrepancies": [ - { - "productName": "Perfume XYZ", - "productBarcode": "7891234567890", - "quantitySent": 10.0, - "quantityReceived": 9.0, - "difference": -1.0, - "type": "SHORTAGE" - } - ], - "totalShortage": 1.0, - "totalOverage": 0.0 - } -} -``` - -### Response (400 Bad Request) -```json -{ - "timestamp": "2026-02-06T01:30:00", - "status": 400, - "error": "Bad Request", - "message": "Discrepancy report only available for transfers with discrepancies", - "path": "/stockshift/api/transfers/{id}/discrepancy-report" -} -``` +### POST /api/transfers/{id}/complete-validation +Finaliza validacao, gera entradas no destino e conclui em: ---- +- `COMPLETED`, ou +- `COMPLETED_WITH_DISCREPANCY` -## GET /stockshift/api/transfers/{id}/validation-logs -**Summary**: Get the validation scan logs for a transfer. +### GET /api/transfers/{id}/discrepancy-report +Retorna relatorio de discrepancia. -### Description -Returns all barcode scan events recorded during the validation process. +- Disponivel apenas quando status for `COMPLETED_WITH_DISCREPANCY`. -### Response (200 OK) -```json -{ - "success": true, - "message": "Validation logs retrieved successfully", - "data": [ - { - "id": "880e8400-e29b-41d4-a716-446655440004", - "transferItemId": "990e8400-e29b-41d4-a716-446655440005", - "barcode": "7891234567890", - "validatedByUserId": "aa0e8400-e29b-41d4-a716-446655440006", - "validatedAt": "2026-02-04T14:30:00Z", - "valid": true - } - ] -} -``` - ---- +### GET /api/transfers/{id}/validation-logs +Retorna logs de scan da transferencia. -## DELETE /stockshift/api/transfers/{id} -**Summary**: Cancel a transfer. +## Error Handling (relevante para transfer) -### Authorization -**Required Permissions**: `TRANSFER_CANCEL` or `ROLE_ADMIN` +- `400 Bad Request`: validacoes de payload/regra de negocio. +- `403 Forbidden`: warehouse sem acesso para a acao. +- `404 Not Found`: transferencia, batch ou warehouse inexistente. +- `409 Conflict`: + - transicao de estado invalida (`IllegalStateException`) + - violacao de constraints de banco (`DataIntegrityViolationException`) -### Description -Cancels a transfer. If the transfer is `IN_TRANSIT`, stock movements are reverted and a reason is required. Can only be performed by users in the source warehouse. +Exemplo de conflito de constraint: -### Request Body (optional for DRAFT, required for IN_TRANSIT) ```json { - "reason": "Erro no preenchimento dos itens" + "status": 409, + "error": "Conflict", + "message": "Transfer code already exists. Please retry the operation." } ``` - -### Response (200 OK) -```json -{ - "success": true, - "message": "Transfer cancelled successfully", - "data": { ... } -} -``` - ---- - -## Frontend Implementation Guide -1. **Lifecycle Management**: Enable buttons (Execute, Validate, Scan) based on the current `status` of the transfer. -2. **Warehouse Context**: The source warehouse is inferred from the user's current active warehouse. -3. **Real-time Scanning**: The `/scan` endpoint is optimized for high-frequency use with handheld scanners. -4. **Validation UX**: Show a list of items to be received and highlight discrepancies in real-time as the user scans. -5. **Validation Resume**: Use `GET /stockshift/api/transfers/{id}` and `items[].quantityReceived` to resume an in-progress validation (`PENDING_VALIDATION`) without starting it again. -6. **Discrepancy Report Rule**: Call `/discrepancy-report` only when status is `COMPLETED_WITH_DISCREPANCY`; otherwise expect `400 Bad Request`. diff --git a/docs/endpoints/users.md b/docs/endpoints/users.md index 83a4bbb..34a0879 100644 --- a/docs/endpoints/users.md +++ b/docs/endpoints/users.md @@ -1,18 +1,50 @@ # User Management Endpoints ## Overview -These endpoints handle user management within a tenant. All endpoints require authentication and ADMIN role. +These endpoints handle user management within a tenant. All endpoints require authentication and the matching user permission. Users with `ROLE_ADMIN`, `ROLE_SUPER_ADMIN`, or `*` are allowed by the permission guard. **Base URL**: `/api/users` --- +## Error Response Format + +Validation, business rule, permission, and not found errors use the global error response format: + +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", + "message": "Error message", + "path": "/api/users" +} +``` + +Validation errors include a `validationErrors` object: + +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Validation Failed", + "message": "Invalid input", + "path": "/api/users", + "validationErrors": { + "email": "Invalid email format" + } +} +``` + +--- + ## GET /api/users **Summary**: List all users in the current tenant ### Request **Method**: `GET` -**Authentication**: Required (ROLE_ADMIN) +**Authentication**: Required (Bearer token) +**Required Permissions**: `users:read` ### Response **Status Code**: `200 OK` @@ -56,7 +88,8 @@ These endpoints handle user management within a tenant. All endpoints require au ### Request **Method**: `POST` **Content-Type**: `application/json` -**Authentication**: Required (ROLE_ADMIN) +**Authentication**: Required (Bearer token) +**Required Permissions**: `users:create` #### Request Body ```json @@ -107,45 +140,66 @@ These endpoints handle user management within a tenant. All endpoints require au **400 Bad Request** (Email already exists): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Email already registered in this tenant", - "data": null + "path": "/api/users" } ``` **400 Bad Request** (Invalid role): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Role not found: ", - "data": null + "path": "/api/users" +} +``` + +**400 Bad Request** (Role from another tenant): +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", + "message": "Role does not belong to this tenant: ", + "path": "/api/users" } ``` **400 Bad Request** (Invalid warehouse): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Warehouse not found: ", - "data": null + "path": "/api/users" } ``` **400 Bad Request** (Inactive warehouse): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Warehouse is inactive: ", - "data": null + "path": "/api/users" } ``` -**403 Forbidden** (Not admin): +**403 Forbidden** (Missing permission): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 403, + "error": "Forbidden", "message": "You don't have permission to access this resource", - "data": null + "path": "/api/users" } ``` @@ -157,7 +211,8 @@ These endpoints handle user management within a tenant. All endpoints require au ### Request **Method**: `GET` **URL Parameters**: `id` (UUID) - User identifier -**Authentication**: Required (ROLE_ADMIN) +**Authentication**: Required (Bearer token) +**Required Permissions**: `users:read` ### Response **Status Code**: `200 OK` @@ -185,9 +240,11 @@ These endpoints handle user management within a tenant. All endpoints require au **404 Not Found**: ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 404, + "error": "Not Found", "message": "User not found with id: ", - "data": null + "path": "/api/users/" } ``` @@ -200,7 +257,8 @@ These endpoints handle user management within a tenant. All endpoints require au **Method**: `PUT` **URL Parameters**: `id` (UUID) - User identifier **Content-Type**: `application/json` -**Authentication**: Required (ROLE_ADMIN) +**Authentication**: Required (Bearer token) +**Required Permissions**: `users:update` #### Request Body ```json @@ -246,18 +304,55 @@ These endpoints handle user management within a tenant. All endpoints require au **404 Not Found**: ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 404, + "error": "Not Found", "message": "User not found with id: ", - "data": null + "path": "/api/users/" } ``` **400 Bad Request** (Invalid role): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Role not found: ", - "data": null + "path": "/api/users/" +} +``` + +**400 Bad Request** (Role from another tenant): +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", + "message": "Role does not belong to this tenant: ", + "path": "/api/users/" +} +``` + +**400 Bad Request** (Invalid warehouse): +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", + "message": "Warehouse not found: ", + "path": "/api/users/" +} +``` + +**400 Bad Request** (Inactive warehouse): +```json +{ + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", + "message": "Warehouse is inactive: ", + "path": "/api/users/" } ``` @@ -276,7 +371,8 @@ These endpoints handle user management within a tenant. All endpoints require au ### Request **Method**: `DELETE` **URL Parameters**: `id` (UUID) - User identifier -**Authentication**: Required (ROLE_ADMIN) +**Authentication**: Required (Bearer token) +**Required Permissions**: `users:delete` ### Response **Status Code**: `200 OK` @@ -294,18 +390,22 @@ These endpoints handle user management within a tenant. All endpoints require au **404 Not Found**: ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 404, + "error": "Not Found", "message": "User not found with id: ", - "data": null + "path": "/api/users/" } ``` **400 Bad Request** (Self-deletion): ```json { - "success": false, + "timestamp": "2026-01-27T10:30:00", + "status": 400, + "error": "Business Rule Violation", "message": "Cannot delete your own account", - "data": null + "path": "/api/users/" } ``` @@ -325,23 +425,22 @@ Users are associated with specific warehouses. This controls what data they can | User Type | Warehouse Access | |-----------|------------------| -| ADMIN | Full access to all warehouses | -| Non-admin with warehouses | Access only to assigned warehouses | -| Non-admin without warehouses | Blocked (403 Forbidden) | +| ADMIN | Full access flag in the authenticated principal | +| Non-admin with warehouses | Access only to assigned warehouses/current warehouse context | +| Non-admin without warehouses | No warehouse scope available for warehouse-scoped operations | ### Affected Operations When a non-admin user accesses the system: - **Warehouses**: Only sees assigned warehouses - **Batches**: Only sees batches from assigned warehouses -- **Stock Movements**: Only sees movements involving assigned warehouses - **Sales**: Can only create sales in assigned warehouses ### Frontend Implementation Guide 1. **User Creation Form**: Include warehouse selection (multi-select) 2. **Temporary Password**: Display the temporary password clearly and advise admin to share securely -3. **Role Selection**: Fetch available roles from `/api/roles` (to be implemented) +3. **Role Selection**: Fetch available roles from `/api/roles` 4. **Warehouse Selection**: Fetch available warehouses from `/api/warehouses` 5. **Error Handling**: Display specific validation errors 6. **User List**: Show roles and warehouses for each user @@ -354,6 +453,6 @@ When a non-admin user accesses the system: - `201`: Created - `400`: Bad Request (validation errors) - `401`: Unauthorized (not authenticated) -- `403`: Forbidden (not admin or no warehouse access) +- `403`: Forbidden (missing permission or no warehouse access) - `404`: Not Found - `500`: Internal Server Error diff --git a/docs/endpoints/warehouses.md b/docs/endpoints/warehouses.md index a2ad152..6994827 100644 --- a/docs/endpoints/warehouses.md +++ b/docs/endpoints/warehouses.md @@ -1,550 +1,121 @@ # Warehouse Endpoints ## Overview -These endpoints manage warehouses (physical locations where stock is stored) in the StockShift system. -**Base URL**: `/api/warehouses` -**Authentication**: Required (Bearer token) +Gerencia warehouses do tenant atual. ---- +Base path de aplicacao: `/stockshift` -## POST /api/warehouses -**Summary**: Create a new warehouse +Base path do recurso: `/api/warehouses` -### Authorization -**Required Permissions**: `WAREHOUSE_CREATE` or `ROLE_ADMIN` +Base URL efetiva: `/stockshift/api/warehouses` -### Request -**Method**: `POST` -**Content-Type**: `application/json` +Autenticacao: obrigatoria. -#### Request Body -```json -{ - "name": "Main Warehouse", - "city": "New York", - "state": "NY", - "address": "123 Storage St, City, State 12345", - "isActive": true -} -``` - -**Field Details**: -- `name`: Required, warehouse name (2-255 characters) -- `code`: Optional, unique warehouse code (max 20 chars, uppercase letters, numbers, hyphens). If not provided, the backend auto-generates a human-readable code based on the warehouse name and city (e.g., "Main Warehouse" in "New York" → `MAI-NE`) -- `city`: Required, city name (max 100 characters) -- `state`: Required, state code (2 uppercase letters) -- `address`: Optional, physical address (max 500 characters) -- `isActive`: Optional, default `true`, warehouse status - -**Auto-generated Code Format**: -- Pattern: `{3 letters from name}-{2 letters from city}` -- If code already exists, a numeric suffix is added: `MAI-NE-02`, `MAI-NE-03`, etc. -- Examples: - - "Central Depot" + "São Paulo" → `CEN-SA` - - "North Warehouse" + "Rio de Janeiro" → `NOR-RI` - -### Response -**Status Code**: `201 CREATED` - -```json -{ - "success": true, - "message": null, - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Main Warehouse", - "code": "WH-001", - "city": "New York", - "state": "NY", - "address": "123 Storage St, City, State 12345", - "isActive": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } -} -``` - -### Frontend Implementation Guide -1. **Form Fields**: Create comprehensive form with all fields -2. **Code Generation**: Auto-generate unique warehouse code -3. **Address Input**: Use address autocomplete/geocoding -4. **Phone Validation**: Validate phone number format -5. **Email Validation**: Validate email format -6. **Map Integration**: Show warehouse location on map (optional) -7. **Success Flow**: Redirect to warehouse list or detail view - ---- - -## GET /api/warehouses -**Summary**: Get all warehouses - -### Authorization -**Required Permissions**: `WAREHOUSE_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Main Warehouse", - "code": "WH-001", - "city": "New York", - "state": "NY", - "address": "123 Storage St, City, State 12345", - "isActive": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } - ] -} -``` - -### Frontend Implementation Guide -1. **List View**: Display warehouses in table or card grid -2. **Key Info**: Show name, code, address, active status -3. **Status Badge**: Visual indicator for active/inactive status -4. **Quick Actions**: Edit, view details, activate/deactivate -5. **Filtering**: Filter by active status -6. **Sorting**: Sort by name, code, created date -7. **Stock Summary**: Show total stock value per warehouse (if available) -8. **Map View**: Optional map view showing all warehouse locations - ---- - -## GET /api/warehouses/{id}/products -**Summary**: Get all products with aggregated stock for a specific warehouse - -### Authorization -**Required Permissions**: `WAREHOUSE_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` -**URL Parameters**: `id` (UUID) - Warehouse identifier - -**Query Parameters** (optional): -- `page`: Page number (default: 0) -- `size`: Page size (default: 20) -- `sort`: Sort field and direction (e.g., "name,asc") - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "data": { - "content": [ - { - "id": "550e8400-e29b-41d4-a716-446655440001", - "name": "Product A", - "sku": "SKU-001", - "barcode": "1234567890", - "barcodeType": "EAN13", - "description": "Product description", - "categoryId": "550e8400-e29b-41d4-a716-446655440010", - "categoryName": "Electronics", - "brand": { - "id": "550e8400-e29b-41d4-a716-446655440020", - "name": "Brand Name", - "logoUrl": "https://example.com/logo.png", - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - }, - "isKit": false, - "attributes": {}, - "hasExpiration": false, - "active": true, - "totalQuantity": 125.00, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } - ], - "pageable": { - "pageNumber": 0, - "pageSize": 20, - "sort": [], - "offset": 0, - "unpaged": false, - "paged": true - }, - "totalElements": 150, - "totalPages": 8, - "number": 0, - "size": 20, - "empty": false - } -} -``` - -### Error Responses - -#### 404 Not Found -```json -{ - "success": false, - "message": "Warehouse not found", - "data": null -} -``` - -#### 400 Bad Request - Invalid Pagination -```json -{ - "success": false, - "message": "Invalid page size", - "data": null -} -``` - -### Features -- **Aggregated Stock**: Sums quantity across all batches per product -- **Multi-tenant Aware**: Filters by both batch and product tenant IDs -- **Soft Delete Aware**: Excludes soft-deleted products -- **Optional Relationships**: Supports products without category or brand (LEFT JOIN) -- **Pagination**: Supports configurable page size (default 20) -- **Sorting**: Supports sorting by product entity fields only -- **Zero Stock**: Includes products with zero current inventory - -### Valid Sort Fields -The following fields can be used for sorting: -- `name` - Product name -- `sku` - Stock keeping unit -- `barcode` - Barcode code -- `active` - Active status -- `createdAt` - Creation timestamp -- `updatedAt` - Last update timestamp - -**Note**: Sorting by aggregated fields like `totalQuantity` is not supported due to SQL GROUP BY limitations. - -### Frontend Implementation Guide -1. **Inventory Grid**: Display products in paginated table view -2. **Stock Display**: Show total quantity with visual indicators (low, normal, high) -3. **Product Info**: Display name, SKU, barcode, category (if available), brand (if available) -4. **Missing Relationships**: Handle null category and brand gracefully -5. **Actions**: Add stock movement, view batch history, edit product details -6. **Pagination**: Implement page selector (5, 10, 20, 50 items per page) -7. **Filtering**: Option to hide zero-stock products -8. **Sort Validation**: Only allow sorting by valid fields listed above -9. **Export**: Export product list with quantities to CSV/PDF -10. **Real-time**: Refresh button to reload current page data -11. **Search**: Filter products by name or SKU (frontend side or add API param) -12. **Batch History**: Click product to view batch-level details with dates/expiration - -### Usage Examples -```bash -# Get first page of products (default size) -curl -H "Authorization: Bearer {token}" \ - https://api.example.com/api/warehouses/550e8400-e29b-41d4-a716-446655440000/products - -# Get with custom pagination -curl -H "Authorization: Bearer {token}" \ - https://api.example.com/api/warehouses/550e8400-e29b-41d4-a716-446655440000/products?page=0&size=50 - -# Sort by product name ascending -curl -H "Authorization: Bearer {token}" \ - https://api.example.com/api/warehouses/550e8400-e29b-41d4-a716-446655440000/products?page=0&size=20&sort=name,asc - -# Sort by creation date descending -curl -H "Authorization: Bearer {token}" \ - https://api.example.com/api/warehouses/550e8400-e29b-41d4-a716-446655440000/products?page=0&size=20&sort=createdAt,desc - -# Multiple sort fields -curl -H "Authorization: Bearer {token}" \ - https://api.example.com/api/warehouses/550e8400-e29b-41d4-a716-446655440000/products?sort=active,desc&sort=name,asc -``` - -### Implementation Notes -- **Query Aggregation**: Uses SQL GROUP BY with SUM for quantity aggregation -- **Performance**: Optimized with COUNT DISTINCT for pagination -- **Null Handling**: Category and Brand are optional fields; NULL values are preserved -- **Tenant Isolation**: Enforces strict multi-tenant isolation on both batch and product level - ---- - -## GET /api/warehouses/{id} -**Summary**: Get warehouse by ID - -### Authorization -**Required Permissions**: `WAREHOUSE_READ` or `ROLE_ADMIN` - -### Request -**Method**: `GET` -**URL Parameters**: `id` (UUID) - Warehouse identifier - -### Response -**Status Code**: `200 OK` - -```json -{ - "success": true, - "message": null, - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "name": "Main Warehouse", - "code": "WH-001", - "city": "New York", - "state": "NY", - "address": "123 Storage St, City, State 12345", - "isActive": true, - "createdAt": "2025-12-28T10:00:00Z", - "updatedAt": "2025-12-28T10:00:00Z" - } -} -``` - -### Frontend Implementation Guide -1. **Detail View**: Display all warehouse information -2. **Sections**: Organize into sections (Info, Contact, Stock, Activity) -3. **Stock Overview**: Show batches and stock levels in this warehouse -4. **Recent Movements**: Display recent stock movements -5. **Statistics**: Total products, total quantity, stock value -6. **Map**: Show warehouse location on map -7. **Edit Button**: Quick access to edit form -8. **Activity Log**: Show recent activities in warehouse - ---- +## Authorization Matrix -## GET /api/warehouses/active/{isActive} -**Summary**: Get warehouses by active status +- `POST /api/warehouses`: `warehouses:create` ou `ROLE_ADMIN` +- `GET /api/warehouses`: `warehouses:read` ou `ROLE_ADMIN` +- `GET /api/warehouses/stock-summary`: `warehouses:read` ou `ROLE_ADMIN` +- `GET /api/warehouses/{id}`: `warehouses:read` ou `ROLE_ADMIN` +- `GET /api/warehouses/active/{isActive}`: `warehouses:read` ou `ROLE_ADMIN` +- `PUT /api/warehouses/{id}`: `warehouses:update` ou `ROLE_ADMIN` +- `DELETE /api/warehouses/{id}`: `warehouses:delete` ou `ROLE_ADMIN` +- `GET /api/warehouses/{id}/products`: `warehouses:read` + `warehouseGuard.isCurrent` ou `ROLE_ADMIN` -### Authorization -**Required Permissions**: `WAREHOUSE_READ` or `ROLE_ADMIN` +## WarehouseRequest -### Request -**Method**: `GET` -**URL Parameters**: `isActive` (Boolean) - `true` for active, `false` for inactive +Campos: -### Response -Same format as GET /api/warehouses (returns array of warehouses) +- `name` (obrigatorio, max 255) +- `city` (obrigatorio, max 100) +- `state` (obrigatorio, 2 letras maiusculas) +- `code` (opcional, max 20, `A-Z0-9-`) +- `address` (opcional, max 500) +- `isActive` (opcional, default `true`) -### Frontend Implementation Guide -1. **Status Filter**: Toggle or tabs to switch between active/inactive -2. **Visual Distinction**: Different styling for inactive warehouses -3. **Activation**: Quick activate/deactivate action -4. **Default View**: Default to showing only active warehouses -5. **Bulk Actions**: Allow bulk activation/deactivation - ---- - -## PUT /api/warehouses/{id} -**Summary**: Update warehouse - -### Authorization -**Required Permissions**: `WAREHOUSE_UPDATE` or `ROLE_ADMIN` - -### Request -**Method**: `PUT` -**URL Parameters**: `id` (UUID) - Warehouse identifier -**Content-Type**: `application/json` - -#### Request Body -Same structure as POST /api/warehouses - -### Response -**Status Code**: `200 OK` +Exemplo: ```json { - "success": true, - "message": "Warehouse updated successfully", - "data": { - // Updated warehouse object - } + "name": "Main Warehouse", + "city": "New York", + "state": "NY", + "code": "MAIN-NY", + "address": "123 Storage St", + "isActive": true } ``` -### Frontend Implementation Guide -1. **Edit Form**: Pre-populate with current data -2. **Code Immutability**: Consider making code read-only after creation -3. **Validation**: Same as create form -4. **Change Tracking**: Highlight changed fields -5. **Impact Warning**: Warn if deactivating warehouse with stock -6. **Confirmation**: Require confirmation for major changes -7. **Optimistic Update**: Update UI immediately, rollback on error - ---- - -## DELETE /api/warehouses/{id} -**Summary**: Delete warehouse - -### Authorization -**Required Permissions**: `WAREHOUSE_DELETE` or `ROLE_ADMIN` - -### Request -**Method**: `DELETE` -**URL Parameters**: `id` (UUID) - Warehouse identifier +Se `code` nao for enviado, o backend gera automaticamente com base em nome/cidade. -### Response -**Status Code**: `200 OK` +## Endpoints -```json -{ - "success": true, - "message": "Warehouse deleted successfully", - "data": null -} -``` +### POST /api/warehouses +Cria warehouse para o tenant atual. -### Frontend Implementation Guide -1. **Strict Confirmation**: Strong confirmation required (type warehouse name) -2. **Constraint Check**: Check if warehouse has stock or batches -3. **Error Message**: Clear message if deletion blocked by constraints -4. **Alternative**: Suggest deactivation instead of deletion -5. **Data Migration**: Offer to transfer stock to another warehouse -6. **Audit Trail**: Ensure action is logged -7. **Irreversible Warning**: Make it clear deletion is permanent - ---- - -## Frontend Component Examples - -### Warehouse Selector -```typescript -interface WarehouseSelectorProps { - value: string | null; - onChange: (warehouseId: string) => void; - activeOnly?: boolean; - placeholder?: string; -} +Retorna `201 Created`. -// Component should: -// - Load warehouses from API -// - Filter by active status if activeOnly=true -// - Show warehouse code and name in dropdown -// - Support search/autocomplete -// - Show inactive warehouses with visual distinction -``` +### GET /api/warehouses +Lista warehouses visiveis para o usuario atual. -### Warehouse Card -```typescript -interface WarehouseCardProps { - warehouse: Warehouse; - onEdit: (id: string) => void; - onDelete: (id: string) => void; - onToggleActive: (id: string) => void; - showStats?: boolean; -} +- Admin (`ROLE_ADMIN`) ve todas do tenant. +- Usuario sem full-access ve apenas warehouses permitidas. -// Card should display: -// - Warehouse name and code -// - Active/inactive badge -// - Address summary -// - Contact info (icon + text) -// - Stock statistics (if showStats=true) -// - Action buttons (Edit, Delete, Toggle Active) -``` +### GET /api/warehouses/stock-summary +Retorna resumo de estoque por warehouse acessivel. -### Warehouse List Filters -```typescript -interface WarehouseFilters { - activeStatus: 'all' | 'active' | 'inactive'; - searchQuery: string; - sortBy: 'name' | 'code' | 'createdAt'; - sortOrder: 'asc' | 'desc'; -} +Response (lista de `WarehouseStockSummaryResponse`): -// Filter bar should include: -// - Search input (by name or code) -// - Status filter (All/Active/Inactive) -// - Sort dropdown -// - Clear filters button -// - Create new warehouse button -``` +- `warehouseId` (UUID) +- `productCount` (Long) +- `batchCount` (Long) +- `totalQuantity` (BigDecimal) ---- +### GET /api/warehouses/{id} +Retorna warehouse por ID com validacao de acesso. -## Frontend Best Practices +### GET /api/warehouses/active/{isActive} +Filtra por status ativo/inativo. -### State Management -1. **Cache**: Cache warehouse list for quick access -2. **Invalidation**: Refresh on create/update/delete -3. **Active Only**: Default to loading active warehouses only -4. **Lazy Loading**: Load details on demand -5. **Optimistic Updates**: Update UI immediately for better UX +### PUT /api/warehouses/{id} +Atualiza dados da warehouse. -### Validation -1. **Unique Code**: Check code uniqueness before submission -2. **Required Fields**: Validate name and code -3. **Format Validation**: Validate email and phone formats -4. **Length Limits**: Enforce character limits -5. **Real-time Feedback**: Show validation errors as user types +### DELETE /api/warehouses/{id} +Remove warehouse. -### User Experience -1. **Quick Actions**: Provide quick access to common operations -2. **Contextual Info**: Show relevant info in context (e.g., stock count) -3. **Visual Status**: Clear visual indicators for active/inactive -4. **Search**: Fast, responsive search functionality -5. **Responsive**: Mobile-friendly warehouse management -6. **Shortcuts**: Keyboard shortcuts for common actions +### GET /api/warehouses/{id}/products +Retorna produtos com estoque agregado no warehouse. -### Permissions -1. **Hide Actions**: Hide unavailable actions based on permissions -2. **Disable Buttons**: Disable buttons for unauthorized actions -3. **Tooltips**: Explain why actions are disabled -4. **Role-based Views**: Adjust UI based on user role +Requer adicionalmente `warehouseGuard.isCurrent(#id)` — o warehouse +solicitado deve ser o mesmo do contexto do token JWT. ---- +Query params: -## Common Error Responses +- `search` (opcional) — busca por nome, SKU ou barcode do produto +- `page` — pagina (zero-based) +- `size` — tamanho da pagina +- `sort` — campo,direcao (ex: `name,asc`) -### 400 Bad Request - Duplicate Code -```json -{ - "success": false, - "message": "Warehouse code already exists", - "data": null -} -``` +Sort permitido apenas em: -### 400 Bad Request - Has Stock -```json -{ - "success": false, - "message": "Cannot delete warehouse: contains active stock batches", - "data": { - "batchCount": 45, - "totalQuantity": 1250 - } -} -``` +- `name` +- `sku` +- `barcode` +- `active` +- `createdAt` +- `updatedAt` -### 404 Not Found -```json -{ - "success": false, - "message": "Warehouse not found", - "data": null -} -``` +Exemplo: -### 409 Conflict -```json -{ - "success": false, - "message": "Cannot deactivate: warehouse has pending stock movements", - "data": { - "pendingMovements": 3 - } -} +```bash +curl -H "Authorization: Bearer " \ + "http://localhost:8080/stockshift/api/warehouses/{id}/products?page=0&size=20&sort=name,asc" ``` ---- - -## Integration Points - -### With Batches -- GET `/api/batches/warehouse/{warehouseId}` - Get all batches in warehouse -- Used in warehouse detail view to show inventory +## Error Handling (comum) -### With Reports -- GET `/api/reports/stock` - Filter by warehouse -- Warehouse-specific stock reports and analytics +- `400 Bad Request`: payload invalido. +- `403 Forbidden`: usuario sem acesso ao warehouse. +- `404 Not Found`: warehouse inexistente. +- `409 Conflict`: conflitos de integridade/regras de negocio. From 4dab1529badad9fe1219f1815e03da19cb8f32ce Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sat, 9 May 2026 20:21:32 -0300 Subject: [PATCH 09/19] refactor: migrate batch detail logic to view-model pattern and consolidate status formatting types --- .../[id]/batches-detail.components.tsx | 253 +++++------------- .../batches/[id]/batches-detail.model.test.ts | 214 +++++++++++++-- .../batches/[id]/batches-detail.model.ts | 179 ++++++++++++- .../batches/[id]/batches-detail.types.ts | 55 +++- .../batches/[id]/batches-detail.view.test.tsx | 78 ------ .../batches/[id]/batches-detail.view.tsx | 231 ++++++---------- app/(pages)/batches/[id]/page.client.tsx | 34 +-- 7 files changed, 581 insertions(+), 463 deletions(-) delete mode 100644 app/(pages)/batches/[id]/batches-detail.view.test.tsx diff --git a/app/(pages)/batches/[id]/batches-detail.components.tsx b/app/(pages)/batches/[id]/batches-detail.components.tsx index 872a066..a6f3710 100644 --- a/app/(pages)/batches/[id]/batches-detail.components.tsx +++ b/app/(pages)/batches/[id]/batches-detail.components.tsx @@ -6,20 +6,12 @@ import { Archive, CalendarDays, CheckCircle2, + GitBranch, Package, Pencil, Trash2, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; -import { - differenceInCalendarDays, - format, - isPast, - isToday, - isValid, - parseISO, -} from "date-fns"; -import { ptBR } from "date-fns/locale"; import { AlertDialog, AlertDialogAction, @@ -35,169 +27,36 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { PermissionGate } from "@/components/permission-gate"; import { cn } from "@/lib/utils"; -import type { Batch } from "../batches.types"; - -/* ─── State Definitions ─── */ - -export interface BatchStateView { - label: string; - description: string; - Icon: LucideIcon; - badgeClass: string; - panelClass: string; - textClass: string; - meterClass: string; -} - -export const LOW_STOCK_THRESHOLD = 10; - -const activeState: BatchStateView = { - label: "Operacional", - description: "Disponível para venda", - Icon: CheckCircle2, - badgeClass: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400", - panelClass: "border-emerald-500/30 bg-emerald-950/10", - textClass: "text-emerald-400", - meterClass: "bg-emerald-500", -}; - -const lowStockState: BatchStateView = { - label: "Estoque baixo", - description: `Abaixo de ${LOW_STOCK_THRESHOLD + 1} unidades`, - Icon: Archive, - badgeClass: "border-blue-500/30 bg-blue-500/10 text-blue-400", - panelClass: "border-blue-500/30 bg-blue-950/10", - textClass: "text-blue-400", - meterClass: "bg-blue-500", -}; - -const expiringState: BatchStateView = { - label: "Atenção", - description: "Validade próxima", - Icon: CalendarDays, - badgeClass: "border-amber-500/30 bg-amber-500/10 text-amber-400", - panelClass: "border-amber-500/30 bg-amber-950/10", - textClass: "text-amber-400", - meterClass: "bg-amber-500", -}; - -const expiredState: BatchStateView = { - label: "Expirado", - description: "Revisar antes de vender", - Icon: AlertTriangle, - badgeClass: "border-rose-500/30 bg-rose-500/10 text-rose-400", - panelClass: "border-rose-500/30 bg-rose-950/10", - textClass: "text-rose-400", - meterClass: "bg-rose-500", -}; - -const emptyState: BatchStateView = { - label: "Sem estoque", - description: "Indisponível para venda", - Icon: Package, - badgeClass: "border-neutral-700 bg-neutral-900 text-neutral-400", - panelClass: "border-neutral-700 bg-neutral-900/50", - textClass: "text-neutral-400", - meterClass: "bg-neutral-600", -}; - -/* ─── Utilities ─── */ - -const parseBatchDate = (value?: string | null): Date | null => { - if (!value) return null; - const parsedDate = parseISO(value); - return isValid(parsedDate) ? parsedDate : null; -}; - -export const formatDate = (value?: string | null): string => { - const parsedDate = parseBatchDate(value); - if (!parsedDate) return value || "-"; - return format(parsedDate, "dd 'de' MMM, yyyy", { locale: ptBR }); -}; - -export const formatDateTime = (value?: string | null): string => { - const parsedDate = parseBatchDate(value); - if (!parsedDate) return value || "-"; - return format(parsedDate, "dd/MM/yyyy 'às' HH:mm", { locale: ptBR }); -}; - -export const formatCurrency = (value?: number | null): string => { - if (value === null || value === undefined) return "-"; - return new Intl.NumberFormat("pt-BR", { - style: "currency", - currency: "BRL", - }).format(value / 100); -}; - -export const formatOptionalText = (value?: string | null): string => { - const trimmedValue = value?.trim(); - return trimmedValue ? trimmedValue : "-"; -}; - -export const getBatchCode = (batch: Batch): string => { - return batch.batchNumber || batch.batchCode || batch.id; -}; - -export const getCurrencyTotal = ( - price?: number | null, - quantity = 0, -): string => { - if (price === null || price === undefined) return "-"; - return formatCurrency(price * quantity); -}; +import type { BatchStatusView } from "./batches-detail.types"; -export const getMarginLabel = ( - cost?: number | null, - selling?: number | null, -): string => { - if (!cost || !selling) return "-"; - return `${Math.round(((selling - cost) / cost) * 100)}%`; -}; - -export const getMarginClass = ( - cost?: number | null, - selling?: number | null, -): string => { - if (!cost || !selling) return "text-neutral-500"; - return selling >= cost ? "text-emerald-400" : "text-rose-400"; -}; - -export const getExpirationDistance = ( - expirationDate?: string | null, -): number | null => { - const parsedDate = parseBatchDate(expirationDate); - return parsedDate ? differenceInCalendarDays(parsedDate, new Date()) : null; -}; - -export const getExpirationDetail = (daysToExpire: number | null): string => { - if (daysToExpire === null) return "Sem validade cadastrada"; - if (daysToExpire < 0) return `Venceu há ${Math.abs(daysToExpire)} dia(s)`; - if (daysToExpire === 0) return "Vence hoje"; - return `Vence em ${daysToExpire} dia(s)`; -}; +/* ─── Status Icon Map ─── */ -export const getStockMeterWidth = (quantity: number): number => { - if (quantity <= 0) return 0; - return Math.min(100, Math.max(8, quantity)); +export const STATUS_ICON_MAP: Record = { + expired: AlertTriangle, + expiring: CalendarDays, + low_stock: Archive, + ok: CheckCircle2, }; -export const getBatchState = ( - batch: Batch, - daysToExpire: number | null, -): BatchStateView => { - const expirationDate = parseBatchDate(batch.expirationDate); - const isExpired = expirationDate - ? isPast(expirationDate) && !isToday(expirationDate) - : false; +/* ─── StatusBadge ─── */ - if (batch.quantity <= 0) return emptyState; - if (isExpired) return expiredState; - if (daysToExpire !== null && daysToExpire <= 30) return expiringState; - if (batch.quantity <= LOW_STOCK_THRESHOLD) return lowStockState; - return activeState; +export const StatusBadge = ({ status }: { status: BatchStatusView }) => { + const Icon = STATUS_ICON_MAP[status.kind] ?? Package; + return ( + + + {status.label} + + ); }; -/* ─── Sub-Components ─── */ +/* ─── MetricTile ─── */ interface MetricTileProps { label: string; @@ -241,13 +100,15 @@ export const MetricTile = ({
); -interface LedgerItemProps { +/* ─── InfoRow ─── */ + +interface InfoRowProps { label: string; value: string; icon: LucideIcon; } -export const LedgerItem = ({ label, value, icon: Icon }: LedgerItemProps) => ( +export const InfoRow = ({ label, value, icon: Icon }: InfoRowProps) => (
@@ -257,6 +118,8 @@ export const LedgerItem = ({ label, value, icon: Icon }: LedgerItemProps) => (
); +/* ─── TimelineStep ─── */ + interface TimelineStepProps { label: string; value: string; @@ -283,24 +146,46 @@ export const TimelineStep = ({
); -export const StatusBadge = ({ state }: { state: BatchStateView }) => { - const StatusIcon = state.Icon; +/* ─── OriginSection ─── */ + +interface OriginSectionProps { + movementId: string | null; + movementCode: string | null; +} + +export const OriginSection = ({ movementId, movementCode }: OriginSectionProps) => { + if (!movementId) { + return ( +
+
+ + Origem +
+

Criado manualmente

+
+ ); + } + return ( - - - {state.label} - +
+ + Origem +
+

+ {movementCode ?? movementId} +

+ ); }; +/* ─── BatchActions ─── */ + interface BatchActionsProps { - batch: Batch; + batchId: string; isDeleteOpen: boolean; onDeleteOpenChange: (open: boolean) => void; isDeleting: boolean; @@ -308,7 +193,7 @@ interface BatchActionsProps { } export const BatchActions = ({ - batch, + batchId, isDeleteOpen, onDeleteOpenChange, isDeleting, @@ -316,7 +201,7 @@ export const BatchActions = ({ }: BatchActionsProps) => (
- +
- {/* Row 3: Location · Lifecycle · Notes */} -
+ {/* Row 3: Location + Lifecycle */} +
{/* Location */}
@@ -248,16 +224,23 @@ export const BatchesDetailView = ({

- Warehouse -

-

- {batch.warehouseName} + Armazém

+ +

+ {batch.warehouseName} +

+ + + Abrir armazém + +
- - COD: {formatOptionalText(batch.warehouseCode)} - +
@@ -273,89 +256,31 @@ export const BatchesDetailView = ({
- - {/* Notes + IDs */} -
-
- -

- Observações -

-
- -
-

- {formatOptionalText(batch.notes)} -

-
- -
- copyToClipboard(batch.id, "ID do Lote")} - /> - copyToClipboard(batch.productId, "ID do Produto")} - /> -
-
); }; - -/* ─── Helper: ID Row ─── */ - -function IdField({ - label, - value, - onCopy, -}: { - label: string; - value: string; - onCopy: () => void; -}) { - return ( -
-

- {label} -

- -
- ); -} diff --git a/app/(pages)/batches/[id]/page.client.tsx b/app/(pages)/batches/[id]/page.client.tsx index b04159b..dc92826 100644 --- a/app/(pages)/batches/[id]/page.client.tsx +++ b/app/(pages)/batches/[id]/page.client.tsx @@ -7,25 +7,27 @@ import { BatchesDetailView } from "./batches-detail.view"; export function PageClient() { const params = useParams(); const batchId = params.id as string; - const { - batch, - isLoading, - error, - onDelete, - isDeleting, - isDeleteOpen, - onDeleteOpenChange, - } = useBatchDetailModel(batchId); + const model = useBatchDetailModel(batchId); return ( ); } From 7792c5b7722ab5c84e491d3cd106bf8f2a314a1e Mon Sep 17 00:00:00 2001 From: lexmarcos Date: Sun, 10 May 2026 14:56:09 -0300 Subject: [PATCH 10/19] refactor: remove unused ui primitives and templates --- .claude/templates/detail-page.template.tsx | 194 ----- .claude/templates/form-page.template.tsx | 184 ----- .claude/templates/list-page.template.tsx | 189 ----- .../scanner-drawer/scanner-drawer.schema.ts | 28 - .../product/scanner-drawer/scanner-drawer.tsx | 326 -------- .../scanner-drawer/scanner-drawer.types.ts | 42 - .../scanner-drawer/use-scanner-drawer.ts | 113 --- components/ui/alert-dialog.tsx | 2 - components/ui/aspect-ratio.tsx | 11 - components/ui/avatar.tsx | 53 -- components/ui/badge.tsx | 2 +- components/ui/breadcrumb.tsx | 109 --- components/ui/button-group.tsx | 83 -- components/ui/calendar.tsx | 213 ----- components/ui/card.tsx | 11 - components/ui/carousel.tsx | 241 ------ components/ui/chart.tsx | 295 ++++--- components/ui/collapsible.tsx | 33 - components/ui/command.tsx | 184 ----- components/ui/context-menu.tsx | 252 ------ components/ui/dialog.tsx | 16 - components/ui/drawer.tsx | 9 - components/ui/dropdown-menu.tsx | 149 ---- components/ui/empty-state.tsx | 6 +- components/ui/empty.tsx | 104 --- components/ui/error-state.tsx | 4 +- components/ui/field.tsx | 244 ------ components/ui/form-section.tsx | 2 +- components/ui/form.tsx | 5 +- components/ui/hover-card.tsx | 44 -- components/ui/icon-box.tsx | 58 -- components/ui/input-group.tsx | 170 ---- components/ui/input-otp.tsx | 77 -- components/ui/insight-card.tsx | 4 +- components/ui/item.tsx | 193 ----- components/ui/kbd.tsx | 28 - components/ui/loading-state.tsx | 2 +- components/ui/menubar.tsx | 276 ------- components/ui/navigation-menu.tsx | 168 ---- components/ui/number-input.tsx | 2 +- components/ui/page-header.tsx | 2 +- components/ui/pagination.tsx | 127 --- components/ui/popover.tsx | 8 +- components/ui/progress.tsx | 31 - components/ui/radio-group.tsx | 45 -- components/ui/resizable.tsx | 56 -- components/ui/responsive-modal.tsx | 9 +- components/ui/scroll-area.tsx | 2 +- components/ui/section-label.tsx | 2 +- components/ui/select.tsx | 37 - components/ui/separator.tsx | 28 - components/ui/sheet.tsx | 139 ---- components/ui/sidebar.tsx | 726 ------------------ components/ui/slider.tsx | 63 -- components/ui/spinner.tsx | 16 - components/ui/status-card.tsx | 44 -- components/ui/table.tsx | 28 - components/ui/tabs.tsx | 66 -- components/ui/toggle-group.tsx | 73 -- components/ui/toggle.tsx | 47 -- components/ui/tooltip.tsx | 61 -- components/users/user-modal.tsx | 251 ------ 62 files changed, 155 insertions(+), 5832 deletions(-) delete mode 100644 .claude/templates/detail-page.template.tsx delete mode 100644 .claude/templates/form-page.template.tsx delete mode 100644 .claude/templates/list-page.template.tsx delete mode 100644 components/product/scanner-drawer/scanner-drawer.schema.ts delete mode 100644 components/product/scanner-drawer/scanner-drawer.tsx delete mode 100644 components/product/scanner-drawer/scanner-drawer.types.ts delete mode 100644 components/product/scanner-drawer/use-scanner-drawer.ts delete mode 100644 components/ui/aspect-ratio.tsx delete mode 100644 components/ui/avatar.tsx delete mode 100644 components/ui/breadcrumb.tsx delete mode 100644 components/ui/button-group.tsx delete mode 100644 components/ui/calendar.tsx delete mode 100644 components/ui/carousel.tsx delete mode 100644 components/ui/collapsible.tsx delete mode 100644 components/ui/command.tsx delete mode 100644 components/ui/context-menu.tsx delete mode 100644 components/ui/empty.tsx delete mode 100644 components/ui/field.tsx delete mode 100644 components/ui/hover-card.tsx delete mode 100644 components/ui/icon-box.tsx delete mode 100644 components/ui/input-group.tsx delete mode 100644 components/ui/input-otp.tsx delete mode 100644 components/ui/item.tsx delete mode 100644 components/ui/kbd.tsx delete mode 100644 components/ui/menubar.tsx delete mode 100644 components/ui/navigation-menu.tsx delete mode 100644 components/ui/pagination.tsx delete mode 100644 components/ui/progress.tsx delete mode 100644 components/ui/radio-group.tsx delete mode 100644 components/ui/resizable.tsx delete mode 100644 components/ui/separator.tsx delete mode 100644 components/ui/sheet.tsx delete mode 100644 components/ui/sidebar.tsx delete mode 100644 components/ui/slider.tsx delete mode 100644 components/ui/spinner.tsx delete mode 100644 components/ui/status-card.tsx delete mode 100644 components/ui/tabs.tsx delete mode 100644 components/ui/toggle-group.tsx delete mode 100644 components/ui/toggle.tsx delete mode 100644 components/ui/tooltip.tsx delete mode 100644 components/users/user-modal.tsx diff --git a/.claude/templates/detail-page.template.tsx b/.claude/templates/detail-page.template.tsx deleted file mode 100644 index 32acc1b..0000000 --- a/.claude/templates/detail-page.template.tsx +++ /dev/null @@ -1,194 +0,0 @@ -/** - * TEMPLATE: Página de Detalhe - * - * Este arquivo é referência para agentes AI, NÃO é código executável. - * Copie e adapte os padrões abaixo ao criar páginas de detalhe/visualização. - * - * Estrutura: PageContainer + PageHeader(com backLink) + StatusCard + SectionLabels + Info grids - */ - -// ============================================================ -// page.tsx (ViewModel) — rota filha, usa useBreadcrumb -// ============================================================ - -"use client"; - -import { useBreadcrumb } from "@/components/breadcrumb/use-breadcrumb"; -import { useExampleDetailModel } from "./example-detail.model"; -import { ExampleDetailView } from "./example-detail.view"; - -export default function ExampleDetailPage() { - useBreadcrumb({ - title: "Detalhes do Item", - backUrl: "/items", - section: "Itens", - subsection: "Detalhes", - }); - - const model = useExampleDetailModel(); - return ; -} - -// ============================================================ -// example-detail.types.ts -// ============================================================ - -export interface ExampleDetailItem { - id: string; - name: string; - description: string; - status: "active" | "inactive" | "pending"; - createdAt: string; - category: string; - quantity: number; -} - -export interface ExampleDetailViewProps { - item: ExampleDetailItem | null; - isLoading: boolean; - error: Error | null; - onRetry: () => void; -} - -// ============================================================ -// example-detail.model.ts -// ============================================================ - -// import { useParams } from "next/navigation"; -// import useSWR from "swr"; -// import { api } from "@/lib/api"; -// import type { ExampleDetailViewProps } from "./example-detail.types"; -// -// export function useExampleDetailModel(): ExampleDetailViewProps { -// const { id } = useParams(); -// const { data, error, isLoading, mutate } = useSWR( -// `/api/example/${id}`, -// (url: string) => api.get(url).json() -// ); -// -// return { -// item: data ?? null, -// isLoading, -// error: error ?? null, -// onRetry: () => mutate(), -// }; -// } - -// ============================================================ -// example-detail.view.tsx (View) -// ============================================================ - -import { - Package, - Edit, - Info, - Clock, - Tag, - Boxes, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { PageContainer } from "@/components/ui/page-container"; -import { PageHeader } from "@/components/ui/page-header"; -import { StatusCard } from "@/components/ui/status-card"; -import { SectionLabel } from "@/components/ui/section-label"; -import { LoadingState } from "@/components/ui/loading-state"; -import { ErrorState } from "@/components/ui/error-state"; -// import type { ExampleDetailViewProps } from "./example-detail.types"; - -function ExampleDetailView(/* props: ExampleDetailViewProps */) { - // if (isLoading) return ; - // if (error) return ; - // if (!item) return null; - - return ( - - {/* Header com ação de editar */} - - - EDITAR - - } - /> - - {/* Card de status principal */} - -
-
- -
-

Ativo

-

- Criado em 01/01/2025 -

-
-
- - 128 - -
-
- - {/* Seção: Informações Gerais */} - - Informações Gerais - -
-
-

- Nome -

-

Item de Exemplo

-
-
-

- Categoria -

-

Eletrônicos

-
-
-

- Quantidade -

-

- 128 -

-
-
- - {/* Seção: Histórico */} - - Histórico - -
- -
-
-

Atualização de estoque

-

01/01/2025 às 14:30

-
- - +50 - -
-
- -
-
-

Saída de estoque

-

28/12/2024 às 09:15

-
- - -20 - -
-
-
-
- ); -} - -export { ExampleDetailView }; diff --git a/.claude/templates/form-page.template.tsx b/.claude/templates/form-page.template.tsx deleted file mode 100644 index 269bf2b..0000000 --- a/.claude/templates/form-page.template.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/** - * TEMPLATE: Página de Formulário - * - * Este arquivo é referência para agentes AI, NÃO é código executável. - * Copie e adapte os padrões abaixo ao criar páginas de formulário. - * - * Estrutura: PageContainer(fixed-bar) + Form + FormSections em grid + FixedBottomBar - */ - -// ============================================================ -// page.tsx (ViewModel) — rota filha, usa useBreadcrumb -// ============================================================ - -"use client"; - -import { useBreadcrumb } from "@/components/breadcrumb/use-breadcrumb"; -import { useExampleCreateModel } from "./example-create.model"; -import { ExampleCreateView } from "./example-create.view"; - -export default function ExampleCreatePage() { - useBreadcrumb({ - title: "Novo Item", - backUrl: "/items", - section: "Itens", - subsection: "Criar", - }); - - const model = useExampleCreateModel(); - return ; -} - -// ============================================================ -// example-create.types.ts -// ============================================================ - -export interface ExampleCreateViewProps { - onSubmit: (data: ExampleFormData) => void; - isSubmitting: boolean; -} - -export interface ExampleFormData { - name: string; - description: string; - category: string; -} - -// ============================================================ -// example-create.schema.ts -// ============================================================ - -// import { z } from "zod"; -// -// export const exampleCreateSchema = z.object({ -// name: z.string().min(1, "Nome é obrigatório"), -// description: z.string().optional(), -// category: z.string().min(1, "Categoria é obrigatória"), -// }); - -// ============================================================ -// example-create.model.ts -// ============================================================ - -// import { useState } from "react"; -// import { api } from "@/lib/api"; -// import type { ExampleCreateViewProps, ExampleFormData } from "./example-create.types"; -// -// export function useExampleCreateModel(): ExampleCreateViewProps { -// const [isSubmitting, setIsSubmitting] = useState(false); -// -// async function onSubmit(data: ExampleFormData) { -// setIsSubmitting(true); -// try { -// await api.post("/api/example", { json: data }).json(); -// } finally { -// setIsSubmitting(false); -// } -// } -// -// return { onSubmit, isSubmitting }; -// } - -// ============================================================ -// example-create.view.tsx (View) -// ============================================================ - -import { Package, Settings, FileText, Save } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { PageContainer } from "@/components/ui/page-container"; -import { PageHeader } from "@/components/ui/page-header"; -import { FormSection } from "@/components/ui/form-section"; -import { FixedBottomBar } from "@/components/ui/fixed-bottom-bar"; -// import type { ExampleCreateViewProps } from "./example-create.types"; - -function ExampleCreateView(/* props: ExampleCreateViewProps */) { - return ( - - - -
- {/* Coluna principal (2/3) */} -
- -
- - -
-
- -