Improve loan dashboard interactions
This commit is contained in:
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "فئة جديدة",
|
||||
"newCategoryPlaceholder": "اسم الفئة",
|
||||
"newSubcategoryTitle": "فئة فرعية جديدة",
|
||||
"newSubcategoryPlaceholder": "اسم الفئة الفرعية"
|
||||
"newSubcategoryPlaceholder": "اسم الفئة الفرعية",
|
||||
"loanStatusFilterLabel": "فلتر حالة القرض",
|
||||
"loanStatusActive": "نشطة",
|
||||
"loanStatusPaid": "مدفوعة",
|
||||
"loanStatusAll": "الكل",
|
||||
"loanTransactions": "معاملات القرض",
|
||||
"loanInstallmentNumber": "القسط {{number}} من {{total}}",
|
||||
"loanReportTitle": "تقرير القرض",
|
||||
"loanNoTransactions": "لم يتم تسجيل أي دفعات بعد."
|
||||
},
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "Νέα κατηγορία",
|
||||
"newCategoryPlaceholder": "Όνομα κατηγορίας",
|
||||
"newSubcategoryTitle": "Νέα υποκατηγορία",
|
||||
"newSubcategoryPlaceholder": "Όνομα υποκατηγορίας"
|
||||
"newSubcategoryPlaceholder": "Όνομα υποκατηγορίας",
|
||||
"loanStatusFilterLabel": "Φίλτρο κατάστασης δανείων",
|
||||
"loanStatusActive": "Ενεργά",
|
||||
"loanStatusPaid": "Πληρωμένα",
|
||||
"loanStatusAll": "Όλα",
|
||||
"loanTransactions": "Συναλλαγές δανείου",
|
||||
"loanInstallmentNumber": "Δόση {{number}} από {{total}}",
|
||||
"loanReportTitle": "Αναφορά δανείου",
|
||||
"loanNoTransactions": "Δεν έχουν καταγραφεί πληρωμές ακόμα."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ρυθμίσεις",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "नई श्रेणी",
|
||||
"newCategoryPlaceholder": "श्रेणी का नाम",
|
||||
"newSubcategoryTitle": "नई उपश्रेणी",
|
||||
"newSubcategoryPlaceholder": "उपश्रेणी का नाम"
|
||||
"newSubcategoryPlaceholder": "उपश्रेणी का नाम",
|
||||
"loanStatusFilterLabel": "उधार स्थिति फ़िल्टर",
|
||||
"loanStatusActive": "सक्रिय",
|
||||
"loanStatusPaid": "चुकाया गया",
|
||||
"loanStatusAll": "सभी",
|
||||
"loanTransactions": "उधार लेन-देन",
|
||||
"loanInstallmentNumber": "{{total}} में से किस्त {{number}}",
|
||||
"loanReportTitle": "उधार रिपोर्ट",
|
||||
"loanNoTransactions": "अभी कोई भुगतान दर्ज नहीं है।"
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग्स",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "新しいカテゴリ",
|
||||
"newCategoryPlaceholder": "カテゴリ名",
|
||||
"newSubcategoryTitle": "新しいサブカテゴリ",
|
||||
"newSubcategoryPlaceholder": "サブカテゴリ名"
|
||||
"newSubcategoryPlaceholder": "サブカテゴリ名",
|
||||
"loanStatusFilterLabel": "貸付ステータスフィルター",
|
||||
"loanStatusActive": "進行中",
|
||||
"loanStatusPaid": "完済",
|
||||
"loanStatusAll": "すべて",
|
||||
"loanTransactions": "貸付取引",
|
||||
"loanInstallmentNumber": "{{total}} 回中 {{number}} 回目",
|
||||
"loanReportTitle": "貸付レポート",
|
||||
"loanNoTransactions": "返済はまだ記録されていません。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "Новая категория",
|
||||
"newCategoryPlaceholder": "Название категории",
|
||||
"newSubcategoryTitle": "Новая подкатегория",
|
||||
"newSubcategoryPlaceholder": "Название подкатегории"
|
||||
"newSubcategoryPlaceholder": "Название подкатегории",
|
||||
"loanStatusFilterLabel": "Фильтр статуса займов",
|
||||
"loanStatusActive": "Активные",
|
||||
"loanStatusPaid": "Оплаченные",
|
||||
"loanStatusAll": "Все",
|
||||
"loanTransactions": "Операции займа",
|
||||
"loanInstallmentNumber": "Платёж {{number}} из {{total}}",
|
||||
"loanReportTitle": "Отчёт по займу",
|
||||
"loanNoTransactions": "Платежи ещё не записаны."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "Нова категорія",
|
||||
"newCategoryPlaceholder": "Назва категорії",
|
||||
"newSubcategoryTitle": "Нова підкатегорія",
|
||||
"newSubcategoryPlaceholder": "Назва підкатегорії"
|
||||
"newSubcategoryPlaceholder": "Назва підкатегорії",
|
||||
"loanStatusFilterLabel": "Фільтр статусу позик",
|
||||
"loanStatusActive": "Активні",
|
||||
"loanStatusPaid": "Сплачені",
|
||||
"loanStatusAll": "Усі",
|
||||
"loanTransactions": "Операції позики",
|
||||
"loanInstallmentNumber": "Платіж {{number}} з {{total}}",
|
||||
"loanReportTitle": "Звіт по позиці",
|
||||
"loanNoTransactions": "Платежі ще не записано."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Налаштування",
|
||||
|
||||
@@ -609,7 +609,15 @@
|
||||
"newCategoryTitle": "新类别",
|
||||
"newCategoryPlaceholder": "类别名称",
|
||||
"newSubcategoryTitle": "新子类别",
|
||||
"newSubcategoryPlaceholder": "子类别名称"
|
||||
"newSubcategoryPlaceholder": "子类别名称",
|
||||
"loanStatusFilterLabel": "借款状态筛选",
|
||||
"loanStatusActive": "进行中",
|
||||
"loanStatusPaid": "已还清",
|
||||
"loanStatusAll": "全部",
|
||||
"loanTransactions": "借款交易",
|
||||
"loanInstallmentNumber": "第 {{number}} / {{total}} 期",
|
||||
"loanReportTitle": "借款报告",
|
||||
"loanNoTransactions": "尚未记录还款。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
|
||||
+116
-4
@@ -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 `
|
||||
<section class="budget-loans">
|
||||
@@ -498,6 +499,14 @@ function renderLoansDashboard() {
|
||||
amount: formatAmount(summary.remaining_amount ?? 0),
|
||||
})}</div>
|
||||
</div>
|
||||
<div class="budget-loans__filters" role="group" aria-label="${t('budget.loanStatusFilterLabel')}">
|
||||
<button class="budget-loans__filter ${state.loanStatusFilter === 'active' ? 'budget-loans__filter--active' : ''}"
|
||||
type="button" data-loan-status="active">${t('budget.loanStatusActive')}</button>
|
||||
<button class="budget-loans__filter ${state.loanStatusFilter === 'paid' ? 'budget-loans__filter--active' : ''}"
|
||||
type="button" data-loan-status="paid">${t('budget.loanStatusPaid')}</button>
|
||||
<button class="budget-loans__filter ${state.loanStatusFilter === 'all' ? 'budget-loans__filter--active' : ''}"
|
||||
type="button" data-loan-status="all">${t('budget.loanStatusAll')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-loans__stats">
|
||||
<div>
|
||||
@@ -513,17 +522,55 @@ function renderLoansDashboard() {
|
||||
<strong>${formatAmount(summary.paid_amount ?? 0)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
${activeLoans.length ? `
|
||||
${visibleLoans.length ? `
|
||||
<div class="budget-loans__list">
|
||||
${activeLoans.map(renderLoanCard).join('')}
|
||||
${visibleLoans.map(renderLoanCard).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="budget-loans__empty">${t('budget.loansEmpty')}</div>
|
||||
`}
|
||||
${renderLoanTransactions(visibleLoans)}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `<div class="budget-loan-transactions">
|
||||
<div class="budget-loan-transactions__title">${t('budget.loanTransactions')}</div>
|
||||
<div class="budget-loan-transactions__list">
|
||||
${payments.map(({ loan, ...payment }) => `
|
||||
<div class="budget-loan-transaction">
|
||||
<div>
|
||||
<strong>${esc(loan.title)}</strong>
|
||||
<span>${esc(loan.borrower)} · ${t('budget.loanInstallmentNumber', {
|
||||
number: payment.installment_number,
|
||||
total: loan.installment_count,
|
||||
})}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>${formatAmount(payment.amount)}</strong>
|
||||
<span>${formatEntryDate(payment.paid_date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="loan-report">
|
||||
<div class="loan-report__hero">
|
||||
<div>
|
||||
<div class="loan-report__borrower">${esc(loan.borrower)}</div>
|
||||
<div class="loan-report__title">${esc(loan.title)}</div>
|
||||
</div>
|
||||
<span class="loan-report__status loan-report__status--${loan.status}">
|
||||
${loan.status === 'paid' ? t('budget.loanStatusPaid') : t('budget.loanStatusActive')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="loan-report__grid">
|
||||
<div><span>${t('budget.loanAmountLabel')}</span><strong>${formatAmount(loan.total_amount)}</strong></div>
|
||||
<div><span>${t('budget.loanRemainingAmount')}</span><strong>${formatAmount(loan.remaining_amount)}</strong></div>
|
||||
<div><span>${t('budget.loanPaidAmount')}</span><strong>${formatAmount(loan.paid_amount)}</strong></div>
|
||||
<div><span>${t('budget.loanRemainingInstallments')}</span><strong>${loan.remaining_installments}</strong></div>
|
||||
</div>
|
||||
<div class="loan-report__section-title">${t('budget.loanTransactions')}</div>
|
||||
${payments.length ? `
|
||||
<div class="loan-report__transactions">
|
||||
${payments.map((payment) => `
|
||||
<div class="budget-loan-transaction">
|
||||
<div>
|
||||
<strong>${t('budget.loanInstallmentNumber', { number: payment.installment_number, total: loan.installment_count })}</strong>
|
||||
<span>${formatEntryDate(payment.paid_date)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>${formatAmount(payment.amount)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : `<div class="budget-loans__empty">${t('budget.loanNoTransactions')}</div>`}
|
||||
</div>
|
||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
||||
<div></div>
|
||||
<button class="btn btn--primary" id="loan-report-close">${t('common.close')}</button>
|
||||
</div>`;
|
||||
|
||||
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 `
|
||||
<article class="budget-loan-card">
|
||||
<article class="budget-loan-card" data-loan-id="${loan.id}">
|
||||
<div class="budget-loan-card__main">
|
||||
<div class="budget-loan-card__title-row">
|
||||
<div class="budget-loan-card__title">${esc(loan.title)}</div>
|
||||
|
||||
+163
-5
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user