/* Fabricação — Insumos, Receitas, Produção */ function Fabricacao({ ctx }) { const [sub, setSub] = useState("insumos"); const [editingInput, setEditingInput] = useState(null); const [editingRecipe, setEditingRecipe] = useState(null); const [producing, setProducing] = useState(null); const [purchasing, setPurchasing] = useState(false); const [payTarget, setPayTarget] = useState(null); const [expensing, setExpensing] = useState(false); const [payExpenseTarget, setPayExpenseTarget] = useState(null); const pendingPurchases = (ctx.state.inputPurchases || []).filter(p => p.paid < p.total).length; const pendingExpenses = (ctx.state.expenses || []).filter(e => e.paid < e.total).length; return ( <>

Fabricação

Insumos, receitas, produção e gastos da fábrica

{sub === "insumos" && ( <> setPurchasing(true)} /> )} {sub === "receitas" && } {sub === "producao" && } {sub === "gastos" && }
{sub === "insumos" && setPurchasing(true)} />} {sub === "receitas" && } {sub === "producao" && } {sub === "gastos" && setExpensing(true)} />} {sub === "pendencias" && } {editingInput && ( setEditingInput(null)} onSave={(v) => { if (editingInput === "new") ctx.actions.addInput(v); else ctx.actions.updateInput(editingInput.id, v); setEditingInput(null); }} /> )} {editingRecipe && ( setEditingRecipe(null)} onSave={(v) => { if (editingRecipe === "new") ctx.actions.addRecipe(v); else ctx.actions.updateRecipe(editingRecipe.id, v); setEditingRecipe(null); }} /> )} {producing && ( setProducing(null)} onConfirm={(batches, warehouseId) => { ctx.actions.recordProduction({ recipeId: producing.id, batches, warehouseId }); setProducing(null); }} /> )} {purchasing && ( setPurchasing(false)} onConfirm={(payload) => { ctx.actions.recordInputPurchase(payload); setPurchasing(false); }} /> )} {payTarget && ( setPayTarget(null)} onConfirm={(payload) => { ctx.actions.payInputPurchase({ purchaseId: payTarget.id, ...payload }); setPayTarget(null); }} /> )} {expensing && ( setExpensing(false)} onConfirm={(payload) => { ctx.actions.recordExpense(payload); setExpensing(false); }} /> )} {payExpenseTarget && ( setPayExpenseTarget(null)} onConfirm={(payload) => { ctx.actions.payExpense({ expenseId: payExpenseTarget.id, ...payload }); setPayExpenseTarget(null); }} /> )} ); } /* CTA destacado: "Comprar insumo" — mais visível que um botão neutro, mostra também pendências de compras a pagar para incentivar o reabastecimento. */ function BuyInputButton({ ctx, onBuy }) { const inputs = ctx.state.inputs || []; const critical = inputs.filter(i => i.stock < i.min).length; const pendingPurchases = (ctx.state.inputPurchases || []).filter(p => p.paid < p.total).length; const disabled = inputs.length === 0; return ( ); } function InsumosTab({ ctx, onEdit, onBuy }) { const inputs = ctx.state.inputs; const total = inputs.reduce((s, i) => s + i.stock * i.unitCost, 0); const critical = inputs.filter(i => i.stock < i.min).length; const onDelete = (i) => ctx.askConfirm({ title: "Excluir insumo?", message: `Excluir "${i.name}"?`, detail: "Receitas que usam este insumo precisarão ser ajustadas.", danger: true, onConfirm: () => ctx.actions.deleteInput(i.id), }); return ( <>
} label="Itens em estoque" value={fmtInt(inputs.length)} sub="Em diferentes unidades" /> } label="Valor estocado" value={fmtBRL(total)} sub="Custo a preços atuais" /> } label="Itens críticos" value={critical + ""} sub="abaixo do mínimo" />
0 ? : null} > {inputs.length === 0 ? (

Nenhum insumo cadastrado

Cadastre matérias-primas para criar receitas e registrar produção.

) : ( {inputs.map(i => { const ratio = i.min > 0 ? i.stock / i.min : 2; const tone = ratio < 1 ? "danger" : (ratio < 1.5 ? "warning" : "ok"); const lastPurchase = (ctx.state.inputPurchases || []) .filter(p => p.items.some(it => it.inputId === i.id)) .sort((a,b) => b.date.localeCompare(a.date))[0]; return ( ); })}
Insumo Unidade Estoque Margem Custo unit. Valor estoque Última compra Validade
{i.name} {i.unit} {fmtInt(i.stock)} {i.unit}
min. {i.min}
{fmtBRL(i.unitCost)} {fmtBRL(i.unitCost * i.stock)} {lastPurchase ? (
{fmtDate(lastPurchase.date)} {paymentChip(lastPurchase.payment)} {lastPurchase.paid < lastPurchase.total && pendente}
) : }
{i.expires ? fmtDate(i.expires) : "—"}
)}
); } function ReceitasTab({ ctx, onEdit, onProduce }) { const onDelete = (r) => ctx.askConfirm({ title: "Excluir receita?", message: `Excluir "${r.name}"?`, danger: true, onConfirm: () => ctx.actions.deleteRecipe(r.id), }); if (ctx.state.recipes.length === 0) { return (

Nenhuma receita cadastrada

{ctx.state.inputs.length === 0 ? "Cadastre insumos primeiro, depois crie receitas." : "Crie a primeira receita para começar a produzir."}

); } return (
{ctx.state.recipes.map(r => { const product = ctx.state.products.find(p => p.id === r.productId); if (!product) return null; const cost = r.items.reduce((s, it) => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); return s + (inp?.unitCost || 0) * it.qty; }, 0); const costPerUnit = r.yield > 0 ? cost / r.yield : 0; const margin = product.price > 0 ? ((product.price - costPerUnit) / product.price) * 100 : 0; return ( {product.flavor}} >
{r.items.map(it => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); const subtotal = (inp?.unitCost || 0) * it.qty; return (
{inp?.name || insumo removido} {it.qty} {inp?.unit || ""} {fmtBRL(subtotal)}
); })}
{fmtBRL(cost)}Custo da batelada
{fmtBRL(costPerUnit)}Custo por unidade
50 ? "var(--success)" : "var(--warning)" }}>{margin.toFixed(0)}%Margem bruta
); })}
); } function ProducaoTab({ ctx }) { // Build history from finance ("Produção" category) const history = ctx.state.finance.filter(f => f.cat === "Produção").map((f, i) => ({ id: f.id, date: f.date, ref: f.ref, value: f.value })); const today = new Date().toISOString().slice(0,10); const todayCost = ctx.state.finance.filter(f => f.cat === "Produção" && f.date === today).reduce((s,x) => s+x.value, 0); // Estoque de produtos fabricados (somado em todos os armazéns) const stockByProduct = ctx.state.products.map(p => { const rows = ctx.state.stock.filter(s => s.productId === p.id); return { product: p, units: rows.reduce((sm, r) => sm + r.units, 0), minUnits: rows.reduce((sm, r) => sm + r.minUnits, 0), perBox: p.boxSize || 60, }; }); const totalUnitsStock = stockByProduct.reduce((s, x) => s + x.units, 0); const criticalProducts = stockByProduct.filter(x => x.units < x.minUnits).length; return ( <>
} label="Receitas cadastradas" value={ctx.state.recipes.length+""} sub="Disponíveis para produção" /> } label="Unidades em estoque" value={fmtInt(totalUnitsStock)} sub={`${stockByProduct.filter(x => x.units > 0).length} sabor(es) com produção`} /> } label="Produções registradas" value={history.length+""} sub="Histórico completo" /> } label="Custo da produção hoje" value={fmtBRL(todayCost)} sub="Insumos consumidos" />
{stockByProduct .sort((a, b) => b.units - a.units) .map(x => { const boxes = Math.floor(x.units / x.perBox); const looseRest = x.units % x.perBox; const isCritical = x.units < x.minUnits; const isEmpty = x.units === 0; return ( ); })}
Produto Estoque (un) Caixas equivalentes Preço un. Valor estoque Status
{x.product.flavor}
{x.product.line}
{fmtInt(x.units)} {boxes} cx{looseRest > 0 ? ` + ${looseRest} un` : ""} ({x.perBox}/cx) {fmtBRL(x.product.price)} {fmtBRL(x.units * x.product.price)} {isEmpty ? esgotado : isCritical ? crítico : ok}
{history.length === 0 ? (

Nenhuma produção registrada

Use o botão "Produzir" em uma receita para registrar a primeira produção.

) : ( {history.map(h => ( ))}
Lote Data Receita / Lote Custo
{h.id} {fmtDate(h.date)} {h.ref} {fmtBRL(h.value)} concluída
)}
); } function InsumoModal({ initial, onClose, onSave }) { const [name, setName] = useState(initial?.name || ""); const [stock, setStock] = useState(initial?.stock != null ? String(initial.stock) : ""); const [unit, setUnit] = useState(initial?.unit || "kg"); const [cost, setCost] = useState(initial?.unitCost != null ? String(initial.unitCost) : ""); const [min, setMin] = useState(initial?.min != null ? String(initial.min) : ""); const [expires, setExpires] = useState(initial?.expires || ""); const [err, setErr] = useState({}); const submit = () => { const e = {}; if (!name.trim()) e.name = "Informe o nome"; if (!stock || isNaN(parseFloat(stock))) e.stock = "Quantidade inválida"; if (!cost || isNaN(parseFloat(cost))) e.cost = "Custo inválido"; setErr(e); if (Object.keys(e).length) return; onSave({ name: name.trim(), stock: parseFloat(stock), unit, unitCost: parseFloat(cost), min: parseFloat(min || 0), expires }); }; return ( } >
setName(e.target.value)} autoFocus /> setStock(e.target.value.replace(/[^\d.,]/g,"").replace(",","."))} /> setCost(e.target.value.replace(/[^\d.,]/g,"").replace(",","."))} /> setMin(e.target.value.replace(/[^\d.,]/g,"").replace(",","."))} /> setExpires(e.target.value)} />
); } function ReceitaModal({ ctx, initial, onClose, onSave }) { const [name, setName] = useState(initial?.name || ""); const [productId, setProductId] = useState(initial?.productId || ctx.state.products[0]?.id || ""); const [yld, setYld] = useState(initial?.yield != null ? String(initial.yield) : "60"); const [items, setItems] = useState(initial?.items || []); const [err, setErr] = useState({}); const addRow = () => setItems([...items, { inputId: ctx.state.inputs[0]?.id || "", qty: 1 }]); const setRow = (i, patch) => setItems(items.map((x, idx) => idx === i ? { ...x, ...patch } : x)); const delRow = (i) => setItems(items.filter((_, idx) => idx !== i)); const submit = () => { const e = {}; if (!name.trim()) e.name = "Informe o nome"; if (!productId) e.productId = "Selecione o sabor"; if (!yld || isNaN(parseInt(yld, 10))) e.yld = "Rendimento inválido"; if (items.length === 0) e.items = "Adicione ao menos um insumo"; setErr(e); if (Object.keys(e).length) return; onSave({ name: name.trim(), productId, yield: parseInt(yld, 10), items }); }; return ( } >
setName(e.target.value)} placeholder="Ex.: Picolé de Morango — base 60u" autoFocus /> setYld(e.target.value.replace(/\D/g,""))} />
} onClick={addRow}>Adicionar insumo} /> {items.length === 0 ? (

Nenhum insumo. Clique em "Adicionar insumo".

) : (
{items.map((row, i) => { const inp = ctx.state.inputs.find(x => x.id === row.inputId); return (
setRow(i, { qty: parseFloat(e.target.value.replace(",", ".")) || 0 })} style={{ width:100 }} /> {inp?.unit || ""} {fmtBRL((inp?.unitCost || 0) * row.qty)}
); })}
)}
); } function ProduzirModal({ ctx, recipe, onClose, onConfirm }) { const [batches, setBatches] = useState(1); const [warehouseId, setWarehouseId] = useState(ctx.state.warehouses[0]?.id || WH_DISTRIB_ID); const product = ctx.state.products.find(p => p.id === recipe.productId); if (!product) return null; const totalCost = recipe.items.reduce((s, it) => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); return s + (inp?.unitCost || 0) * it.qty; }, 0) * batches; const units = recipe.yield * batches; const insufficient = recipe.items.some(it => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); return !inp || inp.stock < it.qty * batches; }); return ( Ao confirmar, os insumos serão abatidos do estoque automaticamente.
} >
{product.line}
Bateladas

