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