Compare commits
3 Commits
bc74a1089b
...
eb5cc01d56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5cc01d56 | ||
|
|
4c3df482be | ||
|
|
377754c26e |
@@ -34,7 +34,7 @@ import { resolveProductType, type ProductTypeKey } from '../productClassificatio
|
|||||||
import { getPlanningStock } from '../planningStock';
|
import { getPlanningStock } from '../planningStock';
|
||||||
import type { CuttingSettings, DateRange, ProductAnalyticsItem, StockData, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
|
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';
|
type ReceiptView = 'new' | 'pending' | 'history';
|
||||||
|
|
||||||
const pageClassName = 'flex w-full flex-col gap-6';
|
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 }> = [
|
const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [
|
||||||
{ id: 'dashboard', name: 'Dashboard', icon: Warehouse },
|
{ 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: 'balance', name: 'Saldo', icon: BarChart3 },
|
||||||
{ id: 'receipts', name: 'Recebimentos', icon: Truck },
|
{ id: 'receipts', name: 'Recebimentos', icon: Truck },
|
||||||
{ id: 'inventory', name: 'Inventário', icon: ClipboardCheck },
|
{ id: 'inventory', name: 'Inventário', icon: ClipboardCheck },
|
||||||
@@ -959,7 +959,7 @@ const ReceiptsTab = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TinyStockTab = ({
|
const StockTab = ({
|
||||||
stock,
|
stock,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
}: {
|
}: {
|
||||||
@@ -968,6 +968,8 @@ const TinyStockTab = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [stockFilter, setStockFilter] = useState('all');
|
const [stockFilter, setStockFilter] = useState('all');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(20);
|
||||||
const normalizedSearch = normalizeSearch(search);
|
const normalizedSearch = normalizeSearch(search);
|
||||||
const totalBalance = stock.reduce((total, item) => total + Number(item.saldo || 0), 0);
|
const totalBalance = stock.reduce((total, item) => total + Number(item.saldo || 0), 0);
|
||||||
const updatedItems = stock.filter(item => Number(item.delta_estoque || 0) !== 0).length;
|
const updatedItems = stock.filter(item => Number(item.delta_estoque || 0) !== 0).length;
|
||||||
@@ -987,12 +989,16 @@ const TinyStockTab = ({
|
|||||||
(stockFilter === 'changed' && delta !== 0);
|
(stockFilter === 'changed' && delta !== 0);
|
||||||
return matchesSearch && matchesFilter;
|
return matchesSearch && matchesFilter;
|
||||||
}).sort((a, b) => Number(b.saldo || 0) - Number(a.saldo || 0));
|
}).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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-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">
|
<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>
|
<p className="mt-2 text-2xl font-bold text-dark-text">{formatNumber(stock.length, 0)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-dark-border bg-dark-card p-4 shadow-sm">
|
<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={`${panelClassName} p-4`}>
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-dark-text">Estoque vindo do Tiny</h2>
|
<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 Tiny para Graphs.</p>
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Saldo de produtos sincronizado pelo fluxo de estoque para Graphs.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => exportCsv('estoque-tiny.csv', visibleStock.map(item => ({
|
onClick={() => exportCsv('estoque.csv', visibleStock.map(item => ({
|
||||||
produto_id: item.produto_id,
|
produto_id: item.produto_id,
|
||||||
nome: item.nome,
|
nome: item.nome,
|
||||||
saldo: item.saldo,
|
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" />
|
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
||||||
<input
|
<input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
className={`${inputClassName} pl-9`}
|
className={`${inputClassName} pl-9`}
|
||||||
placeholder="Buscar por SKU ou produto..."
|
placeholder="Buscar por SKU ou produto..."
|
||||||
/>
|
/>
|
||||||
</label>
|
</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="all">Todos os saldos</option>
|
||||||
<option value="positive">Com saldo</option>
|
<option value="positive">Com saldo</option>
|
||||||
<option value="empty">Sem saldo</option>
|
<option value="empty">Sem saldo</option>
|
||||||
@@ -1056,14 +1072,14 @@ const TinyStockTab = ({
|
|||||||
{visibleStock.length ? (
|
{visibleStock.length ? (
|
||||||
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
|
<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">
|
<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>Produto</span>
|
||||||
<span className="text-right">Saldo</span>
|
<span className="text-right">Saldo</span>
|
||||||
<span className="text-right">Delta</span>
|
<span className="text-right">Delta</span>
|
||||||
<span className="text-right">Atualizado</span>
|
<span className="text-right">Atualizado</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-dark-border">
|
<div className="divide-y divide-dark-border">
|
||||||
{visibleStock.map(item => {
|
{paginatedStock.map(item => {
|
||||||
const delta = Number(item.delta_estoque || 0);
|
const delta = Number(item.delta_estoque || 0);
|
||||||
return (
|
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">
|
<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>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className={emptyStateClassName}>
|
<div className={emptyStateClassName}>
|
||||||
<PackageSearch className="h-8 w-8 text-brand-primary" />
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1368,19 +1401,19 @@ const MovementsTab = ({
|
|||||||
const InventoryScreen = () => {
|
const InventoryScreen = () => {
|
||||||
const [activeTab, setActiveTab] = useState<InventoryTab>('dashboard');
|
const [activeTab, setActiveTab] = useState<InventoryTab>('dashboard');
|
||||||
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
const [summary, setSummary] = useState<SupplySummary>(emptySupplySummary);
|
||||||
const [tinyStock, setTinyStock] = useState<StockData[]>([]);
|
const [stockSnapshot, setStockSnapshot] = useState<StockData[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isBusy, setIsBusy] = useState(false);
|
const [isBusy, setIsBusy] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
const loadInventoryData = async () => {
|
const loadInventoryData = async () => {
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
const [nextSummary, nextTinyStock] = await Promise.all([
|
const [nextSummary, nextStockSnapshot] = await Promise.all([
|
||||||
fetchSupplySummary(),
|
fetchSupplySummary(),
|
||||||
fetchStock(),
|
fetchStock(),
|
||||||
]);
|
]);
|
||||||
setSummary(nextSummary);
|
setSummary(nextSummary);
|
||||||
setTinyStock(nextTinyStock);
|
setStockSnapshot(nextStockSnapshot);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
@@ -1389,9 +1422,9 @@ const InventoryScreen = () => {
|
|||||||
setSummary(nextSummary);
|
setSummary(nextSummary);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadTinyStock = async () => {
|
const loadStockSnapshot = async () => {
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
setTinyStock(await fetchStock());
|
setStockSnapshot(await fetchStock());
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1400,13 +1433,13 @@ const InventoryScreen = () => {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [nextSummary, nextTinyStock] = await Promise.all([
|
const [nextSummary, nextStockSnapshot] = await Promise.all([
|
||||||
fetchSupplySummary(),
|
fetchSupplySummary(),
|
||||||
fetchStock(),
|
fetchStock(),
|
||||||
]);
|
]);
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setSummary(nextSummary);
|
setSummary(nextSummary);
|
||||||
setTinyStock(nextTinyStock);
|
setStockSnapshot(nextStockSnapshot);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (isMounted) setIsLoading(false);
|
if (isMounted) setIsLoading(false);
|
||||||
@@ -1461,7 +1494,7 @@ const InventoryScreen = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{activeTab === 'dashboard' && <InventoryDashboard summary={summary} onNewReceipt={() => setActiveTab('receipts')} />}
|
{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 === 'balance' && <BalanceTab summary={summary} onRefresh={loadSummary} />}
|
||||||
{activeTab === 'receipts' && (
|
{activeTab === 'receipts' && (
|
||||||
<ReceiptsTab
|
<ReceiptsTab
|
||||||
@@ -1589,8 +1622,66 @@ const FabricPlanningScreen = () => {
|
|||||||
{errorMessage}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[420px_1fr] xl:items-start">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<form onSubmit={handleSubmit} className={`${panelClassName} p-5 xl:sticky xl:top-6`}>
|
<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">Planos ativos</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.length}</p>
|
||||||
|
</div>
|
||||||
|
<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">Kg planejado</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalKg)} kg</p>
|
||||||
|
</div>
|
||||||
|
<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">Críticos</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.filter(plan => plan.priority === 'Crítico').length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 items-stretch gap-6 xl:grid-cols-[1fr_420px]">
|
||||||
|
<div className={`${panelClassName} flex flex-col overflow-hidden`}>
|
||||||
|
<div className="border-b border-dark-border p-5">
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={`${emptyStateClassName} min-h-[220px] flex-1`}>
|
||||||
|
<Ruler className="h-8 w-8 text-brand-primary" />
|
||||||
|
<h3 className="text-base font-bold text-dark-text">Carregando planos...</h3>
|
||||||
|
</div>
|
||||||
|
) : plans.length ? (
|
||||||
|
<div className="divide-y divide-dark-border">
|
||||||
|
{plans.map(plan => (
|
||||||
|
<div key={plan.id} className="grid grid-cols-1 gap-3 p-4 md:grid-cols-[1.4fr_1fr_100px_100px_44px] md:items-center">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-dark-text">{plan.material}</p>
|
||||||
|
<p className="text-xs font-semibold text-dark-muted">{plan.color}</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">{plan.supplier}</p>
|
||||||
|
<p className="text-sm font-bold text-dark-text">{formatNumber(plan.quantityKg)} kg</p>
|
||||||
|
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-text">{plan.priority}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Remover plano"
|
||||||
|
aria-label={`Remover plano ${plan.material}`}
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => removePlan(plan.id)}
|
||||||
|
className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-red-400 transition-colors hover:bg-red-500/10 hover:text-red-300 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={`${emptyStateClassName} flex-1`}>
|
||||||
|
<Ruler className="h-8 w-8 text-brand-primary" />
|
||||||
|
<h3 className="text-base font-bold text-dark-text">Nenhum plano de malha cadastrado</h3>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">Cadastre o primeiro plano no formulário ao lado.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className={`${panelClassName} p-5`}>
|
||||||
<h2 className="text-base font-bold text-dark-text">Novo plano de malha</h2>
|
<h2 className="text-base font-bold text-dark-text">Novo plano de malha</h2>
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3">
|
<div className="mt-4 grid grid-cols-1 gap-3">
|
||||||
<label className="text-xs font-bold text-dark-muted">
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
@@ -1625,66 +1716,6 @@ const FabricPlanningScreen = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
||||||
<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">Planos ativos</p>
|
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.length}</p>
|
|
||||||
</div>
|
|
||||||
<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">Kg planejado</p>
|
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalKg)} kg</p>
|
|
||||||
</div>
|
|
||||||
<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">Críticos</p>
|
|
||||||
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.filter(plan => plan.priority === 'Crítico').length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`${panelClassName} overflow-hidden`}>
|
|
||||||
<div className="border-b border-dark-border p-5">
|
|
||||||
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p>
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className={`${emptyStateClassName} min-h-[220px]`}>
|
|
||||||
<Ruler className="h-8 w-8 text-brand-primary" />
|
|
||||||
<h3 className="text-base font-bold text-dark-text">Carregando planos...</h3>
|
|
||||||
</div>
|
|
||||||
) : plans.length ? (
|
|
||||||
<div className="divide-y divide-dark-border">
|
|
||||||
{plans.map(plan => (
|
|
||||||
<div key={plan.id} className="grid grid-cols-1 gap-3 p-4 md:grid-cols-[1.4fr_1fr_100px_100px_44px] md:items-center">
|
|
||||||
<div>
|
|
||||||
<p className="font-bold text-dark-text">{plan.material}</p>
|
|
||||||
<p className="text-xs font-semibold text-dark-muted">{plan.color}</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-semibold text-dark-muted">{plan.supplier}</p>
|
|
||||||
<p className="text-sm font-bold text-dark-text">{formatNumber(plan.quantityKg)} kg</p>
|
|
||||||
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-text">{plan.priority}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Remover plano"
|
|
||||||
aria-label={`Remover plano ${plan.material}`}
|
|
||||||
disabled={isBusy}
|
|
||||||
onClick={() => removePlan(plan.id)}
|
|
||||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg text-red-400 transition-colors hover:bg-red-500/10 hover:text-red-300 disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={emptyStateClassName}>
|
|
||||||
<Ruler className="h-8 w-8 text-brand-primary" />
|
|
||||||
<h3 className="text-base font-bold text-dark-text">Nenhum plano de malha cadastrado</h3>
|
|
||||||
<p className="text-sm font-semibold text-dark-muted">Cadastre o primeiro plano no formulário ao lado.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user