Unidades produzidas{units}
Custo total estimado{fmtBRL(totalCost)}
Custo por unidade{fmtBRL(totalCost / Math.max(1, units))}
Receita estimada{fmtBRL(product.price * units)}
{recipe.items.map(it => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); if (!inp) return (
Insumo não encontrado · receita precisa ser ajustada
); const need = it.qty * batches; const ok = inp.stock >= need; return (
{inp.name}
Estoque: {inp.stock} {inp.unit}
−{need} {inp.unit}
{!ok &&
Estoque insuficiente
} {ok &&
Sobrará {(inp.stock - need).toFixed(2)} {inp.unit}
}
); })}
{insufficient && (
Estoque insuficiente para um ou mais insumos. Reduza as bateladas ou reabasteça antes de produzir.
)}
); } function FlavorTagInline({ product }) { return ( {product.flavor} ); } window.Fabricacao = Fabricacao; /* ============ Pendências de compras (fiado / parcial) ============ */ function PendenciasInsumos({ ctx, onPay, onPayExpense }) { const pendingPurchases = (ctx.state.inputPurchases || []) .filter(p => p.paid < p.total) .sort((a,b) => a.date.localeCompare(b.date)); const pendingExpenses = (ctx.state.expenses || []) .filter(e => e.paid < e.total) .sort((a,b) => a.date.localeCompare(b.date)); const totalPendingPurch = pendingPurchases.reduce((s, p) => s + (p.total - p.paid), 0); const totalPendingExp = pendingExpenses.reduce((s, e) => s + (e.total - e.paid), 0); const totalPending = totalPendingPurch + totalPendingExp; const totalPaid = pendingPurchases.reduce((s, p) => s + p.paid, 0) + pendingExpenses.reduce((s, e) => s + e.paid, 0); const totalCount = pendingPurchases.length + pendingExpenses.length; return ( <>
} label="A pagar (total)" value={fmtBRL(totalPending)} sub={`${totalCount} pendência(s)`} /> } label="Já pago (parciais)" value={fmtBRL(totalPaid)} sub="De compras/gastos em aberto" /> } label="Volume com pendência" value={fmtBRL(totalPending + totalPaid)} sub="Valor original" />
{totalCount === 0 ? (

Nenhuma pendência

Todas as compras e gastos estão quitados.

) : ( <> {pendingPurchases.length > 0 && ( 0 ? 14 : 0 }} > {pendingPurchases.map(p => { const pendingVal = p.total - p.paid; const pct = (p.paid / p.total) * 100; return ( ); })}
Fornecedor Data Itens Forma original Total Pago Pendente Progresso
{p.supplier} {fmtDate(p.date)} {p.items.map(it => { const inp = ctx.state.inputs.find(x => x.id === it.inputId); return `${inp?.name||"?"} ${it.qty}${inp?.unit||""}`; }).join(" · ")} {paymentChip(p.payment)} {fmtBRL(p.total)} 0 ? "var(--success)" : "var(--ink-400)" }}>{p.paid > 0 ? fmtBRL(p.paid) : "—"} {fmtBRL(pendingVal)}
= 100 ? "ok" : pct >= 50 ? "warn" : "danger"} /> {Math.round(pct)}%
)} {pendingExpenses.length > 0 && ( {pendingExpenses.map(e => { const pendingVal = e.total - e.paid; const pct = (e.paid / e.total) * 100; return ( ); })}
Gasto Categoria Data Forma original Total Pago Pendente Progresso
{e.name} {e.qty ?
{e.qty} {e.unit}
: null}
{e.category||"Outros"} {fmtDate(e.date)} {paymentChip(e.payment)} {fmtBRL(e.total)} 0 ? "var(--success)" : "var(--ink-400)" }}>{e.paid > 0 ? fmtBRL(e.paid) : "—"} {fmtBRL(pendingVal)}
= 100 ? "ok" : pct >= 50 ? "warn" : "danger"} /> {Math.round(pct)}%
)} )} ); } /* Visual chip por forma de pagamento (com "Fiado" destacado) */ function paymentChip(payment) { if (payment === "Fiado") return Fiado; if (payment === "PIX") return PIX; if (payment === "Dinheiro") return Dinheiro; if ((payment||"").startsWith("Cartão")) return {payment}; if ((payment||"").startsWith("Boleto") || (payment||"").startsWith("Prazo")) return {payment}; return {payment || "—"}; } /* ============ Modal: Nova compra de insumo ============ */ const PURCHASE_METHODS = ["PIX","Dinheiro","Cartão","Transferência","Boleto 7d","Boleto 14d","Prazo 30d","Fiado"]; function ComprarInsumoModal({ ctx, onClose, onConfirm }) { const [supplier, setSupplier] = useState(""); const [date, setDate] = useState(new Date().toISOString().slice(0,10)); const [items, setItems] = useState([{ inputId: ctx.state.inputs[0]?.id || "", qty: 1, unitCost: ctx.state.inputs[0]?.unitCost || 0 }]); const [payment, setPayment] = useState("PIX"); const [partialPaid, setPartialPaid] = useState(""); const [note, setNote] = useState(""); const [err, setErr] = useState({}); const total = items.reduce((s, it) => s + (parseFloat(it.qty)||0) * (parseFloat(it.unitCost)||0), 0); const addRow = () => setItems(prev => [...prev, { inputId: ctx.state.inputs[0]?.id || "", qty: 1, unitCost: ctx.state.inputs[0]?.unitCost || 0 }]); const setRow = (i, patch) => setItems(prev => prev.map((x, idx) => idx === i ? { ...x, ...patch } : x)); const delRow = (i) => setItems(prev => prev.filter((_, idx) => idx !== i)); /* Pré-preenche custo unit. ao trocar o insumo */ const setInput = (i, inputId) => { const inp = ctx.state.inputs.find(x => x.id === inputId); setRow(i, { inputId, unitCost: inp?.unitCost || 0 }); }; const isFiado = payment === "Fiado"; const isPartial = !isFiado && partialPaid !== "" && parseFloat(partialPaid) < total; const paidValue = isFiado ? (partialPaid ? parseFloat(partialPaid) : 0) : (partialPaid !== "" ? parseFloat(partialPaid) : total); const submit = () => { const e = {}; if (!supplier.trim()) e.supplier = "Informe o fornecedor"; if (items.length === 0) e.items = "Adicione ao menos um item"; if (items.some(it => !it.inputId || !it.qty || it.qty <= 0)) e.items = "Confira quantidades dos itens"; if (total <= 0) e.items = "Total inválido"; if (paidValue < 0 || paidValue > total + 0.001) e.paid = "Valor pago inválido"; setErr(e); if (Object.keys(e).length) return; onConfirm({ supplier: supplier.trim(), date, items: items.map(it => ({ inputId: it.inputId, qty: parseFloat(it.qty), unitCost: parseFloat(it.unitCost) })), payment, paid: paidValue, note: note.trim(), }); }; return ( } >
setSupplier(e.target.value)} placeholder="Ex.: Distribuidora Frutas S/A" autoFocus /> setDate(e.target.value)} />
} onClick={addRow}>Adicionar item} /> {items.length === 0 ? (

Nenhum item. Clique em "Adicionar item".

) : (
InsumoQtd.Custo unit. (R$)Subtotal
{items.map((row, i) => { const inp = ctx.state.inputs.find(x => x.id === row.inputId); const subtotal = (parseFloat(row.qty)||0) * (parseFloat(row.unitCost)||0); return (
setRow(i, { qty: e.target.value.replace(/[^\d.,]/g,"").replace(",", ".") })} style={{ textAlign:"right", padding:"6px 8px" }} /> {inp?.unit||""}
setRow(i, { unitCost: e.target.value.replace(/[^\d.,]/g,"").replace(",", ".") })} style={{ textAlign:"right", padding:"6px 8px" }} /> {fmtBRL(subtotal)}
); })}
Total da compra {fmtBRL(total)}
)}
{PURCHASE_METHODS.map(m => { const on = payment === m; const isFiadoOpt = m === "Fiado"; return ( ); })}
setPartialPaid(e.target.value.replace(/[^\d.,]/g,"").replace(",", "."))} placeholder={isFiado ? "0,00" : fmtBRL(total)} /> setNote(e.target.value)} placeholder="NF, lote, recebimento..." />
{isFiado ? <>Compra fiada: insumos entram no estoque, mas nada entra em Movimentações até que pagamentos sejam registrados. Saldo: {fmtBRL(total - paidValue)}. : isPartial ? <>Pagamento parcial: {fmtBRL(paidValue)} entra em Movimentações agora. Saldo de {fmtBRL(total - paidValue)} fica em Pendências. : <>Pagamento integral: o valor total ({fmtBRL(total)}) entra em Movimentações como saída confirmada. }
); } /* ============ Modal: Pagar compra fiada ============ */ function PayPurchaseModal({ purchase, methods, onClose, onConfirm }) { const pendingVal = purchase.total - purchase.paid; const [amount, setAmount] = useState(pendingVal); const [method, setMethod] = useState(methods?.[0] || "PIX"); const [date, setDate] = useState(new Date().toISOString().slice(0,10)); const [err, setErr] = useState(""); const submit = () => { const v = parseFloat(String(amount).replace(",", ".")); if (!v || v <= 0) { setErr("Valor inválido"); return; } if (v > pendingVal + 0.001) { setErr(`Valor maior que o saldo pendente (${fmtBRL(pendingVal)})`); return; } onConfirm({ amount: v, method, date }); }; const quickFills = [ { label: "100% (quitar)", value: pendingVal }, { label: "50%", value: Math.round(pendingVal * 0.5 * 100) / 100 }, { label: "25%", value: Math.round(pendingVal * 0.25 * 100) / 100 }, ]; return ( } >
Total da compra
{fmtBRL(purchase.total)}
Já pago
{fmtBRL(purchase.paid)}
Saldo pendente
{fmtBRL(pendingVal)}
{ setErr(""); setAmount(e.target.value.replace(/[^\d.,]/g,"").replace(",", ".")); }} autoFocus style={{ fontSize:20, fontWeight:700, padding:"10px 14px" }} />
{quickFills.map(q => ( ))}
setDate(e.target.value)} />
); } /* ============ Gastos extras ============ */ const EXPENSE_CATEGORIES = ["Embalagem", "Limpeza", "Manutenção", "Combustível", "Energia / Água", "Aluguel", "Marketing", "Equipamentos", "Outros"]; const EXPENSE_UNITS = ["un", "kg", "g", "L", "ml", "m", "pacote", "caixa", "rolo", "fardo", "saco", "hora", "diária", "serviço"]; function GastosTab({ ctx, onNewExpense }) { const expenses = (ctx.state.expenses || []).slice().sort((a,b) => (b.date+b.time).localeCompare(a.date+a.time)); const totalPaid = expenses.reduce((s, e) => s + e.paid, 0); const totalAll = expenses.reduce((s, e) => s + e.total, 0); const totalPending = totalAll - totalPaid; /* Agrupado por categoria — visual quick-stat */ const byCategory = {}; expenses.forEach(e => { if (!byCategory[e.category]) byCategory[e.category] = { count: 0, total: 0 }; byCategory[e.category].count++; byCategory[e.category].total += e.total; }); return ( <>
} label="Gastos lançados" value={expenses.length+""} sub="Embalagens, manutenção, etc." /> } label="Total pago" value={fmtBRL(totalPaid)} sub={`${fmtBRL(totalAll)} lançado`} /> } label="A pagar" value={fmtBRL(totalPending)} sub="Em pendências" /> } label="Categorias usadas" value={Object.keys(byCategory).length+""} sub={Object.keys(byCategory).slice(0,2).join(", ") || "—"} />
} onClick={onNewExpense}>Novo gasto} > {expenses.length === 0 ? (

Nenhum gasto registrado

Use "Novo gasto" para registrar despesas como embalagens, materiais de limpeza, manutenção, etc. — com forma de pagamento e opção de fiado.

) : ( {expenses.map(e => ( ))}
Gasto Categoria Qtd. Data Pagamento Total Pago Status
{e.name}
{e.note &&
{e.note}
}
{e.category || "Outros"} {e.qty ? `${e.qty} ${e.unit||"un"}` : "—"} {fmtDate(e.date)} {e.time &&
{e.time}
}
{paymentChip(e.payment)} {fmtBRL(e.total)} = e.total ? "var(--success)" : (e.paid > 0 ? "var(--info)" : "var(--ink-400)") }}>{e.paid > 0 ? fmtBRL(e.paid) : "—"} {e.status === "confirmado" ? quitado : e.status === "parcial" ? parcial : a pagar}
)}
); } function NovoGastoModal({ onClose, onConfirm }) { const [name, setName] = useState(""); const [category, setCategory] = useState(EXPENSE_CATEGORIES[0]); const [qty, setQty] = useState("1"); const [unit, setUnit] = useState("un"); const [unitMode, setUnitMode] = useState("preset"); // "preset" | "custom" const [customUnit, setCustomUnit] = useState(""); const [total, setTotal] = useState(""); const [date, setDate] = useState(new Date().toISOString().slice(0,10)); const [payment, setPayment] = useState("PIX"); const [partialPaid, setPartialPaid] = useState(""); const [note, setNote] = useState(""); const [err, setErr] = useState({}); const finalUnit = unitMode === "custom" ? (customUnit || "un") : unit; const totalNum = parseFloat(String(total).replace(/[^\d.,]/g, "").replace(",", ".")) || 0; const isFiado = payment === "Fiado"; const isPartial = !isFiado && partialPaid !== "" && parseFloat(partialPaid) < totalNum; const paidValue = isFiado ? (partialPaid ? parseFloat(partialPaid) : 0) : (partialPaid !== "" ? parseFloat(partialPaid) : totalNum); const submit = () => { const e = {}; if (!name.trim()) e.name = "Informe o nome do gasto"; if (totalNum <= 0) e.total = "Total inválido"; if (paidValue < 0 || paidValue > totalNum + 0.001) e.paid = "Valor pago inválido"; setErr(e); if (Object.keys(e).length) return; onConfirm({ name: name.trim(), category, qty: parseFloat(qty) || 0, unit: finalUnit, total: totalNum, date, payment, paid: paidValue, note: note.trim(), }); }; return ( } >
setName(e.target.value)} placeholder="Ex.: Embalagem para picolé, Tinta para impressora, Conta de água..." autoFocus /> setDate(e.target.value)} />
setQty(e.target.value.replace(/[^\d.,]/g,"").replace(",","."))} placeholder="Ex.: 100" />
{unitMode === "preset" ? (
{EXPENSE_UNITS.map(u => ( ))}
) : ( setCustomUnit(e.target.value)} placeholder="Ex.: cartela, conjunto, kit..." /> )}
setTotal(e.target.value.replace(/[^\d.,]/g,""))} placeholder="0,00" style={{ fontSize:20, fontWeight:700, padding:"10px 14px" }} />
{PURCHASE_METHODS.map(m => { const on = payment === m; const isFiadoOpt = m === "Fiado"; return ( ); })}
setPartialPaid(e.target.value.replace(/[^\d.,]/g,"").replace(",","."))} placeholder={isFiado ? "0,00" : fmtBRL(totalNum)} /> setNote(e.target.value)} placeholder="NF, fornecedor, motivo..." />
{isFiado ? <>Gasto fiado: nada entra em Movimentações até registrar pagamentos. Saldo: {fmtBRL(totalNum - paidValue)}. : isPartial ? <>Pagamento parcial: {fmtBRL(paidValue)} entra em Movimentações agora. Saldo {fmtBRL(totalNum - paidValue)} em Pendências. : <>Pagamento integral: o valor total ({fmtBRL(totalNum)}) entra em Movimentações como saída confirmada. }
); } function PayExpenseModal({ expense, methods, onClose, onConfirm }) { const pendingVal = expense.total - expense.paid; const [amount, setAmount] = useState(pendingVal); const [method, setMethod] = useState(methods?.[0] || "PIX"); const [date, setDate] = useState(new Date().toISOString().slice(0,10)); const [err, setErr] = useState(""); const submit = () => { const v = parseFloat(String(amount).replace(",", ".")); if (!v || v <= 0) { setErr("Valor inválido"); return; } if (v > pendingVal + 0.001) { setErr(`Valor maior que o saldo pendente (${fmtBRL(pendingVal)})`); return; } onConfirm({ amount: v, method, date }); }; const quickFills = [ { label: "100% (quitar)", value: pendingVal }, { label: "50%", value: Math.round(pendingVal * 0.5 * 100) / 100 }, { label: "25%", value: Math.round(pendingVal * 0.25 * 100) / 100 }, ]; return ( } >
Total
{fmtBRL(expense.total)}
Já pago
{fmtBRL(expense.paid)}
Saldo
{fmtBRL(pendingVal)}
{ setErr(""); setAmount(e.target.value.replace(/[^\d.,]/g,"").replace(",","."));}} autoFocus style={{ fontSize:20, fontWeight:700, padding:"10px 14px" }} />
{quickFills.map(q => ( ))}
setDate(e.target.value)} />
); }