Clarify synchronized and local inventory
This commit is contained in:
@@ -831,7 +831,17 @@ const StatGrid = ({ summary }: { summary: SupplySummary }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary; onNewReceipt: () => void }) => {
|
||||
const InventoryDashboard = ({
|
||||
summary,
|
||||
stock,
|
||||
onNewReceipt,
|
||||
onOpenStock,
|
||||
}: {
|
||||
summary: SupplySummary;
|
||||
stock: StockData[];
|
||||
onNewReceipt: () => void;
|
||||
onOpenStock: () => void;
|
||||
}) => {
|
||||
const categories = summary.lots.reduce<Record<string, { quantity: number; unit: string; lots: number }>>((acc, lot) => {
|
||||
const current = acc[lot.category] || { quantity: 0, unit: lot.unit, lots: 0 };
|
||||
acc[lot.category] = {
|
||||
@@ -842,12 +852,46 @@ const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary;
|
||||
return acc;
|
||||
}, {});
|
||||
const latestReceipts = summary.receipts.slice(0, 4);
|
||||
const negativeStockCount = stock.filter(item => Number(item.saldo || 0) < 0).length;
|
||||
const zeroStockCount = stock.filter(item => Number(item.saldo || 0) === 0).length;
|
||||
const latestStockUpdate = stock
|
||||
.map(item => item.updated_at ? new Date(item.updated_at).getTime() : 0)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b - a)[0] || null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<StatGrid summary={summary} />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<button type="button" onClick={onOpenStock} className="rounded-2xl border border-dark-border bg-dark-card p-5 text-left shadow-sm transition-colors hover:border-brand-primary/35 cursor-pointer">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Estoque sincronizado</p>
|
||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(stock.length, 0)}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">SKUs recebidos do Tiny</p>
|
||||
</button>
|
||||
<button type="button" onClick={onOpenStock} className={`rounded-2xl border p-5 text-left shadow-sm transition-colors cursor-pointer ${negativeStockCount ? 'border-red-400/35 bg-red-400/[0.055] hover:border-red-400/60' : 'border-dark-border bg-dark-card hover:border-brand-primary/35'}`}>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Saldos a conferir</p>
|
||||
<p className={`mt-2 text-3xl font-bold ${negativeStockCount ? 'text-red-300' : 'text-dark-text'}`}>{formatNumber(negativeStockCount, 0)}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">SKUs com saldo negativo</p>
|
||||
</button>
|
||||
<button type="button" onClick={onNewReceipt} className="rounded-2xl border border-dark-border bg-dark-card p-5 text-left shadow-sm transition-colors hover:border-brand-primary/35 cursor-pointer">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Recebimentos pendentes</p>
|
||||
<p className="mt-2 text-3xl font-bold text-amber-300">{formatNumber(summary.stats.pendingReceipts, 0)}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">Entradas aguardando aprovação</p>
|
||||
</button>
|
||||
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Lotes locais</p>
|
||||
<p className="mt-2 text-3xl font-bold text-brand-primary">{formatNumber(summary.stats.activeLots, 0)}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">Controle físico registrado aqui</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(340px,.85fr)]">
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<h2 className="text-base font-bold text-dark-text">Por categoria de material</h2>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-dark-text">Estoque físico por categoria</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Lotes aprovados por recebimento ou inventário local.</p>
|
||||
</div>
|
||||
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-muted">{formatNumber(summary.stats.rolls, 0)} rolos</span>
|
||||
</div>
|
||||
{Object.keys(categories).length ? (
|
||||
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{Object.entries(categories).map(([category, data]) => (
|
||||
@@ -861,14 +905,25 @@ const InventoryDashboard = ({ summary, onNewReceipt }: { summary: SupplySummary;
|
||||
) : (
|
||||
<div className={emptyStateClassName}>
|
||||
<Warehouse className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhuma categoria com saldo</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">Registre entradas para agrupar o estoque por tipo de material.</p>
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhum lote físico registrado</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">O saldo do Tiny aparece na aba Estoque. Registre recebimentos para controlar lotes aqui.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<h2 className="flex items-center gap-2 text-base font-bold text-dark-text"><AlertTriangle className="h-4 w-4 text-yellow-400" /> Alertas de estoque</h2>
|
||||
<p className="mt-4 text-sm font-semibold text-dark-muted">Nenhum alerta no momento.</p>
|
||||
<h2 className="flex items-center gap-2 text-base font-bold text-dark-text"><AlertTriangle className="h-4 w-4 text-yellow-400" /> Conferir antes de planejar</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
<button type="button" onClick={onOpenStock} className="flex w-full items-center justify-between gap-3 rounded-xl border border-red-400/25 bg-red-400/[0.045] px-3.5 py-3 text-left transition-colors hover:border-red-400/55 cursor-pointer">
|
||||
<span><span className="block text-sm font-bold text-dark-text">Saldos negativos</span><span className="block text-xs font-semibold text-dark-muted">Confirmar ajuste ou entrada no Tiny</span></span>
|
||||
<span className="text-lg font-bold text-red-300">{formatNumber(negativeStockCount, 0)}</span>
|
||||
</button>
|
||||
<button type="button" onClick={onOpenStock} className="flex w-full items-center justify-between gap-3 rounded-xl border border-dark-border bg-dark-input/45 px-3.5 py-3 text-left transition-colors hover:border-brand-primary/45 cursor-pointer">
|
||||
<span><span className="block text-sm font-bold text-dark-text">Sem saldo no Tiny</span><span className="block text-xs font-semibold text-dark-muted">SKUs sincronizados com zero</span></span>
|
||||
<span className="text-lg font-bold text-dark-text">{formatNumber(zeroStockCount, 0)}</span>
|
||||
</button>
|
||||
<p className="pt-1 text-xs font-semibold text-dark-muted">Última sincronização: {latestStockUpdate ? formatDateTime(new Date(latestStockUpdate).toISOString()) : 'sem atualização registrada'}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -1732,7 +1787,7 @@ const InventoryScreen = () => {
|
||||
<div className={`${panelClassName} p-5 text-sm font-bold text-dark-muted`}>Carregando estoque...</div>
|
||||
) : (
|
||||
<>
|
||||
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} onNewReceipt={() => setActiveTab('receipts')} />}
|
||||
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} stock={stockSnapshot} onNewReceipt={() => setActiveTab('receipts')} onOpenStock={() => setActiveTab('stock')} />}
|
||||
{activeTab === 'stock' && <StockTab stock={stockSnapshot} onRefresh={loadStockSnapshot} />}
|
||||
{activeTab === 'balance' && <BalanceTab summary={summary} onRefresh={loadSummary} />}
|
||||
{activeTab === 'receipts' && (
|
||||
@@ -2244,10 +2299,10 @@ const PurchaseNeedsScreen = () => {
|
||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const initialTab = searchParams.get('tab');
|
||||
const initialFilter = searchParams.get('filter');
|
||||
const [activeTab, setActiveTab] = useState<PurchaseNeedTab>(initialTab === 'services' || initialTab === 'mapping' ? initialTab : 'materials');
|
||||
const [materialFilter, setMaterialFilter] = useState<MaterialPurchaseFilter>(initialFilter === 'negative_balance' || initialFilter === 'no_stock' || initialFilter === 'all' ? initialFilter : 'to_buy');
|
||||
const requestedTab = searchParams.get('tab');
|
||||
const requestedFilter = searchParams.get('filter');
|
||||
const activeTab: PurchaseNeedTab = requestedTab === 'services' || requestedTab === 'mapping' ? requestedTab : 'materials';
|
||||
const materialFilter: MaterialPurchaseFilter = requestedFilter === 'negative_balance' || requestedFilter === 'no_stock' || requestedFilter === 'all' ? requestedFilter : 'to_buy';
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
@@ -2276,18 +2331,7 @@ const PurchaseNeedsScreen = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const tab = searchParams.get('tab');
|
||||
const filter = searchParams.get('filter');
|
||||
setActiveTab(tab === 'services' || tab === 'mapping' ? tab : 'materials');
|
||||
setMaterialFilter(filter === 'negative_balance' || filter === 'no_stock' || filter === 'all' ? filter : 'to_buy');
|
||||
setSearch('');
|
||||
setCurrentPage(1);
|
||||
}, [searchParams]);
|
||||
|
||||
const openQueue = (tab: PurchaseNeedTab, filter: MaterialPurchaseFilter = 'to_buy') => {
|
||||
setActiveTab(tab);
|
||||
setMaterialFilter(filter);
|
||||
setSearch('');
|
||||
setCurrentPage(1);
|
||||
setSearchParams(tab === 'materials' ? { tab, filter } : { tab });
|
||||
|
||||
Reference in New Issue
Block a user