Add supply purchase order workflow
This commit is contained in:
@@ -31,11 +31,11 @@ import PaginationControls from '../components/PaginationControls';
|
||||
import ProductTypeBadge from '../components/ProductTypeBadge';
|
||||
import StockGraphsBalanceModal from '../components/StockGraphsBalanceModal';
|
||||
import { classifyCutFamily } from '../analytics/cutting';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplySummary, fetchSupplySuppliers, isSuperAdmin, saveSupplyMaterialDetails } from '../dataService';
|
||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyPurchaseOrder, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, downloadDatabaseDiagnostic, fetchCuttingSettings, fetchProductAnalytics, fetchProductionOrders, fetchStock, fetchSupplyPurchaseOrders, fetchSupplySummary, fetchSupplySuppliers, isSuperAdmin, saveSupplyMaterialDetails, updateSupplyPurchaseOrderStatus } from '../dataService';
|
||||
import { parseProductName } from '../productParsing';
|
||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||
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, SupplyPurchaseOrder, SupplyPurchaseOrderStatus, SupplyReceipt, SupplySummary } from '../types';
|
||||
|
||||
type InventoryTab = 'dashboard' | 'stock' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||
type ReceiptView = 'new' | 'pending' | 'history';
|
||||
@@ -411,6 +411,7 @@ const SuppliesHub = () => {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-4">
|
||||
<ModuleCard title="Estoque e recebimentos" description="Saldo, lotes, recebimentos, inventário e movimentações de materiais." icon={Package} to="/supplies/inventory" meta={`${formatNumber(summary.stats.pendingReceipts, 0)} recebimentos pendentes · ${formatNumber(summary.stats.activeLots, 0)} lotes ativos`} />
|
||||
<ModuleCard title="Ordens de compra" description="Formalize pedidos de materiais, fornecedor, prazo e valor antes do recebimento." icon={ClipboardList} to="/supplies/purchase-orders" meta="Criar e acompanhar OCs" />
|
||||
<ModuleCard title="Planejamento de malha" description="Organize a fila de matéria-prima para abastecer compras e corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||
<ModuleCard title="Prioridades de estoque" description="Analise reposição, cobertura e itens que precisam de revisão de cadastro." icon={PackageSearch} to="/supplies/current-planning" />
|
||||
<ModuleCard title="Dados para revisar" description="Consulte exceções de estoque, composição e cadastro fora do cálculo atual." icon={ClipboardList} to="/planning-issues" meta="Abrir pendências" />
|
||||
@@ -2656,6 +2657,30 @@ const PurchaseNeedsScreen = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const PurchaseOrdersScreen = () => {
|
||||
const [orders, setOrders] = useState<SupplyPurchaseOrder[]>([]);
|
||||
const [suppliers, setSuppliers] = useState<string[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [form, setForm] = useState({ supplier: '', material: '', quantity: '', unit: 'kg', unitCost: '', expectedDate: '', notes: '' });
|
||||
const load = async () => {
|
||||
const [nextOrders, nextSuppliers] = await Promise.all([fetchSupplyPurchaseOrders(), fetchSupplySuppliers()]);
|
||||
setOrders(nextOrders); setSuppliers(nextSuppliers);
|
||||
};
|
||||
useEffect(() => { void load().catch(error => setMessage(error instanceof Error ? error.message : 'Não foi possível carregar as OCs.')); }, []);
|
||||
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); setBusy(true); setMessage('');
|
||||
try {
|
||||
await createSupplyPurchaseOrder({ supplier: form.supplier, expectedDate: form.expectedDate || null, notes: form.notes, items: [{ material: form.material, quantity: parseDecimal(form.quantity), unit: form.unit, unitCost: form.unitCost ? parseDecimal(form.unitCost) : null }] });
|
||||
setForm({ supplier: '', material: '', quantity: '', unit: 'kg', unitCost: '', expectedDate: '', notes: '' }); await load(); setMessage('Ordem de compra criada. Envie ao fornecedor e atualize o status conforme o andamento.');
|
||||
} catch (error) { setMessage(error instanceof Error ? error.message : 'Não foi possível criar a OC.'); } finally { setBusy(false); }
|
||||
};
|
||||
const changeStatus = async (id: number, status: SupplyPurchaseOrderStatus) => { setBusy(true); try { await updateSupplyPurchaseOrderStatus(id, status); await load(); } catch (error) { setMessage(error instanceof Error ? error.message : 'Não foi possível atualizar a OC.'); } finally { setBusy(false); } };
|
||||
return <div className={pageClassName}><Header title="Ordens de Compra" subtitle="Formalize compras a partir das necessidades de material e acompanhe o compromisso com o fornecedor." backTo="/supplies" />
|
||||
{message && <div className="rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-bold text-dark-muted">{message}</div>}
|
||||
<div className="grid grid-cols-1 gap-5 xl:grid-cols-[390px_minmax(0,1fr)]"><form onSubmit={submit} className={`${panelClassName} p-5`}><h2 className="text-base font-bold text-dark-text">Nova ordem de compra</h2><p className="mt-1 text-sm font-semibold text-dark-muted">Uma OC começa com um material. Use uma OC por fornecedor para manter o recebimento rastreável.</p><div className="mt-5 space-y-3"><label className="block text-xs font-bold text-dark-muted">Fornecedor<input required list="purchase-order-suppliers" value={form.supplier} onChange={event => setForm(current => ({ ...current, supplier: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Selecione ou informe" /></label><datalist id="purchase-order-suppliers">{suppliers.map(supplier => <option key={supplier} value={supplier} />)}</datalist><label className="block text-xs font-bold text-dark-muted">Material<input required value={form.material} onChange={event => setForm(current => ({ ...current, material: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Ex.: Malha 30.1 preta" /></label><div className="grid grid-cols-2 gap-3"><label className="text-xs font-bold text-dark-muted">Quantidade<input required inputMode="decimal" value={form.quantity} onChange={event => setForm(current => ({ ...current, quantity: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="0" /></label><label className="text-xs font-bold text-dark-muted">Unidade<select value={form.unit} onChange={event => setForm(current => ({ ...current, unit: event.target.value }))} className={`${inputClassName} mt-1`}><option value="kg">kg</option><option value="un.">un.</option><option value="rolos">rolos</option></select></label></div><div className="grid grid-cols-2 gap-3"><label className="text-xs font-bold text-dark-muted">Custo unitário<input inputMode="decimal" value={form.unitCost} onChange={event => setForm(current => ({ ...current, unitCost: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Opcional" /></label><label className="text-xs font-bold text-dark-muted">Previsão<input type="date" value={form.expectedDate} onChange={event => setForm(current => ({ ...current, expectedDate: event.target.value }))} className={`${inputClassName} mt-1`} /></label></div><label className="block text-xs font-bold text-dark-muted">Observações<textarea value={form.notes} onChange={event => setForm(current => ({ ...current, notes: event.target.value }))} className={`${inputClassName} mt-1 min-h-20`} placeholder="Condição, cor, lote ou referência do fornecedor" /></label><button disabled={busy} className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast disabled:opacity-60 cursor-pointer"><ClipboardList className="h-4 w-4" />Criar OC</button></div></form><div className={panelClassName}><div className="border-b border-dark-border p-5"><h2 className="text-base font-bold text-dark-text">OCs em andamento</h2><p className="mt-1 text-sm font-semibold text-dark-muted">A aprovação financeira e o recebimento físico continuam sendo controlados em Estoque e recebimentos.</p></div>{orders.length ? <div className="divide-y divide-dark-border">{orders.map(order => <div key={order.id} className="p-5"><div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><div><p className="font-mono text-xs font-bold text-brand-primary">{order.number}</p><h3 className="mt-1 text-sm font-bold text-dark-text">{order.supplier}</h3><p className="mt-1 text-xs font-semibold text-dark-muted">{order.items.map(item => `${item.material} · ${formatNumber(item.quantity)} ${item.unit}`).join(' | ')}</p></div><select value={order.status} disabled={busy} onChange={event => void changeStatus(order.id, event.target.value as SupplyPurchaseOrderStatus)} className={`${inputClassName} h-9 w-auto text-xs`}><option value="draft">Rascunho</option><option value="sent">Enviada</option><option value="approved">Aprovada</option><option value="received">Recebida</option><option value="canceled">Cancelada</option></select></div><div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-xs font-semibold text-dark-muted"><span>Previsão: {order.expectedDate || '-'}</span><span>Total estimado: R$ {formatNumber(order.total, 2)}</span>{order.notes && <span>{order.notes}</span>}</div></div>)}</div> : <div className={emptyStateClassName}><ClipboardList className="h-8 w-8 text-brand-primary" /><h3 className="text-base font-bold text-dark-text">Nenhuma OC criada</h3><p className="text-sm font-semibold text-dark-muted">Use a necessidade de compra para decidir o que pedir e registre a OC aqui.</p></div>}</div></div></div>;
|
||||
};
|
||||
|
||||
const Supplies = () => {
|
||||
const { section } = useParams<{ section?: string }>();
|
||||
|
||||
@@ -2664,6 +2689,7 @@ const Supplies = () => {
|
||||
if (section === 'inventory') return <InventoryScreen />;
|
||||
if (section === 'fabric-planning') return <FabricPlanningScreen />;
|
||||
if (section === 'purchase-needs') return <PurchaseNeedsScreen />;
|
||||
if (section === 'purchase-orders') return <PurchaseOrdersScreen />;
|
||||
|
||||
return <Navigate to="/supplies" replace />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user