import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { liabilityAccountService } from '../services/api'; import { useToast } from '../components/Toast'; import { ConfirmModal } from '../components/Modal'; import { useFormatters } from '../hooks'; const LiabilityAccounts = () => { const { t } = useTranslation(); const toast = useToast(); const { currency: formatCurrency } = useFormatters(); // Mobile detection const [isMobile, setIsMobile] = useState(window.innerWidth < 768); useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth < 768); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // States const [accounts, setAccounts] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [showImportModal, setShowImportModal] = useState(false); const [showDetailModal, setShowDetailModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showReconcileModal, setShowReconcileModal] = useState(false); const [selectedAccount, setSelectedAccount] = useState(null); const [selectedInstallment, setSelectedInstallment] = useState(null); const [eligibleTransactions, setEligibleTransactions] = useState([]); const [loadingTransactions, setLoadingTransactions] = useState(false); const [reconcileSearch, setReconcileSearch] = useState(''); const [markAsPaidOnReconcile, setMarkAsPaidOnReconcile] = useState(true); const [saving, setSaving] = useState(false); const [filter, setFilter] = useState({ status: '', is_active: '' }); const [showPriceAnalysisModal, setShowPriceAnalysisModal] = useState(false); // Import form state const [importForm, setImportForm] = useState({ file: null, name: '', creditor: '', contract_number: '', currency: 'EUR', description: '', }); useEffect(() => { loadAccounts(); }, [filter]); const loadAccounts = async () => { try { setLoading(true); const params = {}; if (filter.status) params.status = filter.status; if (filter.is_active !== '') params.is_active = filter.is_active; const response = await liabilityAccountService.getAll(params); if (response.success) { setAccounts(response.data); setSummary(response.summary); } } catch (error) { toast.error(t('liabilities.loadError')); } finally { setLoading(false); } }; const handleOpenImportModal = () => { setImportForm({ file: null, name: '', creditor: '', contract_number: '', currency: 'EUR', description: '', }); setShowImportModal(true); }; const handleCloseImportModal = () => { setShowImportModal(false); }; const handleImportChange = (e) => { const { name, value, files } = e.target; if (name === 'file') { setImportForm(prev => ({ ...prev, file: files[0] })); } else { setImportForm(prev => ({ ...prev, [name]: value })); } }; const handleImportSubmit = async (e) => { e.preventDefault(); if (!importForm.file) { toast.error(t('liabilities.selectFile')); return; } if (!importForm.name.trim()) { toast.error(t('validation.required')); return; } setSaving(true); try { const formData = new FormData(); formData.append('file', importForm.file); formData.append('name', importForm.name); formData.append('creditor', importForm.creditor); formData.append('contract_number', importForm.contract_number); formData.append('currency', importForm.currency); formData.append('description', importForm.description); const response = await liabilityAccountService.import(formData); if (response.success) { toast.success(t('liabilities.importSuccess', { count: response.imported_installments })); handleCloseImportModal(); loadAccounts(); // Abrir detalhes do contrato importado setSelectedAccount(response.data); setShowDetailModal(true); } } catch (error) { toast.error(error.response?.data?.message || t('liabilities.importError')); } finally { setSaving(false); } }; const handleOpenDetail = async (account) => { try { const response = await liabilityAccountService.getById(account.id); if (response.success) { setSelectedAccount(response.data); setShowDetailModal(true); } } catch (error) { toast.error(t('liabilities.loadError')); } }; const handleCloseDetail = () => { setShowDetailModal(false); setSelectedAccount(null); }; const handleDelete = async () => { if (!selectedAccount) return; setSaving(true); try { const response = await liabilityAccountService.delete(selectedAccount.id); if (response.success) { toast.success(t('liabilities.deleteSuccess')); setShowDeleteModal(false); setSelectedAccount(null); loadAccounts(); } } catch (error) { toast.error(t('liabilities.deleteError')); } finally { setSaving(false); } }; const handleMarkInstallmentPaid = async (installment) => { try { const response = await liabilityAccountService.updateInstallment( selectedAccount.id, installment.id, { status: 'paid' } ); if (response.success) { toast.success(t('liabilities.installmentPaid')); // Recarregar conta atualizada const accountResponse = await liabilityAccountService.getById(selectedAccount.id); if (accountResponse.success) { setSelectedAccount(accountResponse.data); } loadAccounts(); } } catch (error) { toast.error(t('liabilities.updateError')); } }; // ============================================ // Conciliação de Parcelas // ============================================ const handleOpenReconcileModal = async (installment) => { setSelectedInstallment(installment); setReconcileSearch(''); setMarkAsPaidOnReconcile(true); setShowReconcileModal(true); await loadEligibleTransactions(installment); }; const handleCloseReconcileModal = () => { setShowReconcileModal(false); setSelectedInstallment(null); setEligibleTransactions([]); }; const loadEligibleTransactions = async (installment, search = '') => { setLoadingTransactions(true); try { const params = {}; if (search) params.search = search; const response = await liabilityAccountService.getEligibleTransactions( selectedAccount.id, installment.id, params ); if (response.success) { setEligibleTransactions(response.data); } } catch (error) { toast.error(t('liabilities.loadError')); } finally { setLoadingTransactions(false); } }; const handleReconcileSearch = async (e) => { const value = e.target.value; setReconcileSearch(value); // Debounce search if (value.length >= 2 || value.length === 0) { await loadEligibleTransactions(selectedInstallment, value); } }; const handleReconcile = async (transaction) => { setSaving(true); try { const response = await liabilityAccountService.reconcile( selectedAccount.id, selectedInstallment.id, transaction.id, markAsPaidOnReconcile ); if (response.success) { toast.success(t('liabilities.reconcileSuccess')); handleCloseReconcileModal(); // Recarregar conta atualizada const accountResponse = await liabilityAccountService.getById(selectedAccount.id); if (accountResponse.success) { setSelectedAccount(accountResponse.data); } loadAccounts(); } } catch (error) { toast.error(error.response?.data?.message || t('liabilities.reconcileError')); } finally { setSaving(false); } }; const handleUnreconcile = async (installment) => { if (!confirm(t('liabilities.unreconcile') + '?')) return; setSaving(true); try { const response = await liabilityAccountService.unreconcile( selectedAccount.id, installment.id ); if (response.success) { toast.success(t('liabilities.unreconcileSuccess')); // Recarregar conta atualizada const accountResponse = await liabilityAccountService.getById(selectedAccount.id); if (accountResponse.success) { setSelectedAccount(accountResponse.data); } loadAccounts(); } } catch (error) { toast.error(t('liabilities.unreconcileError')); } finally { setSaving(false); } }; const formatPercent = (value) => { if (value === null || value === undefined) return '-'; return `${parseFloat(value).toFixed(2)}%`; }; const formatDate = (dateString) => { if (!dateString) return '-'; return new Date(dateString).toLocaleDateString(); }; const getStatusBadge = (status) => { const badges = { active: 'bg-primary', paid_off: 'bg-success', defaulted: 'bg-danger', renegotiated: 'bg-warning text-dark', pending: 'bg-secondary', paid: 'bg-success', partial: 'bg-info', overdue: 'bg-danger', cancelled: 'bg-dark', }; return badges[status] || 'bg-secondary'; }; const getStatusLabel = (status, isInstallment = false) => { const labels = isInstallment ? liabilityAccountService.installmentStatuses : liabilityAccountService.statuses; return labels[status] || status; }; // Calcular totais por moeda const getTotalsByCurrency = () => { const totals = {}; accounts.forEach(account => { const currency = account.currency || 'EUR'; if (!totals[currency]) { totals[currency] = { principal: 0, paid: 0, pending: 0, interest: 0, }; } totals[currency].principal += parseFloat(account.principal_amount) || 0; totals[currency].paid += parseFloat(account.total_paid) || 0; totals[currency].pending += parseFloat(account.total_pending) || 0; totals[currency].interest += parseFloat(account.total_interest) || 0; }); return totals; }; const totalsByCurrency = getTotalsByCurrency(); return (
{t('liabilities.importHint')}
| # | {t('liabilities.dueDate')} | {t('liabilities.installmentAmount')} | {t('liabilities.paidAmount')} | {t('liabilities.capital')} | {t('liabilities.interest')} | {t('liabilities.fees')} | {t('common.status')} | {t('liabilities.reconciliation')} | |
|---|---|---|---|---|---|---|---|---|---|
| {inst.installment_number} | {formatDate(inst.due_date)} | {formatCurrency(inst.installment_amount, selectedAccount.currency)} | {inst.paid_amount > 0 ? ( inst.installment_amount ? 'text-warning' : ''}> {formatCurrency(inst.paid_amount, selectedAccount.currency)} {inst.paid_amount > inst.installment_amount && ( )} ) : '-'} | {formatCurrency(inst.principal_amount, selectedAccount.currency)} | {formatCurrency(inst.interest_amount, selectedAccount.currency)} | {inst.fee_amount > 0 ? formatCurrency(inst.fee_amount, selectedAccount.currency) : '-'} | {getStatusLabel(inst.status, true)} | {inst.reconciled_transaction_id ? ( {t('liabilities.reconciled')} ) : ( {t('liabilities.notReconciled')} )} |
{inst.status !== 'paid' && !inst.reconciled_transaction_id && (
)}
{!inst.reconciled_transaction_id ? (
) : (
)}
|
| {t('common.total')} | {formatCurrency(selectedAccount.total_contract_value, selectedAccount.currency)} | {formatCurrency(selectedAccount.principal_amount, selectedAccount.currency)} | {formatCurrency(selectedAccount.total_interest, selectedAccount.currency)} | {formatCurrency(selectedAccount.total_fees, selectedAccount.currency)} | |||||
| {t('transactions.date')} | {t('transactions.description')} | {t('transactions.account')} | {t('transactions.amount')} | |
|---|---|---|---|---|
| {formatDate(tx.effective_date || tx.planned_date)} |
{tx.description || tx.original_description}
|
{tx.account?.name || '-'} | {formatCurrency(Math.abs(tx.amount), tx.account?.currency)} |
{t('liabilities.priceOverviewText')}
{t('liabilities.whatIsPriceText')}
{t('liabilities.priceWhere')}:
PMT = {t('liabilities.pricePMT')}PV = {t('liabilities.pricePV')}i = {t('liabilities.priceI')}n = {t('liabilities.priceN')}| # | {t('liabilities.installmentValue')} | {t('liabilities.interest')} | {t('liabilities.amortization')} |
|---|---|---|---|
| {selectedAccount.installments[0].installment_number} | {formatCurrency(selectedAccount.installments[0].installment_amount, selectedAccount.currency)} | {formatCurrency(selectedAccount.installments[0].interest_amount, selectedAccount.currency)} | {formatCurrency(selectedAccount.installments[0].principal_amount, selectedAccount.currency)} |
| {midInst.installment_number} | {formatCurrency(midInst.installment_amount, selectedAccount.currency)} | {formatCurrency(midInst.interest_amount, selectedAccount.currency)} | {formatCurrency(midInst.principal_amount, selectedAccount.currency)} |
| {selectedAccount.installments[selectedAccount.installments.length - 1].installment_number} | {formatCurrency(selectedAccount.installments[selectedAccount.installments.length - 1].installment_amount, selectedAccount.currency)} | {formatCurrency(selectedAccount.installments[selectedAccount.installments.length - 1].interest_amount, selectedAccount.currency)} | {formatCurrency(selectedAccount.installments[selectedAccount.installments.length - 1].principal_amount, selectedAccount.currency)} |
{t('liabilities.contractCostText')}