// Financial reports and monthly plans use canonical, authenticated API results. function FFPlanningButton({ children, primary = false, ...props }) { return ; } function FFPlanningError({ error, retry }) { return error ?
{error}{retry && Tentar novamente}
: null; } function FFPlanningPercent({ bps }) { return <>{(bps / 100).toLocaleString('pt-BR', { maximumFractionDigits: 2 })}%; } function FFSpendingReports({ onNavigate }) { const { currentMonth, currentMember } = useApp(); const [result, setResult] = React.useState(null); const [error, setError] = React.useState(''); const [loading, setLoading] = React.useState(true); const [retry, setRetry] = React.useState(0); const month = `${currentMonth.year}-${String(currentMonth.month).padStart(2, '0')}`; const today = new Date(); const asOf = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; const key = `${currentMember?.id}:${month}:${asOf}`; React.useEffect(() => { let active = true; const controller = new AbortController(), token = localStorage.getItem('ff_token'); setLoading(true); setError(''); setResult(null); apiFetch(`/reports/spending?month=${month}&asOf=${asOf}`, { signal: controller.signal }) .then(data => { if (active && localStorage.getItem('ff_token') === token) setResult({ key, data }); }) .catch(e => { if (active && e.name !== 'AbortError') setError(e.message); }) .finally(() => { if (active) setLoading(false); }); return () => { active = false; controller.abort(); }; }, [key, retry]); const data = result?.key === key ? result.data : null; const panels = data ? [ { title: 'Despesas por meio de pagamento', sub: `Sua responsabilidade no mês ${month.split('-').reverse().join('/')}. O percentual considera suas cotas, não o valor integral de terceiros.`, report: data.monthly, daily: false }, { title: 'Dia a Dia · últimos 90 dias', sub: `${parseLocalDate(data.daily90.from).toLocaleDateString('pt-BR')} a ${parseLocalDate(data.daily90.to).toLocaleDateString('pt-BR')} · sua responsabilidade por descrição do gasto.`, report: data.daily90, daily: true }, ] : []; return

Análise para o orçamento

Percentuais e valores da sua responsabilidade.

onNavigate('orcamento')}>Abrir orçamento mensal
{loading &&
Carregando relatórios…
} setRetry(r => r + 1)} /> {panels.map(panel =>

{panel.title}

{panel.sub}

{fmtBRL(panel.report.totalCents / 100)} {!panel.report.rows.length ? :
{panel.report.rows.map(row =>
{panel.daily ? row.description : row.name}
{fmtBRL(row.amountCents / 100)}{panel.daily && {row.count} lançamento{row.count === 1 ? '' : 's'} · estimativa mensal {fmtBRL(row.monthlyEstimateCents / 100)}}
)}
} {panel.daily &&

A estimativa é a média de 30 dias do período. Use-a para decidir quais gastos recorrentes incluir no orçamento; ela não cria uma despesa.

}
)}
; } function FFBudgetShareDraft(members, shares, currentMemberId) { const source = shares || [{ memberId: currentMemberId, percentageBps: 10000 }]; const participants = [...members, ...source.filter(s => !members.some(m => m.id === s.memberId)).map(s => ({ id: s.memberId, name: 'Membro indisponível' }))]; return participants.map(m => { const s = source.find(x => x.memberId === m.id); return { memberId: m.id, included: !!s, percent: s ? (s.percentageBps / 100).toFixed(2).replace('.', ',') : '' }; }); } function FFBudgetDraft(plan, members, actor, month) { return { name: plan?.name || `Orçamento ${month.split('-').reverse().join('/')}`, version: plan?.version || 0, defaultShares: FFBudgetShareDraft(members, plan?.defaultShares, actor), categoryRules: (plan?.categoryRules || []).map(r => ({ categoryId: r.categoryId, shares: FFBudgetShareDraft(members, r.shares, actor) })), items: (plan?.items || []).map(i => ({ id: i.id, description: i.description, categoryId: i.categoryId, amount: (i.amountCents / 100).toFixed(2).replace('.', ','), customShares: !!i.sharesOverride, shares: FFBudgetShareDraft(members, i.sharesOverride || i.shares, actor) })), }; } function FFBudgetShares({ label, members, value, onChange, disabled }) { return
{label}

Marque somente os participantes. Os percentuais selecionados devem somar 100%.

{value.map((row, index) => { const member = members.find(m => m.id === row.memberId); return
; })}
; } function FFBudgetSummary({ plan, members, shared = false }) { return

{shared ? plan.name : plan.preview ? 'Prévia do orçamento' : 'Totais do orçamento salvo'}

{shared &&

Compartilhado por {members.find(m => m.id === plan.ownerMemberId)?.name || 'outro membro'} · somente os itens em que você participa. Consulta sem edição.

} {fmtBRL(plan.totalCents / 100)} {(plan.totalsByMember || []).map(s =>
{members.find(m => m.id === s.memberId)?.name || 'Membro'}{fmtBRL(s.amountCents / 100)}
)} {(plan.items || []).map((item, index) =>
{item.description}{fmtBRL(item.amountCents / 100)}
{!shared &&

Divisão: {({ item: 'personalizada neste item', category: 'regra da categoria', default: 'padrão do orçamento' })[item.shareSource]}

} {item.shares.map(s =>
{members.find(m => m.id === s.memberId)?.name || 'Membro'} · {fmtBRL(s.amountCents / 100)}
)}
)}
; } function OrcamentoMensal() { const { currentMonth, currentMember, members, categories } = useApp(); const month = `${currentMonth.year}-${String(currentMonth.month).padStart(2, '0')}`; const key = `${currentMember?.id}:${month}`; const [loaded, setLoaded] = React.useState(null), [draftState, setDraftState] = React.useState(null); const [loading, setLoading] = React.useState(true), [saving, setSaving] = React.useState(false); const [loadError, setLoadError] = React.useState(''), [sharedError, setSharedError] = React.useState(''), [saveError, setSaveError] = React.useState(''), [notice, setNotice] = React.useState(''); const [retry, setRetry] = React.useState(0); const [preview, setPreview] = React.useState(null), [previewing, setPreviewing] = React.useState(false), [previewError, setPreviewError] = React.useState(''); const previewSequence = React.useRef(0), liveDraftSignature = React.useRef(''); const liveKey = React.useRef(key); liveKey.current = key; const generation = React.useRef(0); const draft = draftState?.key === key ? draftState.data : null; const plans = loaded?.key === key ? loaded : null; const draftSignature = JSON.stringify(draft); liveDraftSignature.current = draftSignature; const currentPreview = preview?.key === key && preview.signature === draftSignature ? preview.plan : null; React.useEffect(() => { let active = true; generation.current++; previewSequence.current++; setSaving(false); setPreviewing(false); setPreview(null); setPreviewError(''); const controller = new AbortController(), token = localStorage.getItem('ff_token'); setLoading(true); setLoadError(''); setSharedError(''); setSaveError(''); setNotice(''); setLoaded(null); setDraftState(null); Promise.allSettled([apiFetch(`/budgets/${month}`, { signal: controller.signal }), apiFetch(`/budgets/${month}/shared`, { signal: controller.signal })]).then(([own, shared]) => { if (!active || localStorage.getItem('ff_token') !== token) return; setLoaded({ key, own: own.status === 'fulfilled' ? own.value : null, shared: shared.status === 'fulfilled' ? shared.value : [] }); if (own.status === 'fulfilled') setDraftState({ key, data: FFBudgetDraft(own.value, members, currentMember.id, month) }); else if (own.reason.name !== 'AbortError') setLoadError(own.reason.message); if (shared.status === 'rejected' && shared.reason.name !== 'AbortError') setSharedError(shared.reason.message); setLoading(false); }); return () => { active = false; controller.abort(); }; }, [key, retry, members.map(m => m.id).join(':')]); function update(change) { previewSequence.current++; setPreviewing(false); setPreview(null); setPreviewError(''); setDraftState(s => s?.key === key ? { key, data: { ...s.data, ...change } } : s); setNotice(''); } function updateItem(index, change) { update({ items: draft.items.map((i, n) => n === index ? { ...i, ...change } : i) }); } async function calculatePreview() { if (previewing || saving || !draft) return; setPreviewError(''); setSaveError(''); let payload; try { payload = window.FFBudgetInput.budgetToPayload(draft); } catch (e) { setPreviewError(e.message); return; } const requestKey = key, signature = draftSignature, sequence = ++previewSequence.current, token = localStorage.getItem('ff_token'); setPreviewing(true); setPreview(null); try { const plan = await apiFetch(`/budgets/${month}/preview`, { method: 'POST', body: payload }); if (sequence === previewSequence.current && liveKey.current === requestKey && liveDraftSignature.current === signature && localStorage.getItem('ff_token') === token) setPreview({ key, signature, plan }); } catch (e) { if (sequence === previewSequence.current && liveKey.current === requestKey && localStorage.getItem('ff_token') === token) setPreviewError(e.message); } finally { if (sequence === previewSequence.current && liveKey.current === requestKey) setPreviewing(false); } } async function save(event) { event.preventDefault(); if (saving || !draft) return; if (!currentPreview) { setSaveError('Calcule e confira a prévia atual antes de salvar.'); return; } setSaveError(''); setNotice(''); let payload; try { payload = window.FFBudgetInput.budgetToPayload(draft); } catch (e) { setSaveError(e.message); return; } const requestGeneration = generation.current, requestKey = key, token = localStorage.getItem('ff_token'); setSaving(true); try { const plan = await apiFetch(`/budgets/${month}`, { method: 'PUT', body: payload }); if (generation.current !== requestGeneration || liveKey.current !== requestKey || localStorage.getItem('ff_token') !== token) return; setLoaded(s => ({ ...(s || {}), key, own: plan })); setDraftState({ key, data: FFBudgetDraft(plan, members, currentMember.id, month) }); setPreview(null); setNotice('Orçamento salvo. Nenhuma despesa ou cobrança foi criada.'); } catch (e) { if (generation.current === requestGeneration && liveKey.current === requestKey && localStorage.getItem('ff_token') === token) setSaveError(e.status === 409 ? 'O orçamento foi atualizado em outra tela. Seu rascunho está preservado. Recarregue a versão salva antes de editar novamente.' : e.message); } finally { if (generation.current === requestGeneration && liveKey.current === requestKey) setSaving(false); } } return

Orçamento mensal

Planejamento de {month.split('-').reverse().join('/')} · seu plano pode incluir qualquer membro da família.

Planeje com divisões flexíveis

Defina uma divisão padrão. Uma regra por categoria substitui esse padrão; uma divisão por item tem prioridade sobre as duas. Quem não participa fica fora daquela despesa. Salvar o orçamento não cria lançamentos, receitas ou dívidas, nem altera o limite de Dia a Dia.

{loading &&
Carregando orçamento…
} setRetry(r => r + 1)} /> {draft &&
update({ defaultShares })} disabled={saving} />

