/* ============================================================ AGENDA — Financeiro, Tarefas, Histórico ============================================================ */ /* ---------------- FINANCEIRO ---------------- */ function FinanceScreen(ctx) { const { terms, transactions, services, clients, suppliers = [], businessType, addClient, addSupplier, addTransaction, updateTransaction, payTransaction, deleteTransaction, toast } = ctx; const DB = window.DB; const [range, setRange] = React.useState('mes'); const [periodDate, setPeriodDate] = React.useState(() => new Date(DB.today)); const [filter, setFilter] = React.useState('Todas'); const [search, setSearch] = React.useState(''); const [typeFilter, setTypeFilter] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState(''); const [clientFilter, setClientFilter] = React.useState(''); const [supplierFilter, setSupplierFilter] = React.useState(''); const [adding, setAdding] = React.useState(false); const [editing, setEditing] = React.useState(null); const [viewing, setViewing] = React.useState(null); const [receiving, setReceiving] = React.useState(null); const cleanMethod = (method) => String(method || 'Pix').replace(/\s+\d+\/\d+$/, '').trim() || 'Pix'; const txPaymentPlan = (t) => Array.isArray(t.paymentPlan) ? t.paymentPlan .map((p) => ({ method: cleanMethod(p.method), value: Math.abs(Number(p.value) || 0), installments: Math.max(1, Number(p.installments) || 1) })) .filter((p) => p.value > 0) : []; const paymentSummary = (t) => { if (t.status === 'pendente') return 'A definir'; const plan = txPaymentPlan(t); if (plan.length > 0) { return plan.map((p) => p.method + ' ' + DB.money(p.value) + (p.installments > 1 ? ' (' + p.installments + 'x)' : '')).join(' + '); } return t.method || 'A definir'; }; const supplierName = (id) => { const s = suppliers.find((x) => String(x.id) === String(id)); return s ? s.name : ''; }; const clientName = (id) => { const c = clients.find((x) => String(x.id) === String(id)); return c ? c.name : ''; }; const norm = (v) => String(v || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); const saveTx = async (data) => { try { await addTransaction(data); toast('Lançamento registrado!'); setAdding(false); } catch (e) { toast(e.message, 'x'); } }; const saveEditTx = async (data) => { if (!editing || !updateTransaction) return; try { await updateTransaction(editing.id, data); toast('Lançamento atualizado!'); setEditing(null); setViewing(null); } catch (e) { toast(e.message, 'x'); } }; const saveReceive = async (data) => { if (!receiving) return; await payTransaction(receiving.id, data); setReceiving(null); }; const cloneDate = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()); const startOfWeek = (d) => { const out = cloneDate(d); const wd = (out.getDay() + 6) % 7; out.setDate(out.getDate() - wd); return out; }; const addPeriod = (dir) => { setPeriodDate((prev) => { const d = cloneDate(prev); if (range === 'semana') d.setDate(d.getDate() + (dir * 7)); else if (range === 'mes') return new Date(d.getFullYear(), d.getMonth() + dir, 1); else return new Date(d.getFullYear() + dir, d.getMonth(), 1); return d; }); }; const periodBounds = (() => { const base = cloneDate(periodDate); if (range === 'semana') { const start = startOfWeek(base); const end = new Date(start); end.setDate(start.getDate() + 6); return { start, end }; } if (range === 'mes') { return { start: new Date(base.getFullYear(), base.getMonth(), 1), end: new Date(base.getFullYear(), base.getMonth() + 1, 0) }; } return { start: new Date(base.getFullYear(), 0, 1), end: new Date(base.getFullYear(), 11, 31) }; })(); const periodTitle = (() => { if (range === 'semana') { const a = periodBounds.start.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }); const b = periodBounds.end.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' }); return a + ' a ' + b; } if (range === 'mes') { const label = periodBounds.start.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }); return label.charAt(0).toUpperCase() + label.slice(1); } return String(periodBounds.start.getFullYear()); })(); // recorte por período const inRange = (t) => { const d = new Date(t.date + 'T00:00'); return d >= periodBounds.start && d <= periodBounds.end; }; const txs = transactions.filter(inRange); const paidTxs = txs.filter(t => t.status === 'pago'); const receitas = paidTxs.filter(t => t.value > 0).reduce((s, t) => s + t.value, 0); const despesas = paidTxs.filter(t => t.value < 0).reduce((s, t) => s + Math.abs(t.value), 0); const saldo = receitas - despesas; const pendentes = txs.filter(t => t.status === 'pendente' && t.value > 0); const pendente = pendentes.reduce((s, t) => s + t.value, 0); const aPagar = txs.filter(t => t.status === 'pendente' && t.value < 0).reduce((s, t) => s + Math.abs(t.value), 0); const searchText = norm(search); const filtered = txs.filter((t) => { if (filter !== 'Todas') { if (filter === 'Receitas' && t.value <= 0) return false; if (filter === 'Despesas' && t.value >= 0) return false; if (filter === 'Pendentes' && t.status !== 'pendente') return false; } if (typeFilter && t.cat !== typeFilter) return false; if (statusFilter && t.status !== statusFilter) return false; if (clientFilter && String(t.clientId || '') !== String(clientFilter)) return false; if (supplierFilter && String(t.supplierId || '') !== String(supplierFilter)) return false; if (searchText) { const hay = norm([ t.desc, t.cat, t.status, t.tag, paymentSummary(t), clientName(t.clientId), supplierName(t.supplierId), ].filter(Boolean).join(' ')); if (!hay.includes(searchText)) return false; } return true; }); // receita recebida no periodo selecionado const paidIncomeOn = (start, end = start) => transactions .filter((t) => { const d = new Date(t.date + 'T00:00'); return t.status === 'pago' && t.value > 0 && d >= start && d <= end; }) .reduce((s, t) => s + t.value, 0); const chart = (() => { if (range === 'semana') { const days = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab', 'Dom']; return days.map((lbl, i) => { const d = new Date(periodBounds.start); d.setDate(periodBounds.start.getDate() + i); return [lbl, paidIncomeOn(d)]; }); } if (range === 'mes') { const points = []; for (let start = new Date(periodBounds.start); start <= periodBounds.end; start.setDate(start.getDate() + 7)) { const end = new Date(start); end.setDate(start.getDate() + 6); if (end > periodBounds.end) end.setTime(periodBounds.end.getTime()); points.push([String(start.getDate()).padStart(2, '0') + '-' + String(end.getDate()).padStart(2, '0'), paidIncomeOn(start, end)]); } return points; } return Array.from({ length: 12 }, (_, i) => { const start = new Date(periodBounds.start.getFullYear(), i, 1); const end = new Date(periodBounds.start.getFullYear(), i + 1, 0); const label = start.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', ''); return [label.charAt(0).toUpperCase() + label.slice(1), paidIncomeOn(start, end)]; }); })(); const maxC = Math.max(1, ...chart.map(c => c[1])); const chartValueLabel = (v) => { if (v <= 0) return ''; if (v < 1000) return DB.money0(v); if (v < 1000000) { const mil = v / 1000; return 'R$ ' + mil.toLocaleString('pt-BR', { maximumFractionDigits: mil >= 10 ? 0 : 1 }) + ' mil'; } const mi = v / 1000000; return 'R$ ' + mi.toLocaleString('pt-BR', { maximumFractionDigits: mi >= 10 ? 0 : 1 }) + ' mi'; }; // formas de pagamento (% sobre receitas do período) const recTx = paidTxs.filter(t => t.value > 0); const totalRec = recTx.reduce((s, t) => s + t.value, 0) || 1; const methodColors = ['var(--ev-teal)', 'var(--ev-blue)', 'var(--ev-amber)', 'var(--ev-cyan)', 'var(--ev-rose)', 'var(--ev-green)']; const methodParts = recTx.map((t) => ({ method: cleanMethod(t.method), value: Math.abs(Number(t.value) || 0) })); const methods = [...new Set(methodParts.map(p => p.method))] .map(m => [m, Math.round(methodParts.filter(p => p.method === m).reduce((s, p) => s + p.value, 0) / totalRec * 100)]) .sort((a, b) => b[1] - a[1]).slice(0, 6); const rangeLabel = periodTitle; const chartTitle = range === 'semana' ? 'Receita por dia' : range === 'mes' ? 'Receita por semana' : 'Receita por mês'; const cards = [ { lbl: 'Receitas', val: DB.money(receitas), color: 'var(--success)', bg: 'var(--success-soft)', ic: 'trendUp', delta: recTx.length + ' recebidos' }, { lbl: 'Despesas', val: DB.money(despesas), color: 'var(--danger)', bg: 'var(--danger-soft)', ic: 'trendDown', delta: paidTxs.filter(t => t.value < 0).length + ' pagos' }, { lbl: 'Saldo', val: DB.money(saldo), color: 'var(--primary)', bg: 'var(--primary-soft)', ic: 'wallet', delta: rangeLabel }, { lbl: 'A receber', val: DB.money(pendente), color: 'var(--warning)', bg: 'var(--warning-soft)', ic: 'clock', delta: aPagar > 0 ? DB.money(aPagar) + ' a pagar' : pendentes.length + ' pendente' + (pendentes.length === 1 ? '' : 's') }, ]; const exportar = () => { window.exportCSV('financeiro-' + range + '.csv', [ ['Data', 'Descrição', 'Categoria', terms.client || 'Cliente', 'Fornecedor', 'Forma', 'Status', 'Valor'], ...filtered.map(t => [t.date, t.desc, t.cat, clientName(t.clientId), supplierName(t.supplierId), paymentSummary(t), t.status, String(t.value).replace('.', ',')]), ]); toast('Relatério exportado!', 'download'); }; return (
{periodTitle}
Exportar setAdding(true)}>Novo lançamento
} />
{cards.map((c, i) => (
{c.lbl}
{c.val}
{c.delta}
))}
{chartTitle}
{periodTitle}
{DB.money(chart.reduce((s, c) => s + c[1], 0))}
{chart.map(([d, v]) => (
{chartValueLabel(v)}
0 ? 'var(--primary)' : 'var(--primary-soft-2)', borderRadius: '6px 6px 0 0', transition: 'height .4s' }} />
{d}
))}
Formas de pagamento
{rangeLabel}
{methods.length === 0 &&
Sem receitas no período.
} {methods.map(([m, pct], i) => (
{m}{pct}%
))}

