diff --git a/public/locales/ar.json b/public/locales/ar.json index bb06e0e..c319ce2 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -609,7 +609,15 @@ "newCategoryTitle": "فئة جديدة", "newCategoryPlaceholder": "اسم الفئة", "newSubcategoryTitle": "فئة فرعية جديدة", - "newSubcategoryPlaceholder": "اسم الفئة الفرعية" + "newSubcategoryPlaceholder": "اسم الفئة الفرعية", + "loanStatusFilterLabel": "فلتر حالة القرض", + "loanStatusActive": "نشطة", + "loanStatusPaid": "مدفوعة", + "loanStatusAll": "الكل", + "loanTransactions": "معاملات القرض", + "loanInstallmentNumber": "القسط {{number}} من {{total}}", + "loanReportTitle": "تقرير القرض", + "loanNoTransactions": "لم يتم تسجيل أي دفعات بعد." }, "settings": { "title": "الإعدادات", diff --git a/public/locales/de.json b/public/locales/de.json index 8600a5d..11793da 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -634,7 +634,15 @@ "newCategoryTitle": "Neue Kategorie", "newCategoryPlaceholder": "Kategoriename", "newSubcategoryTitle": "Neue Unterkategorie", - "newSubcategoryPlaceholder": "Name der Unterkategorie" + "newSubcategoryPlaceholder": "Name der Unterkategorie", + "loanStatusFilterLabel": "Darlehensstatus filtern", + "loanStatusActive": "Aktiv", + "loanStatusPaid": "Bezahlt", + "loanStatusAll": "Alle", + "loanTransactions": "Darlehenstransaktionen", + "loanInstallmentNumber": "Rate {{number}} von {{total}}", + "loanReportTitle": "Darlehensbericht", + "loanNoTransactions": "Noch keine Zahlungen erfasst." }, "settings": { "title": "Einstellungen", diff --git a/public/locales/el.json b/public/locales/el.json index 84cce00..f570d37 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Νέα κατηγορία", "newCategoryPlaceholder": "Όνομα κατηγορίας", "newSubcategoryTitle": "Νέα υποκατηγορία", - "newSubcategoryPlaceholder": "Όνομα υποκατηγορίας" + "newSubcategoryPlaceholder": "Όνομα υποκατηγορίας", + "loanStatusFilterLabel": "Φίλτρο κατάστασης δανείων", + "loanStatusActive": "Ενεργά", + "loanStatusPaid": "Πληρωμένα", + "loanStatusAll": "Όλα", + "loanTransactions": "Συναλλαγές δανείου", + "loanInstallmentNumber": "Δόση {{number}} από {{total}}", + "loanReportTitle": "Αναφορά δανείου", + "loanNoTransactions": "Δεν έχουν καταγραφεί πληρωμές ακόμα." }, "settings": { "title": "Ρυθμίσεις", diff --git a/public/locales/en.json b/public/locales/en.json index 0e65b43..d11437c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -609,7 +609,15 @@ "newCategoryTitle": "New category", "newCategoryPlaceholder": "Category name", "newSubcategoryTitle": "New subcategory", - "newSubcategoryPlaceholder": "Subcategory name" + "newSubcategoryPlaceholder": "Subcategory name", + "loanStatusFilterLabel": "Loan status filter", + "loanStatusActive": "Active", + "loanStatusPaid": "Paid", + "loanStatusAll": "All", + "loanTransactions": "Loan transactions", + "loanInstallmentNumber": "Installment {{number}} of {{total}}", + "loanReportTitle": "Loan report", + "loanNoTransactions": "No payments recorded yet." }, "settings": { "title": "Settings", diff --git a/public/locales/es.json b/public/locales/es.json index 00bdbc1..26e044e 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Nueva categoría", "newCategoryPlaceholder": "Nombre de la categoría", "newSubcategoryTitle": "Nueva subcategoría", - "newSubcategoryPlaceholder": "Nombre de la subcategoría" + "newSubcategoryPlaceholder": "Nombre de la subcategoría", + "loanStatusFilterLabel": "Filtro de estado de préstamos", + "loanStatusActive": "Activos", + "loanStatusPaid": "Pagados", + "loanStatusAll": "Todos", + "loanTransactions": "Transacciones del préstamo", + "loanInstallmentNumber": "Cuota {{number}} de {{total}}", + "loanReportTitle": "Informe del préstamo", + "loanNoTransactions": "Aún no hay pagos registrados." }, "settings": { "title": "Ajustes", diff --git a/public/locales/fr.json b/public/locales/fr.json index bd02cdf..e1e3438 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Nouvelle catégorie", "newCategoryPlaceholder": "Nom de la catégorie", "newSubcategoryTitle": "Nouvelle sous-catégorie", - "newSubcategoryPlaceholder": "Nom de la sous-catégorie" + "newSubcategoryPlaceholder": "Nom de la sous-catégorie", + "loanStatusFilterLabel": "Filtre de statut des prêts", + "loanStatusActive": "Actifs", + "loanStatusPaid": "Payés", + "loanStatusAll": "Tous", + "loanTransactions": "Transactions du prêt", + "loanInstallmentNumber": "Échéance {{number}} sur {{total}}", + "loanReportTitle": "Rapport du prêt", + "loanNoTransactions": "Aucun paiement enregistré." }, "settings": { "title": "Paramètres", diff --git a/public/locales/hi.json b/public/locales/hi.json index 12d81b9..91df594 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -609,7 +609,15 @@ "newCategoryTitle": "नई श्रेणी", "newCategoryPlaceholder": "श्रेणी का नाम", "newSubcategoryTitle": "नई उपश्रेणी", - "newSubcategoryPlaceholder": "उपश्रेणी का नाम" + "newSubcategoryPlaceholder": "उपश्रेणी का नाम", + "loanStatusFilterLabel": "उधार स्थिति फ़िल्टर", + "loanStatusActive": "सक्रिय", + "loanStatusPaid": "चुकाया गया", + "loanStatusAll": "सभी", + "loanTransactions": "उधार लेन-देन", + "loanInstallmentNumber": "{{total}} में से किस्त {{number}}", + "loanReportTitle": "उधार रिपोर्ट", + "loanNoTransactions": "अभी कोई भुगतान दर्ज नहीं है।" }, "settings": { "title": "सेटिंग्स", diff --git a/public/locales/it.json b/public/locales/it.json index c5925a6..cc01c3a 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Nuova categoria", "newCategoryPlaceholder": "Nome categoria", "newSubcategoryTitle": "Nuova sottocategoria", - "newSubcategoryPlaceholder": "Nome sottocategoria" + "newSubcategoryPlaceholder": "Nome sottocategoria", + "loanStatusFilterLabel": "Filtro stato prestiti", + "loanStatusActive": "Attivi", + "loanStatusPaid": "Pagati", + "loanStatusAll": "Tutti", + "loanTransactions": "Movimenti del prestito", + "loanInstallmentNumber": "Rata {{number}} di {{total}}", + "loanReportTitle": "Report del prestito", + "loanNoTransactions": "Nessun pagamento registrato." }, "settings": { "title": "Impostazioni", diff --git a/public/locales/ja.json b/public/locales/ja.json index e9936dd..bcc9bc6 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -609,7 +609,15 @@ "newCategoryTitle": "新しいカテゴリ", "newCategoryPlaceholder": "カテゴリ名", "newSubcategoryTitle": "新しいサブカテゴリ", - "newSubcategoryPlaceholder": "サブカテゴリ名" + "newSubcategoryPlaceholder": "サブカテゴリ名", + "loanStatusFilterLabel": "貸付ステータスフィルター", + "loanStatusActive": "進行中", + "loanStatusPaid": "完済", + "loanStatusAll": "すべて", + "loanTransactions": "貸付取引", + "loanInstallmentNumber": "{{total}} 回中 {{number}} 回目", + "loanReportTitle": "貸付レポート", + "loanNoTransactions": "返済はまだ記録されていません。" }, "settings": { "title": "設定", diff --git a/public/locales/pt.json b/public/locales/pt.json index 7e8130b..783ca78 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Nova categoria", "newCategoryPlaceholder": "Nome da categoria", "newSubcategoryTitle": "Nova subcategoria", - "newSubcategoryPlaceholder": "Nome da subcategoria" + "newSubcategoryPlaceholder": "Nome da subcategoria", + "loanStatusFilterLabel": "Filtro de status dos empréstimos", + "loanStatusActive": "Ativos", + "loanStatusPaid": "Pagos", + "loanStatusAll": "Todos", + "loanTransactions": "Transações do empréstimo", + "loanInstallmentNumber": "Parcela {{number}} de {{total}}", + "loanReportTitle": "Relatório do empréstimo", + "loanNoTransactions": "Nenhum pagamento registrado ainda." }, "settings": { "title": "Configurações", diff --git a/public/locales/ru.json b/public/locales/ru.json index e82839a..7dac1a0 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Новая категория", "newCategoryPlaceholder": "Название категории", "newSubcategoryTitle": "Новая подкатегория", - "newSubcategoryPlaceholder": "Название подкатегории" + "newSubcategoryPlaceholder": "Название подкатегории", + "loanStatusFilterLabel": "Фильтр статуса займов", + "loanStatusActive": "Активные", + "loanStatusPaid": "Оплаченные", + "loanStatusAll": "Все", + "loanTransactions": "Операции займа", + "loanInstallmentNumber": "Платёж {{number}} из {{total}}", + "loanReportTitle": "Отчёт по займу", + "loanNoTransactions": "Платежи ещё не записаны." }, "settings": { "title": "Настройки", diff --git a/public/locales/sv.json b/public/locales/sv.json index 2433464..d3c9234 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Ny kategori", "newCategoryPlaceholder": "Kategorinamn", "newSubcategoryTitle": "Ny underkategori", - "newSubcategoryPlaceholder": "Underkategorinamn" + "newSubcategoryPlaceholder": "Underkategorinamn", + "loanStatusFilterLabel": "Filter för lånestatus", + "loanStatusActive": "Aktiva", + "loanStatusPaid": "Betalda", + "loanStatusAll": "Alla", + "loanTransactions": "Lånetransaktioner", + "loanInstallmentNumber": "Delbetalning {{number}} av {{total}}", + "loanReportTitle": "Lånrapport", + "loanNoTransactions": "Inga betalningar registrerade ännu." }, "settings": { "title": "Inställningar", diff --git a/public/locales/tr.json b/public/locales/tr.json index 21d8586..6eba08c 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Yeni kategori", "newCategoryPlaceholder": "Kategori adı", "newSubcategoryTitle": "Yeni alt kategori", - "newSubcategoryPlaceholder": "Alt kategori adı" + "newSubcategoryPlaceholder": "Alt kategori adı", + "loanStatusFilterLabel": "Borç durumu filtresi", + "loanStatusActive": "Aktif", + "loanStatusPaid": "Ödendi", + "loanStatusAll": "Tümü", + "loanTransactions": "Borç işlemleri", + "loanInstallmentNumber": "{{total}} taksitten {{number}}.", + "loanReportTitle": "Borç raporu", + "loanNoTransactions": "Henüz ödeme kaydedilmedi." }, "settings": { "title": "Ayarlar", diff --git a/public/locales/uk.json b/public/locales/uk.json index 7c5d60c..f41aeae 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -609,7 +609,15 @@ "newCategoryTitle": "Нова категорія", "newCategoryPlaceholder": "Назва категорії", "newSubcategoryTitle": "Нова підкатегорія", - "newSubcategoryPlaceholder": "Назва підкатегорії" + "newSubcategoryPlaceholder": "Назва підкатегорії", + "loanStatusFilterLabel": "Фільтр статусу позик", + "loanStatusActive": "Активні", + "loanStatusPaid": "Сплачені", + "loanStatusAll": "Усі", + "loanTransactions": "Операції позики", + "loanInstallmentNumber": "Платіж {{number}} з {{total}}", + "loanReportTitle": "Звіт по позиці", + "loanNoTransactions": "Платежі ще не записано." }, "settings": { "title": "Налаштування", diff --git a/public/locales/zh.json b/public/locales/zh.json index 59204a9..285a9d4 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -609,7 +609,15 @@ "newCategoryTitle": "新类别", "newCategoryPlaceholder": "类别名称", "newSubcategoryTitle": "新子类别", - "newSubcategoryPlaceholder": "子类别名称" + "newSubcategoryPlaceholder": "子类别名称", + "loanStatusFilterLabel": "借款状态筛选", + "loanStatusActive": "进行中", + "loanStatusPaid": "已还清", + "loanStatusAll": "全部", + "loanTransactions": "借款交易", + "loanInstallmentNumber": "第 {{number}} / {{total}} 期", + "loanReportTitle": "借款报告", + "loanNoTransactions": "尚未记录还款。" }, "settings": { "title": "设置", diff --git a/public/pages/budget.js b/public/pages/budget.js index ef7f8bf..5259966 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -127,6 +127,7 @@ let state = { loans: { loans: [], summary: { active_count: 0, remaining_amount: 0, remaining_installments: 0 } }, activeTab: 'budget', loanFilterId: null, + loanStatusFilter: 'active', currency: 'EUR', meta: { expenseCategories: [], incomeCategories: [], expenseSubcategories: {} }, }; @@ -486,7 +487,7 @@ function renderLoansDashboard() { if (!loans.length) return ''; const summary = state.loans?.summary ?? {}; - const activeLoans = loans.filter((loan) => loan.status === 'active'); + const visibleLoans = filteredLoans(); return `
@@ -498,6 +499,14 @@ function renderLoansDashboard() { amount: formatAmount(summary.remaining_amount ?? 0), })} +
+ + + +
@@ -513,17 +522,55 @@ function renderLoansDashboard() { ${formatAmount(summary.paid_amount ?? 0)}
- ${activeLoans.length ? ` + ${visibleLoans.length ? `
- ${activeLoans.map(renderLoanCard).join('')} + ${visibleLoans.map(renderLoanCard).join('')}
` : `
${t('budget.loansEmpty')}
`} + ${renderLoanTransactions(visibleLoans)}
`; } +function filteredLoans() { + const loans = state.loans?.loans ?? []; + if (state.loanStatusFilter === 'all') return loans; + return loans.filter((loan) => loan.status === state.loanStatusFilter); +} + +function loanPaymentsFor(loans) { + return loans.flatMap((loan) => (loan.payments ?? []).map((payment) => ({ ...payment, loan }))) + .sort((a, b) => new Date(b.paid_date) - new Date(a.paid_date) || b.installment_number - a.installment_number); +} + +function renderLoanTransactions(loans) { + const payments = loanPaymentsFor(loans); + if (!payments.length) return ''; + + return `
+
${t('budget.loanTransactions')}
+
+ ${payments.map(({ loan, ...payment }) => ` +
+
+ ${esc(loan.title)} + ${esc(loan.borrower)} · ${t('budget.loanInstallmentNumber', { + number: payment.installment_number, + total: loan.installment_count, + })} +
+
+ ${formatAmount(payment.amount)} + ${formatEntryDate(payment.paid_date)} +
+
+ `).join('')} +
+
`; +} + function renderLoansPage() { const loans = state.loans?.loans ?? []; if (!loans.length) { @@ -547,6 +594,19 @@ function renderLoansPage() { function wireLoansPage() { _container.querySelector('#budget-empty-loan')?.addEventListener('click', () => openBudgetModal({ mode: 'create', initialType: 'loan' })); + _container.querySelectorAll('[data-loan-status]').forEach((btn) => { + btn.addEventListener('click', () => { + state.loanStatusFilter = btn.dataset.loanStatus; + renderBody(); + }); + }); + _container.querySelectorAll('.budget-loan-card[data-loan-id]').forEach((card) => { + card.addEventListener('click', (event) => { + if (event.target.closest('button, a')) return; + const loan = state.loans.loans.find((item) => item.id === parseInt(card.dataset.loanId, 10)); + if (loan) openLoanReport(loan); + }); + }); _container.querySelectorAll('[data-action="loan-pay"]').forEach((btn) => { btn.addEventListener('click', async () => { await markLoanPayment(parseInt(btn.dataset.id, 10)); @@ -573,13 +633,65 @@ function wireLoansPage() { }); } +function openLoanReport(loan) { + const payments = (loan.payments ?? []).slice() + .sort((a, b) => new Date(b.paid_date) - new Date(a.paid_date) || b.installment_number - a.installment_number); + const content = ` +
+
+
+
${esc(loan.borrower)}
+
${esc(loan.title)}
+
+ + ${loan.status === 'paid' ? t('budget.loanStatusPaid') : t('budget.loanStatusActive')} + +
+
+
${t('budget.loanAmountLabel')}${formatAmount(loan.total_amount)}
+
${t('budget.loanRemainingAmount')}${formatAmount(loan.remaining_amount)}
+
${t('budget.loanPaidAmount')}${formatAmount(loan.paid_amount)}
+
${t('budget.loanRemainingInstallments')}${loan.remaining_installments}
+
+
${t('budget.loanTransactions')}
+ ${payments.length ? ` +
+ ${payments.map((payment) => ` +
+
+ ${t('budget.loanInstallmentNumber', { number: payment.installment_number, total: loan.installment_count })} + ${formatEntryDate(payment.paid_date)} +
+
+ ${formatAmount(payment.amount)} +
+
+ `).join('')} +
+ ` : `
${t('budget.loanNoTransactions')}
`} +
+ `; + + openSharedModal({ + title: t('budget.loanReportTitle'), + content, + size: 'md', + onSave(panel) { + panel.querySelector('#loan-report-close')?.addEventListener('click', closeModal); + }, + }); +} + function renderLoanCard(loan) { const paidPct = Math.min(100, Math.round((loan.paid_amount / loan.total_amount) * 100)); const nextDue = loan.next_due_month ? formatMonthLabel(loan.next_due_month) : t('budget.loanPaidStatus'); const payDisabled = loan.remaining_installments <= 0 ? 'disabled' : ''; return ` -
+
${esc(loan.title)}
diff --git a/public/styles/budget.css b/public/styles/budget.css index bcf9582..f5ee47c 100644 --- a/public/styles/budget.css +++ b/public/styles/budget.css @@ -86,7 +86,7 @@ .budget-tab { min-height: 34px; padding: 0 var(--space-3); - border: 0; + border: 1px solid transparent; border-radius: var(--radius-xs); background: transparent; color: var(--color-text-secondary); @@ -95,11 +95,34 @@ font-weight: var(--font-weight-medium); } +.budget-tab[data-tab="budget"] { + color: var(--module-budget); +} + +.budget-tab[data-tab="loans"] { + color: var(--color-info); +} + +.budget-tab[data-tab="budget"]:not(.budget-tab--active) { + background: color-mix(in srgb, var(--module-budget) 10%, transparent); +} + +.budget-tab[data-tab="loans"]:not(.budget-tab--active) { + background: color-mix(in srgb, var(--color-info) 10%, transparent); +} + .budget-tab--active { - background: var(--module-accent); color: var(--color-text-on-accent); } +.budget-tab[data-tab="budget"].budget-tab--active { + background: var(--module-budget); +} + +.budget-tab[data-tab="loans"].budget-tab--active { + background: var(--color-info); +} + .budget-tab-panel { flex: 1; min-height: 0; @@ -269,8 +292,30 @@ margin-top: 2px; } -.budget-loans__add { - flex-shrink: 0; +.budget-loans__filters { + display: flex; + gap: var(--space-1); + padding: 2px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface-2); +} + +.budget-loans__filter { + min-height: 30px; + padding: 0 var(--space-2); + border: 0; + border-radius: var(--radius-xs); + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + font-size: var(--text-xs); + font-weight: var(--font-weight-medium); +} + +.budget-loans__filter--active { + background: var(--color-info); + color: var(--color-text-on-accent); } .budget-loans__stats { @@ -328,6 +373,13 @@ padding: var(--space-3); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); + cursor: pointer; + transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast); +} + +.budget-loan-card:hover { + border-color: var(--color-info); + background: var(--color-surface-2); } .budget-loan-card__main { @@ -402,6 +454,107 @@ gap: var(--space-2); } +.budget-loan-transactions { + margin-top: var(--space-4); + border-top: 1px solid var(--color-border); + padding-top: var(--space-3); +} + +.budget-loan-transactions__title, +.loan-report__section-title { + font-size: var(--text-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--space-2); +} + +.budget-loan-transactions__list, +.loan-report__transactions { + display: grid; + gap: var(--space-2); +} + +.budget-loan-transaction { + display: flex; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-3); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-sm); + background: var(--color-surface-2); +} + +.budget-loan-transaction span { + display: block; + margin-top: 2px; + color: var(--color-text-secondary); + font-size: var(--text-xs); +} + +.budget-loan-transaction > div:last-child { + text-align: right; +} + +.loan-report__hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-4); + border-radius: var(--radius-md); + background: var(--color-surface-2); +} + +.loan-report__borrower { + color: var(--color-text-secondary); + font-size: var(--text-sm); +} + +.loan-report__title { + font-size: var(--text-xl); + font-weight: var(--font-weight-bold); + margin-top: 2px; +} + +.loan-report__status { + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-full); + font-size: var(--text-xs); + font-weight: var(--font-weight-semibold); +} + +.loan-report__status--active { + background: var(--color-info); + color: var(--color-text-on-accent); +} + +.loan-report__status--paid { + background: var(--color-success); + color: var(--color-text-on-accent); +} + +.loan-report__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-2); + margin: var(--space-3) 0; +} + +.loan-report__grid > div { + padding: var(--space-3); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-sm); +} + +.loan-report__grid span { + display: block; + color: var(--color-text-secondary); + font-size: var(--text-xs); + margin-bottom: 2px; +} + .budget-page .form-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -641,10 +794,15 @@ } .amount-type-btn--loan.amount-type-btn--active { - background-color: var(--module-accent); + background-color: var(--module-budget); color: var(--color-text-on-accent); } +.modal-panel .js-entry-field[hidden], +.modal-panel #bm-loan-fields[hidden] { + display: none !important; +} + .budget-field-header { display: flex; align-items: center; diff --git a/server/routes/budget.js b/server/routes/budget.js index f914e09..ab403b2 100644 --- a/server/routes/budget.js +++ b/server/routes/budget.js @@ -889,6 +889,17 @@ router.put('/:id', (req, res) => { if (linkedPayment && amount !== undefined && Number(amount) <= 0) { return res.status(400).json({ error: 'Loan repayment entries must remain income.', code: 400 }); } + if (linkedPayment && amount !== undefined) { + const loan = db.get().prepare('SELECT total_amount FROM budget_loans WHERE id = ?').get(linkedPayment.loan_id); + const otherPaid = db.get().prepare(` + SELECT COALESCE(SUM(amount), 0) AS total + FROM budget_loan_payments + WHERE loan_id = ? AND id != ? + `).get(linkedPayment.loan_id, linkedPayment.id).total; + if (Number(amount) - (Number(loan?.total_amount || 0) - Number(otherPaid || 0)) > 0.005) { + return res.status(400).json({ error: 'Amount cannot be greater than the remaining loan amount.', code: 400 }); + } + } const nextCategory = category ?? entry.category; const subcategory = requestedSubcategory !== undefined || category !== undefined ? validateSubcategory(nextCategory, requestedSubcategory ?? entry.subcategory)