diff --git a/public/i18n.js b/public/i18n.js index b45a0fe..fc7f077 100644 --- a/public/i18n.js +++ b/public/i18n.js @@ -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', 'dmy_slash', '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() { @@ -113,7 +115,12 @@ function formatDateParts(date, useUtc = false) { const day = String(useUtc ? d.getUTCDate() : d.getDate()).padStart(2, '0'); switch (getDateFormatPreference()) { case 'dmy': return `${day}.${month}.${year}`; + case 'mdy_dot': return `${month}.${day}.${year}`; + case 'dmy_dot': return `${day}.${month}.${year}`; + case 'dmy_slash': 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}`; } } @@ -140,7 +147,12 @@ export function formatDate(date) { export function dateInputPlaceholder() { switch (getDateFormatPreference()) { case 'dmy': return 'DD.MM.YYYY'; + case 'mdy_dot': return 'MM.DD.YYYY'; + case 'dmy_dot': return 'DD.MM.YYYY'; + case 'dmy_slash': 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 +169,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]; diff --git a/public/locales/ar.json b/public/locales/ar.json index 2ca8c8d..ff71833 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -112,7 +112,13 @@ "customizeMoveUp": "للأعلى", "customizeMoveDown": "للأسفل", "overdueTasksChip": "{{count}} مهمة متأخرة", - "overdueTasksChipPlural": "{{count}} مهام متأخرة" + "overdueTasksChipPlural": "{{count}} مهام متأخرة", + "customizeManage": "الأدوات", + "customizeExit": "إنهاء التخصيص", + "customizeDrag": "اسحب الأداة", + "customizeSize": "الحجم", + "customizeSizeFor": "حجم {{widget}}", + "customizeHide": "إخفاء {{widget}}" }, "tasks": { "title": "المهام", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "القروض / الفوائد", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "إضافة إدخال" + "emptyAction": "إضافة إدخال", + "loansTitle": "القروض", + "loansSummary": "{{count}} نشط · المتبقي {{amount}}", + "newLoan": "قرض جديد", + "createLoan": "إنشاء قرض", + "editLoan": "تعديل القرض", + "deleteLoan": "حذف القرض", + "deleteLoanConfirm": "هل تريد حذف القرض \"{{title}}\"؟ ستتم إزالة الدفعات المسجلة في الميزانية أيضًا.", + "deleteLoanPaymentConfirm": "هل تريد حذف دفعة القرض هذه؟", + "loanRemainingAmount": "المتبقي", + "loanRemainingInstallments": "الأقساط المتبقية", + "loanPaidAmount": "المدفوع", + "loansEmpty": "لا توجد قروض نشطة.", + "loanInstallmentMeta": "تم دفع {{paid}} من {{total}} أقساط", + "loanRemainingOf": "من {{total}}", + "loanNextDue": "التالي: {{month}}", + "loanPaidStatus": "مدفوع", + "markLoanPaid": "تسجيل الدفع", + "loanBorrowerLabel": "الشخص *", + "loanBorrowerPlaceholder": "مثال: Lais", + "loanTitleLabel": "عنوان القرض", + "loanTitlePlaceholder": "مثال: قرض شخصي", + "loanAmountLabel": "المبلغ الإجمالي *", + "loanInstallmentsLabel": "الأقساط *", + "loanStartMonthLabel": "أول شهر استحقاق *", + "loanNotesLabel": "ملاحظات", + "loanBorrowerRequired": "الشخص مطلوب", + "loanInstallmentsRequired": "أدخل عدد الأقساط", + "loanStartMonthRequired": "أدخل أول شهر استحقاق", + "loanAddedToast": "تمت إضافة القرض", + "loanSavedToast": "تم حفظ القرض", + "loanDeletedToast": "تم حذف القرض", + "loanPaymentAddedToast": "تم تسجيل الدفع", + "loanPaymentTitle": "سداد القرض: {{borrower}}", + "typeLoan": "قرض", + "tabsLabel": "أقسام الميزانية", + "budgetTab": "الميزانية", + "loansTab": "القروض", + "filteredTransactions": "المعاملات المصفاة", + "clearLoanFilter": "مسح الفلتر", + "loanFilterActive": "القرض: {{title}}", + "filterLoanTransactions": "عرض معاملات هذا القرض", + "loansEmptyDescription": "أنشئ قرضًا من زر + واختر قرض.", + "newCategoryTitle": "فئة جديدة", + "newCategoryPlaceholder": "اسم الفئة", + "newSubcategoryTitle": "فئة فرعية جديدة", + "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 aec8962..661f0c0 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -118,7 +118,13 @@ "customizeMoveUp": "Nach oben", "customizeMoveDown": "Nach unten", "overdueTasksChip": "{{count}} überfällige Aufgabe", - "overdueTasksChipPlural": "{{count}} überfällige Aufgaben" + "overdueTasksChipPlural": "{{count}} überfällige Aufgaben", + "customizeManage": "Widgets", + "customizeExit": "Anpassung beenden", + "customizeDrag": "Widget ziehen", + "customizeSize": "Größe", + "customizeSizeFor": "Größe für {{widget}}", + "customizeHide": "{{widget}} ausblenden" }, "tasks": { "title": "Aufgaben", @@ -590,7 +596,61 @@ "newCategoryPrompt": "Name der neuen Kategorie:", "newSubcategoryPrompt": "Name der neuen Unterkategorie:", "categoryAddedToast": "Kategorie hinzugefügt.", - "subcategoryAddedToast": "Unterkategorie hinzugefügt." + "subcategoryAddedToast": "Unterkategorie hinzugefügt.", + "loansTitle": "Darlehen", + "loansSummary": "{{count}} aktiv · {{amount}} offen", + "newLoan": "Neues Darlehen", + "createLoan": "Darlehen erstellen", + "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", + "loansEmpty": "Keine aktiven Darlehen.", + "loanInstallmentMeta": "{{paid}} von {{total}} Raten bezahlt", + "loanRemainingOf": "von {{total}}", + "loanNextDue": "Nächste: {{month}}", + "loanPaidStatus": "Bezahlt", + "markLoanPaid": "Als bezahlt markieren", + "loanBorrowerLabel": "Person *", + "loanBorrowerPlaceholder": "z. B. Lais", + "loanTitleLabel": "Darlehenstitel", + "loanTitlePlaceholder": "z. B. Persönliches Darlehen", + "loanAmountLabel": "Gesamtbetrag *", + "loanInstallmentsLabel": "Raten *", + "loanStartMonthLabel": "Erster Fälligkeitsmonat *", + "loanNotesLabel": "Notizen", + "loanBorrowerRequired": "Person ist erforderlich", + "loanInstallmentsRequired": "Anzahl der Raten eingeben", + "loanStartMonthRequired": "Ersten Fälligkeitsmonat eingeben", + "loanAddedToast": "Darlehen hinzugefügt", + "loanSavedToast": "Darlehen gespeichert", + "loanDeletedToast": "Darlehen gelöscht", + "loanPaymentAddedToast": "Zahlung erfasst", + "loanPaymentTitle": "Darlehensrückzahlung: {{borrower}}", + "typeLoan": "Darlehen", + "tabsLabel": "Budgetbereiche", + "budgetTab": "Budget", + "loansTab": "Darlehen", + "filteredTransactions": "Gefilterte Transaktionen", + "clearLoanFilter": "Filter löschen", + "loanFilterActive": "Darlehen: {{title}}", + "filterLoanTransactions": "Transaktionen dieses Darlehens anzeigen", + "loansEmptyDescription": "Erstelle ein Darlehen über die +-Schaltfläche und wähle Darlehen.", + "newCategoryTitle": "Neue Kategorie", + "newCategoryPlaceholder": "Kategoriename", + "newSubcategoryTitle": "Neue 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 1d9229f..a6a3ad4 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Πάνω", "customizeMoveDown": "Κάτω", "overdueTasksChip": "{{count}} εκπρόθεσμη εργασία", - "overdueTasksChipPlural": "{{count}} εκπρόθεσμες εργασίες" + "overdueTasksChipPlural": "{{count}} εκπρόθεσμες εργασίες", + "customizeManage": "Widget", + "customizeExit": "Έξοδος από προσαρμογή", + "customizeDrag": "Σύρετε widget", + "customizeSize": "Μέγεθος", + "customizeSizeFor": "Μέγεθος για {{widget}}", + "customizeHide": "Απόκρυψη {{widget}}" }, "tasks": { "title": "Εργασίες", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "Δάνεια / Τόκοι", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "Προσθήκη εγγραφής" + "emptyAction": "Προσθήκη εγγραφής", + "loansTitle": "Δάνεια", + "loansSummary": "{{count}} ενεργά · απομένουν {{amount}}", + "newLoan": "Νέο δάνειο", + "createLoan": "Δημιουργία δανείου", + "editLoan": "Επεξεργασία δανείου", + "deleteLoan": "Διαγραφή δανείου", + "deleteLoanConfirm": "Να διαγραφεί το δάνειο «{{title}}»; Οι πληρωμές που έχουν ήδη περαστεί στον προϋπολογισμό θα αφαιρεθούν επίσης.", + "deleteLoanPaymentConfirm": "Διαγραφή αυτής της πληρωμής δανείου;", + "loanRemainingAmount": "Υπόλοιπο", + "loanRemainingInstallments": "Δόσεις που απομένουν", + "loanPaidAmount": "Πληρωμένο", + "loansEmpty": "Δεν υπάρχουν ενεργά δάνεια.", + "loanInstallmentMeta": "{{paid}} από {{total}} δόσεις πληρωμένες", + "loanRemainingOf": "από {{total}}", + "loanNextDue": "Επόμενη: {{month}}", + "loanPaidStatus": "Πληρωμένο", + "markLoanPaid": "Σήμανση πληρωμής", + "loanBorrowerLabel": "Άτομο *", + "loanBorrowerPlaceholder": "π.χ. Lais", + "loanTitleLabel": "Τίτλος δανείου", + "loanTitlePlaceholder": "π.χ. Προσωπικό δάνειο", + "loanAmountLabel": "Συνολικό ποσό *", + "loanInstallmentsLabel": "Δόσεις *", + "loanStartMonthLabel": "Πρώτος μήνας λήξης *", + "loanNotesLabel": "Σημειώσεις", + "loanBorrowerRequired": "Το άτομο είναι υποχρεωτικό", + "loanInstallmentsRequired": "Εισαγάγετε τον αριθμό δόσεων", + "loanStartMonthRequired": "Εισαγάγετε τον πρώτο μήνα λήξης", + "loanAddedToast": "Το δάνειο προστέθηκε", + "loanSavedToast": "Το δάνειο αποθηκεύτηκε", + "loanDeletedToast": "Το δάνειο διαγράφηκε", + "loanPaymentAddedToast": "Η πληρωμή καταγράφηκε", + "loanPaymentTitle": "Πληρωμή δανείου: {{borrower}}", + "typeLoan": "Δάνειο", + "tabsLabel": "Ενότητες προϋπολογισμού", + "budgetTab": "Προϋπολογισμός", + "loansTab": "Δάνεια", + "filteredTransactions": "Φιλτραρισμένες συναλλαγές", + "clearLoanFilter": "Καθαρισμός φίλτρου", + "loanFilterActive": "Δάνειο: {{title}}", + "filterLoanTransactions": "Εμφάνιση συναλλαγών αυτού του δανείου", + "loansEmptyDescription": "Δημιουργήστε δάνειο από το κουμπί + και επιλέξτε Δάνειο.", + "newCategoryTitle": "Νέα κατηγορία", + "newCategoryPlaceholder": "Όνομα κατηγορίας", + "newSubcategoryTitle": "Νέα υποκατηγορία", + "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 aecf24a..764229c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Move up", "customizeMoveDown": "Move down", "overdueTasksChip": "{{count}} overdue task", - "overdueTasksChipPlural": "{{count}} overdue tasks" + "overdueTasksChipPlural": "{{count}} overdue tasks", + "customizeManage": "Widgets", + "customizeExit": "Exit customization", + "customizeDrag": "Drag widget", + "customizeSize": "Size", + "customizeSizeFor": "Size for {{widget}}", + "customizeHide": "Hide {{widget}}" }, "tasks": { "title": "Tasks", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "Add entry" + "emptyAction": "Add entry", + "loansTitle": "Loans", + "loansSummary": "{{count}} active · {{amount}} remaining", + "newLoan": "New loan", + "createLoan": "Create loan", + "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", + "loansEmpty": "No active loans.", + "loanInstallmentMeta": "{{paid}} of {{total}} installments paid", + "loanRemainingOf": "of {{total}}", + "loanNextDue": "Next: {{month}}", + "loanPaidStatus": "Paid", + "markLoanPaid": "Mark paid", + "loanBorrowerLabel": "Borrower *", + "loanBorrowerPlaceholder": "e.g. Lais", + "loanTitleLabel": "Loan title", + "loanTitlePlaceholder": "e.g. Personal loan", + "loanAmountLabel": "Total amount *", + "loanInstallmentsLabel": "Installments *", + "loanStartMonthLabel": "First due month *", + "loanNotesLabel": "Notes", + "loanBorrowerRequired": "Borrower is required", + "loanInstallmentsRequired": "Enter the number of installments", + "loanStartMonthRequired": "Enter the first due month", + "loanAddedToast": "Loan added", + "loanSavedToast": "Loan saved", + "loanDeletedToast": "Loan deleted", + "loanPaymentAddedToast": "Payment recorded", + "loanPaymentTitle": "Loan repayment: {{borrower}}", + "typeLoan": "Loan", + "tabsLabel": "Budget sections", + "budgetTab": "Budget", + "loansTab": "Loans", + "filteredTransactions": "Filtered transactions", + "clearLoanFilter": "Clear filter", + "loanFilterActive": "Loan: {{title}}", + "filterLoanTransactions": "Show transactions for this loan", + "loansEmptyDescription": "Create a loan from the + button and choose Loan.", + "newCategoryTitle": "New category", + "newCategoryPlaceholder": "Category name", + "newSubcategoryTitle": "New subcategory", + "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 407dcab..126c08a 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Subir", "customizeMoveDown": "Bajar", "overdueTasksChip": "{{count}} tarea vencida", - "overdueTasksChipPlural": "{{count}} tareas vencidas" + "overdueTasksChipPlural": "{{count}} tareas vencidas", + "customizeManage": "Widgets", + "customizeExit": "Salir de personalización", + "customizeDrag": "Arrastrar widget", + "customizeSize": "Tamaño", + "customizeSizeFor": "Tamaño de {{widget}}", + "customizeHide": "Ocultar {{widget}}" }, "tasks": { "title": "Tareas", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Nombre de la nueva subcategoría:", "categoryAddedToast": "Categoría añadida.", "subcategoryAddedToast": "Subcategoría añadida.", - "emptyAction": "Agregar entrada" + "emptyAction": "Agregar entrada", + "loansTitle": "Préstamos", + "loansSummary": "{{count}} activos · {{amount}} restantes", + "newLoan": "Nuevo préstamo", + "createLoan": "Crear préstamo", + "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", + "loansEmpty": "No hay préstamos activos.", + "loanInstallmentMeta": "{{paid}} de {{total}} cuotas pagadas", + "loanRemainingOf": "de {{total}}", + "loanNextDue": "Siguiente: {{month}}", + "loanPaidStatus": "Pagado", + "markLoanPaid": "Marcar pagado", + "loanBorrowerLabel": "Persona *", + "loanBorrowerPlaceholder": "Ej. Lais", + "loanTitleLabel": "Título del préstamo", + "loanTitlePlaceholder": "Ej. Préstamo personal", + "loanAmountLabel": "Importe total *", + "loanInstallmentsLabel": "Cuotas *", + "loanStartMonthLabel": "Primer mes de vencimiento *", + "loanNotesLabel": "Notas", + "loanBorrowerRequired": "La persona es obligatoria", + "loanInstallmentsRequired": "Introduce el número de cuotas", + "loanStartMonthRequired": "Introduce el primer mes de vencimiento", + "loanAddedToast": "Préstamo añadido", + "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", + "loansTab": "Préstamos", + "filteredTransactions": "Transacciones filtradas", + "clearLoanFilter": "Limpiar filtro", + "loanFilterActive": "Préstamo: {{title}}", + "filterLoanTransactions": "Mostrar transacciones de este préstamo", + "loansEmptyDescription": "Crea un préstamo con el botón + y elige Préstamo.", + "newCategoryTitle": "Nueva categoría", + "newCategoryPlaceholder": "Nombre de la categoría", + "newSubcategoryTitle": "Nueva 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 0733002..fb5764f 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Monter", "customizeMoveDown": "Descendre", "overdueTasksChip": "{{count}} tâche en retard", - "overdueTasksChipPlural": "{{count}} tâches en retard" + "overdueTasksChipPlural": "{{count}} tâches en retard", + "customizeManage": "Widgets", + "customizeExit": "Quitter la personnalisation", + "customizeDrag": "Faire glisser le widget", + "customizeSize": "Taille", + "customizeSizeFor": "Taille de {{widget}}", + "customizeHide": "Masquer {{widget}}" }, "tasks": { "title": "Tâches", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Nom de la nouvelle sous-catégorie :", "categoryAddedToast": "Catégorie ajoutée.", "subcategoryAddedToast": "Sous-catégorie ajoutée.", - "emptyAction": "Ajouter une entrée" + "emptyAction": "Ajouter une entrée", + "loansTitle": "Prêts", + "loansSummary": "{{count}} actifs · {{amount}} restants", + "newLoan": "Nouveau prêt", + "createLoan": "Créer le prêt", + "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é", + "loansEmpty": "Aucun prêt actif.", + "loanInstallmentMeta": "{{paid}} sur {{total}} échéances payées", + "loanRemainingOf": "sur {{total}}", + "loanNextDue": "Prochaine : {{month}}", + "loanPaidStatus": "Payé", + "markLoanPaid": "Marquer payé", + "loanBorrowerLabel": "Personne *", + "loanBorrowerPlaceholder": "Ex. Lais", + "loanTitleLabel": "Titre du prêt", + "loanTitlePlaceholder": "Ex. Prêt personnel", + "loanAmountLabel": "Montant total *", + "loanInstallmentsLabel": "Échéances *", + "loanStartMonthLabel": "Premier mois d’échéance *", + "loanNotesLabel": "Notes", + "loanBorrowerRequired": "La personne est obligatoire", + "loanInstallmentsRequired": "Indiquez le nombre d’échéances", + "loanStartMonthRequired": "Indiquez le premier mois d’échéance", + "loanAddedToast": "Prêt ajouté", + "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", + "loansTab": "Prêts", + "filteredTransactions": "Transactions filtrées", + "clearLoanFilter": "Effacer le filtre", + "loanFilterActive": "Prêt : {{title}}", + "filterLoanTransactions": "Afficher les transactions de ce prêt", + "loansEmptyDescription": "Créez un prêt avec le bouton + puis choisissez Prêt.", + "newCategoryTitle": "Nouvelle catégorie", + "newCategoryPlaceholder": "Nom de la catégorie", + "newSubcategoryTitle": "Nouvelle 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 4ac5f3a..2530042 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -112,7 +112,13 @@ "customizeMoveUp": "ऊपर ले जाएं", "customizeMoveDown": "नीचे ले जाएं", "overdueTasksChip": "{{count}} विलंबित कार्य", - "overdueTasksChipPlural": "{{count}} विलंबित कार्य" + "overdueTasksChipPlural": "{{count}} विलंबित कार्य", + "customizeManage": "विजेट", + "customizeExit": "अनुकूलन से बाहर निकलें", + "customizeDrag": "विजेट खींचें", + "customizeSize": "आकार", + "customizeSizeFor": "{{widget}} का आकार", + "customizeHide": "{{widget}} छिपाएँ" }, "tasks": { "title": "कार्य", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "उधार / ब्याज", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "प्रविष्टि जोड़ें" + "emptyAction": "प्रविष्टि जोड़ें", + "loansTitle": "उधार", + "loansSummary": "{{count}} सक्रिय · {{amount}} बाकी", + "newLoan": "नया उधार", + "createLoan": "उधार बनाएं", + "editLoan": "उधार संपादित करें", + "deleteLoan": "उधार हटाएं", + "deleteLoanConfirm": "उधार \"{{title}}\" हटाएं? बजट में दर्ज भुगतान भी हटा दिए जाएंगे।", + "deleteLoanPaymentConfirm": "यह ऋण भुगतान हटाएँ?", + "loanRemainingAmount": "बाकी", + "loanRemainingInstallments": "बाकी किस्तें", + "loanPaidAmount": "भुगतान किया", + "loansEmpty": "कोई सक्रिय उधार नहीं।", + "loanInstallmentMeta": "{{total}} में से {{paid}} किस्तें चुकाई गईं", + "loanRemainingOf": "{{total}} में से", + "loanNextDue": "अगली: {{month}}", + "loanPaidStatus": "चुकाया गया", + "markLoanPaid": "भुगतान दर्ज करें", + "loanBorrowerLabel": "व्यक्ति *", + "loanBorrowerPlaceholder": "जैसे Lais", + "loanTitleLabel": "उधार का शीर्षक", + "loanTitlePlaceholder": "जैसे व्यक्तिगत उधार", + "loanAmountLabel": "कुल राशि *", + "loanInstallmentsLabel": "किस्तें *", + "loanStartMonthLabel": "पहला देय महीना *", + "loanNotesLabel": "नोट्स", + "loanBorrowerRequired": "व्यक्ति आवश्यक है", + "loanInstallmentsRequired": "किस्तों की संख्या दर्ज करें", + "loanStartMonthRequired": "पहला देय महीना दर्ज करें", + "loanAddedToast": "उधार जोड़ा गया", + "loanSavedToast": "उधार सहेजा गया", + "loanDeletedToast": "उधार हटाया गया", + "loanPaymentAddedToast": "भुगतान दर्ज किया गया", + "loanPaymentTitle": "ऋण भुगतान: {{borrower}}", + "typeLoan": "उधार", + "tabsLabel": "बजट अनुभाग", + "budgetTab": "बजट", + "loansTab": "उधार", + "filteredTransactions": "फ़िल्टर किए गए लेन-देन", + "clearLoanFilter": "फ़िल्टर हटाएं", + "loanFilterActive": "उधार: {{title}}", + "filterLoanTransactions": "इस उधार के लेन-देन दिखाएं", + "loansEmptyDescription": "+ बटन से उधार चुनकर नया उधार बनाएं।", + "newCategoryTitle": "नई श्रेणी", + "newCategoryPlaceholder": "श्रेणी का नाम", + "newSubcategoryTitle": "नई उपश्रेणी", + "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 815a04e..8b42816 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Su", "customizeMoveDown": "Giù", "overdueTasksChip": "{{count}} compito scaduto", - "overdueTasksChipPlural": "{{count}} compiti scaduti" + "overdueTasksChipPlural": "{{count}} compiti scaduti", + "customizeManage": "Widget", + "customizeExit": "Esci dalla personalizzazione", + "customizeDrag": "Trascina widget", + "customizeSize": "Dimensione", + "customizeSizeFor": "Dimensione di {{widget}}", + "customizeHide": "Nascondi {{widget}}" }, "tasks": { "title": "Compiti", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Nome della nuova sottocategoria:", "categoryAddedToast": "Categoria aggiunta.", "subcategoryAddedToast": "Sottocategoria aggiunta.", - "emptyAction": "Aggiungi voce" + "emptyAction": "Aggiungi voce", + "loansTitle": "Prestiti", + "loansSummary": "{{count}} attivi · {{amount}} rimanenti", + "newLoan": "Nuovo prestito", + "createLoan": "Crea prestito", + "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", + "loansEmpty": "Nessun prestito attivo.", + "loanInstallmentMeta": "{{paid}} di {{total}} rate pagate", + "loanRemainingOf": "di {{total}}", + "loanNextDue": "Prossima: {{month}}", + "loanPaidStatus": "Pagato", + "markLoanPaid": "Segna pagato", + "loanBorrowerLabel": "Persona *", + "loanBorrowerPlaceholder": "Es. Lais", + "loanTitleLabel": "Titolo del prestito", + "loanTitlePlaceholder": "Es. Prestito personale", + "loanAmountLabel": "Importo totale *", + "loanInstallmentsLabel": "Rate *", + "loanStartMonthLabel": "Primo mese di scadenza *", + "loanNotesLabel": "Note", + "loanBorrowerRequired": "La persona è obbligatoria", + "loanInstallmentsRequired": "Inserisci il numero di rate", + "loanStartMonthRequired": "Inserisci il primo mese di scadenza", + "loanAddedToast": "Prestito aggiunto", + "loanSavedToast": "Prestito salvato", + "loanDeletedToast": "Prestito eliminato", + "loanPaymentAddedToast": "Pagamento registrato", + "loanPaymentTitle": "Rimborso del prestito: {{borrower}}", + "typeLoan": "Prestito", + "tabsLabel": "Sezioni del bilancio", + "budgetTab": "Bilancio", + "loansTab": "Prestiti", + "filteredTransactions": "Movimenti filtrati", + "clearLoanFilter": "Cancella filtro", + "loanFilterActive": "Prestito: {{title}}", + "filterLoanTransactions": "Mostra i movimenti di questo prestito", + "loansEmptyDescription": "Crea un prestito dal pulsante + e scegli Prestito.", + "newCategoryTitle": "Nuova categoria", + "newCategoryPlaceholder": "Nome categoria", + "newSubcategoryTitle": "Nuova 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 e0099a1..87df852 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -112,7 +112,13 @@ "customizeMoveUp": "上へ", "customizeMoveDown": "下へ", "overdueTasksChip": "期限超過のタスク {{count}} 件", - "overdueTasksChipPlural": "期限超過のタスク {{count}} 件" + "overdueTasksChipPlural": "期限超過のタスク {{count}} 件", + "customizeManage": "ウィジェット", + "customizeExit": "カスタマイズを終了", + "customizeDrag": "ウィジェットをドラッグ", + "customizeSize": "サイズ", + "customizeSizeFor": "{{widget}} のサイズ", + "customizeHide": "{{widget}} を非表示" }, "tasks": { "title": "タスク", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "貸付 / 利息", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "エントリを追加" + "emptyAction": "エントリを追加", + "loansTitle": "貸付", + "loansSummary": "{{count}} 件が進行中 · 残り {{amount}}", + "newLoan": "新しい貸付", + "createLoan": "貸付を作成", + "editLoan": "貸付を編集", + "deleteLoan": "貸付を削除", + "deleteLoanConfirm": "貸付「{{title}}」を削除しますか?予算に記録済みの返済も削除されます。", + "deleteLoanPaymentConfirm": "このローン支払いを削除しますか?", + "loanRemainingAmount": "残額", + "loanRemainingInstallments": "残り回数", + "loanPaidAmount": "返済済み", + "loansEmpty": "進行中の貸付はありません。", + "loanInstallmentMeta": "{{total}} 回中 {{paid}} 回返済済み", + "loanRemainingOf": "{{total}} のうち", + "loanNextDue": "次回:{{month}}", + "loanPaidStatus": "完済", + "markLoanPaid": "返済済みにする", + "loanBorrowerLabel": "相手 *", + "loanBorrowerPlaceholder": "例:Lais", + "loanTitleLabel": "貸付タイトル", + "loanTitlePlaceholder": "例:個人貸付", + "loanAmountLabel": "合計金額 *", + "loanInstallmentsLabel": "分割回数 *", + "loanStartMonthLabel": "初回支払月 *", + "loanNotesLabel": "メモ", + "loanBorrowerRequired": "相手を入力してください", + "loanInstallmentsRequired": "分割回数を入力してください", + "loanStartMonthRequired": "初回支払月を入力してください", + "loanAddedToast": "貸付を追加しました", + "loanSavedToast": "貸付を保存しました", + "loanDeletedToast": "貸付を削除しました", + "loanPaymentAddedToast": "返済を記録しました", + "loanPaymentTitle": "ローン返済: {{borrower}}", + "typeLoan": "貸付", + "tabsLabel": "予算セクション", + "budgetTab": "予算", + "loansTab": "貸付", + "filteredTransactions": "絞り込み済み取引", + "clearLoanFilter": "フィルター解除", + "loanFilterActive": "貸付:{{title}}", + "filterLoanTransactions": "この貸付の取引を表示", + "loansEmptyDescription": "+ ボタンから貸付を選んで作成します。", + "newCategoryTitle": "新しいカテゴリ", + "newCategoryPlaceholder": "カテゴリ名", + "newSubcategoryTitle": "新しいサブカテゴリ", + "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 0bba398..845b74f 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Mover para cima", "customizeMoveDown": "Mover para baixo", "overdueTasksChip": "{{count}} tarefa vencida", - "overdueTasksChipPlural": "{{count}} tarefas vencidas" + "overdueTasksChipPlural": "{{count}} tarefas vencidas", + "customizeManage": "Widgets", + "customizeExit": "Sair da personalização", + "customizeDrag": "Arrastar widget", + "customizeSize": "Tamanho", + "customizeSizeFor": "Tamanho de {{widget}}", + "customizeHide": "Ocultar {{widget}}" }, "tasks": { "title": "Tarefas", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Nome da nova subcategoria:", "categoryAddedToast": "Categoria adicionada.", "subcategoryAddedToast": "Subcategoria adicionada.", - "emptyAction": "Adicionar entrada" + "emptyAction": "Adicionar entrada", + "loansTitle": "Empréstimos", + "loansSummary": "{{count}} ativos · {{amount}} restantes", + "newLoan": "Novo empréstimo", + "createLoan": "Criar empréstimo", + "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", + "loansEmpty": "Nenhum empréstimo ativo.", + "loanInstallmentMeta": "{{paid}} de {{total}} parcelas pagas", + "loanRemainingOf": "de {{total}}", + "loanNextDue": "Próxima: {{month}}", + "loanPaidStatus": "Pago", + "markLoanPaid": "Dar baixa", + "loanBorrowerLabel": "Pessoa *", + "loanBorrowerPlaceholder": "Ex.: Lais", + "loanTitleLabel": "Título do empréstimo", + "loanTitlePlaceholder": "Ex.: Empréstimo pessoal", + "loanAmountLabel": "Valor total *", + "loanInstallmentsLabel": "Parcelas *", + "loanStartMonthLabel": "Primeiro mês de vencimento *", + "loanNotesLabel": "Observações", + "loanBorrowerRequired": "Informe a pessoa", + "loanInstallmentsRequired": "Informe a quantidade de parcelas", + "loanStartMonthRequired": "Informe o primeiro mês de vencimento", + "loanAddedToast": "Empréstimo adicionado", + "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", + "loansTab": "Empréstimos", + "filteredTransactions": "Transações filtradas", + "clearLoanFilter": "Limpar filtro", + "loanFilterActive": "Empréstimo: {{title}}", + "filterLoanTransactions": "Mostrar transações deste empréstimo", + "loansEmptyDescription": "Crie um empréstimo pelo botão + e escolha Empréstimo.", + "newCategoryTitle": "Nova categoria", + "newCategoryPlaceholder": "Nome da categoria", + "newSubcategoryTitle": "Nova 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 dafba6e..bdeb66c 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Вверх", "customizeMoveDown": "Вниз", "overdueTasksChip": "{{count}} просроченная задача", - "overdueTasksChipPlural": "{{count}} просроченных задач" + "overdueTasksChipPlural": "{{count}} просроченных задач", + "customizeManage": "Виджеты", + "customizeExit": "Выйти из настройки", + "customizeDrag": "Перетащить виджет", + "customizeSize": "Размер", + "customizeSizeFor": "Размер для {{widget}}", + "customizeHide": "Скрыть {{widget}}" }, "tasks": { "title": "Задачи", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "Займы / Проценты", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "Добавить запись" + "emptyAction": "Добавить запись", + "loansTitle": "Займы", + "loansSummary": "{{count}} активных · осталось {{amount}}", + "newLoan": "Новый займ", + "createLoan": "Создать займ", + "editLoan": "Изменить займ", + "deleteLoan": "Удалить займ", + "deleteLoanConfirm": "Удалить займ «{{title}}»? Платежи, уже добавленные в бюджет, тоже будут удалены.", + "deleteLoanPaymentConfirm": "Удалить этот платеж по займу?", + "loanRemainingAmount": "Осталось", + "loanRemainingInstallments": "Осталось платежей", + "loanPaidAmount": "Оплачено", + "loansEmpty": "Нет активных займов.", + "loanInstallmentMeta": "Оплачено {{paid}} из {{total}} платежей", + "loanRemainingOf": "из {{total}}", + "loanNextDue": "Следующий: {{month}}", + "loanPaidStatus": "Оплачено", + "markLoanPaid": "Отметить оплату", + "loanBorrowerLabel": "Человек *", + "loanBorrowerPlaceholder": "Напр. Lais", + "loanTitleLabel": "Название займа", + "loanTitlePlaceholder": "Напр. Личный займ", + "loanAmountLabel": "Общая сумма *", + "loanInstallmentsLabel": "Платежи *", + "loanStartMonthLabel": "Первый месяц оплаты *", + "loanNotesLabel": "Заметки", + "loanBorrowerRequired": "Укажите человека", + "loanInstallmentsRequired": "Укажите количество платежей", + "loanStartMonthRequired": "Укажите первый месяц оплаты", + "loanAddedToast": "Займ добавлен", + "loanSavedToast": "Займ сохранён", + "loanDeletedToast": "Займ удалён", + "loanPaymentAddedToast": "Платёж записан", + "loanPaymentTitle": "Платеж по займу: {{borrower}}", + "typeLoan": "Займ", + "tabsLabel": "Разделы бюджета", + "budgetTab": "Бюджет", + "loansTab": "Займы", + "filteredTransactions": "Отфильтрованные операции", + "clearLoanFilter": "Сбросить фильтр", + "loanFilterActive": "Займ: {{title}}", + "filterLoanTransactions": "Показать операции этого займа", + "loansEmptyDescription": "Создайте займ кнопкой + и выберите Займ.", + "newCategoryTitle": "Новая категория", + "newCategoryPlaceholder": "Название категории", + "newSubcategoryTitle": "Новая подкатегория", + "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 1c7ab10..54e94cc 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Flytta upp", "customizeMoveDown": "Flytta ner", "overdueTasksChip": "{{count}} förfallen uppgift", - "overdueTasksChipPlural": "{{count}} förfallna uppgifter" + "overdueTasksChipPlural": "{{count}} förfallna uppgifter", + "customizeManage": "Widgetar", + "customizeExit": "Avsluta anpassning", + "customizeDrag": "Dra widget", + "customizeSize": "Storlek", + "customizeSizeFor": "Storlek för {{widget}}", + "customizeHide": "Dölj {{widget}}" }, "tasks": { "title": "Uppgifter", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Namn på den nya underkategorin:", "categoryAddedToast": "Kategori tillagd.", "subcategoryAddedToast": "Underkategori har lagts till.", - "emptyAction": "Lägg till post" + "emptyAction": "Lägg till post", + "loansTitle": "Lån", + "loansSummary": "{{count}} aktiva · {{amount}} kvar", + "newLoan": "Nytt lån", + "createLoan": "Skapa lån", + "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", + "loansEmpty": "Inga aktiva lån.", + "loanInstallmentMeta": "{{paid}} av {{total}} delbetalningar betalda", + "loanRemainingOf": "av {{total}}", + "loanNextDue": "Nästa: {{month}}", + "loanPaidStatus": "Betalt", + "markLoanPaid": "Markera betalt", + "loanBorrowerLabel": "Person *", + "loanBorrowerPlaceholder": "t.ex. Lais", + "loanTitleLabel": "Lånetitel", + "loanTitlePlaceholder": "t.ex. Privat lån", + "loanAmountLabel": "Totalbelopp *", + "loanInstallmentsLabel": "Delbetalningar *", + "loanStartMonthLabel": "Första förfallomånaden *", + "loanNotesLabel": "Anteckningar", + "loanBorrowerRequired": "Person krävs", + "loanInstallmentsRequired": "Ange antal delbetalningar", + "loanStartMonthRequired": "Ange första förfallomånaden", + "loanAddedToast": "Lån tillagt", + "loanSavedToast": "Lån sparat", + "loanDeletedToast": "Lån borttaget", + "loanPaymentAddedToast": "Betalning registrerad", + "loanPaymentTitle": "Låneåterbetalning: {{borrower}}", + "typeLoan": "Lån", + "tabsLabel": "Budgetsektioner", + "budgetTab": "Budget", + "loansTab": "Lån", + "filteredTransactions": "Filtrerade transaktioner", + "clearLoanFilter": "Rensa filter", + "loanFilterActive": "Lån: {{title}}", + "filterLoanTransactions": "Visa transaktioner för detta lån", + "loansEmptyDescription": "Skapa ett lån med +-knappen och välj Lån.", + "newCategoryTitle": "Ny kategori", + "newCategoryPlaceholder": "Kategorinamn", + "newSubcategoryTitle": "Ny underkategori", + "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 4175e73..b76bd3d 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Yukarı", "customizeMoveDown": "Aşağı", "overdueTasksChip": "{{count}} gecikmiş görev", - "overdueTasksChipPlural": "{{count}} gecikmiş görev" + "overdueTasksChipPlural": "{{count}} gecikmiş görev", + "customizeManage": "Widgetlar", + "customizeExit": "Özelleştirmeden çık", + "customizeDrag": "Widgetı sürükle", + "customizeSize": "Boyut", + "customizeSizeFor": "{{widget}} boyutu", + "customizeHide": "{{widget}} gizle" }, "tasks": { "title": "Görevler", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "Borçlar / Faiz", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "Giriş ekle" + "emptyAction": "Giriş ekle", + "loansTitle": "Borçlar", + "loansSummary": "{{count}} aktif · {{amount}} kaldı", + "newLoan": "Yeni borç", + "createLoan": "Borç oluştur", + "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", + "loansEmpty": "Aktif borç yok.", + "loanInstallmentMeta": "{{paid}} / {{total}} taksit ödendi", + "loanRemainingOf": "{{total}} içinden", + "loanNextDue": "Sonraki: {{month}}", + "loanPaidStatus": "Ödendi", + "markLoanPaid": "Ödendi işaretle", + "loanBorrowerLabel": "Kişi *", + "loanBorrowerPlaceholder": "Örn. Lais", + "loanTitleLabel": "Borç başlığı", + "loanTitlePlaceholder": "Örn. Kişisel borç", + "loanAmountLabel": "Toplam tutar *", + "loanInstallmentsLabel": "Taksitler *", + "loanStartMonthLabel": "İlk vade ayı *", + "loanNotesLabel": "Notlar", + "loanBorrowerRequired": "Kişi gerekli", + "loanInstallmentsRequired": "Taksit sayısını girin", + "loanStartMonthRequired": "İlk vade ayını girin", + "loanAddedToast": "Borç eklendi", + "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", + "loansTab": "Borçlar", + "filteredTransactions": "Filtrelenmiş işlemler", + "clearLoanFilter": "Filtreyi temizle", + "loanFilterActive": "Borç: {{title}}", + "filterLoanTransactions": "Bu borcun işlemlerini göster", + "loansEmptyDescription": "+ düğmesinden Borç seçerek yeni bir borç oluşturun.", + "newCategoryTitle": "Yeni kategori", + "newCategoryPlaceholder": "Kategori adı", + "newSubcategoryTitle": "Yeni alt kategori", + "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 c2d66a7..88f277e 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -112,7 +112,13 @@ "customizeMoveUp": "Перемістити вгору", "customizeMoveDown": "Перемістити вниз", "overdueTasksChip": "{{count}} прострочене завдання", - "overdueTasksChipPlural": "{{count}} прострочених завдань" + "overdueTasksChipPlural": "{{count}} прострочених завдань", + "customizeManage": "Віджети", + "customizeExit": "Вийти з налаштування", + "customizeDrag": "Перетягнути віджет", + "customizeSize": "Розмір", + "customizeSizeFor": "Розмір для {{widget}}", + "customizeHide": "Приховати {{widget}}" }, "tasks": { "title": "Завдання", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "Позики / Відсотки", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "Додати запис" + "emptyAction": "Додати запис", + "loansTitle": "Позики", + "loansSummary": "{{count}} активних · залишилось {{amount}}", + "newLoan": "Нова позика", + "createLoan": "Створити позику", + "editLoan": "Редагувати позику", + "deleteLoan": "Видалити позику", + "deleteLoanConfirm": "Видалити позику «{{title}}»? Платежі, вже додані до бюджету, також буде видалено.", + "deleteLoanPaymentConfirm": "Видалити цей платіж за позикою?", + "loanRemainingAmount": "Залишилось", + "loanRemainingInstallments": "Залишилось платежів", + "loanPaidAmount": "Сплачено", + "loansEmpty": "Немає активних позик.", + "loanInstallmentMeta": "Сплачено {{paid}} з {{total}} платежів", + "loanRemainingOf": "з {{total}}", + "loanNextDue": "Наступний: {{month}}", + "loanPaidStatus": "Сплачено", + "markLoanPaid": "Позначити сплату", + "loanBorrowerLabel": "Людина *", + "loanBorrowerPlaceholder": "Напр. Lais", + "loanTitleLabel": "Назва позики", + "loanTitlePlaceholder": "Напр. Особиста позика", + "loanAmountLabel": "Загальна сума *", + "loanInstallmentsLabel": "Платежі *", + "loanStartMonthLabel": "Перший місяць сплати *", + "loanNotesLabel": "Нотатки", + "loanBorrowerRequired": "Вкажіть людину", + "loanInstallmentsRequired": "Вкажіть кількість платежів", + "loanStartMonthRequired": "Вкажіть перший місяць сплати", + "loanAddedToast": "Позику додано", + "loanSavedToast": "Позику збережено", + "loanDeletedToast": "Позику видалено", + "loanPaymentAddedToast": "Платіж записано", + "loanPaymentTitle": "Платіж за позикою: {{borrower}}", + "typeLoan": "Позика", + "tabsLabel": "Розділи бюджету", + "budgetTab": "Бюджет", + "loansTab": "Позики", + "filteredTransactions": "Відфільтровані операції", + "clearLoanFilter": "Очистити фільтр", + "loanFilterActive": "Позика: {{title}}", + "filterLoanTransactions": "Показати операції цієї позики", + "loansEmptyDescription": "Створіть позику кнопкою + і виберіть Позика.", + "newCategoryTitle": "Нова категорія", + "newCategoryPlaceholder": "Назва категорії", + "newSubcategoryTitle": "Нова підкатегорія", + "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 e12f0c5..0220a8b 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -112,7 +112,13 @@ "customizeMoveUp": "上移", "customizeMoveDown": "下移", "overdueTasksChip": "{{count}} 个逾期任务", - "overdueTasksChipPlural": "{{count}} 个逾期任务" + "overdueTasksChipPlural": "{{count}} 个逾期任务", + "customizeManage": "小组件", + "customizeExit": "退出自定义", + "customizeDrag": "拖动小组件", + "customizeSize": "大小", + "customizeSizeFor": "{{widget}} 的大小", + "customizeHide": "隐藏 {{widget}}" }, "tasks": { "title": "任务", @@ -572,7 +578,7 @@ "subcatCoursesCollege": "Courses / College", "subcatSchoolSupplies": "School supplies", "subcatLanguages": "Languages", - "subcatLoansInterest": "Loans / Interest", + "subcatLoansInterest": "借款 / 利息", "subcatBankFees": "Bank fees", "subcatInsuranceOther": "Insurance", "subcatInvestments": "Investments", @@ -584,7 +590,61 @@ "newSubcategoryPrompt": "Name of the new subcategory:", "categoryAddedToast": "Category added.", "subcategoryAddedToast": "Subcategory added.", - "emptyAction": "添加记录" + "emptyAction": "添加记录", + "loansTitle": "借款", + "loansSummary": "{{count}} 笔进行中 · 剩余 {{amount}}", + "newLoan": "新建借款", + "createLoan": "创建借款", + "editLoan": "编辑借款", + "deleteLoan": "删除借款", + "deleteLoanConfirm": "删除借款“{{title}}”?已记入预算的还款也会被删除。", + "deleteLoanPaymentConfirm": "删除这笔借款还款?", + "loanRemainingAmount": "剩余金额", + "loanRemainingInstallments": "剩余期数", + "loanPaidAmount": "已还金额", + "loansEmpty": "没有进行中的借款。", + "loanInstallmentMeta": "已还 {{paid}} / {{total}} 期", + "loanRemainingOf": "共 {{total}}", + "loanNextDue": "下一期:{{month}}", + "loanPaidStatus": "已还清", + "markLoanPaid": "标记已还", + "loanBorrowerLabel": "借款人 *", + "loanBorrowerPlaceholder": "例如:Lais", + "loanTitleLabel": "借款标题", + "loanTitlePlaceholder": "例如:个人借款", + "loanAmountLabel": "总金额 *", + "loanInstallmentsLabel": "期数 *", + "loanStartMonthLabel": "首期月份 *", + "loanNotesLabel": "备注", + "loanBorrowerRequired": "请填写借款人", + "loanInstallmentsRequired": "请输入期数", + "loanStartMonthRequired": "请输入首期月份", + "loanAddedToast": "借款已添加", + "loanSavedToast": "借款已保存", + "loanDeletedToast": "借款已删除", + "loanPaymentAddedToast": "还款已记录", + "loanPaymentTitle": "借款还款:{{borrower}}", + "typeLoan": "借款", + "tabsLabel": "预算分区", + "budgetTab": "预算", + "loansTab": "借款", + "filteredTransactions": "已筛选交易", + "clearLoanFilter": "清除筛选", + "loanFilterActive": "借款:{{title}}", + "filterLoanTransactions": "显示此借款的交易", + "loansEmptyDescription": "点击 + 按钮并选择借款来创建。", + "newCategoryTitle": "新类别", + "newCategoryPlaceholder": "类别名称", + "newSubcategoryTitle": "新子类别", + "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 c454be1..c076aa3 100644 --- a/public/pages/budget.js +++ b/public/pages/budget.js @@ -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 } from '/i18n.js'; import { esc } from '/utils/html.js'; @@ -124,6 +124,10 @@ let state = { entries: [], summary: null, prevSummary: null, // Vormonat für Monatsvergleich + loans: { loans: [], summary: { active_count: 0, remaining_amount: 0, remaining_installments: 0 } }, + activeTab: 'budget', + loanFilterId: null, + loanStatusFilter: 'active', currency: 'EUR', meta: { expenseCategories: [], incomeCategories: [], expenseSubcategories: {} }, }; @@ -148,6 +152,11 @@ function addMonths(ym, n) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } +function setHtml(element, html) { + element.replaceChildren(); + element.insertAdjacentHTML('afterbegin', html); +} + // -------------------------------------------------------- // API // -------------------------------------------------------- @@ -155,21 +164,24 @@ function addMonths(ym, n) { async function loadMonth(month) { const prevMonth = addMonths(month, -1); try { - const [entriesRes, summaryRes, prevSummaryRes] = await Promise.all([ + const [entriesRes, summaryRes, prevSummaryRes, loansRes] = await Promise.all([ api.get(`/budget?month=${month}`), api.get(`/budget/summary?month=${month}`), api.get(`/budget/summary?month=${prevMonth}`), + api.get('/budget/loans'), ]); state.month = month; state.entries = entriesRes.data; state.summary = summaryRes.data; state.prevSummary = prevSummaryRes.data; + state.loans = loansRes.data; } catch (err) { console.error('[Budget] loadMonth Fehler:', err); state.month = month; state.entries = []; state.summary = { income: 0, expenses: 0, balance: 0, byCategory: [] }; state.prevSummary = null; + state.loans = { loans: [], summary: { active_count: 0, remaining_amount: 0, remaining_installments: 0 } }; window.oikos?.showToast(t('budget.loadError'), 'danger'); } } @@ -206,7 +218,7 @@ export async function render(container, { user }) { state.currency = prefsRes.data?.currency ?? 'EUR'; } catch (_) { /* Fallback auf EUR */ } - container.innerHTML = ` + setHtml(container, `

${t('budget.title')}

@@ -215,6 +227,14 @@ export async function render(container, { user }) { +
+ + +
@@ -229,7 +249,7 @@ export async function render(container, { user }) {
- `; + `); if (window.lucide) lucide.createIcons(); @@ -264,6 +284,12 @@ function wireNav() { const addHandler = () => openBudgetModal({ mode: 'create' }); _container.querySelector('#budget-add').addEventListener('click', addHandler); _container.querySelector('#fab-new-budget').addEventListener('click', addHandler); + _container.querySelectorAll('.budget-tab').forEach((tab) => { + tab.addEventListener('click', () => { + state.activeTab = tab.dataset.tab; + renderBody(); + }); + }); updateLabel(); } @@ -283,10 +309,19 @@ function renderBody() { const s = state.summary; const p = state.prevSummary; + updateTabs(); + if (state.activeTab === 'loans') { + setHtml(body, renderLoansPage()); + wireLoansPage(); + if (window.lucide) lucide.createIcons(); + return; + } + const balanceClass = s.balance >= 0 ? 'budget-summary-card--balance-positive' : 'budget-summary-card--balance-negative'; const prevLabel = p ? formatMonthLabel(p.month).split(' ')[0].slice(0, 3) : ''; - body.innerHTML = ` + setHtml(body, ` +
@@ -318,18 +353,23 @@ function renderBody() {
- ${t('budget.transactions')} +
+ ${t('budget.transactions')} +
+
${state.entries.length ? ` CSV ` : ''} +
${renderEntries()}
- `; +
+ `); if (window.lucide) lucide.createIcons(); _container.querySelector('#empty-cta-budget')?.addEventListener('click', () => { @@ -349,6 +389,14 @@ function renderBody() { }); } +function updateTabs() { + _container.querySelectorAll('.budget-tab').forEach((tab) => { + const active = tab.dataset.tab === state.activeTab; + tab.classList.toggle('budget-tab--active', active); + tab.setAttribute('aria-selected', String(active)); + }); +} + function renderCategoryBars(byCategory) { const maxAbs = Math.max(...byCategory.map((c) => Math.abs(c.total)), 1); @@ -415,6 +463,310 @@ function renderEntries() { }).join(''); } +function renderLoansDashboard() { + const loans = state.loans?.loans ?? []; + if (!loans.length) return ''; + + const summary = state.loans?.summary ?? {}; + const visibleLoans = filteredLoans(); + + return ` +
+
+
+
${t('budget.loansTitle')}
+
${t('budget.loansSummary', { + count: summary.active_count ?? 0, + amount: formatAmount(summary.remaining_amount ?? 0), + })}
+ ${state.loanFilterId ? `
${esc(activeLoanLabel())}
` : ''} +
+
+ ${state.loanFilterId ? ` + ` : ''} + + + +
+
+
+
+ ${t('budget.loanRemainingAmount')} + ${formatAmount(summary.remaining_amount ?? 0)} +
+
+ ${t('budget.loanRemainingInstallments')} + ${summary.remaining_installments ?? 0} +
+
+ ${t('budget.loanPaidAmount')} + ${formatAmount(summary.paid_amount ?? 0)} +
+
+ ${visibleLoans.length ? ` +
+ ${visibleLoans.map(renderLoanCard).join('')} +
+ ` : ` +
${t('budget.loansEmpty')}
+ `} + ${renderLoanTransactions(visibleLoans)} +
+ `; +} + +function filteredLoans() { + const loans = state.loans?.loans ?? []; + 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) { + 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 }) => renderLoanPaymentEntry(loan, payment)).join('')} +
+
`; +} + +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 ` +
+
+
+
${esc(payment.entry_title || t('budget.loanPaymentTitle', { borrower: loan.borrower }))}
+ +
+
+${formatAmount(payment.amount)}
+
+ ${entry ? ` + ` : ''} + +
+
+ `; +} + +function renderLoansPage() { + const loans = state.loans?.loans ?? []; + if (!loans.length) { + return `
+
+ +
${t('budget.loansEmpty')}
+
${t('budget.loansEmptyDescription')}
+ +
+
`; + } + + return `
+ ${renderLoansDashboard()} +
`; +} + +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; + 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)); + }); + }); + _container.querySelectorAll('[data-action="loan-edit"]').forEach((btn) => { + btn.addEventListener('click', () => { + const loan = state.loans.loans.find((item) => item.id === parseInt(btn.dataset.id, 10)); + if (loan) openLoanModal(loan); + }); + }); + _container.querySelectorAll('[data-action="loan-delete"]').forEach((btn) => { + btn.addEventListener('click', async () => { + await deleteLoan(parseInt(btn.dataset.id, 10)); + }); + }); + _container.querySelectorAll('[data-action="loan-filter"]').forEach((btn) => { + 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) { + 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)}
+ +
+
${esc(loan.borrower)} · ${t('budget.loanInstallmentMeta', { + paid: loan.paid_installments, + total: loan.installment_count, + })}
+
+
+ ${formatAmount(loan.remaining_amount)} + ${t('budget.loanRemainingOf', { total: formatAmount(loan.total_amount) })} +
+
+ +
+ +
+ `; +} + /** * Rendert eine Trend-Zeile im Vergleich zum Vormonat. * Alle drei Metriken (income, expenses, balance) nutzen dieselbe Logik: @@ -438,16 +790,17 @@ function renderTrend(current, prev, prevLabel) { } function formatEntryDate(dateStr) { - return formatDate(new Date(dateStr + 'T00:00:00')); + return formatDate(dateStr); } // -------------------------------------------------------- // Modal // -------------------------------------------------------- -function openBudgetModal({ mode, entry = null }) { +function openBudgetModal({ mode, entry = null, initialType = '' }) { const isEdit = mode === 'edit'; const today = new Date().toISOString().slice(0, 10); + const todayMonth = today.slice(0, 7); const isExpense = isEdit ? entry.amount < 0 : true; const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : ''; @@ -463,27 +816,29 @@ function openBudgetModal({ mode, entry = null }) { ).join(''); const content = ` -
+
+ ${!isEdit ? `` : ''}
-
+
-
+
-
+
@@ -491,7 +846,7 @@ function openBudgetModal({ mode, entry = null }) {
-
+
@@ -499,13 +854,13 @@ function openBudgetModal({ mode, entry = null }) {
-
+
-
+
+ + +
+ + +
+ +
+ `); + panel.append(overlay); + if (window.lucide) lucide.createIcons(); + + const input = overlay.querySelector('#budget-inline-name'); + const cleanup = (value = '') => { + overlay.remove(); + resolve(value); + }; + overlay.querySelectorAll('[data-action="inline-cancel"]').forEach((btn) => { + btn.addEventListener('click', () => cleanup('')); + }); + overlay.querySelector('[data-action="inline-save"]').addEventListener('click', () => { + cleanup(input.value.trim()); + }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') cleanup(input.value.trim()); + if (e.key === 'Escape') cleanup(''); + }); + input.focus(); + }); +} + +async function saveLoanFromPanel(panel, saveBtn, { loan = null, closeAfterSave = false } = {}) { + const isEdit = Boolean(loan); + const borrower = panel.querySelector('#lm-borrower').value.trim(); + const title = panel.querySelector('#lm-title').value.trim() || borrower; + const total_amount = parseFloat(panel.querySelector('#lm-amount').value); + const installment_count = parseInt(panel.querySelector('#lm-installments').value, 10); + const start_month = panel.querySelector('#lm-start').value; + const notes = panel.querySelector('#lm-notes').value.trim(); + + if (!borrower) { window.oikos?.showToast(t('budget.loanBorrowerRequired'), 'error'); return; } + if (isNaN(total_amount) || total_amount <= 0) { window.oikos?.showToast(t('budget.validAmountRequired'), 'error'); return; } + if (!Number.isInteger(installment_count) || installment_count < 1) { window.oikos?.showToast(t('budget.loanInstallmentsRequired'), 'error'); return; } + if (!/^\d{4}-\d{2}$/.test(start_month)) { window.oikos?.showToast(t('budget.loanStartMonthRequired'), 'error'); return; } + + saveBtn.disabled = true; + saveBtn.textContent = '...'; + try { + const body = { borrower, title, total_amount, installment_count, start_month, notes }; + if (isEdit) { + await api.put(`/budget/loans/${loan.id}`, body); + } else { + await api.post('/budget/loans', body); + } + await loadMonth(state.month); + if (closeAfterSave) closeModal({ force: true }); + renderBody(); + window.oikos?.showToast(isEdit ? t('budget.loanSavedToast') : t('budget.loanAddedToast'), 'success'); + } catch (err) { + window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); + saveBtn.disabled = false; + saveBtn.textContent = isEdit ? t('common.save') : t('budget.createLoan'); + } +} + +function openLoanModal(loan = null) { + const isEdit = Boolean(loan); + const todayMonth = new Date().toISOString().slice(0, 7); + const content = ` +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ `; + + openSharedModal({ + title: isEdit ? t('budget.editLoan') : t('budget.newLoan'), + content, + size: 'sm', + onSave(panel) { + panel.querySelector('#lm-cancel').addEventListener('click', closeModal); + panel.querySelector('#lm-save').addEventListener('click', async () => { + const saveBtn = panel.querySelector('#lm-save'); + await saveLoanFromPanel(panel, saveBtn, { loan, closeAfterSave: true }); + }); + }, + }); +} + +async function markLoanPayment(id) { + const loan = state.loans.loans.find((item) => item.id === id); + if (!loan?.next_installment_number) return; + const today = new Date().toISOString().slice(0, 10); + try { + await api.post(`/budget/loans/${id}/payments`, { + installment_number: loan.next_installment_number, + amount: loan.next_installment_number === loan.installment_count + ? loan.remaining_amount + : Math.min(loan.installment_amount, loan.remaining_amount), + paid_date: today, + }); + await loadMonth(state.month); + renderBody(); + window.oikos?.showToast(t('budget.loanPaymentAddedToast'), 'success'); + } catch (err) { + window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); + } +} + +async function deleteLoan(id) { + const loan = state.loans.loans.find((item) => item.id === id); + if (!loan) 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); + renderBody(); + window.oikos?.showToast(t('budget.loanDeletedToast'), 'success'); + } catch (err) { + window.oikos?.showToast(err.data?.error ?? t('common.unknownError'), 'error'); + } +} + +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 // -------------------------------------------------------- @@ -685,8 +1274,7 @@ async function deleteEntry(id) { if (undone) return; try { await api.delete(`/budget/${id}`); - const sumRes = await api.get(`/budget/summary?month=${state.month}`); - state.summary = sumRes.data; + await loadMonth(state.month); renderBody(); } catch (err) { if (entry) { diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js index ca79eb2..38049e2 100644 --- a/public/pages/dashboard.js +++ b/public/pages/dashboard.js @@ -113,7 +113,47 @@ function showOnboarding(appContainer) { // NEU — primäre Inhalte (tasks, calendar) ganz oben const WIDGET_IDS = ['tasks', 'calendar', 'weather', 'meals', 'shopping', 'birthdays', 'budget', 'family', 'notes']; -const DEFAULT_WIDGET_CONFIG = WIDGET_IDS.map((id, i) => ({ id, visible: true, order: i })); +const WIDGET_SIZE_OPTIONS = ['1x1', '1x2', '1x3', '1x4', '2x1', '2x2', '2x3', '2x4', '3x1', '3x2', '3x3', '3x4', '4x1', '4x2', '4x3', '4x4']; + +function widgetSizeLabel(size) { + return size; +} + +function defaultWidgetSize(id) { + if (['tasks', 'calendar'].includes(id)) return '2x2'; + if (['weather', 'shopping'].includes(id)) return '2x1'; + if (id === 'notes') return '2x1'; + return '1x1'; +} + +const DEFAULT_WIDGET_CONFIG = WIDGET_IDS.map((id, i) => ({ id, visible: true, order: i, size: defaultWidgetSize(id) })); + +function normalizeDashboardConfig(input) { + const valid = Array.isArray(input) + ? input + .filter((w) => w && typeof w === 'object' && WIDGET_IDS.includes(w.id)) + .map((w, i) => ({ + id: w.id, + visible: w.visible !== false, + order: Number.isFinite(Number(w.order)) ? Number(w.order) : i, + size: WIDGET_SIZE_OPTIONS.includes(w.size) ? w.size : defaultWidgetSize(w.id), + })) + : []; + const presentIds = new Set(valid.map((w) => w.id)); + for (const id of WIDGET_IDS) { + if (!presentIds.has(id)) { + valid.push({ id, visible: true, order: valid.length, size: defaultWidgetSize(id) }); + } + } + return valid + .sort((a, b) => a.order - b.order) + .map((w, i) => ({ ...w, order: i })); +} + +function setHtml(element, html) { + element.replaceChildren(); + element.insertAdjacentHTML('afterbegin', html); +} function widgetLabel(id) { const map = { @@ -523,7 +563,7 @@ function renderQuickAction({ route, label, icon, tone = '' }) { } -function renderDashboardOverview(user) { +function renderDashboardOverview(user, editing = false) { const dateLabel = formatDate(new Date()); const actions = [ @@ -541,10 +581,21 @@ function renderDashboardOverview(user) {

${greeting(user.display_name)}

-
${actions}
+ ${editing ? ` + ` : `
${actions}
`}
@@ -552,17 +603,48 @@ function renderDashboardOverview(user) { `; } -function widgetTileClass(id) { - // Primär: immer 2 Spalten breit (die wichtigsten Inhalte) - const primaryIds = ['tasks', 'calendar']; - // Sekundär: 2 Spalten ab 3-Spalten-Breakpoint (1024px) - const secondaryIds = ['weather', 'shopping']; - if (primaryIds.includes(id)) return 'widget--wide'; - if (secondaryIds.includes(id)) return 'widget--secondary'; - return ''; +function widgetSizeClass(size) { + return WIDGET_SIZE_OPTIONS.includes(size) ? `widget-size--${size}` : 'widget-size--1x1'; } -function renderDashboardLayout(cfg, data, weather, currency) { +function renderSizeMiniGrid(size) { + return ``; +} + +function renderSizeMiniGridCells(size) { + const [cols, rows] = size.split('x').map(Number); + return Array.from({ length: 16 }, (_, i) => { + const col = (i % 4) + 1; + const row = Math.floor(i / 4) + 1; + return ``; + }).join(''); +} + +function renderWidgetCustomizeControls(w) { + const sizeOptions = WIDGET_SIZE_OPTIONS.map((size) => ` + + `).join(''); + + return ` +
+ + + +
+ `; +} + +function renderDashboardLayout(cfg, data, weather, currency, { editing = false } = {}) { const widgetById = { tasks: () => renderUrgentTasks(data.urgentTasks ?? []), calendar: () => renderUpcomingEvents(data.upcomingEvents ?? []), @@ -580,11 +662,15 @@ function renderDashboardLayout(cfg, data, weather, currency) { .map((w) => { const html = widgetById[w.id](); if (!html) return ''; - return `
${html}
`; + return `
+ ${editing ? renderWidgetCustomizeControls(w) : ''} + ${html} +
`; }) .join(''); - return `
${tiles}
`; + return `
${tiles}
`; } function renderDashboardSkeleton() { @@ -800,6 +886,9 @@ function openCustomizeModal(currentConfig, onSave) { return draft.map((w, i) => { const isFirst = i === 0; const isLast = i === draft.length - 1; + const sizeOptions = WIDGET_SIZE_OPTIONS.map((size) => ` + + `).join(''); return `
${widgetLabel(w.id)} +