Transações

{filtered.length}
{['Todas', 'Receitas', 'Despesas', 'Pendentes'].map(f => )}
setSearch(e.target.value)} />
setTypeFilter(e.target.value)} style={{ flex: '0 0 150px' }}> setStatusFilter(e.target.value)} style={{ flex: '0 0 150px' }}> setClientFilter(e.target.value)} style={{ flex: '0 0 190px' }}> {clients.map((c) => )} setSupplierFilter(e.target.value)} style={{ flex: '0 0 190px' }}> {suppliers.map((s) => )} { setSearch(''); setTypeFilter(''); setStatusFilter(''); setClientFilter(''); setSupplierFilter(''); setFilter('Todas'); }}>Limpar
{filtered.length === 0 ? : (
{filtered.map(t => { const cName = clientName(t.clientId); const sName = supplierName(t.supplierId); return ( setViewing(t)}> ); })}
Descrição{terms.client || 'Cliente'}FornecedorDataFormaStatusValor
0 ? 'var(--success-soft)' : 'var(--danger-soft)', color: t.value > 0 ? 'var(--success)' : 'var(--danger)', display: 'grid', placeItems: 'center', flex: 'none' }}> 0 ? 'trendUp' : 'trendDown'} size={15} />
{t.desc}
{cName ? ( {cName} ) : -} {sName ? ( {sName} ) : -} {new Date(t.date + 'T00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' })} {paymentSummary(t)} {t.status === 'pago' ? 'Pago' : 'Pendente'} 0 ? 'var(--success)' : 'var(--text)' }}>{DB.money(t.value)}
{t.status === 'pendente' && } {false && t.status === 'pendente' && ( e.stopPropagation()}>{t.value > 0 ? 'Receber' : 'Pagar'}}>
Forma de pagamento
{['Pix', 'Cartão', 'Dinheiro', 'Transf.', 'Boleto'].map(m => ( ))}
)}
)}
{adding && setAdding(false)} onSave={saveTx} />} {editing && setEditing(null)} onSave={saveEditTx} />} {viewing && setViewing(null)} onEdit={() => { setEditing(viewing); setViewing(null); }} />} {receiving && setReceiving(null)} onSave={saveReceive} />}
); } function TransactionDetailsModal({ transaction, terms = {}, clients = [], suppliers = [], paymentSummary, onClose, onEdit }) { const DB = window.DB; const clientLabel = terms.client || 'Cliente'; const client = clients.find((c) => String(c.id) === String(transaction.clientId)); const supplier = suppliers.find((s) => String(s.id) === String(transaction.supplierId)); const isExpense = Number(transaction.value) < 0; const dueText = transaction.dueDate ? new Date(transaction.dueDate + 'T00:00').toLocaleDateString('pt-BR') : '-'; const dateText = transaction.date ? new Date(transaction.date + 'T00:00').toLocaleDateString('pt-BR') : '-'; const partyText = client ? client.name : (supplier ? supplier.name : '-'); const Detail = ({ label, children }) => (
{label}
{children || '-'}
); return (
FecharEditar}>
{transaction.desc}
{isExpense ? 'Despesa' : 'Receita'} #{transaction.id}
{DB.money(transaction.value)} {transaction.status === 'pago' ? 'Pago' : 'Pendente'} {dateText} {dueText} {transaction.tag || '-'} {paymentSummary(transaction)}
Vínculos
{(client || supplier) && } {partyText} {client && {client.phone || client.email || '-'}} {supplier && {supplier.phone || supplier.email || supplier.document || '-'}} {transaction.cat || (isExpense ? 'Despesa' : 'Receita')}
); } /* ---------- NOVO LANÇAMENTO (entrada/saída avulsa) ---------- */ function TxModal({ transaction = null, terms, services = [], clients = [], suppliers = [], businessType, addClient, addSupplier, onClose, onSave }) { const DB = window.DB; const apptPlural = ((terms && terms.appt) || 'atendimento').toLowerCase() + 's'; const isEdit = !!transaction; const cleanMethod = (method) => String(method || 'Pix').replace(/\s+\d+\/\d+$/, '').trim() || 'Pix'; const txPayments = isEdit && Array.isArray(transaction.paymentPlan) ? transaction.paymentPlan .map((p) => ({ method: cleanMethod(p.method), value: Math.abs(Number(p.value) || 0), installments: Math.max(1, Number(p.installments) || 1) })) .filter((p) => p.value > 0) : []; const initialForm = isEdit ? { cat: Number(transaction.value) < 0 ? 'Despesa' : 'Receita', desc: transaction.desc || '', value: Math.abs(Number(transaction.value) || 0), date: transaction.date || DB.iso(DB.today), dueDate: transaction.dueDate || '', tag: transaction.tag || '', status: transaction.status || 'pendente', weekendPolicy: 'keep', clientId: transaction.clientId ? String(transaction.clientId) : '', supplierId: transaction.supplierId ? String(transaction.supplierId) : '', serviceId: '', } : { cat: 'Receita', desc: '', value: '', date: DB.iso(DB.today), dueDate: '', tag: '', status: 'pendente', weekendPolicy: 'keep', clientId: '', supplierId: '', serviceId: '' }; const [f, setF] = React.useState(initialForm); const [localClients, setLocalClients] = React.useState(clients || []); const [localSuppliers, setLocalSuppliers] = React.useState(suppliers || []); const [clientQ, setClientQ] = React.useState(''); const [clientOpen, setClientOpen] = React.useState(false); const [newClientOpen, setNewClientOpen] = React.useState(false); const [newClient, setNewClient] = React.useState({ name: '', phone: '', email: '' }); const [clientSaving, setClientSaving] = React.useState(false); const [clientErr, setClientErr] = React.useState(''); const [supplierQ, setSupplierQ] = React.useState(''); const [supplierOpen, setSupplierOpen] = React.useState(false); const [newSupplierOpen, setNewSupplierOpen] = React.useState(false); const [newSupplier, setNewSupplier] = React.useState({ name: '', phone: '', email: '', document: '' }); const [supplierSaving, setSupplierSaving] = React.useState(false); const [supplierErr, setSupplierErr] = React.useState(''); const [fixo, setFixo] = React.useState(false); const [payments, setPayments] = React.useState(txPayments.length > 0 ? txPayments : [{ method: cleanMethod(transaction?.method), value: isEdit ? Math.abs(Number(transaction.value) || 0) : '', installments: 1 }]); const [saving, setSaving] = React.useState(false); const clientComboRef = React.useRef(null); const supplierComboRef = React.useRef(null); const set = (k) => (e) => setF(prev => ({ ...prev, [k]: e.target.value })); const pricedServices = (services || []).filter((s) => Number(s.price) > 0); const selectClientLegacy = (e) => { const cid = e.target.value; const client = clients.find((c) => String(c.id) === String(cid)); if (!client) { setF(prev => ({ ...prev, clientId: '' })); return; } const planPrice = client.planPrice > 0 ? client.planPrice : (client.planId ? (pricedServices.find(s => s.id === client.planId)?.price || 0) : 0); const planName = client.planId ? pricedServices.find(s => s.id === client.planId)?.name : ''; setF(prev => { const v = planPrice > 0 ? Number(planPrice) : prev.value; setPayments([{ method: 'Pix', value: v, installments: 1 }]); return { ...prev, clientId: cid, cat: 'Receita', desc: planName ? `Mensalidade - ${planName}` : `Cobrança - ${client.name}`, value: v, status: 'pendente' }; }); }; React.useEffect(() => { setLocalClients(clients || []); }, [clients]); React.useEffect(() => { setLocalSuppliers(suppliers || []); }, [suppliers]); React.useEffect(() => { if (!clientOpen) return; const close = (e) => { if (clientComboRef.current && !clientComboRef.current.contains(e.target)) setClientOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [clientOpen]); React.useEffect(() => { if (!supplierOpen) return; const close = (e) => { if (supplierComboRef.current && !supplierComboRef.current.contains(e.target)) setSupplierOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [supplierOpen]); const clientLabel = terms.client || 'Cliente'; const selectedClient = localClients.find((c) => String(c.id) === String(f.clientId)); const selectedSupplier = localSuppliers.find((s) => String(s.id) === String(f.supplierId)); const norm = (v) => String(v || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); const clientFilter = norm(clientQ); const clientDigits = DB.mask.digits(clientQ); const filteredClients = (localClients || []).filter((c) => { if (!clientFilter && !clientDigits) return true; return norm(c.name).includes(clientFilter) || norm(c.email).includes(clientFilter) || norm(c.phone).includes(clientFilter) || (clientDigits && DB.mask.digits(c.phone || '').includes(clientDigits)) || (clientDigits && DB.mask.digits(c.cpf || '').includes(clientDigits)); }).slice(0, 25); const supplierFilter = norm(supplierQ); const supplierDigits = DB.mask.digits(supplierQ); const filteredSuppliers = (localSuppliers || []).filter((s) => { if (!supplierFilter && !supplierDigits) return true; return norm(s.name).includes(supplierFilter) || norm(s.email).includes(supplierFilter) || norm(s.phone).includes(supplierFilter) || norm(s.document).includes(supplierFilter) || (supplierDigits && DB.mask.digits(s.phone || '').includes(supplierDigits)) || (supplierDigits && DB.mask.digits(s.document || '').includes(supplierDigits)); }).slice(0, 25); const applyClient = (client) => { if (!client) { setF(prev => ({ ...prev, clientId: '' })); return; } const planPrice = client.planPrice > 0 ? client.planPrice : (client.planId ? (pricedServices.find(s => s.id === client.planId)?.price || 0) : 0); const planName = client.planId ? pricedServices.find(s => s.id === client.planId)?.name : ''; setF(prev => { if (isEdit) return { ...prev, clientId: String(client.id), cat: 'Receita' }; const v = planPrice > 0 ? Number(planPrice) : prev.value; setPayments([{ method: 'Pix', value: v, installments: 1 }]); return { ...prev, clientId: String(client.id), cat: 'Receita', desc: planName ? `Mensalidade - ${planName}` : `Cobranca - ${client.name}`, value: v, status: 'pendente' }; }); }; const selectClient = (client) => { applyClient(client); setClientQ(''); setClientOpen(false); setNewClientOpen(false); setClientErr(''); }; const clearClient = () => { setF(prev => ({ ...prev, clientId: '' })); setClientQ(''); setClientOpen(false); }; const setNewClientField = (k) => (e) => setNewClient(prev => ({ ...prev, [k]: e.target.value })); const createInlineClient = async () => { const name = (newClient.name || clientQ || '').trim(); if (!name || clientSaving || !addClient) return; setClientSaving(true); setClientErr(''); try { const created = await addClient({ ...newClient, name }); setLocalClients(prev => prev.some(c => c.id === created.id) ? prev : [created, ...prev]); selectClient(created); setNewClient({ name: '', phone: '', email: '' }); } catch (e) { setClientErr(e.message || 'Nao foi possivel cadastrar.'); } finally { setClientSaving(false); } }; const selectSupplier = (supplier) => { if (!supplier) return; setF(prev => ({ ...prev, supplierId: String(supplier.id), cat: 'Despesa' })); setSupplierQ(''); setSupplierOpen(false); setNewSupplierOpen(false); setSupplierErr(''); }; const clearSupplier = () => { setF(prev => ({ ...prev, supplierId: '' })); setSupplierQ(''); setSupplierOpen(false); }; const setNewSupplierField = (k) => (e) => setNewSupplier(prev => ({ ...prev, [k]: e.target.value })); const createInlineSupplier = async () => { const name = (newSupplier.name || supplierQ || '').trim(); if (!name || supplierSaving || !addSupplier) return; setSupplierSaving(true); setSupplierErr(''); try { const created = await addSupplier({ ...newSupplier, name }); setLocalSuppliers(prev => prev.some(s => s.id === created.id) ? prev : [created, ...prev]); selectSupplier(created); setNewSupplier({ name: '', phone: '', email: '', document: '' }); } catch (e) { setSupplierErr(e.message || 'Nao foi possivel cadastrar.'); } finally { setSupplierSaving(false); } }; const selectService = (e) => { const val = e.target.value; const service = pricedServices.find((s) => String(s.id) === String(val)); if (!service) { setF(prev => ({ ...prev, serviceId: '' })); return; } const v = Number(service.price) || ''; setPayments([{ method: 'Pix', value: v, installments: 1 }]); setF(prev => ({ ...prev, serviceId: String(service.id), cat: 'Receita', desc: service.name, value: v, status: 'pendente' })); }; const total = Math.abs(Number(f.value) || 0); const sum = payments.reduce((acc, p) => acc + (Number(p.value) || 0), 0); const remaining = Math.round((total - sum) * 100) / 100; const updatePayment = (idx, key, value) => { setPayments((items) => items.map((item, i) => i === idx ? { ...item, [key]: value } : item)); }; const addPayment = () => { setPayments((items) => [...items, { method: 'Pix', value: Math.max(0, remaining), installments: 1 }]); }; const removePayment = (idx) => { setPayments((items) => items.length <= 1 ? items : items.filter((_, i) => i !== idx)); }; const changeStatus = (e) => { const status = e.target.value; setF(prev => ({ ...prev, status })); if (status === 'pago') { setPayments((items) => { if (items.length > 0 && cleanMethod(items[0].method) !== 'A definir') return items; return [{ method: 'Pix', value: total || Math.abs(Number(f.value) || 0), installments: 1 }]; }); } }; const semValor = f.cat === 'Despesa' && total === 0; const showPayments = f.status !== 'pendente'; const valid = f.desc.trim() !== '' && (semValor || (total > 0 && (!showPayments || ( payments.every((p) => Number(p.value) > 0 && Number(p.installments) >= 1) && Math.abs(remaining) <= 0.01 )))); const save = async () => { if (!valid || saving) return; setSaving(true); let payload = { ...f, value: total }; if (!isEdit && fixo) { payload.recurrence = ''; if (f.dueDate) payload.date = f.dueDate; } else { payload.recurrence = 1; } payload.clientId = f.cat === 'Receita' ? f.clientId : ''; payload.supplierId = f.cat === 'Despesa' ? f.supplierId : ''; payload.method = showPayments ? (payments[0]?.method || 'Pix') : 'A definir'; payload.payments = showPayments ? payments.map((p) => ({ method: p.method, value: Number(p.value) || 0, installments: Math.max(1, Number(p.installments) || 1) })) : []; if (isEdit) payload.splitPayments = false; try { await onSave(payload); } finally { setSaving(false); } }; return (
Cancelar {saving ? 'Salvando...' : 'Salvar'}}>
setF({ ...f, cat: v })} options={[{ value: 'Receita', label: 'Entrada (receita)' }, { value: 'Despesa', label: 'Saída (despesa)' }]} /> {f.cat === 'Receita' && (
{ setClientOpen(true); setClientQ(''); }} onChange={(e) => { setClientQ(e.target.value); setClientOpen(true); setNewClient(prev => ({ ...prev, name: e.target.value })); }} />
{selectedClient && ( )}
{selectedClient && !clientOpen && (
{selectedClient.name} {(selectedClient.phone || selectedClient.email) && {selectedClient.phone || selectedClient.email}}
)} {clientOpen && (
{filteredClients.length === 0 &&
Nenhum cadastro encontrado.
} {filteredClients.map((c) => ( ))}
{!newClientOpen ? ( ) : (
{clientErr &&
{clientErr}
}
{ setNewClientOpen(false); setClientErr(''); }}>Cancelar {clientSaving ? 'Salvando...' : 'Cadastrar'}
)}
)}
)} {f.cat === 'Despesa' && (
{ setSupplierOpen(true); setSupplierQ(''); }} onChange={(e) => { setSupplierQ(e.target.value); setSupplierOpen(true); setNewSupplier(prev => ({ ...prev, name: e.target.value })); }} />
{selectedSupplier && ( )}
{selectedSupplier && !supplierOpen && (
{selectedSupplier.name} {(selectedSupplier.phone || selectedSupplier.email) && {selectedSupplier.phone || selectedSupplier.email}}
)} {supplierOpen && (
{filteredSuppliers.length === 0 &&
Nenhum fornecedor encontrado.
} {filteredSuppliers.map((s) => ( ))}
{!newSupplierOpen ? ( ) : (
{supplierErr &&
{supplierErr}
}
{ setNewSupplierOpen(false); setSupplierErr(''); }}>Cancelar {supplierSaving ? 'Salvando...' : 'Cadastrar'}
)}
)}
)} {f.cat === 'Receita' && businessType === 'pilates' && pricedServices.length > 0 && ( {pricedServices.map((s) => )} )} { setF({ ...f, value: n }); if (payments.length === 1) updatePayment(0, 'value', n); }} /> {f.cat === 'Despesa' && f.dueDate && !fixo && ( )}
{showPayments &&
Pagamento
{payments.map((p, idx) => (
updatePayment(idx, 'method', e.target.value)}> updatePayment(idx, 'value', n)} /> updatePayment(idx, 'installments', e.target.value)} />
))}
Adicionar forma
Soma: {DB.money(sum)}
} {!isEdit &&
setFixo(!fixo)}> setFixo(!fixo)} style={{ width: 18, height: 18, accentColor: 'var(--primary)' }} />
Fixo (repete todo mês)
{(() => { const dia = f.dueDate ? Number(f.dueDate.split('-')[2]) : Number(f.date.split('-')[2]); if (!dia) return 'Defina o vencimento para escolher o dia'; if (dia > 28) return `Todo dia ${dia} (ou Último dia do mês quando não existir)`; return `Todo dia ${dia} de cada mês`; })()}
} {!isEdit && fixo && f.cat === 'Despesa' && (
)} ); } function ReceiveTxModal({ transaction, clients = [], suppliers = [], terms = {}, onClose, onSave }) { const DB = window.DB; const total = Math.abs(Number(transaction.value) || 0); const cleanMethod = (method) => String(method || 'Pix').replace(/\s+\d+\/\d+$/, '').trim() || 'Pix'; const plannedPayments = Array.isArray(transaction.paymentPlan) ? transaction.paymentPlan .map((p) => ({ method: cleanMethod(p.method), value: Math.abs(Number(p.value) || 0), installments: Math.max(1, Number(p.installments) || 1) })) .filter((p) => p.value > 0) : []; const [payments, setPayments] = React.useState(plannedPayments.length > 0 ? plannedPayments : [{ method: cleanMethod(transaction.method), value: total, installments: 1 }]); const [saving, setSaving] = React.useState(false); const methods = [...new Set(['Pix', 'Cartão', 'Dinheiro', 'Transf.', 'Boleto', ...payments.map((p) => cleanMethod(p.method))])]; const clientLabel = terms.client || 'Cliente'; const client = clients.find((c) => String(c.id) === String(transaction.clientId)); const clientText = client ? client.name : ''; const supplier = suppliers.find((s) => String(s.id) === String(transaction.supplierId)); const supplierText = supplier ? supplier.name : ''; const partyText = transaction.value < 0 ? supplierText : clientText; const partyLabel = transaction.value < 0 ? 'Fornecedor' : clientLabel; const partyIcon = transaction.value < 0 ? 'building' : 'user'; const partyDisplay = partyText || 'Não informado'; const modalSub = `${partyLabel}: ${partyDisplay} - ${transaction.desc} - ${DB.money(total)}`; const sum = payments.reduce((acc, p) => acc + (Number(p.value) || 0), 0); const remaining = Math.round((total - sum) * 100) / 100; const update = (idx, key, value) => { setPayments((items) => items.map((item, i) => i === idx ? { ...item, [key]: value } : item)); }; const addPayment = () => { setPayments((items) => [...items, { method: 'Pix', value: Math.max(0, remaining), installments: 1 }]); }; const removePayment = (idx) => { setPayments((items) => items.length <= 1 ? items : items.filter((_, i) => i !== idx)); }; const valid = total > 0 && Math.abs(remaining) <= 0.01 && payments.every((p) => Number(p.value) > 0 && Number(p.installments) >= 1); const save = async () => { if (!valid || saving) return; setSaving(true); try { await onSave({ desc: transaction.desc, cat: transaction.value < 0 ? 'Despesa' : 'Receita', value: total, date: DB.iso(DB.today), payments: payments.map((p) => ({ method: p.method, value: Number(p.value) || 0, installments: Math.max(1, Number(p.installments) || 1), })), }); } finally { setSaving(false); } }; return (
Cancelar {saving ? 'Salvando...' : 'Confirmar'}}>
{partyLabel}: {partyDisplay}
{payments.map((p, idx) => (
update(idx, 'method', e.target.value)}> {methods.map((m) => )} update(idx, 'value', n)} /> update(idx, 'installments', e.target.value)} />
))}
Adicionar forma
Soma: {DB.money(sum)}
); } /* ---------------- TAREFAS ---------------- */ function TasksScreen(ctx) { const { tasks, toast } = ctx; const [adding, setAdding] = React.useState(false); const [text, setText] = React.useState(''); const cols = [ { key: 'alta', label: 'Prioridade alta', cls: 'red' }, { key: 'media', label: 'Prioridade média', cls: 'amber' }, { key: 'baixa', label: 'Prioridade baixa', cls: 'blue' }, ]; const done = tasks.filter(t => t.done).length; const create = async () => { if (!text.trim()) return; await ctx.addTask(text.trim()); setText(''); setAdding(false); toast('Tarefa criada!'); }; return (
setAdding(true)}>Nova tarefa } /> {adding && (
setText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') create(); }} /> setAdding(false)}>Cancelar
)}
{cols.map(col => { const list = tasks.filter(t => t.prio === col.key); return (
{col.label}{list.length}
{list.length === 0 &&
Nada por aqui
} {list.map(t => (
ctx.toggleTask(t.id, !t.done)} />
{t.text}
{t.due} {t.tag}
))}
); })}
); } /* ---------------- HISTÓRICO ---------------- */ function HistoryScreen(ctx) { const { terms, appts, clients, services, team, statusMap, toast } = ctx; const DB = window.DB; const apptPlural = terms.appt.toLowerCase() + 's'; const initialClientId = ctx.historyClientId !== 'all' && clients.some((client) => Number(client.id) === Number(ctx.historyClientId)) ? String(ctx.historyClientId) : 'all'; const [clientId, setClientId] = React.useState(initialClientId); const [serviceId, setServiceId] = React.useState('all'); const [proId, setProId] = React.useState('all'); const [status, setStatus] = React.useState('all'); const [dateFrom, setDateFrom] = React.useState(''); const [dateTo, setDateTo] = React.useState(''); React.useEffect(() => { const requested = ctx.historyClientId !== 'all' && clients.some((client) => Number(client.id) === Number(ctx.historyClientId)) ? String(ctx.historyClientId) : 'all'; setClientId(requested); }, [ctx.historyClientId]); const todayMin = DB.iso(DB.today); const past = appts.filter(a => a.date <= todayMin).sort((a, b) => (b.date + b.start).localeCompare(a.date + a.start)); const cli = (id) => clients.find(c => c.id === id); const svc = (id) => services.find(s => s.id === id); const pro = (id) => team.find(p => p.id === id); const ini = (n) => String(n || '?').split(' ').map(x => x[0]).slice(0, 2).join('').toUpperCase(); const statusOptions = ['confirmado', 'pendente', 'concluido', 'faltou', 'cancelado']; const shown = past.filter((a) => { if (clientId !== 'all' && Number(a.clientId) !== Number(clientId)) return false; if (serviceId === 'manual' && !a.manualServiceName) return false; if (serviceId !== 'all' && serviceId !== 'manual' && Number(a.serviceId) !== Number(serviceId)) return false; if (proId !== 'all' && Number(a.proId) !== Number(proId) && !(a.participants || []).some((part) => Number(part.proId) === Number(proId))) return false; if (status !== 'all' && a.status !== status) return false; if (dateFrom && a.date < dateFrom) return false; if (dateTo && a.date > dateTo) return false; return true; }); const selectedClient = clientId === 'all' ? null : cli(Number(clientId)); const hasManualServices = past.some((a) => !!a.manualServiceName); // agrupar por data const groups = {}; shown.forEach(a => { (groups[a.date] = groups[a.date] || []).push(a); }); const fmtDate = (d) => { const dt = new Date(d + 'T00:00'); if (d === todayMin) return 'Hoje'; if (d === DB.iso(DB.addDays(-1))) return 'Ontem'; return dt.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }); }; const exportar = () => { const suffix = selectedClient ? '-' + String(selectedClient.name || selectedClient.id).toLowerCase().replace(/[^a-z0-9]+/g, '-') : ''; window.exportCSV('historico' + suffix + '.csv', [ ['Data', 'Inicio', 'Fim', terms.client, terms.service, terms.pro, 'Valor', 'Status'], ...shown.map(a => { const c = cli(a.clientId), s = a.manualServiceName ? { name: a.manualServiceName, price: a.manualServicePrice || 0, color: 'violet' } : svc(a.serviceId), p = pro(a.proId); const st = statusMap[a.status] || { label: a.status }; return [a.date, a.start, window.apptEnd(a), c ? c.name : (a.clientLabel || `Sem ${terms.client.toLowerCase()}`), s ? s.name : '', p ? p.name : '', s ? String(s.price).replace('.', ',') : '', st.label]; }), ]); toast('Histórico exportado!', 'download'); }; const clearFilters = () => { setClientId('all'); setServiceId('all'); setProId('all'); setStatus('all'); setDateFrom(''); setDateTo(''); }; return (
Exportar } />
setClientId(e.target.value)}> {[...clients].sort((a, b) => String(a.name).localeCompare(String(b.name), 'pt-BR')).map((client) => )} setServiceId(e.target.value)}> {services.map((service) => )} {hasManualServices && } setProId(e.target.value)}> {team.filter((member) => !member.systemIdentity).map((member) => )} setStatus(e.target.value)}> {statusOptions.map((value) => )} setDateFrom(e.target.value)} /> setDateTo(e.target.value)} />
{shown.length} registro{shown.length === 1 ? '' : 's'} encontrado{shown.length === 1 ? '' : 's'} Limpar filtros
{Object.keys(groups).length === 0 && }
{Object.keys(groups).map(date => (

{fmtDate(date)}

{groups[date].length}
{groups[date].map((a, i) => { const c = cli(a.clientId), s = a.manualServiceName ? { name: a.manualServiceName, price: a.manualServicePrice || 0, color: 'violet' } : svc(a.serviceId), p = pro(a.proId), st = statusMap[a.status] || { label: a.status, cls: 'plain' }; if (!s || !p) return null; const displayClientName = c ? c.name : (a.clientLabel || p.name || `Sem ${terms.client.toLowerCase()}`); return (
ctx.openAppt(a)}>
{a.start}–{window.apptEnd(a)}
{displayClientName}
{s.name} · {p.name.replace('Dra. ', '')}
{s.price > 0 ? DB.money0(s.price) : '—'} {st.label}
); })}
))}
); } function TasksScreenFull(ctx) { const { tasks, clients, terms, toast } = ctx; const DB = window.DB; const blank = () => ({ title: '', dueDate: DB.iso(DB.today), dueTime: '09:00', clientId: 'none', prio: 'media' }); const [adding, setAdding] = React.useState(false); const [form, setForm] = React.useState(blank); const cols = [ { key: 'alta', label: 'Prioridade alta', cls: 'red' }, { key: 'media', label: 'Prioridade media', cls: 'amber' }, { key: 'baixa', label: 'Prioridade baixa', cls: 'blue' }, ]; const done = tasks.filter(t => t.done).length; const set = (key) => (e) => setForm({ ...form, [key]: e.target.value }); const clientName = (id) => { const c = clients.find(x => x.id === Number(id)); return c ? c.name : ''; }; const dueLabel = (t) => { if (t.dueDate) { const d = new Date(t.dueDate + 'T00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }); return t.dueTime ? d + ', ' + t.dueTime : d; } return t.due || 'Sem data'; }; const create = async () => { if (!form.title.trim()) return; const selectedClient = clientName(form.clientId); await ctx.addTask({ title: form.title.trim(), dueDate: form.dueDate, dueTime: form.dueTime, prio: form.prio, clientId: form.clientId === 'none' ? null : Number(form.clientId), tag: selectedClient || 'Geral', }); setForm(blank()); setAdding(false); toast('Tarefa criada!'); }; return (
setAdding(true)}>Nova tarefa } /> {adding && (
{ if (e.key === 'Enter') create(); }} /> {clients.map(c => )}
{ setAdding(false); setForm(blank()); }}>Cancelar Salvar tarefa
)}
{cols.map(col => { const list = tasks.filter(t => t.prio === col.key); return (
{col.label}{list.length}
{list.length === 0 &&
Nada por aqui
} {list.map(t => (
ctx.toggleTask(t.id, !t.done)} />
{t.text}
{dueLabel(t)} {(t.clientId || t.tag) && {clientName(t.clientId) || t.tag}}
))}
); })}
); } /* ---------------- MENSALIDADE (cobrança do SaaS — visão do cliente/admin) ---------------- */ function BillingScreen(ctx) { const DB = window.DB; const { toast } = ctx; const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [selPlanId, setSelPlanId] = React.useState(null); const [selMonths, setSelMonths] = React.useState(1); const [showCheckout, setShowCheckout] = React.useState(false); const [showPlanFeatures, setShowPlanFeatures] = React.useState(false); const [form, setForm] = React.useState({ name: '', doc: '', email: '' }); const [submitting, setSubmitting] = React.useState(false); const [checking, setChecking] = React.useState(false); const [cancelling, setCancelling] = React.useState(false); const [pixData, setPixData] = React.useState(null); const [pixLoading, setPixLoading] = React.useState(false); const load = React.useCallback(async () => { try { const d = await window.API.billing.get(); setData(d); setForm({ name: d.customer.name || '', doc: d.customer.doc || '', email: d.customer.email || '' }); setSelPlanId(d.currentPlanId || (d.plans && d.plans[0] && d.plans[0].id) || null); setError(null); } catch (e) { setError(e.message || 'Erro ao carregar a mensalidade.'); } finally { setLoading(false); } }, []); React.useEffect(() => { load(); }, [load]); // carrega o QR/copia-e-cola do PIX quando há uma cobrança em aberto const openPayId = data && data.openPayment ? data.openPayment.id : null; React.useEffect(() => { if (!openPayId) { setPixData(null); return; } let alive = true; setPixLoading(true); (async () => { try { const r = await window.API.billing.pix(openPayId); if (alive) setPixData(r && r.payload ? r : null); } catch { if (alive) setPixData(null); } finally { if (alive) setPixLoading(false); } })(); return () => { alive = false; }; }, [openPayId]); const statusMeta = { em_dia: { label: 'Em dia', cls: 'green', soft: 'success' }, pendente: { label: 'Pendente', cls: 'amber', soft: 'warning' }, atrasado: { label: 'Atrasado', cls: 'red', soft: 'danger' }, }; const typeLabel = { MANUAL: 'Manual', CREDIT: 'Crédito', PIX: 'PIX', BOLETO: 'Boleto', CREDIT_CARD: 'Cartão', UNDEFINED: 'Online' }; const payStatusLabel = { RECEIVED: 'Recebido', CONFIRMED: 'Confirmado', PENDING: 'Aguardando', OVERDUE: 'Vencido', REFUNDED: 'Estornado', CANCELLED: 'Cancelada' }; const fmtDate = (d) => d ? new Date(String(d).replace(' ', 'T')).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'; const monthLabel = (m) => m === 1 ? '1 mês' : m + ' meses'; if (loading) { return (
Carregando...
); } if (error) { return (
); } const plans = data.plans || []; const payments = data.payments || []; const durations = data.durations || [1]; const discounts = data.discounts || {}; const sm = statusMeta[data.status] || statusMeta.em_dia; const currentPlan = plans.find(p => p.id === data.currentPlanId) || null; const selPlan = plans.find(p => p.id === selPlanId) || plans[0] || null; const selectedIsCurrentPlan = !!(currentPlan && selPlan && currentPlan.id === selPlan.id); const totalPaid = payments.filter(p => p.status === 'RECEIVED' || p.status === 'CONFIRMED').reduce((s, p) => s + p.value, 0); const accessUntil = [data.paidUntil, data.trialEndsAt].filter(Boolean).sort().pop() || null; const priceFor = (p, m) => p ? Math.round(p.price * m * (1 - ((discounts[String(m)] || 0) / 100)) * 100) / 100 : 0; const planLimits = (p) => !p ? '' : [ ((p.maxUsers ?? p.maxAdmins) === 0 ? 'Acessos ilimitados' : (p.maxUsers ?? p.maxAdmins) + ((p.maxUsers ?? p.maxAdmins) === 1 ? ' acesso com login' : ' acessos com login')), (p.hasTeam ? (p.maxPros === 0 ? 'Equipe ilimitada' : 'Até ' + p.maxPros + ' na equipe') : 'Sem equipe'), ].join(' · '); const planComparisonRows = [ { label: 'Agenda e calendário', value: () => true }, { label: 'Cadastro de clientes / alunos', value: () => true }, { label: 'Serviços, planos e aulas', value: () => true }, { label: 'Tarefas e histórico', value: () => true }, { label: 'Financeiro', value: (p) => p.hasFinance }, { label: 'PIX, boleto e cartão', value: () => true }, { label: 'Usuários com login e senha', value: (p) => (p.maxUsers ?? p.maxAdmins) === 0 ? 'Ilimitados' : String(p.maxUsers ?? p.maxAdmins) }, { label: 'Equipe / profissionais', value: (p) => !p.hasTeam ? 'Não inclui' : (p.maxPros === 0 ? 'Ilimitada' : `Até ${p.maxPros}`) }, { label: 'Gestáo das agendas da equipe', value: (p) => p.hasTeam }, { label: 'Confirmações e lembretes por e-mail', value: (p) => p.hasAppointmentEmail }, { label: 'Agendamento em grupo / reunião', value: (p) => p.hasGroupMeetings }, { label: 'Chat interno', value: (p) => p.hasInternalChat ? 'Em breve' : false }, { label: 'Sincronização com Google Drive', value: (p) => ['profissional', 'empresarial'].includes(p.slug) ? 'Em breve' : false }, { label: 'Sincronização com Google Agenda', value: (p) => ['profissional', 'empresarial'].includes(p.slug) ? 'Em breve' : false }, { label: 'Sincronização com Google Meet', value: (p) => p.slug === 'empresarial' ? 'Em breve' : false }, { label: 'Suporte', value: (p) => p.hasSupport }, ]; const selTotal = priceFor(selPlan, selMonths); const openPay = data.openPayment || null; // pró-rata: crédito dos dias restantes do plano atual ao TROCAR de plano const lastPaid = payments.find(p => (p.status === 'RECEIVED' || p.status === 'CONFIRMED') && p.months > 0 && (p.planId === currentPlan?.id || p.planId == null)); const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); const trialRemainingDays = !currentPlan && data.trialEndsAt ? Math.max(0, Math.ceil((new Date(data.trialEndsAt + 'T00:00:00') - todayStart) / 86400000)) : 0; const accessRemainingDays = data.paidUntil ? Math.max(0, Math.ceil((new Date(data.paidUntil + 'T00:00:00') - todayStart) / 86400000)) : 0; const paidDate = lastPaid?.date ? new Date(String(lastPaid.date).slice(0, 10) + 'T00:00:00') : null; const usedPeriodDays = paidDate ? Math.max(0, Math.floor((todayStart - paidDate) / 86400000)) : 0; const remainingDays = lastPaid ? Math.max(0, lastPaid.months * 30 - usedPeriodDays) : Math.min(30, accessRemainingDays); const isPlanChange = currentPlan && selPlan && currentPlan.id !== selPlan.id && remainingDays > 0; const dailyValue = lastPaid ? ((lastPaid.value + (lastPaid.creditApplied || 0)) / Math.max(1, lastPaid.months * 30)) : (currentPlan ? currentPlan.price / 30 : 0); const periodCredit = isPlanChange ? Math.round(dailyValue * remainingDays * 100) / 100 : 0; const availableCredit = Math.round(((data.creditBalance || 0) + periodCredit) * 100) / 100; const credit = Math.min(availableCredit, selTotal); const creditRemaining = Math.round((availableCredit - credit) * 100) / 100; const netTotal = Math.round((selTotal - credit) * 100) / 100; const checkoutActionLabel = !selPlan ? 'Selecione um plano' : ( selectedIsCurrentPlan ? 'Renovar ' + selPlan.name : (currentPlan ? 'Mudar para ' + selPlan.name : 'Assinar ' + selPlan.name) ) + ' · ' + monthLabel(selMonths) + ' · ' + DB.money(netTotal); const usage = data.usage || {}; const loginUsers = Number(usage.loginUsers || 0); const teamMembers = Number(usage.teamMembers || 0); const maxPlanUsers = selPlan ? Number(selPlan.maxUsers ?? selPlan.maxAdmins ?? 0) : 0; const maxPlanTeam = selPlan ? Number(selPlan.maxPros || 0) : 0; const blockedUsers = maxPlanUsers > 0 ? Math.max(0, loginUsers - maxPlanUsers) : 0; const archivedTeam = selPlan && !selPlan.hasTeam ? teamMembers : 0; const excessTeam = selPlan && selPlan.hasTeam && maxPlanTeam > 0 ? Math.max(0, teamMembers - maxPlanTeam) : 0; const firstAdminName = usage.firstAdmin && usage.firstAdmin.name ? usage.firstAdmin.name : 'o primeiro administrador cadastrado'; const planRank = { individual: 1, profissional: 2, empresarial: 3 }; const isPlanRegression = !!(currentPlan && selPlan && (planRank[selPlan.slug] || 0) < (planRank[currentPlan.slug] || 0)); const lostFeatures = !selPlan ? [] : [ (!currentPlan || currentPlan.hasFinance) && !selPlan.hasFinance ? 'Financeiro' : null, (!currentPlan || currentPlan.hasTeam) && !selPlan.hasTeam ? 'Equipe' : null, (!currentPlan || currentPlan.hasGroupMeetings) && !selPlan.hasGroupMeetings ? 'reuniões em grupo' : null, (!currentPlan || currentPlan.hasInternalChat) && !selPlan.hasInternalChat ? 'chat interno' : null, ].filter(Boolean); const hasPlanImpact = isPlanRegression && (blockedUsers > 0 || archivedTeam > 0 || excessTeam > 0 || lostFeatures.length > 0); const startCheckout = () => { if (!data.asaasReady) { toast('Pagamento online ainda não configurado. Fale com o suporte.', 'x'); return; } if (!selPlan) { toast('Selecione um plano.', 'x'); return; } setShowCheckout(true); }; const confirmCheckout = async () => { if (submitting || !selPlan) return; setSubmitting(true); try { const r = await window.API.billing.checkout({ planId: selPlan.id, months: selMonths, name: form.name, doc: form.doc, email: form.email }); setShowCheckout(false); toast(r.confirmed ? 'Plano ativado usando seu crédito!' : ((r.credit > 0 ? 'Cobrança gerada com ' + DB.money(r.credit) + ' de crédito abatido!' : 'Cobrança gerada!') + ' O PIX aparecerá nesta tela.'), 'check'); await load(); } catch (e) { toast(e.message || 'Erro ao gerar a cobrança', 'x'); } finally { setSubmitting(false); } }; const verifyPayment = async () => { if (!openPay || checking) return; setChecking(true); try { const r = await window.API.billing.check(openPay.id); if (r.confirmed) { toast('Pagamento confirmado! Seu acesso foi estendido.', 'check'); load(); } else if (r.cancelled) { toast('Essa cobrança foi cancelada e não está mais disponível. Gere uma nova se quiser pagar.', 'info'); load(); } else { toast('Pagamento ainda não identificado. Se já pagou, aguarde alguns instantes e tente de novo.', 'info'); } } catch (e) { toast(e.message || 'Erro ao verificar o pagamento', 'x'); } finally { setChecking(false); } }; const cancelCharge = async () => { if (!openPay || cancelling) return; if (!window.confirm('Cancelar esta cobrança em aberto? Depois você poderá gerar outra.')) return; setCancelling(true); try { await window.API.billing.cancel(openPay.id); toast('Cobrança cancelada.', 'check'); load(); } catch (e) { toast(e.message || 'Erro ao cancelar a cobrança', 'x'); } finally { setCancelling(false); } }; return (
Seu plano
{currentPlan ? currentPlan.name : 'Período de teste'}
{currentPlan ? DB.money(currentPlan.price) + '/mês' : 'Escolha um plano abaixo'}
Situação
{sm.label}
Último pagamento: {fmtDate(data.lastPaymentAt)}
Acesso até
{accessUntil ? fmtDate(accessUntil) : '—'}
{totalPaid > 0 ? 'Total pago: ' + DB.money(totalPaid) : (currentPlan ? '' : 'Período de teste')}
{openPay && (
Cobrança em aberto — aguardando pagamento
{(openPay.months ? monthLabel(openPay.months) + ' · ' : '') + DB.money(openPay.value)}. Pague com PIX aqui (ou boleto/cartão pelo link) e confirme — o acesso é estendido na confirmação. Para assinar outro plano, cancele esta cobrança primeiro.
{pixData && pixData.payload && (
{pixData.encodedImage && QR Code PIX}
Pague com PIX agora
Escaneie o QR Code ou copie o código abaixo no app do seu banco. A confirmação é automática.
e.target.select()} style={{ fontSize: 11, fontFamily: 'var(--font-mono)' }} /> { if (navigator.clipboard) navigator.clipboard.writeText(pixData.payload); toast('Código PIX copiado!', 'check'); }}>Copiar
)} {pixLoading && !pixData &&
Carregando PIX...
}
{openPay.invoiceUrl && window.open(openPay.invoiceUrl, '_blank')}>Boleto / cartão} {checking ? 'Verificando...' : 'Já paguei / Verificar'} {cancelling ? 'Cancelando...' : 'Cancelar cobrança'}
)} {!openPay && (data.asaasReady ? (
{currentPlan ? 'Renovar ou alterar plano' : 'Escolha seu plano'}
{currentPlan ? 'A renovação soma tempo ao acesso pago atual. Para mudar de plano, selecione outra opção abaixo.' : `O período é somado ao seu acesso atual.${trialRemainingDays > 0 ? ` Você ainda tem ${trialRemainingDays} dia(s) de teste; eles serão mantidos e o plano começará depois do teste.` : (data.trialEndsAt ? ' Seu teste vai até ' + fmtDate(data.trialEndsAt) + '.' : '')}`}
{!currentPlan &&
Durante o teste, todos os recursos e limites ficam liberados. Ao contratar, passam a valer os limites do plano escolhido; nenhum dado será apagado.
}
{plans.map(p => { const isSel = selPlan && p.id === selPlan.id; return (
setShowPlanFeatures(true)}>Ver funcionalidades
); })}
{hasPlanImpact &&
O que muda ao ativar o {selPlan && selPlan.name}
{maxPlanUsers === 1 &&
• Somente {firstAdminName}, o primeiro administrador criado, continuará com acesso.
} {blockedUsers > 0 &&
• {blockedUsers} usuário(s) excedente(s) ficarão sem acesso, mas serão preservados.
} {archivedTeam > 0 &&
• {archivedTeam} membro(s) da equipe ficarão arquivados enquanto este plano estiver ativo.
} {excessTeam > 0 &&
• Sua equipe excede o limite em {excessTeam}; os dados serão preservados e novos cadastros ficarão bloqueados até a adequação.
} {lostFeatures.length > 0 &&
• Recursos indisponíveis: {lostFeatures.join(', ')}.
}
• Agendamentos e históricos não serão apagados. Um upgrade restaura os recursos e acessos conforme o novo limite.
}
Por quanto tempo:
{durations.map(m => { const isSel = m === selMonths; const disc = discounts[String(m)] || 0; return ( ); })}
{availableCredit > 0 && (
Crédito disponível: {DB.money(availableCredit)}{periodCredit > 0 ? ` (${remainingDays} dia(s) restantes do plano ${currentPlan.name})` : ''}. {DB.money(credit)} será abatido e você paga {DB.money(netTotal)}{creditRemaining > 0 ? `; o saldo de ${DB.money(creditRemaining)} fica para a próxima renovação.` : '.'}
)} {checkoutActionLabel}
) : (
Pagamento online
Em breve você poderá assinar um plano direto por aqui (PIX, boleto e cartão).
))}

