Restore fabric planning in supplies
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { type FormEvent, useState } from 'react';
|
||||||
import { Link as RouterLink, Navigate, useParams } from 'react-router-dom';
|
import { Link as RouterLink, Navigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Repeat2,
|
Repeat2,
|
||||||
Ruler,
|
Ruler,
|
||||||
|
Save,
|
||||||
Search,
|
Search,
|
||||||
Scissors,
|
Scissors,
|
||||||
Truck,
|
Truck,
|
||||||
@@ -21,6 +22,14 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||||
|
type FabricPlan = {
|
||||||
|
id: string;
|
||||||
|
material: string;
|
||||||
|
color: string;
|
||||||
|
quantityKg: number;
|
||||||
|
supplier: string;
|
||||||
|
priority: string;
|
||||||
|
};
|
||||||
|
|
||||||
const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6';
|
const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6';
|
||||||
const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm';
|
const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm';
|
||||||
@@ -35,6 +44,26 @@ const stats = [
|
|||||||
{ label: 'Alertas', value: '0' },
|
{ label: 'Alertas', value: '0' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formatNumber = (value: number, maximumFractionDigits = 2) => (
|
||||||
|
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseDecimal = (value: string) => {
|
||||||
|
const number = Number(value.replace(',', '.'));
|
||||||
|
return Number.isFinite(number) ? number : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFabricPlans = (): FabricPlan[] => {
|
||||||
|
try {
|
||||||
|
const rawPlans = localStorage.getItem('nexstar_fabric_plans');
|
||||||
|
if (!rawPlans) return [];
|
||||||
|
const parsed = JSON.parse(rawPlans);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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: 'balance', name: 'Saldo', icon: BarChart3 },
|
{ id: 'balance', name: 'Saldo', icon: BarChart3 },
|
||||||
@@ -100,7 +129,8 @@ const SuppliesHub = () => (
|
|||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<ModuleCard title="Planejamento de Corte" description="Ruptura por cor e tamanho, montagem do corte e prioridade por cobertura." icon={Scissors} to="/cutting" />
|
<ModuleCard title="Planejamento de Corte" description="Ruptura por cor e tamanho, montagem do corte e prioridade por cobertura." icon={Scissors} to="/cutting" />
|
||||||
<ModuleCard title="Controle de Estoque" description="Saldo, lotes, recebimentos, inventário e movimentações." icon={Package} to="/supplies/inventory" />
|
<ModuleCard title="Controle de Estoque" description="Saldo, lotes, recebimentos, inventário e movimentações." icon={Package} to="/supplies/inventory" />
|
||||||
<ModuleCard title="Planos / Ordem de Produção" description="Ordens geradas pelo corte e acompanhamento de produção." icon={ClipboardList} to="/production-orders" />
|
<ModuleCard title="Ordens de Produção" description="Ordens geradas pelo corte e acompanhamento de produção." icon={ClipboardList} to="/production-orders" />
|
||||||
|
<ModuleCard title="Planejamento de Malha" description="Fila de matéria-prima para compra, recebimento e abastecimento do corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||||
<ModuleCard title="Necessidade de Compra" description="Itens abaixo do mínimo e necessidade projetada para compra." icon={BarChart3} to="/supplies/purchase-needs" />
|
<ModuleCard title="Necessidade de Compra" description="Itens abaixo do mínimo e necessidade projetada para compra." icon={BarChart3} to="/supplies/purchase-needs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,20 +301,130 @@ const InventoryScreen = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FabricPlanningScreen = () => (
|
const FabricPlanningScreen = () => {
|
||||||
|
const [plans, setPlans] = useState<FabricPlan[]>(loadFabricPlans);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
material: '',
|
||||||
|
color: '',
|
||||||
|
quantityKg: '',
|
||||||
|
supplier: '',
|
||||||
|
priority: 'Normal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalKg = plans.reduce((total, plan) => total + plan.quantityKg, 0);
|
||||||
|
|
||||||
|
const savePlans = (nextPlans: FabricPlan[]) => {
|
||||||
|
setPlans(nextPlans);
|
||||||
|
localStorage.setItem('nexstar_fabric_plans', JSON.stringify(nextPlans));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const material = form.material.trim();
|
||||||
|
const quantityKg = parseDecimal(form.quantityKg);
|
||||||
|
if (!material || quantityKg <= 0) return;
|
||||||
|
|
||||||
|
const nextPlan: FabricPlan = {
|
||||||
|
id: `${Date.now()}`,
|
||||||
|
material,
|
||||||
|
color: form.color.trim() || 'Todas as cores',
|
||||||
|
quantityKg,
|
||||||
|
supplier: form.supplier.trim() || 'Sem fornecedor',
|
||||||
|
priority: form.priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
savePlans([nextPlan, ...plans]);
|
||||||
|
setForm({ material: '', color: '', quantityKg: '', supplier: '', priority: 'Normal' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={pageClassName}>
|
<div className={pageClassName}>
|
||||||
<Header title="Planejamento de Malha" subtitle="Fila de matéria-prima para compra, recebimento e abastecimento do corte." backTo="/supplies" />
|
<Header title="Planejamento de Malha" subtitle="Fila de matéria-prima para compra, recebimento e abastecimento do corte." backTo="/supplies" />
|
||||||
<div className={`${panelClassName} p-5`}>
|
<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="grid grid-cols-1 gap-6 xl:grid-cols-[420px_1fr]">
|
||||||
|
<form onSubmit={handleSubmit} className={`${panelClassName} p-5`}>
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Novo plano de malha</h2>
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-3">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Malha / tecido
|
||||||
|
<input value={form.material} onChange={(event) => setForm(current => ({ ...current, material: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: Meia malha 30.1" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Cor
|
||||||
|
<input value={form.color} onChange={(event) => setForm(current => ({ ...current, color: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Todas as cores" />
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Quantidade kg
|
||||||
|
<input inputMode="decimal" value={form.quantityKg} onChange={(event) => setForm(current => ({ ...current, quantityKg: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: 180" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Prioridade
|
||||||
|
<select value={form.priority} onChange={(event) => setForm(current => ({ ...current, priority: event.target.value }))} className={`${inputClassName} mt-1`}>
|
||||||
|
<option>Normal</option>
|
||||||
|
<option>Atenção</option>
|
||||||
|
<option>Crítico</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Fornecedor
|
||||||
|
<input value={form.supplier} onChange={(event) => setForm(current => ({ ...current, supplier: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Opcional" />
|
||||||
|
</label>
|
||||||
|
<button type="submit" className="mt-2 inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-opacity hover:opacity-90 cursor-pointer">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Salvar plano
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<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>
|
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Fila de matéria-prima para compra, recebimento e abastecimento do corte.</p>
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p>
|
||||||
|
</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_auto] 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" onClick={() => savePlans(plans.filter(item => item.id !== plan.id))} className="text-sm font-bold text-red-400 transition-colors hover:text-red-300 cursor-pointer">
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className={emptyStateClassName}>
|
<div className={emptyStateClassName}>
|
||||||
<Ruler className="h-8 w-8 text-brand-primary" />
|
<Ruler className="h-8 w-8 text-brand-primary" />
|
||||||
<h3 className="text-base font-bold text-dark-text">Nenhum plano de malha cadastrado</h3>
|
<h3 className="text-base font-bold text-dark-text">Nenhum plano de malha cadastrado</h3>
|
||||||
<p className="text-sm font-semibold text-dark-muted">Quando houver planos, eles alimentarão recebimentos e corte.</p>
|
<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>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const PurchaseNeedsScreen = () => (
|
const PurchaseNeedsScreen = () => (
|
||||||
<div className={pageClassName}>
|
<div className={pageClassName}>
|
||||||
|
|||||||
Reference in New Issue
Block a user