Paginate stock inventory tab
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user