Histórico de pagamentos

{payments.length}
{payments.length === 0 ? : (
{payments.map(p => ( ))}
DataPeríodoFormaStatusNota fiscalValor
{fmtDate(p.date)} {p.months > 0 ? monthLabel(p.months) : '—'} {typeLabel[p.type] || p.type} {payStatusLabel[p.status] || p.status} {p.taxInvoiceUrl ? window.open(p.taxInvoiceUrl, '_blank')}>Baixar nota : } {DB.money(p.value)}
)}
{showPlanFeatures && ( setShowPlanFeatures(false)} footer={<>
setShowPlanFeatures(false)}>Fechar}>
{plans.map(p => )} {planComparisonRows.map(row => ( {plans.map(p => { const value = row.value(p); return ; })} ))}
Funcionalidade
{p.name}
{DB.money(p.price)}/mês
{row.label} {typeof value === 'boolean' ? : value}
)} {showCheckout && ( setShowCheckout(false)} footer={<>
setShowCheckout(false)}>Cancelar {submitting ? 'Gerando...' : 'Gerar cobrança'}}>
setForm({ ...form, name: e.target.value })} placeholder="Nome de quem paga" /> setForm({ ...form, doc: e.target.value })} placeholder="Somente números" /> setForm({ ...form, email: e.target.value })} placeholder="para o recibo (opcional)" />
{credit > 0 &&

{DB.money(credit)} de crédito abatido — total a pagar: {DB.money(netTotal)}.{creditRemaining > 0 ? ` Saldo de ${DB.money(creditRemaining)} guardado para a próxima renovação.` : ''}

} {trialRemainingDays > 0 &&

Seus {trialRemainingDays} dia(s) restantes de teste serão preservados e somados ao período comprado.

} {hasPlanImpact &&
Confirme que está ciente das limitações:
{maxPlanUsers === 1 &&
• Apenas {firstAdminName} continuará com acesso.
} {blockedUsers > 0 &&
• {blockedUsers} usuário(s) ficarão bloqueados, sem exclusão dos dados.
} {archivedTeam > 0 &&
• A equipe será arquivada e ficará disponível novamente após upgrade.
} {excessTeam > 0 &&
• A equipe atual excede em {excessTeam} o limite deste plano.
} {lostFeatures.length > 0 &&
• Você deixará de acessar: {lostFeatures.join(', ')}.
}
• Histórico e agendamentos serão preservados.
}

O QR Code PIX aparecerá nesta tela, sem redirecionamento. Se preferir boleto ou cartão, use o link de outras formas de pagamento. O acesso é estendido assim que o pagamento for confirmado.

)}
); } Object.assign(window, { FinanceScreen, TasksScreen: TasksScreenFull, HistoryScreen, BillingScreen });