Make supply receipts interactive
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m54s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m54s
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||
type ReceiptView = 'new' | 'pending' | 'history';
|
||||
type FabricPlan = {
|
||||
id: string;
|
||||
material: string;
|
||||
@@ -30,6 +31,18 @@ type FabricPlan = {
|
||||
supplier: string;
|
||||
priority: string;
|
||||
};
|
||||
type SupplyReceipt = {
|
||||
id: string;
|
||||
category: string;
|
||||
product: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
supplier: string;
|
||||
invoice: string;
|
||||
notes: string;
|
||||
status: 'pending' | 'approved';
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
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';
|
||||
@@ -64,6 +77,17 @@ const loadFabricPlans = (): FabricPlan[] => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadReceipts = (): SupplyReceipt[] => {
|
||||
try {
|
||||
const rawReceipts = localStorage.getItem('nexstar_supply_receipts');
|
||||
if (!rawReceipts) return [];
|
||||
const parsed = JSON.parse(rawReceipts);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [
|
||||
{ id: 'dashboard', name: 'Dashboard', icon: Warehouse },
|
||||
{ id: 'balance', name: 'Saldo', icon: BarChart3 },
|
||||
@@ -72,7 +96,7 @@ const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarCha
|
||||
{ id: 'movements', name: 'Movimentações', icon: Repeat2 },
|
||||
];
|
||||
|
||||
const receiptCategories = [
|
||||
const receiptCategories: Array<{ name: string; icon: typeof Package }> = [
|
||||
{ name: 'Malha / Tecido', icon: Ruler },
|
||||
{ name: 'Embalagem', icon: Package },
|
||||
{ name: 'Material de Limpeza', icon: Boxes },
|
||||
@@ -205,16 +229,130 @@ const BalanceTab = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ReceiptsTab = () => (
|
||||
const ReceiptList = ({
|
||||
receipts,
|
||||
emptyTitle,
|
||||
onApprove,
|
||||
onRemove,
|
||||
}: {
|
||||
receipts: SupplyReceipt[];
|
||||
emptyTitle: string;
|
||||
onApprove: (receiptId: string) => void;
|
||||
onRemove: (receiptId: string) => void;
|
||||
}) => (
|
||||
<div className={`${panelClassName} overflow-hidden`}>
|
||||
{receipts.length ? (
|
||||
<div className="divide-y divide-dark-border">
|
||||
{receipts.map(receipt => (
|
||||
<div key={receipt.id} className="grid grid-cols-1 gap-3 p-4 lg:grid-cols-[1.2fr_120px_1fr_100px_auto] lg:items-center">
|
||||
<div>
|
||||
<p className="font-bold text-dark-text">{receipt.product}</p>
|
||||
<p className="text-xs font-semibold text-dark-muted">{receipt.category} · NF {receipt.invoice}</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-dark-text">{formatNumber(receipt.quantity)} {receipt.unit}</p>
|
||||
<p className="text-sm font-semibold text-dark-muted">{receipt.supplier}</p>
|
||||
<span className={`w-fit rounded-full border px-2.5 py-1 text-xs font-bold ${
|
||||
receipt.status === 'approved'
|
||||
? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
||||
: 'border-amber-400/30 bg-amber-400/10 text-amber-300'
|
||||
}`}>
|
||||
{receipt.status === 'approved' ? 'Aprovado' : 'Pendente'}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2 lg:justify-end">
|
||||
{receipt.status === 'pending' && (
|
||||
<button type="button" onClick={() => onApprove(receipt.id)} className={buttonClassName}>
|
||||
Aprovar
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => onRemove(receipt.id)} className="inline-flex h-10 items-center justify-center rounded-lg px-3 text-sm font-bold text-red-400 transition-colors hover:bg-red-500/10 cursor-pointer">
|
||||
Remover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={emptyStateClassName}>
|
||||
<Truck className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">{emptyTitle}</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">Registre um recebimento para preencher esta lista.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ReceiptsTab = () => {
|
||||
const [activeView, setActiveView] = useState<ReceiptView>('new');
|
||||
const [selectedCategory, setSelectedCategory] = useState(receiptCategories[0].name);
|
||||
const [receipts, setReceipts] = useState<SupplyReceipt[]>(loadReceipts);
|
||||
const [form, setForm] = useState({
|
||||
product: '',
|
||||
quantity: '',
|
||||
unit: 'kg',
|
||||
supplier: '',
|
||||
invoice: '',
|
||||
notes: '',
|
||||
});
|
||||
|
||||
const pendingReceipts = receipts.filter(receipt => receipt.status === 'pending');
|
||||
const visibleReceipts = activeView === 'pending' ? pendingReceipts : receipts;
|
||||
|
||||
const saveReceipts = (nextReceipts: SupplyReceipt[]) => {
|
||||
setReceipts(nextReceipts);
|
||||
localStorage.setItem('nexstar_supply_receipts', JSON.stringify(nextReceipts));
|
||||
};
|
||||
|
||||
const handleReceiptSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const product = form.product.trim();
|
||||
const quantity = parseDecimal(form.quantity);
|
||||
if (!product || quantity <= 0) return;
|
||||
|
||||
const nextReceipt: SupplyReceipt = {
|
||||
id: `${Date.now()}`,
|
||||
category: selectedCategory,
|
||||
product,
|
||||
quantity,
|
||||
unit: form.unit,
|
||||
supplier: form.supplier.trim() || 'Sem fornecedor',
|
||||
invoice: form.invoice.trim() || '-',
|
||||
notes: form.notes.trim(),
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
saveReceipts([nextReceipt, ...receipts]);
|
||||
setForm({ product: '', quantity: '', unit: 'kg', supplier: '', invoice: '', notes: '' });
|
||||
setActiveView('pending');
|
||||
};
|
||||
|
||||
const markApproved = (receiptId: string) => {
|
||||
saveReceipts(receipts.map(receipt => (
|
||||
receipt.id === receiptId ? { ...receipt, status: 'approved' } : receipt
|
||||
)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Novo recebimento', 'Pendentes', 'Histórico'].map((item, index) => (
|
||||
<button key={item} type="button" className={`${buttonClassName} ${index === 0 ? 'border-brand-primary bg-brand-primary text-brand-contrast' : ''}`}>
|
||||
{item}
|
||||
{[
|
||||
{ id: 'new', label: 'Novo recebimento' },
|
||||
{ id: 'pending', label: `Pendentes (${pendingReceipts.length})` },
|
||||
{ id: 'history', label: `Histórico (${receipts.length})` },
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setActiveView(item.id as ReceiptView)}
|
||||
className={`${buttonClassName} ${activeView === item.id ? 'border-brand-primary bg-brand-primary text-brand-contrast' : ''}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
|
||||
{activeView === 'new' && (
|
||||
<form onSubmit={handleReceiptSubmit} className={`${panelClassName} p-5`}>
|
||||
<h2 className="text-base font-bold text-dark-text">Registrar recebimento</h2>
|
||||
<div className="mt-4 rounded-xl border border-dark-border bg-dark-input px-4 py-3 text-sm font-semibold text-dark-muted">
|
||||
Preencha o que chegou. O financeiro vincula OC/NF e aprova o lançamento.
|
||||
@@ -222,15 +360,82 @@ const ReceiptsTab = () => (
|
||||
<p className="mt-5 text-xs font-bold uppercase tracking-widest text-dark-muted">1. Qual categoria de produto chegou?</p>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{receiptCategories.map(category => (
|
||||
<button key={category.name} type="button" className="flex min-h-24 flex-col items-center justify-center gap-3 rounded-xl border border-dark-border bg-dark-input/40 p-4 text-center font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer">
|
||||
<category.icon className="h-6 w-6 text-brand-primary" />
|
||||
<button
|
||||
key={category.name}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategory(category.name)}
|
||||
className={`flex min-h-24 flex-col items-center justify-center gap-3 rounded-xl border p-4 text-center font-bold transition-colors cursor-pointer ${
|
||||
selectedCategory === category.name
|
||||
? 'border-brand-primary bg-brand-primary/12 text-brand-primary'
|
||||
: 'border-dark-border bg-dark-input/40 text-dark-text hover:border-brand-primary'
|
||||
}`}
|
||||
>
|
||||
<category.icon className="h-6 w-6" />
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">2. Detalhes do recebimento</p>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<label className="text-xs font-bold text-dark-muted">
|
||||
Produto / material
|
||||
<input value={form.product} onChange={(event) => setForm(current => ({ ...current, product: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: Meia malha 30.1" />
|
||||
</label>
|
||||
<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>
|
||||
<label className="text-xs font-bold text-dark-muted">
|
||||
Quantidade
|
||||
<input inputMode="decimal" value={form.quantity} onChange={(event) => setForm(current => ({ ...current, quantity: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: 120" />
|
||||
</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="rolos">rolos</option>
|
||||
<option value="un.">un.</option>
|
||||
<option value="caixas">caixas</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs font-bold text-dark-muted">
|
||||
Nota fiscal
|
||||
<input value={form.invoice} onChange={(event) => setForm(current => ({ ...current, invoice: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="NF-001234" />
|
||||
</label>
|
||||
<label className="text-xs font-bold text-dark-muted md:col-span-2">
|
||||
Observações
|
||||
<textarea value={form.notes} onChange={(event) => setForm(current => ({ ...current, notes: event.target.value }))} className="mt-1 min-h-20 w-full rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Condição, divergências, lote..." />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="mt-4 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 transition-opacity hover:opacity-90 cursor-pointer">
|
||||
<Save className="h-4 w-4" />
|
||||
Registrar recebimento
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{activeView === 'pending' && (
|
||||
<ReceiptList
|
||||
receipts={visibleReceipts}
|
||||
emptyTitle="Nenhum recebimento pendente"
|
||||
onApprove={markApproved}
|
||||
onRemove={(receiptId) => saveReceipts(receipts.filter(item => item.id !== receiptId))}
|
||||
/>
|
||||
)}
|
||||
{activeView === 'history' && (
|
||||
<ReceiptList
|
||||
receipts={visibleReceipts}
|
||||
emptyTitle="Nenhum recebimento no histórico"
|
||||
onApprove={markApproved}
|
||||
onRemove={(receiptId) => saveReceipts(receipts.filter(item => item.id !== receiptId))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InventoryCountTab = () => (
|
||||
<div className="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user