}
>;
}
function memberOperationPeriodSummary(form) {
const cents = memberOperationCents(form.amount);
if (form.type === 'installment') {
const n = Number(form.installments);
if (!cents || !Number.isInteger(n) || n < 2 || n > 60) return 'Informe o valor total e de 2 a 60 parcelas.';
const base = Math.floor(cents / n), remainder = cents % n;
const parts = [];
if (remainder) parts.push(`${remainder} parcela${remainder > 1 ? 's' : ''} de ${fmtBRL((base + 1) / 100)}`);
if (n - remainder) parts.push(`${n - remainder} parcela${n - remainder > 1 ? 's' : ''} de ${fmtBRL(base / 100)}`);
return `${n} parcelas mensais: ${parts.join(' + ')}. Total ${fmtBRL(cents / 100)}.`;
}
if (form.type === 'fixed') {
const frequency = form.frequency === 'weekly' ? 'semana' : form.frequency === 'biweekly' ? 'duas semanas' : 'mês';
if (form.durationChoice === 'open') return `${fmtBRL((cents || 0) / 100)} a cada ${frequency}, sem data de término. As próximas ocorrências são mantidas até encerrar a série.`;
const count = fixedOccurrenceCount(form.frequency, form.durationChoice === 'custom' ? form.durationMonths : form.durationChoice);
return `${fmtBRL((cents || 0) / 100)} a cada ${frequency}${count ? `, ${count} ocorrências` : ', escolha a duração'}.`;
}
return `Obrigação única de ${fmtBRL((cents || 0) / 100)}, sem meio de pagamento.`;
}
function MemberOperationForm({ expense, initialDraft = {}, creditorLocked = false, conversionEligibility, onClose, onSaved, onBusyChange = () => {} }) {
const { currentMember, members, createMemberOperation, convertExpenseToMemberOperation } = useApp();
const [form, setForm] = React.useState({ description: expense?.desc || initialDraft.desc || '', amount: expense ? String(expense.value) : String(initialDraft.value || ''), date: expense?.date || initialDraft.date || todayLocalISO(), dueDate: '', kind: 'other', debtorId: conversionEligibility?.debtorId || expense?.responsibleMemberId || (initialDraft.responsibleMemberId !== currentMember.id ? initialDraft.responsibleMemberId : '') || (creditorLocked ? '' : currentMember.id), creditorId: conversionEligibility?.creditorId || (expense || creditorLocked ? currentMember.id : ''), type: initialDraft.type || 'single', installments: initialDraft.installments || 2, frequency: initialDraft.frequency || 'monthly', durationChoice: initialDraft.durationChoice || '', durationMonths: initialDraft.durationMonths || '', scope: conversionEligibility ? (['occurrence','future','series'].find(scope => conversionEligibility.scopes?.[scope]?.eligible) || 'occurrence') : 'occurrence' });
const [error, setError] = React.useState('');
const [busy, setBusy] = React.useState(false);
const [confirmNoPaymentUse, setConfirmNoPaymentUse] = React.useState(false);
const [confirmPreserveReceipts, setConfirmPreserveReceipts] = React.useState(false);
const set = (key, value) => { setForm(previous => ({ ...previous, [key]: value })); if (key === 'scope') setConfirmPreserveReceipts(false); setError(''); };
const participant = form.debtorId === currentMember.id || form.creditorId === currentMember.id;
const selectedScope = conversionEligibility?.scopes?.[form.scope];
const selectedOccurrences = selectedScope?.occurrences || [];
const selectedCount = selectedScope?.count ?? selectedOccurrences.length;
const selectedTotalCents = selectedScope?.amountCents ?? 0;
async function save(event) {
event.preventDefault();
if (busy) return;
if (expense && (!conversionEligibility?.eligible || !conversionEligibility.scopes?.[form.scope]?.eligible)) return setError(conversionEligibility?.scopes?.[form.scope]?.reason || conversionEligibility?.reason || 'Esta conversão não é permitida.');
const amountCents = memberOperationCents(form.amount);
if (!form.description.trim() || !amountCents || !form.date || !form.debtorId || !form.creditorId) return setError('Preencha descrição, valor, data e as duas pessoas.');
if (form.debtorId === form.creditorId) return setError('Quem paga e quem recebe devem ser pessoas diferentes.');
if (expense && selectedScope?.requiresPreserveReceiptsConfirmation && !confirmPreserveReceipts) return setError('Confirme a preservação dos recebimentos existentes.');
if (expense && !confirmNoPaymentUse) return setError('Confirme que não houve compra real no meio de pagamento do credor.');
if (!participant) return setError('Você precisa ser quem paga ou quem recebe este valor.');
if (!expense && form.type === 'installment' && (!Number.isInteger(Number(form.installments)) || Number(form.installments) < 2 || Number(form.installments) > 60)) return setError('Informe de 2 a 60 parcelas.');
if (!expense && form.type === 'installment' && amountCents < Number(form.installments)) return setError('O total deve permitir ao menos R$ 0,01 por parcela.');
if (!expense && form.type === 'fixed') { const issue = validateFixedDuration(form.frequency, form.durationChoice, form.durationMonths); if (issue) return setError(issue); }
const period = { type: form.type };
if (form.type === 'installment') period.installments = Number(form.installments);
if (form.type === 'fixed') { period.frequency = form.frequency; period.openEnded = form.durationChoice === 'open'; if (!period.openEnded) period.occurrenceCount = fixedOccurrenceCount(form.frequency, form.durationChoice === 'custom' ? form.durationMonths : form.durationChoice); }
setBusy(true); onBusyChange(true); setError('');
try {
const result = expense
? await convertExpenseToMemberOperation(expense.id, { debtorId: form.debtorId, creditorId: form.creditorId, kind: form.kind, scope: form.scope, confirmNoPaymentUse: true, ...(selectedScope.requiresPreserveReceiptsConfirmation ? { confirmPreserveReceipts: true } : {}) })
: await createMemberOperation({ description: form.description.trim(), amountCents, date: form.date, dueDate: form.dueDate || undefined, kind: form.kind, debtorId: form.debtorId, creditorId: form.creditorId, ...period });
if (!result?.success) { setError(result?.message || 'Não foi possível salvar. Tente novamente.'); return; }
onSaved?.(result.operation); onClose();
} catch (failure) { setError(failure.message || 'Falha de conexão. Tente novamente.'); }
finally { setBusy(false); onBusyChange(false); }
}
const name = id => members.find(member => member.id === id)?.name || 'Selecione uma pessoa';
return ;
}
function MemberOperationReceiptForm({ operation, onClose, onBusyChange = () => {} }) {
const { recordMemberOperationReceipt } = useApp();
const [amount, setAmount] = React.useState((operation.remainingCents / 100).toFixed(2));
const [date, setDate] = React.useState(todayLocalISO());
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState('');
async function save(event) {
event.preventDefault(); if (busy) return;
const amountCents = memberOperationCents(amount);
if (!amountCents || amountCents > operation.remainingCents) return setError('O recebimento deve ser maior que zero e não pode ultrapassar o valor em aberto.');
setBusy(true); onBusyChange(true); setError('');
try { const result = await recordMemberOperationReceipt(operation.id, { amountCents, date }); if (!result?.success) setError(result?.message || 'Não foi possível registrar.'); else onClose(); }
catch (failure) { setError(failure.message || 'Falha de conexão. Tente novamente.'); }
finally { setBusy(false); onBusyChange(false); }
}
return ;
}
function MemberOperations() {
const { currentMember, memberOperations = [], memberOperationsLoading, memberOperationsError, refreshMemberOperations, cancelMemberOperation, reverseMemberOperationReceipt, closeMemberOperationSeries } = useApp();
const [showForm, setShowForm] = React.useState(false);
const [formBusy, setFormBusy] = React.useState(false);
const [receipt, setReceipt] = React.useState(null);
const [receiptBusy, setReceiptBusy] = React.useState(false);
const [confirmation, setConfirmation] = React.useState(null);
const [reason, setReason] = React.useState('');
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState('');
const [filter, setFilter] = React.useState('open');
React.useEffect(() => { refreshMemberOperations().catch(() => {}); }, []);
const mine = memberOperations.filter(operation => operation.debtorId === currentMember.id || operation.creditorId === currentMember.id);
const shown = mine.filter(operation => filter === 'all' || (operation.status !== 'cancelled' && operation.remainingCents > 0));
async function retry() { try { await refreshMemberOperations(); } catch (_) {} }
async function confirm(event) {
event.preventDefault(); if (busy || !reason.trim()) return;
setBusy(true); setError('');
try { const result = confirmation.seriesId ? await closeMemberOperationSeries(confirmation.seriesId, { reason: reason.trim() }) : confirmation.receiptId ? await reverseMemberOperationReceipt(confirmation.operation.id, confirmation.receiptId, { reason: reason.trim() }) : await cancelMemberOperation(confirmation.operation.id, { reason: reason.trim() }); if (!result?.success) setError(result?.message || 'Não foi possível concluir.'); else { setConfirmation(null); setReason(''); } }
catch (failure) { setError(failure.message || 'Falha de conexão. Tente novamente.'); }
finally { setBusy(false); }
}
const amountLine = (label, cents) =>
{label}{fmtBRL(cents / 100)}
;
return
Valores entre membros
Vendas, serviços, empréstimos e outros valores a pagar ou receber, sem uso de cartão.
setShowForm(true)}>Novo valor
Cada lançamento tem uma origem única. Quem paga vê a despesa; quem recebe vê a receita prevista. Somente o credor confirma o recebimento, inclusive parcial. Os valores abaixo abrangem todos os meses.