-
+
Lotes
@@ -484,7 +492,7 @@ export const BatchesView = ({
@@ -497,13 +505,13 @@ export const BatchesView = ({
0}
count={activeFilterCount}
- icon={}
+ icon={}
label="Filtros"
onClick={onOpenMobileFilters}
/>
}
+ icon={}
label="Status"
value={getStatusFilterLabel(filters.status)}
onClick={onOpenMobileFilters}
@@ -513,14 +521,14 @@ export const BatchesView = ({
sortConfig.key !== "createdAt" ||
sortConfig.direction !== "desc"
}
- icon={}
+ icon={}
label="Ordem"
value={getSortLabel(sortConfig)}
onClick={onOpenMobileFilters}
/>
}
+ icon={}
label="Visão"
value={
isGroupedByProduct ? "Agrupado" : "Lista completa"
@@ -532,7 +540,7 @@ export const BatchesView = ({
filters.lowStockThreshold !==
DEFAULT_LOW_STOCK_THRESHOLD
}
- icon={}
+ icon={}
label="Baixo"
value={`<= ${filters.lowStockThreshold} un.`}
onClick={onOpenMobileFilters}
@@ -546,8 +554,8 @@ export const BatchesView = ({
{/* Total Batches */}
-
-
+
+
Total de Lotes
@@ -566,8 +574,8 @@ export const BatchesView = ({
{/* Expired */}
-
-
+
+
Expirados
@@ -586,8 +594,8 @@ export const BatchesView = ({
{/* Expiring */}
-
-
+
+
Expirando (30d)
@@ -606,8 +614,8 @@ export const BatchesView = ({
{/* Low Stock */}
-
-
+
Baixo Estoque
@@ -628,10 +636,10 @@ export const BatchesView = ({
-
+
setSearchQuery(e.target.value)}
className="w-full rounded-[4px] border-neutral-800 bg-[#171717] pl-10 text-sm text-neutral-200 placeholder:text-neutral-600 focus:border-blue-600 focus:ring-0 transition-all hover:border-neutral-700"
@@ -647,7 +655,7 @@ export const BatchesView = ({
>
-
+
@@ -698,7 +706,7 @@ export const BatchesView = ({
: "text-neutral-400 hover:bg-neutral-800 hover:text-white",
)}
>
-
+
{isGroupedByProduct
? "Lista Completa"
: "Agrupar por Produto"}
@@ -728,9 +736,9 @@ export const BatchesView = ({
{/* Loading */}
{isLoading && (
-
+
- Carregando lotes...
+ Carregando lotes…
)}
@@ -738,9 +746,9 @@ export const BatchesView = ({
{/* Error */}
{error && (
-
+
-
+
Falha na conexão
@@ -753,11 +761,11 @@ export const BatchesView = ({
{/* Empty State */}
{!isLoading && !error && batches.length === 0 && (
-
-
+
+
-
+
{filters.searchQuery || filters.status !== "all"
? "Nenhum resultado encontrado"
: "Nenhum lote cadastrado"}
@@ -780,7 +788,7 @@ export const BatchesView = ({
@@ -803,7 +811,7 @@ export const BatchesView = ({
-
+
{group.productName}
@@ -907,7 +915,7 @@ export const BatchesView = ({
onClick={() => onSortChange("product")}
>
- Produto
+ Produto
@@ -921,7 +929,7 @@ export const BatchesView = ({
onClick={() => onSortChange("quantity")}
>
- Qtd.
+ Qtd.
onSortChange("expiration")}
>
- Validade
+ Validade
@@ -1009,10 +1017,10 @@ export const BatchesView = ({
@@ -1115,7 +1123,7 @@ export const BatchesView = ({
- {renderMobileFiltersPanel(mobileFiltersDraft)}
+ {mobileFiltersPanel}
);
};
diff --git a/app/(pages)/batches/create/batches-create.view.tsx b/app/(pages)/batches/create/batches-create.view.tsx
index 7b70122..370ae9e 100644
--- a/app/(pages)/batches/create/batches-create.view.tsx
+++ b/app/(pages)/batches/create/batches-create.view.tsx
@@ -103,7 +103,7 @@ export const BatchCreateView = ({
-
+
Identificação e Origem
@@ -122,7 +122,7 @@ export const BatchCreateView = ({
-
+
-
+
)}
@@ -154,10 +154,10 @@ export const BatchCreateView = ({
type="button"
variant="outline"
onClick={openScanner}
- className="h-10 w-10 shrink-0 rounded-[4px] border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
+ className="size-10 shrink-0 rounded-[4px] border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
aria-label="Ler código de barras"
>
-
+
@@ -167,8 +167,8 @@ export const BatchCreateView = ({
{isProductSearchLoading && (
-
- Buscando produtos...
+
+ Buscando produtos…
)}
{!isProductSearchLoading &&
@@ -187,7 +187,7 @@ export const BatchCreateView = ({
-
+
)}
@@ -228,7 +228,7 @@ export const BatchCreateView = ({
-
+
Financeiro e Estoque
@@ -252,10 +252,10 @@ export const BatchCreateView = ({
type="button"
variant="outline"
onClick={onQuantityDecrement}
- className="h-10 w-10 rounded-r-none rounded-l-[4px] border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
+ className="size-10 rounded-r-none rounded-l-[4px] border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
aria-label="Diminuir quantidade"
>
-
+
-
+
@@ -377,7 +377,7 @@ export const BatchCreateView = ({
-
+
Vigência
@@ -429,7 +429,7 @@ export const BatchCreateView = ({
-
+
Observações
@@ -443,7 +443,7 @@ export const BatchCreateView = ({
@@ -476,12 +476,12 @@ export const BatchCreateView = ({
>
{isSubmitting ? (
<>
-
- Salvando...
+
+ Salvando…
>
) : (
<>
-
+
Criar Lote
>
)}
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/brands.model.ts b/app/(pages)/brands/brands.model.ts
index 739bda2..0ab84f7 100644
--- a/app/(pages)/brands/brands.model.ts
+++ b/app/(pages)/brands/brands.model.ts
@@ -56,7 +56,7 @@ export const useBrandsModel = () => {
}
// Sort
- const sorted = [...filtered].sort((a, b) => {
+ const sorted = filtered.toSorted((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
diff --git a/app/(pages)/brands/brands.view.tsx b/app/(pages)/brands/brands.view.tsx
index 1a5ff76..b93bff4 100644
--- a/app/(pages)/brands/brands.view.tsx
+++ b/app/(pages)/brands/brands.view.tsx
@@ -44,9 +44,9 @@ import {
import { UseFormReturn } from "react-hook-form";
import { BrandFormData } from "./brands.schema";
import { Brand, SortConfig } from "./brands.types";
-import { format } from "date-fns";
+import { format, parseISO } from "date-fns";
import { ptBR } from "date-fns/locale";
-import { useState, useEffect } from "react";
+import { useState } from "react";
import {
Select,
SelectContent,
@@ -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,
@@ -99,29 +114,14 @@ export const BrandsView = ({
confirmDelete,
isDeleting,
}: BrandsViewProps) => {
- const [logoPreview, setLogoPreview] = useState("");
- const [logoError, setLogoError] = useState(false);
+ const [failedLogoPreview, setFailedLogoPreview] = useState(
+ null,
+ );
const logoUrl = form.watch("logoUrl");
+ const logoPreview = logoUrl?.trim() ?? "";
+ const logoError = failedLogoPreview === logoPreview;
- useEffect(() => {
- if (logoUrl && logoUrl.trim()) {
- setLogoPreview(logoUrl);
- setLogoError(false);
- } else {
- setLogoPreview("");
- setLogoError(false);
- }
- }, [logoUrl]);
-
- const SortIcon = ({ field }: { field: SortConfig["key"] }) => {
- if (sortConfig.key !== field) return ;
- return sortConfig.direction === "asc" ? (
-
- ) : (
-
- );
- };
return (
@@ -131,7 +131,7 @@ export const BrandsView = ({
-
+
Marcas
@@ -143,7 +143,7 @@ export const BrandsView = ({
onClick={openCreateModal}
className="h-10 w-full rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white hover:bg-blue-700 shadow-[0_0_15px_-3px_rgba(37,99,235,0.4)] md:w-auto"
>
-
+
Nova Marca
@@ -152,9 +152,9 @@ export const BrandsView = ({
{/* Bottom Row: Search & Filters */}
-
+
setSearchQuery(e.target.value)}
className="pl-9 md:h-9 w-full rounded-[4px] border-neutral-800 bg-[#171717] text-sm text-neutral-200 placeholder:text-neutral-600 focus:border-blue-600 focus:ring-0 transition-all hover:border-neutral-700"
@@ -215,18 +215,18 @@ export const BrandsView = ({
{/* State Layers */}
{isLoading && (
-
+
- Acessando Diretório...
+ Acessando Diretório…
)}
{error && (
-
+
-
+
Erro de Sincronização
@@ -246,11 +246,11 @@ export const BrandsView = ({
{!isLoading && !error && brands.length === 0 && (
-
-
+
+
-
+
{searchQuery
? "Nenhum Registro Encontrado"
: "Nenhuma Marca Registrada"}
@@ -275,7 +275,7 @@ export const BrandsView = ({
onClick={openCreateModal}
className="rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white"
>
- Registrar Marca
+ Registrar Marca
)}
@@ -298,7 +298,7 @@ export const BrandsView = ({
onClick={() => handleSort("name")}
>
- Nome
+ Nome
handleSort("createdAt")}
>
- Data de Registro
+ Data de Registro
@@ -321,7 +321,7 @@ export const BrandsView = ({
className="group border-b border-neutral-800/50 hover:bg-neutral-800/30 transition-all"
>
-
+
{brand.logoUrl ? (
) : (
-
+
)}
@@ -343,7 +343,7 @@ export const BrandsView = ({
- {format(new Date(brand.createdAt), "dd MMM, yyyy", {
+ {format(parseISO(brand.createdAt), "dd MMM, yyyy", {
locale: ptBR,
})}
@@ -355,9 +355,9 @@ export const BrandsView = ({
variant="ghost"
size="icon"
onClick={() => openEditModal(brand)}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
>
-
+
@@ -365,9 +365,9 @@ export const BrandsView = ({
variant="ghost"
size="icon"
onClick={() => openDeleteDialog(brand)}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-rose-950/20 hover:text-rose-500"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-rose-500"
>
-
+
@@ -387,7 +387,7 @@ export const BrandsView = ({
>
-
+
{brand.logoUrl ? (
) : (
-
+
)}
-
+
{brand.name}
-
+
- {format(new Date(brand.createdAt), "dd/MM/yyyy", {
+ {format(parseISO(brand.createdAt), "dd/MM/yyyy", {
locale: ptBR,
})}
@@ -421,9 +421,9 @@ export const BrandsView = ({
openEditModal(brand)}
className="focus:bg-neutral-800"
>
- Editar
+ Editar
@@ -443,7 +443,7 @@ export const BrandsView = ({
onClick={() => openDeleteDialog(brand)}
className="text-rose-500 focus:bg-rose-950/20"
>
- Excluir
+ Excluir
@@ -479,9 +479,9 @@ export const BrandsView = ({
className="rounded-[4px] bg-blue-600 px-8 text-[10px] font-bold uppercase tracking-widest text-white hover:bg-blue-700 shadow-lg"
>
{form.formState.isSubmitting ? (
-
+
) : (
-
+
)}
{selectedBrand ? "Salvar Alterações" : "Confirmar Registro"}
@@ -549,12 +549,12 @@ export const BrandsView = ({
sizes="240px"
unoptimized
className="rounded-[2px] object-contain shadow-sm"
- onError={() => setLogoError(true)}
+ onError={() => setFailedLogoPreview(logoPreview)}
/>
) : (
-
+
Imagem Inválida
@@ -587,7 +587,7 @@ export const BrandsView = ({
disabled={isDeleting}
className="rounded-[4px] bg-rose-600 text-[10px] font-bold uppercase text-white hover:bg-rose-700 border-none"
>
- {isDeleting ? "Processando..." : "Confirmar Exclusão"}
+ {isDeleting ? "Processando…" : "Confirmar Exclusão"}
>
}
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/categories.model.ts b/app/(pages)/categories/categories.model.ts
index e89128d..9f30e81 100644
--- a/app/(pages)/categories/categories.model.ts
+++ b/app/(pages)/categories/categories.model.ts
@@ -58,13 +58,18 @@ export const useCategoriesModel = () => {
depth: number = 0
): CategoryTree[] {
return categories
- .filter((cat) => cat.parentCategoryId === parentCategoryId)
- .map((cat) => ({
+ .reduce
((children, cat) => {
+ if (cat.parentCategoryId !== parentCategoryId) return children;
+
+ children.push({
...cat,
depth,
children: buildTree(categories, cat.id, depth + 1),
productCount: 0,
- }))
+ });
+
+ return children;
+ }, [])
.sort((a, b) => {
const comparison = a.name.localeCompare(b.name);
return sortConfig.direction === "asc" ? comparison : -comparison;
diff --git a/app/(pages)/categories/categories.types.ts b/app/(pages)/categories/categories.types.ts
index 320f9f7..34745f9 100644
--- a/app/(pages)/categories/categories.types.ts
+++ b/app/(pages)/categories/categories.types.ts
@@ -39,8 +39,8 @@ export interface DeleteCategoryResponse {
data: null;
}
-export type SortKey = "name" | "createdAt";
-export type SortDirection = "asc" | "desc";
+type SortKey = "name" | "createdAt";
+type SortDirection = "asc" | "desc";
export interface SortConfig {
key: SortKey;
diff --git a/app/(pages)/categories/categories.view.tsx b/app/(pages)/categories/categories.view.tsx
index 88d7a4d..4c9c88d 100644
--- a/app/(pages)/categories/categories.view.tsx
+++ b/app/(pages)/categories/categories.view.tsx
@@ -157,7 +157,7 @@ export const CategoriesView = ({
if (hasChildren) toggleNode(node.id);
}}
className={cn(
- "flex h-9 w-9 md:h-8 md:w-8 shrink-0 items-center justify-center rounded-[4px] border border-transparent transition-all",
+ "flex size-9 md:size-8 shrink-0 items-center justify-center rounded-[4px] border border-transparent transition-all",
hasChildren
? "cursor-pointer bg-neutral-900 border-neutral-800 text-neutral-400 hover:border-neutral-600 hover:text-white"
: "invisible",
@@ -167,16 +167,16 @@ export const CategoriesView = ({
>
{hasChildren &&
(isExpanded ? (
-
+
) : (
-
+
))}
{/* Icon */}
{/* Content Info */}
- hasChildren && toggleNode(node.id)}
+
+
{/* Actions */}
@@ -223,9 +228,9 @@ export const CategoriesView = ({
e.stopPropagation();
openEditModal(node);
}}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
>
-
+
@@ -236,9 +241,9 @@ export const CategoriesView = ({
e.stopPropagation();
openDeleteDialog(node);
}}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-rose-950/20 hover:text-rose-500"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-rose-500"
>
-
+
@@ -252,7 +257,7 @@ export const CategoriesView = ({
size="icon"
className="rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white focus:opacity-100"
>
-
+
Ações
@@ -269,7 +274,7 @@ export const CategoriesView = ({
onClick={() => openEditModal(node)}
className="cursor-pointer focus:bg-neutral-800 focus:text-white"
>
-
+
Editar
@@ -278,7 +283,7 @@ export const CategoriesView = ({
onClick={() => openDeleteDialog(node)}
className="cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400"
>
-
+
Excluir
@@ -314,8 +319,8 @@ export const CategoriesView = ({
{/* Icon Box */}
-
-
+
+
{/* Content */}
@@ -362,9 +367,9 @@ export const CategoriesView = ({
variant="ghost"
size="icon"
onClick={() => openEditModal(node)}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
>
-
+
@@ -372,9 +377,9 @@ export const CategoriesView = ({
variant="ghost"
size="icon"
onClick={() => openDeleteDialog(node)}
- className="h-8 w-8 rounded-[4px] text-neutral-500 hover:bg-rose-950/20 hover:text-rose-500"
+ className="size-8 rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-rose-500"
>
-
+
@@ -388,7 +393,7 @@ export const CategoriesView = ({
size="icon"
className="rounded-[4px] text-neutral-500 hover:bg-neutral-800 hover:text-white"
>
-
+
Ações
@@ -405,7 +410,7 @@ export const CategoriesView = ({
onClick={() => openEditModal(node)}
className="cursor-pointer focus:bg-neutral-800 focus:text-white"
>
-
+
Editar
@@ -414,7 +419,7 @@ export const CategoriesView = ({
onClick={() => openDeleteDialog(node)}
className="cursor-pointer text-rose-500 focus:bg-rose-950/20 focus:text-rose-400"
>
-
+
Excluir
@@ -434,7 +439,7 @@ export const CategoriesView = ({
-
+
Categorias
@@ -446,7 +451,7 @@ export const CategoriesView = ({
onClick={openCreateModal}
className="h-10 w-full rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white hover:bg-blue-700 shadow-[0_0_20px_-5px_rgba(37,99,235,0.3)] md:w-auto"
>
-
+
Nova Categoria
@@ -455,9 +460,9 @@ export const CategoriesView = ({
{/* Filters Toolbar */}
@@ -505,14 +510,14 @@ export const CategoriesView = ({
onClick={expandAll}
className="text-[10px] uppercase font-bold text-neutral-500 hover:text-blue-500 transition-colors flex items-center gap-1"
>
-
Expandir Tudo
+
Expandir Tudo
|
)}
@@ -523,9 +528,9 @@ export const CategoriesView = ({
{/* Loading State */}
{isLoading && (
-
+
- Sincronizando...
+ Sincronizando…
)}
@@ -533,9 +538,9 @@ export const CategoriesView = ({
{/* Error State */}
{error && (
-
+
-
+
Falha na conexão
@@ -545,7 +550,7 @@ export const CategoriesView = ({
@@ -558,11 +563,11 @@ export const CategoriesView = ({
flatCategories.length === 0 &&
!searchQuery && (
-
-
+
+
-
+
Catálogo Vazio
@@ -574,7 +579,7 @@ export const CategoriesView = ({
onClick={openCreateModal}
className="rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white hover:bg-blue-700"
>
-
+
Criar Primeira Categoria
@@ -584,7 +589,7 @@ export const CategoriesView = ({
{/* No Results */}
{!isLoading && !error && flatCategories.length === 0 && searchQuery && (
-
+
Nenhum resultado para "{searchQuery}"
@@ -644,9 +649,9 @@ export const CategoriesView = ({
className="rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white hover:bg-blue-700"
>
{form.formState.isSubmitting ? (
-
+
) : (
-
+
)}
{selectedCategory ? "Salvar" : "Criar"}
@@ -695,7 +700,7 @@ export const CategoriesView = ({
>
-
+
@@ -706,17 +711,19 @@ export const CategoriesView = ({
●{" "}
RAIZ (Sem Pai)
- {allCategories
- .filter((cat) => cat.id !== selectedCategory?.id)
- .map((cat) => (
-
- {cat.name}
-
- ))}
+ {allCategories.flatMap((cat) =>
+ cat.id === selectedCategory?.id
+ ? []
+ : [
+
+ {cat.name}
+ ,
+ ],
+ )}
@@ -768,7 +775,7 @@ export const CategoriesView = ({
disabled={isDeleting}
className="rounded-[4px] bg-rose-600 text-white hover:bg-rose-700 border-none"
>
- {isDeleting ? "Excluindo..." : "Sim, excluir"}
+ {isDeleting ? "Excluindo…" : "Sim, excluir"}
>
}
@@ -780,7 +787,7 @@ export const CategoriesView = ({
flatCategories.find((c) => c.id === categoryToDelete.id)!.children
.length > 0 ? (
-
+
Esta categoria contém subcategorias que também serão afetadas.
) : (
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)/dashboard/dashboard.types.ts b/app/(pages)/dashboard/dashboard.types.ts
index 0807581..0de03cd 100644
--- a/app/(pages)/dashboard/dashboard.types.ts
+++ b/app/(pages)/dashboard/dashboard.types.ts
@@ -1,4 +1,4 @@
-export interface DashboardMovementStatsPeriod {
+interface DashboardMovementStatsPeriod {
entries: number;
exits: number;
transfers: number;
@@ -20,7 +20,7 @@ export interface DashboardRecentMovement {
notes: string | null;
}
-export interface DashboardStockByWarehouse {
+interface DashboardStockByWarehouse {
warehouseId: string;
warehouseName: string;
batchCount: number;
@@ -28,7 +28,7 @@ export interface DashboardStockByWarehouse {
productCount: number;
}
-export interface DashboardStockByCategory {
+interface DashboardStockByCategory {
categoryId: string;
categoryName: string;
batchCount: number;
diff --git a/app/(pages)/dashboard/dashboard.view.tsx b/app/(pages)/dashboard/dashboard.view.tsx
index ab6acda..d88340d 100644
--- a/app/(pages)/dashboard/dashboard.view.tsx
+++ b/app/(pages)/dashboard/dashboard.view.tsx
@@ -15,17 +15,10 @@ import {
Clock,
TrendingUp,
} from "lucide-react";
-import { formatDistanceToNow } from "date-fns";
+import dynamic from "next/dynamic";
+import type { ComponentType } from "react";
+import { formatDistanceToNow, parseISO } from "date-fns";
import { ptBR } from "date-fns/locale";
-import {
- Bar,
- BarChart,
- Cell,
- Pie,
- PieChart as RechartsPie,
- XAxis,
- YAxis,
-} from "recharts";
import { PageContainer } from "@/components/ui/page-container";
import { PageHeader } from "@/components/ui/page-header";
import { InsightCard } from "@/components/ui/insight-card";
@@ -46,6 +39,31 @@ import type {
DashboardViewProps,
} from "./dashboard.types";
+type DynamicRechartsProps = Record
;
+type DynamicRechartsComponent = ComponentType;
+
+const Bar = dynamic(() =>
+ import("recharts").then((mod) => mod.Bar as unknown as DynamicRechartsComponent),
+);
+const BarChart = dynamic(() =>
+ import("recharts").then((mod) => mod.BarChart as unknown as DynamicRechartsComponent),
+);
+const Cell = dynamic(() =>
+ import("recharts").then((mod) => mod.Cell as unknown as DynamicRechartsComponent),
+);
+const Pie = dynamic(() =>
+ import("recharts").then((mod) => mod.Pie as unknown as DynamicRechartsComponent),
+);
+const RechartsPie = dynamic(() =>
+ import("recharts").then((mod) => mod.PieChart as unknown as DynamicRechartsComponent),
+);
+const XAxis = dynamic(() =>
+ import("recharts").then((mod) => mod.XAxis as unknown as DynamicRechartsComponent),
+);
+const YAxis = dynamic(() =>
+ import("recharts").then((mod) => mod.YAxis as unknown as DynamicRechartsComponent),
+);
+
const MOVEMENT_COLORS: Record = {
ENTRY: "#059669",
EXIT: "#E11D48",
@@ -79,6 +97,12 @@ const DONUT_COLORS = [
"#84CC16",
];
+const DASHBOARD_CURRENCY_FORMATTER = new Intl.NumberFormat("pt-BR", {
+ style: "currency",
+ currency: "BRL",
+});
+const DASHBOARD_COUNT_FORMATTER = new Intl.NumberFormat("pt-BR");
+
function formatCompactCurrency(value: number): string {
if (value >= 1_000_000) {
return `R$ ${(value / 1_000_000).toFixed(1).replace(".0", "")}M`;
@@ -86,21 +110,15 @@ function formatCompactCurrency(value: number): string {
if (value >= 1_000) {
return `R$ ${(value / 1_000).toFixed(1).replace(".0", "")}K`;
}
- return new Intl.NumberFormat("pt-BR", {
- style: "currency",
- currency: "BRL",
- }).format(value);
+ return DASHBOARD_CURRENCY_FORMATTER.format(value);
}
function formatCurrency(value: number): string {
- return new Intl.NumberFormat("pt-BR", {
- style: "currency",
- currency: "BRL",
- }).format(value);
+ return DASHBOARD_CURRENCY_FORMATTER.format(value);
}
function formatCount(value: number): string {
- return new Intl.NumberFormat("pt-BR").format(value);
+ return DASHBOARD_COUNT_FORMATTER.format(value);
}
// --- Loading Skeleton ---
@@ -232,8 +250,8 @@ function WarehouseChart({
}
/>
- {data.map((_, i) => (
- |
+ {data.map((item, i) => (
+ |
))}
@@ -287,9 +305,9 @@ function CategoryChart({
paddingAngle={2}
strokeWidth={0}
>
- {data.map((_, i) => (
+ {data.map((item, i) => (
|
))}
@@ -313,7 +331,7 @@ function CategoryChart({
{data.map((item, i) => (
Movimentações Recentes
-
+
Nenhuma movimentação recente
@@ -370,11 +388,11 @@ function RecentMovementsTimeline({
{/* Timeline line + icon */}
@@ -399,7 +417,7 @@ function RecentMovementsTimeline({
- {formatDistanceToNow(new Date(movement.createdAt), {
+ {formatDistanceToNow(parseISO(movement.createdAt), {
addSuffix: true,
locale: ptBR,
})}
diff --git a/app/(pages)/layout.tsx b/app/(pages)/layout.tsx
index 66709ea..3ec1bfc 100644
--- a/app/(pages)/layout.tsx
+++ b/app/(pages)/layout.tsx
@@ -54,7 +54,7 @@ export default function PagesLayout({
const { warehouseId } = useSelectedWarehouse();
const { user, logout } = useAuth();
const { selectedWarehouseId, setSelectedWarehouseId } = useWarehouse();
- const router = useRouter();
+ const { replace } = useRouter();
const { data: warehouses = [] } = useSWR(
isOpen ? "mobile-warehouses" : null,
@@ -66,14 +66,14 @@ export default function PagesLayout({
useEffect(() => {
if (user?.mustChangePassword) {
- router.replace("/change-password");
+ replace("/change-password");
return;
}
if (warehouseId === null) {
- router.replace("/warehouses");
+ replace("/warehouses");
}
- }, [warehouseId, user, router]);
+ }, [warehouseId, user, replace]);
const handleWarehouseChange = async (id: string) => {
try {
@@ -95,13 +95,13 @@ export default function PagesLayout({
if (warehouseId === null) {
return (
-
-
+
+
-
+
- Redirecionando para seleção de armazém...
+ Redirecionando para seleção de armazém…
@@ -133,7 +133,12 @@ export default function PagesLayout({
inert={!isOpen}
className={cn("fixed inset-0 z-50 md:hidden", !isOpen && "hidden")}
>
-
+
{/* Logo + close button */}
@@ -148,10 +153,10 @@ export default function PagesLayout({
@@ -164,7 +169,7 @@ export default function PagesLayout({
>
-
+
@@ -198,8 +203,8 @@ export default function PagesLayout({
-
@@ -219,7 +224,7 @@ export default function PagesLayout({
-
+
Perfil
@@ -231,7 +236,7 @@ export default function PagesLayout({
className="cursor-pointer text-xs uppercase tracking-wide text-rose-500 focus:bg-rose-950/50 focus:text-rose-500"
>
-
+
Logout
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..e2da8ec
--- /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 (
+
+
+
+
+
+
+
+ Carregando produto…
+
+
+
+
+
+ );
+ }
+
+ if (!product) {
+ return (
+
+
+
+
+
+
+
+ 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 (
-
-
-
-
-
-
-
- Carregando produto...
-
-
-
-
-
- );
- }
-
- if (!product) {
- return (
-
-
-
-
-
-
-
- Produto não encontrado
-
-
-
-
-
- );
- }
-
- return ;
+export default function Page() {
+ return ;
}
diff --git a/app/(pages)/products/[id]/edit/products-edit.model.ts b/app/(pages)/products/[id]/edit/products-edit.model.ts
index a9c0401..852d8e6 100644
--- a/app/(pages)/products/[id]/edit/products-edit.model.ts
+++ b/app/(pages)/products/[id]/edit/products-edit.model.ts
@@ -79,7 +79,6 @@ export const useProductEditModel = (productId: string) => {
const [updatingBatchId, setUpdatingBatchId] = useState(null);
const nameInputRef = useRef(null);
const loadedProductIdRef = useRef(null);
- const [isFormReady, setIsFormReady] = useState(false);
// Fetch product data
const { data: productData, isLoading: isLoadingProduct } =
@@ -197,17 +196,19 @@ export const useProductEditModel = (productId: string) => {
});
loadedProductIdRef.current = product.id;
- setIsFormReady(true);
-
// Extract custom attributes (excluding weight and dimensions)
if (product.attributes) {
- const attrs = Object.entries(product.attributes)
- .filter(([key]) => key !== "weight" && key !== "dimensions")
- .map(([key, value]) => ({
- id: crypto.randomUUID(),
- key,
- value,
- }));
+ const attrs = Object.entries(product.attributes).flatMap(([key, value]) =>
+ key === "weight" || key === "dimensions"
+ ? []
+ : [
+ {
+ id: crypto.randomUUID(),
+ key,
+ value,
+ },
+ ],
+ );
setCustomAttributes(attrs);
} else {
setCustomAttributes([]);
@@ -247,6 +248,9 @@ export const useProductEditModel = (productId: string) => {
}, [batchesData, replaceBatchFields]);
const product = productData?.data || null;
+ const isFormReady = Boolean(
+ product && loadedProductIdRef.current === product.id,
+ );
const selectedCategory = useMemo(() => {
if (!product) return null;
@@ -296,8 +300,8 @@ export const useProductEditModel = (productId: string) => {
}, [brandsData, selectedBrand]);
const addCustomAttribute = () => {
- setCustomAttributes([
- ...customAttributes,
+ setCustomAttributes((current) => [
+ ...current,
{ id: crypto.randomUUID(), key: "", value: "" },
]);
};
diff --git a/app/(pages)/products/[id]/edit/products-edit.types.ts b/app/(pages)/products/[id]/edit/products-edit.types.ts
index 20d11dd..586e2f9 100644
--- a/app/(pages)/products/[id]/edit/products-edit.types.ts
+++ b/app/(pages)/products/[id]/edit/products-edit.types.ts
@@ -1,4 +1,4 @@
-export interface Batch {
+interface Batch {
id: string;
productId: string;
productName: string;
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/[id]/products-detail.view.tsx b/app/(pages)/products/[id]/products-detail.view.tsx
index 4b17463..0b2d529 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;
@@ -36,6 +42,11 @@ interface ProductDetailViewProps {
batchesError: Error | null;
}
+const PRODUCT_DETAIL_CURRENCY_FORMATTER = new Intl.NumberFormat("pt-BR", {
+ style: "currency",
+ currency: "BRL",
+});
+
export const ProductDetailView = ({
product,
batches,
@@ -44,7 +55,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 +74,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 +83,442 @@ export const ProductDetailView = ({
}
};
- // Loading State
+ const formatCurrency = (value?: number | null) => {
+ if (value === null || value === undefined) return "—";
+ return PRODUCT_DETAIL_CURRENCY_FORMATTER.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.
-
-
-
-
- Voltar para Lista
-
-
-
+
+
+
);
}
+ const totalStock = batches.reduce((sum, batch) => sum + batch.quantity, 0);
+
return (
-
-
-
-
-
-
-
- Editar Produto
+
+ {/* Page Header */}
+
+
+
+
+ Editar
-
-
-
- {/* Left Column: Image & Quick Stats (4 cols) */}
-
- {/* Product Image Card */}
-
-
-
- {product.active ? "Ativo" : "Inativo"}
-
-
+
+ }
+ />
-
- {product.imageUrl ? (
-
-
-
+ {/* Hero: Image + Identity */}
+
+ {/* Product Image */}
+
+
+
+
+ {product.active ? (
+
) : (
-
+
)}
-
+ {product.active ? "Ativo" : "Inativo"}
+
+
- {/* Image Footer Stats */}
-
-
-
- Tipo
-
-
- {product.isKit ? (
-
- ) : (
-
- )}
- {product.isKit ? "KIT / COMBO" : "UNITÁRIO"}
-
+
+ {product.imageUrl ? (
+
+
-
-
- 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
-
- copyToClipboard(product.id, "ID do Produto")}
- className="flex w-full items-center justify-between rounded-[2px] bg-neutral-950 border border-neutral-800 px-3 py-2 text-xs font-mono text-neutral-400 hover:text-white hover:border-neutral-700 transition-colors group"
- >
- {product.id}
-
-
+
+
+
+ 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
+
+ copyToClipboard(product.id, "ID do Produto")}
+ className="flex items-center gap-2 rounded-[4px] bg-neutral-900 border border-neutral-800 px-3 py-1.5 text-[11px] font-mono text-neutral-500 hover:text-neutral-300 hover:border-neutral-700 w-full max-w-full group"
+ >
+ {product.id}
+
+
+
+
+
- {/* Categorization Grid */}
-
- {/* Category */}
-
-
-
-
- {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 && (
+ {
+ e.preventDefault();
+ onCopy();
+ }}
+ className="shrink-0 text-neutral-700 hover:text-neutral-300"
+ aria-label={`Copiar ${label}`}
+ >
+
+
+ )}
-
+
);
-};
+}
diff --git a/app/(pages)/products/components/product-form.types.ts b/app/(pages)/products/components/product-form.types.ts
index b8762f2..4b10f20 100644
--- a/app/(pages)/products/components/product-form.types.ts
+++ b/app/(pages)/products/components/product-form.types.ts
@@ -3,7 +3,7 @@ import { UseFormReturn } from "react-hook-form";
import { ProductCreateFormData, AiFillData } from "../create/products-create.types";
import { CustomAttribute } from "@/components/product/custom-attributes-builder";
-export interface Category {
+interface Category {
id: string;
name: string;
parentCategoryName?: string | null;
@@ -13,7 +13,7 @@ export interface Category {
} | null;
}
-export interface Brand {
+interface Brand {
id: string;
name: string;
logoUrl?: string;
@@ -109,5 +109,3 @@ export interface ProductFormProps {
isFormReady?: boolean; // Optional - only needed in edit mode to prevent race conditions
batchesDrawer?: BatchesDrawerProps;
}
-
-export type { ProductCreateFormData, CustomAttribute };
diff --git a/app/(pages)/products/components/product-form.view.tsx b/app/(pages)/products/components/product-form.view.tsx
index f8ec693..91f5aca 100644
--- a/app/(pages)/products/components/product-form.view.tsx
+++ b/app/(pages)/products/components/product-form.view.tsx
@@ -95,7 +95,7 @@ const BatchModeSwitch = ({
>
-
+
{isInlineMode ? "Modo em lote" : "Modo Contínuo"}
{!compact && (
@@ -245,7 +245,7 @@ export const ProductForm = ({
-
+
Informações Básicas
@@ -258,7 +258,7 @@ export const ProductForm = ({
onClick={openAiModal}
className="w-full md:w-fit h-9 rounded-[4px] bg-blue-600 text-xs font-bold uppercase tracking-wide text-white hover:bg-blue-700 shadow-[0_0_15px_-5px_rgba(37,99,235,0.3)]"
>
-
+
Pegar dados de uma foto
@@ -295,7 +295,7 @@ export const ProductForm = ({
render={({ field }) => (
- Código de Barras
+ Código de Barras
@@ -309,9 +309,9 @@ export const ProductForm = ({
type="button"
variant="outline"
onClick={openScanner}
- className="h-10 w-10 shrink-0 rounded-[4px] border-neutral-800 bg-neutral-900 hover:bg-neutral-800 hover:text-white"
+ className="size-10 shrink-0 rounded-[4px] border-neutral-800 bg-neutral-900 hover:bg-neutral-800 hover:text-white"
>
-
+
@@ -330,7 +330,7 @@ export const ProductForm = ({
@@ -346,7 +346,7 @@ export const ProductForm = ({
-
+
Dimensões e Atributos
@@ -360,7 +360,7 @@ export const ProductForm = ({
render={({ field }) => (
- Peso
+ Peso
(
- Dimensões (C x L x A)
+ Dimensões (C x L x A)
-
+
Atributos Personalizados
-
+
Estoque e Precificação
@@ -542,10 +542,10 @@ export const ProductForm = ({
type="button"
variant="outline"
onClick={onQuantityDecrement}
- className="h-10 w-10 rounded-l-[4px] rounded-r-none border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
+ className="size-10 rounded-l-[4px] rounded-r-none border-neutral-800 bg-neutral-900 p-0 hover:bg-neutral-800 hover:text-white"
aria-label="Diminuir quantidade"
>
-
+
-
+
) : (
@@ -591,7 +591,7 @@ export const ProductForm = ({
render={({ field }) => (
- Fabricação
+ Fabricação
(
- Validade
+ Validade
-
+
Categorização
@@ -673,7 +673,7 @@ export const ProductForm = ({
render={({ field }) => (
-
+
Categoria