Regras por categoria

Exemplo: moradia para todos; educação apenas para quem participa, em percentuais diferentes.

{!draft.categoryRules.length &&

Sem regras específicas: os itens usam a divisão padrão.

} {draft.categoryRules.map((rule, index) =>
update({ categoryRules: draft.categoryRules.map((r, n) => n === index ? { ...r, shares } : r) })} disabled={saving} /> update({ categoryRules: draft.categoryRules.filter((r, n) => n !== index) })}>Remover regra {index + 1}
)} = categories.length} onClick={() => update({ categoryRules: [...draft.categoryRules, { categoryId: categories.find(c => !draft.categoryRules.some(r => r.categoryId === c.id))?.id || '', shares: draft.defaultShares.map(s => ({ ...s })) }] })}>Adicionar regra por categoria

Despesas previstas

{!draft.items.length &&

Nenhum item no plano. Inclua gastos fixos e despesas do Dia a Dia que deseja reservar no orçamento.

} {draft.items.map((item, index) =>
{item.customShares ? updateItem(index, { shares })} disabled={saving} /> :

{draft.categoryRules.some(r => r.categoryId === item.categoryId) ? 'Usa a divisão da categoria.' : 'Usa a divisão padrão.'}

} update({ items: draft.items.filter((i, n) => n !== index) })}>Remover item {index + 1}
)} update({ items: [...draft.items, { description: '', categoryId: '', amount: '', customShares: false, shares: draft.defaultShares.map(s => ({ ...s })) }] })}>Adicionar despesa prevista
{saveError.includes('outra tela') && { if (window.confirm('Recarregar a versão salva descarta seu rascunho atual. Continuar?')) setRetry(r => r + 1); }}>Recarregar versão salva} {notice &&
{notice}
} {previewing ? 'Calculando…' : 'Calcular prévia'} {previewing &&
Calculando a divisão do orçamento…
} {currentPreview &&

Prévia não salva. Confira participantes, percentuais e valores antes de confirmar.

}

Calcule a prévia para conferir os valores. Qualquer alteração exige uma nova prévia antes de salvar.

{saving ? 'Salvando…' : 'Salvar orçamento'}
} {plans?.own && }

Planos compartilhados comigo

setRetry(r => r + 1)} /> {!loading && !sharedError && plans && !plans.shared.length &&

Nenhum plano de outro membro inclui você neste mês.

} {(plans?.shared || []).map(plan => )}
; } Object.assign(window, { FFSpendingReports, OrcamentoMensal });