Paginate stock inventory tab
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s

This commit is contained in:
Cauê Faleiros
2026-07-21 11:30:00 -03:00
parent 4c3df482be
commit eb5cc01d56

View File

@@ -34,7 +34,7 @@ import { resolveProductType, type ProductTypeKey } from '../productClassificatio
import { getPlanningStock } from '../planningStock';
import type { CuttingSettings, DateRange, ProductAnalyticsItem, StockData, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
type InventoryTab = 'dashboard' | 'tiny_stock' | 'balance' | 'receipts' | 'inventory' | 'movements';
type InventoryTab = 'dashboard' | 'stock' | 'balance' | 'receipts' | 'inventory' | 'movements';
type ReceiptView = 'new' | 'pending' | 'history';
const pageClassName = 'flex w-full flex-col gap-6';
@@ -100,7 +100,7 @@ const exportCsv = (filename: string, rows: Array<Record<string, string | number
const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [
{ id: 'dashboard', name: 'Dashboard', icon: Warehouse },
{ id: 'tiny_stock', name: 'Estoque Tiny', icon: PackageSearch },
{ id: 'stock', name: 'Estoque', icon: PackageSearch },
{ id: 'balance', name: 'Saldo', icon: BarChart3 },
{ id: 'receipts', name: 'Recebimentos', icon: Truck },
{ id: 'inventory', name: 'Inventário', icon: ClipboardCheck },
@@ -959,7 +959,7 @@ const ReceiptsTab = ({
);
};
const TinyStockTab = ({
const StockTab = ({
stock,
onRefresh,
}: {
@@ -968,6 +968,8 @@ const TinyStockTab = ({
}) => {
const [search, setSearch] = useState('');
const [stockFilter, setStockFilter] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(20);
const normalizedSearch = normalizeSearch(search);
const totalBalance = stock.reduce((total, item) => total + Number(item.saldo || 0), 0);
const updatedItems = stock.filter(item => Number(item.delta_estoque || 0) !== 0).length;
@@ -987,12 +989,16 @@ const TinyStockTab = ({
(stockFilter === 'changed' && delta !== 0);
return matchesSearch && matchesFilter;
}).sort((a, b) => Number(b.saldo || 0) - Number(a.saldo || 0));
const totalPages = Math.ceil(visibleStock.length / itemsPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
const paginatedStock = visibleStock.slice(startIndex, startIndex + itemsPerPage);
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
<div className="rounded-xl border border-dark-border bg-dark-card p-4 shadow-sm">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">SKUs Tiny</p>
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">SKUs</p>
<p className="mt-2 text-2xl font-bold text-dark-text">{formatNumber(stock.length, 0)}</p>
</div>
<div className="rounded-xl border border-dark-border bg-dark-card p-4 shadow-sm">
@@ -1012,13 +1018,13 @@ const TinyStockTab = ({
<div className={`${panelClassName} p-4`}>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="text-base font-bold text-dark-text">Estoque vindo do Tiny</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Saldo de produtos sincronizado pelo fluxo Tiny para Graphs.</p>
<h2 className="text-base font-bold text-dark-text">Estoque</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Saldo de produtos sincronizado pelo fluxo de estoque para Graphs.</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<button
type="button"
onClick={() => exportCsv('estoque-tiny.csv', visibleStock.map(item => ({
onClick={() => exportCsv('estoque.csv', visibleStock.map(item => ({
produto_id: item.produto_id,
nome: item.nome,
saldo: item.saldo,
@@ -1040,12 +1046,22 @@ const TinyStockTab = ({
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
onChange={(event) => {
setSearch(event.target.value);
setCurrentPage(1);
}}
className={`${inputClassName} pl-9`}
placeholder="Buscar por SKU ou produto..."
/>
</label>
<select value={stockFilter} onChange={(event) => setStockFilter(event.target.value)} className={inputClassName}>
<select
value={stockFilter}
onChange={(event) => {
setStockFilter(event.target.value);
setCurrentPage(1);
}}
className={inputClassName}
>
<option value="all">Todos os saldos</option>
<option value="positive">Com saldo</option>
<option value="empty">Sem saldo</option>
@@ -1056,14 +1072,14 @@ const TinyStockTab = ({
{visibleStock.length ? (
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
<div className="hidden grid-cols-[140px_1fr_120px_120px_160px] border-b border-dark-border bg-dark-input/40 px-4 py-3 text-xs font-bold uppercase tracking-widest text-dark-muted md:grid">
<span>SKU Tiny</span>
<span>SKU</span>
<span>Produto</span>
<span className="text-right">Saldo</span>
<span className="text-right">Delta</span>
<span className="text-right">Atualizado</span>
</div>
<div className="divide-y divide-dark-border">
{visibleStock.map(item => {
{paginatedStock.map(item => {
const delta = Number(item.delta_estoque || 0);
return (
<div key={item.produto_id} className="grid grid-cols-1 gap-2 bg-dark-card px-4 py-3 md:grid-cols-[140px_1fr_120px_120px_160px] md:items-center">
@@ -1078,13 +1094,30 @@ const TinyStockTab = ({
);
})}
</div>
<PaginationControls
totalItems={visibleStock.length}
currentPage={safeCurrentPage}
totalPages={totalPages}
pageSize={itemsPerPage}
pageSizeOptions={[20, 50, 100]}
itemLabel="SKUs"
pageSizeLabel="por página"
startIndex={startIndex}
endIndex={Math.min(startIndex + itemsPerPage, visibleStock.length)}
onPageChange={setCurrentPage}
onPageSizeChange={(pageSize) => {
setItemsPerPage(pageSize);
setCurrentPage(1);
}}
className="border-t border-dark-border px-4 py-3"
/>
</div>
) : (
<div className={emptyStateClassName}>
<PackageSearch className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">{stock.length ? 'Nenhum SKU encontrado' : 'Nenhum estoque Tiny sincronizado'}</h3>
<h3 className="text-base font-bold text-dark-text">{stock.length ? 'Nenhum SKU encontrado' : 'Nenhum estoque sincronizado'}</h3>
<p className="text-sm font-semibold text-dark-muted">
{stock.length ? 'Ajuste a busca ou o filtro.' : 'Quando o fluxo Tiny postar em /api/stock, os saldos aparecem aqui.'}
{stock.length ? 'Ajuste a busca ou o filtro.' : 'Quando o fluxo de estoque postar em /api/stock, os saldos aparecem aqui.'}
</p>
</div>
)}
@@ -1368,19 +1401,19 @@ const MovementsTab = ({
const InventoryScreen = () => {
const [activeTab, setActiveTab] = useState<InventoryTab>('dashboard');
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
const [tinyStock, setTinyStock] = useState<StockData[]>([]);
const [stockSnapshot, setStockSnapshot] = useState<StockData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isBusy, setIsBusy] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const loadInventoryData = async () => {
setErrorMessage('');
const [nextSummary, nextTinyStock] = await Promise.all([
const [nextSummary, nextStockSnapshot] = await Promise.all([
fetchSupplySummary(),
fetchStock(),
]);
setSummary(nextSummary);
setTinyStock(nextTinyStock);
setStockSnapshot(nextStockSnapshot);
};
const loadSummary = async () => {
@@ -1389,9 +1422,9 @@ const InventoryScreen = () => {
setSummary(nextSummary);
};
const loadTinyStock = async () => {
const loadStockSnapshot = async () => {
setErrorMessage('');
setTinyStock(await fetchStock());
setStockSnapshot(await fetchStock());
};
useEffect(() => {
@@ -1400,13 +1433,13 @@ const InventoryScreen = () => {
const load = async () => {
setIsLoading(true);
try {
const [nextSummary, nextTinyStock] = await Promise.all([
const [nextSummary, nextStockSnapshot] = await Promise.all([
fetchSupplySummary(),
fetchStock(),
]);
if (isMounted) {
setSummary(nextSummary);
setTinyStock(nextTinyStock);
setStockSnapshot(nextStockSnapshot);
}
} finally {
if (isMounted) setIsLoading(false);
@@ -1461,7 +1494,7 @@ const InventoryScreen = () => {
) : (
<>
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} onNewReceipt={() => setActiveTab('receipts')} />}
{activeTab === 'tiny_stock' && <TinyStockTab stock={tinyStock} onRefresh={loadTinyStock} />}
{activeTab === 'stock' && <StockTab stock={stockSnapshot} onRefresh={loadStockSnapshot} />}
{activeTab === 'balance' && <BalanceTab summary={summary} onRefresh={loadSummary} />}
{activeTab === 'receipts' && (
<ReceiptsTab