Refine loan tab filtering and date formats

This commit is contained in:
Rafael Foster
2026-05-01 08:24:39 -03:00
parent 79f55cbfbc
commit e34ba33f9b
21 changed files with 187 additions and 72 deletions
+21 -4
View File
@@ -87,9 +87,11 @@ function isDateOnlyString(value) {
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
}
const VALID_DATE_FORMATS = ['mdy', 'dmy', 'ymd', 'mdy_dot', 'dmy_dot', 'ymd_dot', 'ymd_slash'];
function getDateFormatPreference() {
const stored = localStorage.getItem(DATE_FORMAT_KEY);
return ['mdy', 'dmy', 'ymd'].includes(stored) ? stored : DEFAULT_DATE_FORMAT;
return VALID_DATE_FORMATS.includes(stored) ? stored : DEFAULT_DATE_FORMAT;
}
export function getDateFormat() {
@@ -112,8 +114,12 @@ function formatDateParts(date, useUtc = false) {
const month = String((useUtc ? d.getUTCMonth() : d.getMonth()) + 1).padStart(2, '0');
const day = String(useUtc ? d.getUTCDate() : d.getDate()).padStart(2, '0');
switch (getDateFormatPreference()) {
case 'dmy': return `${day}.${month}.${year}`;
case 'dmy': return `${day}/${month}/${year}`;
case 'mdy_dot': return `${month}.${day}.${year}`;
case 'dmy_dot': return `${day}.${month}.${year}`;
case 'ymd': return `${year}-${month}-${day}`;
case 'ymd_dot': return `${year}.${month}.${day}`;
case 'ymd_slash': return `${year}/${month}/${day}`;
default: return `${month}/${day}/${year}`;
}
}
@@ -139,8 +145,12 @@ export function formatDate(date) {
export function dateInputPlaceholder() {
switch (getDateFormatPreference()) {
case 'dmy': return 'DD.MM.YYYY';
case 'dmy': return 'DD/MM/YYYY';
case 'mdy_dot': return 'MM.DD.YYYY';
case 'dmy_dot': return 'DD.MM.YYYY';
case 'ymd': return 'YYYY-MM-DD';
case 'ymd_dot': return 'YYYY.MM.DD';
case 'ymd_slash': return 'YYYY/MM/DD';
default: return 'MM/DD/YYYY';
}
}
@@ -157,11 +167,18 @@ export function parseDateInput(value) {
const isoMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) return isValidDateParts(isoMatch[1], isoMatch[2], isoMatch[3]) ? raw : '';
const ymdSeparatorMatch = raw.match(/^(\d{4})[\/.](\d{1,2})[\/.](\d{1,2})$/);
if (ymdSeparatorMatch && getDateFormatPreference().startsWith('ymd')) {
const [, year, month, day] = ymdSeparatorMatch;
if (!isValidDateParts(year, month, day)) return '';
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
const slashMatch = raw.match(/^(\d{1,2})[\/.](\d{1,2})[\/.](\d{4})$/);
if (!slashMatch) return '';
const [, first, second, year] = slashMatch;
const [month, day] = getDateFormatPreference() === 'dmy'
const [month, day] = getDateFormatPreference().startsWith('dmy')
? [second, first]
: [first, second];
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "تعديل القرض",
"deleteLoan": "حذف القرض",
"deleteLoanConfirm": "هل تريد حذف القرض \"{{title}}\"؟ ستتم إزالة الدفعات المسجلة في الميزانية أيضًا.",
"deleteLoanPaymentConfirm": "هل تريد حذف دفعة القرض هذه؟",
"loanRemainingAmount": "المتبقي",
"loanRemainingInstallments": "الأقساط المتبقية",
"loanPaidAmount": "المدفوع",
@@ -597,6 +598,7 @@
"loanSavedToast": "تم حفظ القرض",
"loanDeletedToast": "تم حذف القرض",
"loanPaymentAddedToast": "تم تسجيل الدفع",
"loanPaymentTitle": "سداد القرض: {{borrower}}",
"typeLoan": "قرض",
"tabsLabel": "أقسام الميزانية",
"budgetTab": "الميزانية",
+2
View File
@@ -598,6 +598,7 @@
"editLoan": "Darlehen bearbeiten",
"deleteLoan": "Darlehen löschen",
"deleteLoanConfirm": "Darlehen \"{{title}}\" löschen? Bereits im Budget verbuchte Zahlungen werden ebenfalls entfernt.",
"deleteLoanPaymentConfirm": "Diese Darlehenszahlung löschen?",
"loanRemainingAmount": "Offen",
"loanRemainingInstallments": "Raten offen",
"loanPaidAmount": "Bezahlt",
@@ -622,6 +623,7 @@
"loanSavedToast": "Darlehen gespeichert",
"loanDeletedToast": "Darlehen gelöscht",
"loanPaymentAddedToast": "Zahlung erfasst",
"loanPaymentTitle": "Darlehensrückzahlung: {{borrower}}",
"typeLoan": "Darlehen",
"tabsLabel": "Budgetbereiche",
"budgetTab": "Budget",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Επεξεργασία δανείου",
"deleteLoan": "Διαγραφή δανείου",
"deleteLoanConfirm": "Να διαγραφεί το δάνειο «{{title}}»; Οι πληρωμές που έχουν ήδη περαστεί στον προϋπολογισμό θα αφαιρεθούν επίσης.",
"deleteLoanPaymentConfirm": "Διαγραφή αυτής της πληρωμής δανείου;",
"loanRemainingAmount": "Υπόλοιπο",
"loanRemainingInstallments": "Δόσεις που απομένουν",
"loanPaidAmount": "Πληρωμένο",
@@ -597,6 +598,7 @@
"loanSavedToast": "Το δάνειο αποθηκεύτηκε",
"loanDeletedToast": "Το δάνειο διαγράφηκε",
"loanPaymentAddedToast": "Η πληρωμή καταγράφηκε",
"loanPaymentTitle": "Πληρωμή δανείου: {{borrower}}",
"typeLoan": "Δάνειο",
"tabsLabel": "Ενότητες προϋπολογισμού",
"budgetTab": "Προϋπολογισμός",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Edit loan",
"deleteLoan": "Delete loan",
"deleteLoanConfirm": "Delete loan \"{{title}}\"? Payments already posted to the budget will also be removed.",
"deleteLoanPaymentConfirm": "Delete this loan payment?",
"loanRemainingAmount": "Remaining",
"loanRemainingInstallments": "Installments left",
"loanPaidAmount": "Paid",
@@ -597,6 +598,7 @@
"loanSavedToast": "Loan saved",
"loanDeletedToast": "Loan deleted",
"loanPaymentAddedToast": "Payment recorded",
"loanPaymentTitle": "Loan repayment: {{borrower}}",
"typeLoan": "Loan",
"tabsLabel": "Budget sections",
"budgetTab": "Budget",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Editar préstamo",
"deleteLoan": "Eliminar préstamo",
"deleteLoanConfirm": "¿Eliminar el préstamo \"{{title}}\"? También se eliminarán los pagos ya registrados en el presupuesto.",
"deleteLoanPaymentConfirm": "¿Eliminar este pago del préstamo?",
"loanRemainingAmount": "Restante",
"loanRemainingInstallments": "Cuotas restantes",
"loanPaidAmount": "Pagado",
@@ -597,6 +598,7 @@
"loanSavedToast": "Préstamo guardado",
"loanDeletedToast": "Préstamo eliminado",
"loanPaymentAddedToast": "Pago registrado",
"loanPaymentTitle": "Pago del préstamo: {{borrower}}",
"typeLoan": "Préstamo",
"tabsLabel": "Secciones del presupuesto",
"budgetTab": "Presupuesto",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Modifier le prêt",
"deleteLoan": "Supprimer le prêt",
"deleteLoanConfirm": "Supprimer le prêt \"{{title}}\" ? Les paiements déjà enregistrés dans le budget seront aussi supprimés.",
"deleteLoanPaymentConfirm": "Supprimer ce paiement de prêt ?",
"loanRemainingAmount": "Restant",
"loanRemainingInstallments": "Échéances restantes",
"loanPaidAmount": "Payé",
@@ -597,6 +598,7 @@
"loanSavedToast": "Prêt enregistré",
"loanDeletedToast": "Prêt supprimé",
"loanPaymentAddedToast": "Paiement enregistré",
"loanPaymentTitle": "Remboursement du prêt : {{borrower}}",
"typeLoan": "Prêt",
"tabsLabel": "Sections du budget",
"budgetTab": "Budget",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "उधार संपादित करें",
"deleteLoan": "उधार हटाएं",
"deleteLoanConfirm": "उधार \"{{title}}\" हटाएं? बजट में दर्ज भुगतान भी हटा दिए जाएंगे।",
"deleteLoanPaymentConfirm": "यह ऋण भुगतान हटाएँ?",
"loanRemainingAmount": "बाकी",
"loanRemainingInstallments": "बाकी किस्तें",
"loanPaidAmount": "भुगतान किया",
@@ -597,6 +598,7 @@
"loanSavedToast": "उधार सहेजा गया",
"loanDeletedToast": "उधार हटाया गया",
"loanPaymentAddedToast": "भुगतान दर्ज किया गया",
"loanPaymentTitle": "ऋण भुगतान: {{borrower}}",
"typeLoan": "उधार",
"tabsLabel": "बजट अनुभाग",
"budgetTab": "बजट",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Modifica prestito",
"deleteLoan": "Elimina prestito",
"deleteLoanConfirm": "Eliminare il prestito \"{{title}}\"? Verranno rimossi anche i pagamenti già registrati nel bilancio.",
"deleteLoanPaymentConfirm": "Eliminare questo pagamento del prestito?",
"loanRemainingAmount": "Rimanente",
"loanRemainingInstallments": "Rate rimanenti",
"loanPaidAmount": "Pagato",
@@ -597,6 +598,7 @@
"loanSavedToast": "Prestito salvato",
"loanDeletedToast": "Prestito eliminato",
"loanPaymentAddedToast": "Pagamento registrato",
"loanPaymentTitle": "Rimborso del prestito: {{borrower}}",
"typeLoan": "Prestito",
"tabsLabel": "Sezioni del bilancio",
"budgetTab": "Bilancio",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "貸付を編集",
"deleteLoan": "貸付を削除",
"deleteLoanConfirm": "貸付「{{title}}」を削除しますか?予算に記録済みの返済も削除されます。",
"deleteLoanPaymentConfirm": "このローン支払いを削除しますか?",
"loanRemainingAmount": "残額",
"loanRemainingInstallments": "残り回数",
"loanPaidAmount": "返済済み",
@@ -597,6 +598,7 @@
"loanSavedToast": "貸付を保存しました",
"loanDeletedToast": "貸付を削除しました",
"loanPaymentAddedToast": "返済を記録しました",
"loanPaymentTitle": "ローン返済: {{borrower}}",
"typeLoan": "貸付",
"tabsLabel": "予算セクション",
"budgetTab": "予算",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Editar empréstimo",
"deleteLoan": "Excluir empréstimo",
"deleteLoanConfirm": "Excluir empréstimo \"{{title}}\"? Pagamentos já lançados no orçamento também serão removidos.",
"deleteLoanPaymentConfirm": "Excluir este pagamento do empréstimo?",
"loanRemainingAmount": "Restante",
"loanRemainingInstallments": "Parcelas restantes",
"loanPaidAmount": "Pago",
@@ -597,6 +598,7 @@
"loanSavedToast": "Empréstimo salvo",
"loanDeletedToast": "Empréstimo excluído",
"loanPaymentAddedToast": "Pagamento registrado",
"loanPaymentTitle": "Pagamento do empréstimo: {{borrower}}",
"typeLoan": "Empréstimo",
"tabsLabel": "Seções do orçamento",
"budgetTab": "Orçamento",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Изменить займ",
"deleteLoan": "Удалить займ",
"deleteLoanConfirm": "Удалить займ «{{title}}»? Платежи, уже добавленные в бюджет, тоже будут удалены.",
"deleteLoanPaymentConfirm": "Удалить этот платеж по займу?",
"loanRemainingAmount": "Осталось",
"loanRemainingInstallments": "Осталось платежей",
"loanPaidAmount": "Оплачено",
@@ -597,6 +598,7 @@
"loanSavedToast": "Займ сохранён",
"loanDeletedToast": "Займ удалён",
"loanPaymentAddedToast": "Платёж записан",
"loanPaymentTitle": "Платеж по займу: {{borrower}}",
"typeLoan": "Займ",
"tabsLabel": "Разделы бюджета",
"budgetTab": "Бюджет",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Redigera lån",
"deleteLoan": "Ta bort lån",
"deleteLoanConfirm": "Ta bort lånet \"{{title}}\"? Betalningar som redan bokförts i budgeten tas också bort.",
"deleteLoanPaymentConfirm": "Ta bort den här lånebetalningen?",
"loanRemainingAmount": "Kvar",
"loanRemainingInstallments": "Delbetalningar kvar",
"loanPaidAmount": "Betalt",
@@ -597,6 +598,7 @@
"loanSavedToast": "Lån sparat",
"loanDeletedToast": "Lån borttaget",
"loanPaymentAddedToast": "Betalning registrerad",
"loanPaymentTitle": "Låneåterbetalning: {{borrower}}",
"typeLoan": "Lån",
"tabsLabel": "Budgetsektioner",
"budgetTab": "Budget",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Borcu düzenle",
"deleteLoan": "Borcu sil",
"deleteLoanConfirm": "\"{{title}}\" borcu silinsin mi? Bütçeye işlenmiş ödemeler de kaldırılır.",
"deleteLoanPaymentConfirm": "Bu kredi ödemesi silinsin mi?",
"loanRemainingAmount": "Kalan",
"loanRemainingInstallments": "Kalan taksit",
"loanPaidAmount": "Ödenen",
@@ -597,6 +598,7 @@
"loanSavedToast": "Borç kaydedildi",
"loanDeletedToast": "Borç silindi",
"loanPaymentAddedToast": "Ödeme kaydedildi",
"loanPaymentTitle": "Kredi geri ödemesi: {{borrower}}",
"typeLoan": "Borç",
"tabsLabel": "Bütçe bölümleri",
"budgetTab": "Bütçe",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "Редагувати позику",
"deleteLoan": "Видалити позику",
"deleteLoanConfirm": "Видалити позику «{{title}}»? Платежі, вже додані до бюджету, також буде видалено.",
"deleteLoanPaymentConfirm": "Видалити цей платіж за позикою?",
"loanRemainingAmount": "Залишилось",
"loanRemainingInstallments": "Залишилось платежів",
"loanPaidAmount": "Сплачено",
@@ -597,6 +598,7 @@
"loanSavedToast": "Позику збережено",
"loanDeletedToast": "Позику видалено",
"loanPaymentAddedToast": "Платіж записано",
"loanPaymentTitle": "Платіж за позикою: {{borrower}}",
"typeLoan": "Позика",
"tabsLabel": "Розділи бюджету",
"budgetTab": "Бюджет",
+2
View File
@@ -573,6 +573,7 @@
"editLoan": "编辑借款",
"deleteLoan": "删除借款",
"deleteLoanConfirm": "删除借款“{{title}}”?已记入预算的还款也会被删除。",
"deleteLoanPaymentConfirm": "删除这笔借款还款?",
"loanRemainingAmount": "剩余金额",
"loanRemainingInstallments": "剩余期数",
"loanPaidAmount": "已还金额",
@@ -597,6 +598,7 @@
"loanSavedToast": "借款已保存",
"loanDeletedToast": "借款已删除",
"loanPaymentAddedToast": "还款已记录",
"loanPaymentTitle": "借款还款:{{borrower}}",
"typeLoan": "借款",
"tabsLabel": "预算分区",
"budgetTab": "预算",
+97 -47
View File
@@ -6,7 +6,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
import { openModal as openSharedModal, closeModal, confirmModal } from '/components/modal.js';
import { stagger, vibrate } from '/utils/ux.js';
import { t, formatDate, getLocale, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js';
import { esc } from '/utils/html.js';
@@ -164,11 +164,8 @@ function setHtml(element, html) {
async function loadMonth(month) {
const prevMonth = addMonths(month, -1);
try {
const entriesPath = state.loanFilterId
? `/budget?loan_id=${encodeURIComponent(state.loanFilterId)}`
: `/budget?month=${month}`;
const [entriesRes, summaryRes, prevSummaryRes, loansRes] = await Promise.all([
api.get(entriesPath),
api.get(`/budget?month=${month}`),
api.get(`/budget/summary?month=${month}`),
api.get(`/budget/summary?month=${prevMonth}`),
api.get('/budget/loans'),
@@ -357,16 +354,10 @@ function renderBody() {
<div class="budget-list-section">
<div class="budget-list-header">
<div>
<span class="budget-list-header__title">${state.loanFilterId ? t('budget.filteredTransactions') : t('budget.transactions')}</span>
${state.loanFilterId ? `<div class="budget-list-header__filter">${esc(activeLoanLabel())}</div>` : ''}
<span class="budget-list-header__title">${t('budget.transactions')}</span>
</div>
<div class="budget-list-header__actions">
${state.loanFilterId ? `
<button class="btn btn--secondary" id="budget-clear-loan-filter"
style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3);">
<i data-lucide="x" style="width:14px;height:14px;margin-right:4px;" aria-hidden="true"></i>${t('budget.clearLoanFilter')}
</button>` : ''}
${state.entries.length && !state.loanFilterId ? `
${state.entries.length ? `
<a href="/api/v1/budget/export?month=${state.month}" class="btn btn--secondary"
style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3);">
<i data-lucide="download" style="width:14px;height:14px;margin-right:4px;" aria-hidden="true"></i>CSV
@@ -384,11 +375,6 @@ function renderBody() {
_container.querySelector('#empty-cta-budget')?.addEventListener('click', () => {
document.querySelector('.page-fab')?.click();
});
_container.querySelector('#budget-clear-loan-filter')?.addEventListener('click', async () => {
state.loanFilterId = null;
await loadMonth(state.month);
renderBody();
});
stagger(_container.querySelector('#budget-list')?.querySelectorAll('.budget-entry') ?? []);
_container.querySelector('#budget-list')?.addEventListener('click', async (e) => {
@@ -411,11 +397,6 @@ function updateTabs() {
});
}
function activeLoanLabel() {
const loan = state.loans.loans.find((item) => item.id === state.loanFilterId);
return loan ? t('budget.loanFilterActive', { title: loan.title }) : '';
}
function renderCategoryBars(byCategory) {
const maxAbs = Math.max(...byCategory.map((c) => Math.abs(c.total)), 1);
@@ -498,8 +479,13 @@ function renderLoansDashboard() {
count: summary.active_count ?? 0,
amount: formatAmount(summary.remaining_amount ?? 0),
})}</div>
${state.loanFilterId ? `<div class="budget-list-header__filter">${esc(activeLoanLabel())}</div>` : ''}
</div>
<div class="budget-loans__filters" role="group" aria-label="${t('budget.loanStatusFilterLabel')}">
${state.loanFilterId ? `
<button class="budget-loans__filter" type="button" id="budget-clear-loan-filter">
<i data-lucide="x" aria-hidden="true"></i>${t('budget.clearLoanFilter')}
</button>` : ''}
<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' : ''}"
@@ -536,8 +522,16 @@ function renderLoansDashboard() {
function filteredLoans() {
const loans = state.loans?.loans ?? [];
if (state.loanStatusFilter === 'all') return loans;
return loans.filter((loan) => loan.status === state.loanStatusFilter);
return loans.filter((loan) => {
const matchesStatus = state.loanStatusFilter === 'all' || loan.status === state.loanStatusFilter;
const matchesLoan = !state.loanFilterId || loan.id === state.loanFilterId;
return matchesStatus && matchesLoan;
});
}
function activeLoanLabel() {
const loan = state.loans.loans.find((item) => item.id === state.loanFilterId);
return loan ? t('budget.loanFilterActive', { title: loan.title }) : '';
}
function loanPaymentsFor(loans) {
@@ -552,25 +546,53 @@ function renderLoanTransactions(loans) {
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('')}
${payments.map(({ loan, ...payment }) => renderLoanPaymentEntry(loan, payment)).join('')}
</div>
</div>`;
}
function loanPaymentToEntry(loan, payment) {
if (!payment.budget_entry_id) return null;
return {
id: payment.budget_entry_id,
title: payment.entry_title || `Loan repayment: ${loan.borrower}`,
amount: Number(payment.amount || 0),
category: payment.entry_category || 'Geschenke & Transfers',
subcategory: payment.entry_subcategory || '',
date: payment.paid_date,
is_recurring: payment.entry_is_recurring || 0,
recurrence_parent_id: payment.entry_recurrence_parent_id || null,
};
}
function renderLoanPaymentEntry(loan, payment) {
const entry = loanPaymentToEntry(loan, payment);
const meta = `${formatEntryDate(payment.paid_date)} · ${esc(loan.title)} · ${t('budget.loanInstallmentNumber', {
number: payment.installment_number,
total: loan.installment_count,
})}`;
return `
<div class="budget-entry budget-entry--loan" data-loan-payment-id="${payment.id}" data-loan-id="${loan.id}" ${entry ? `data-entry-id="${entry.id}"` : ''}>
<div class="budget-entry__indicator budget-entry__indicator--income"></div>
<div class="budget-entry__body">
<div class="budget-entry__title">${esc(payment.entry_title || t('budget.loanPaymentTitle', { borrower: loan.borrower }))}</div>
<div class="budget-entry__meta">${meta}</div>
</div>
<div class="budget-entry__amount budget-entry__amount--income">+${formatAmount(payment.amount)}</div>
<div class="budget-entry__actions">
${entry ? `
<button class="budget-entry__delete" data-action="loan-payment-edit" data-loan-id="${loan.id}" data-payment-id="${payment.id}" data-entry-id="${entry.id}" aria-label="${t('common.edit')}">
<i data-lucide="pencil" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>` : ''}
<button class="budget-entry__delete" data-action="loan-payment-delete" data-loan-id="${loan.id}" data-payment-id="${payment.id}" data-entry-id="${entry?.id ?? ''}" aria-label="${t('budget.deleteLabel')}">
<i data-lucide="trash-2" style="width:14px;height:14px;" aria-hidden="true"></i>
</button>
</div>
</div>
`;
}
function renderLoansPage() {
const loans = state.loans?.loans ?? [];
if (!loans.length) {
@@ -594,6 +616,10 @@ function renderLoansPage() {
function wireLoansPage() {
_container.querySelector('#budget-empty-loan')?.addEventListener('click', () => openBudgetModal({ mode: 'create', initialType: 'loan' }));
_container.querySelector('#budget-clear-loan-filter')?.addEventListener('click', () => {
state.loanFilterId = null;
renderBody();
});
_container.querySelectorAll('[data-loan-status]').forEach((btn) => {
btn.addEventListener('click', () => {
state.loanStatusFilter = btn.dataset.loanStatus;
@@ -624,13 +650,25 @@ function wireLoansPage() {
});
});
_container.querySelectorAll('[data-action="loan-filter"]').forEach((btn) => {
btn.addEventListener('click', async () => {
state.loanFilterId = parseInt(btn.dataset.id, 10);
state.activeTab = 'budget';
await loadMonth(state.month);
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id, 10);
state.loanFilterId = state.loanFilterId === id ? null : id;
renderBody();
});
});
_container.querySelectorAll('[data-action="loan-payment-edit"]').forEach((btn) => {
btn.addEventListener('click', () => {
const loan = state.loans.loans.find((item) => item.id === parseInt(btn.dataset.loanId, 10));
const payment = loan?.payments?.find((item) => item.id === parseInt(btn.dataset.paymentId, 10));
const entry = loan && payment ? loanPaymentToEntry(loan, payment) : null;
if (entry) openBudgetModal({ mode: 'edit', entry });
});
});
_container.querySelectorAll('[data-action="loan-payment-delete"]').forEach((btn) => {
btn.addEventListener('click', async () => {
await deleteLoanPayment(parseInt(btn.dataset.loanId, 10), parseInt(btn.dataset.paymentId, 10));
});
});
}
function openLoanReport(loan) {
@@ -695,7 +733,7 @@ function renderLoanCard(loan) {
<div class="budget-loan-card__main">
<div class="budget-loan-card__title-row">
<div class="budget-loan-card__title">${esc(loan.title)}</div>
<button class="budget-loan-card__filter" data-action="loan-filter" data-id="${loan.id}" aria-label="${t('budget.filterLoanTransactions')}">
<button class="budget-loan-card__filter ${state.loanFilterId === loan.id ? 'budget-loan-card__filter--active' : ''}" data-action="loan-filter" data-id="${loan.id}" aria-label="${t('budget.filterLoanTransactions')}">
<i data-lucide="filter" aria-hidden="true"></i>
</button>
</div>
@@ -752,7 +790,7 @@ function renderTrend(current, prev, prevLabel) {
}
function formatEntryDate(dateStr) {
return formatDate(new Date(dateStr + 'T00:00:00'));
return formatDate(dateStr);
}
// --------------------------------------------------------
@@ -1198,7 +1236,7 @@ async function markLoanPayment(id) {
async function deleteLoan(id) {
const loan = state.loans.loans.find((item) => item.id === id);
if (!loan) return;
if (!window.confirm(t('budget.deleteLoanConfirm', { title: loan.title }))) return;
if (!await confirmModal(t('budget.deleteLoanConfirm', { title: loan.title }), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/budget/loans/${id}`);
await loadMonth(state.month);
@@ -1209,6 +1247,18 @@ async function deleteLoan(id) {
}
}
async function deleteLoanPayment(loanId, paymentId) {
if (!await confirmModal(t('budget.deleteLoanPaymentConfirm'), { danger: true, confirmLabel: t('common.delete') })) return;
try {
await api.delete(`/budget/loans/${loanId}/payments/${paymentId}`);
await loadMonth(state.month);
renderBody();
window.oikos?.showToast(t('budget.deletedToast'), 'success');
} catch (err) {
window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'danger');
}
}
// --------------------------------------------------------
// Eintrag löschen
// --------------------------------------------------------
+4
View File
@@ -340,6 +340,10 @@ export async function render(container, { user }) {
<option value="mdy"${prefs.date_format === 'mdy' ? ' selected' : ''}>MM/DD/YYYY</option>
<option value="dmy"${prefs.date_format === 'dmy' ? ' selected' : ''}>DD/MM/YYYY</option>
<option value="ymd"${prefs.date_format === 'ymd' ? ' selected' : ''}>YYYY-MM-DD</option>
<option value="mdy_dot"${prefs.date_format === 'mdy_dot' ? ' selected' : ''}>MM.DD.YYYY</option>
<option value="dmy_dot"${prefs.date_format === 'dmy_dot' ? ' selected' : ''}>DD.MM.YYYY</option>
<option value="ymd_dot"${prefs.date_format === 'ymd_dot' ? ' selected' : ''}>YYYY.MM.DD</option>
<option value="ymd_slash"${prefs.date_format === 'ymd_slash' ? ' selected' : ''}>YYYY/MM/DD</option>
</select>
<label class="form-label" for="time-format-select" style="margin-top:var(--space-3)">${t('settings.timeFormatLabel')}</label>
<select class="form-input" id="time-format-select">
+27 -19
View File
@@ -84,9 +84,9 @@
}
.budget-tab {
min-height: 34px;
padding: 0 var(--space-3);
border: 1px solid transparent;
min-height: 30px;
padding: 0 var(--space-2);
border: 0;
border-radius: var(--radius-xs);
background: transparent;
color: var(--color-text-secondary);
@@ -95,22 +95,6 @@
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 {
color: var(--color-text-on-accent);
}
@@ -313,6 +297,13 @@
font-weight: var(--font-weight-medium);
}
.budget-loans__filter i {
width: 14px;
height: 14px;
margin-right: 4px;
vertical-align: -2px;
}
.budget-loans__filter--active {
background: var(--color-info);
color: var(--color-text-on-accent);
@@ -420,6 +411,12 @@
border-color: var(--module-accent);
}
.budget-loan-card__filter--active {
color: var(--color-text-on-accent);
border-color: var(--color-info);
background: var(--color-info);
}
.budget-loan-card__filter i {
width: 15px;
height: 15px;
@@ -737,6 +734,13 @@
position: relative;
}
.budget-entry__actions {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
.budget-entry__delete::before {
content: '';
position: absolute;
@@ -747,6 +751,10 @@
opacity: 1;
}
.budget-entry--loan .budget-entry__delete {
opacity: 1;
}
.budget-entry__delete:hover {
color: var(--color-danger);
}