Separate budget and loan views
This commit is contained in:
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "تم حفظ القرض",
|
||||
"loanDeletedToast": "تم حذف القرض",
|
||||
"loanPaymentAddedToast": "تم تسجيل الدفع",
|
||||
"typeLoan": "قرض"
|
||||
"typeLoan": "قرض",
|
||||
"tabsLabel": "أقسام الميزانية",
|
||||
"budgetTab": "الميزانية",
|
||||
"loansTab": "القروض",
|
||||
"filteredTransactions": "المعاملات المصفاة",
|
||||
"clearLoanFilter": "مسح الفلتر",
|
||||
"loanFilterActive": "القرض: {{title}}",
|
||||
"filterLoanTransactions": "عرض معاملات هذا القرض",
|
||||
"loansEmptyDescription": "أنشئ قرضًا من زر + واختر قرض.",
|
||||
"newCategoryTitle": "فئة جديدة",
|
||||
"newCategoryPlaceholder": "اسم الفئة",
|
||||
"newSubcategoryTitle": "فئة فرعية جديدة",
|
||||
"newSubcategoryPlaceholder": "اسم الفئة الفرعية"
|
||||
},
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
|
||||
+13
-1
@@ -622,7 +622,19 @@
|
||||
"loanSavedToast": "Darlehen gespeichert",
|
||||
"loanDeletedToast": "Darlehen gelöscht",
|
||||
"loanPaymentAddedToast": "Zahlung erfasst",
|
||||
"typeLoan": "Darlehen"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Το δάνειο αποθηκεύτηκε",
|
||||
"loanDeletedToast": "Το δάνειο διαγράφηκε",
|
||||
"loanPaymentAddedToast": "Η πληρωμή καταγράφηκε",
|
||||
"typeLoan": "Δάνειο"
|
||||
"typeLoan": "Δάνειο",
|
||||
"tabsLabel": "Ενότητες προϋπολογισμού",
|
||||
"budgetTab": "Προϋπολογισμός",
|
||||
"loansTab": "Δάνεια",
|
||||
"filteredTransactions": "Φιλτραρισμένες συναλλαγές",
|
||||
"clearLoanFilter": "Καθαρισμός φίλτρου",
|
||||
"loanFilterActive": "Δάνειο: {{title}}",
|
||||
"filterLoanTransactions": "Εμφάνιση συναλλαγών αυτού του δανείου",
|
||||
"loansEmptyDescription": "Δημιουργήστε δάνειο από το κουμπί + και επιλέξτε Δάνειο.",
|
||||
"newCategoryTitle": "Νέα κατηγορία",
|
||||
"newCategoryPlaceholder": "Όνομα κατηγορίας",
|
||||
"newSubcategoryTitle": "Νέα υποκατηγορία",
|
||||
"newSubcategoryPlaceholder": "Όνομα υποκατηγορίας"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ρυθμίσεις",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Loan saved",
|
||||
"loanDeletedToast": "Loan deleted",
|
||||
"loanPaymentAddedToast": "Payment recorded",
|
||||
"typeLoan": "Loan"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Préstamo guardado",
|
||||
"loanDeletedToast": "Préstamo eliminado",
|
||||
"loanPaymentAddedToast": "Pago registrado",
|
||||
"typeLoan": "Préstamo"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Prêt enregistré",
|
||||
"loanDeletedToast": "Prêt supprimé",
|
||||
"loanPaymentAddedToast": "Paiement enregistré",
|
||||
"typeLoan": "Prêt"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "उधार सहेजा गया",
|
||||
"loanDeletedToast": "उधार हटाया गया",
|
||||
"loanPaymentAddedToast": "भुगतान दर्ज किया गया",
|
||||
"typeLoan": "उधार"
|
||||
"typeLoan": "उधार",
|
||||
"tabsLabel": "बजट अनुभाग",
|
||||
"budgetTab": "बजट",
|
||||
"loansTab": "उधार",
|
||||
"filteredTransactions": "फ़िल्टर किए गए लेन-देन",
|
||||
"clearLoanFilter": "फ़िल्टर हटाएं",
|
||||
"loanFilterActive": "उधार: {{title}}",
|
||||
"filterLoanTransactions": "इस उधार के लेन-देन दिखाएं",
|
||||
"loansEmptyDescription": "+ बटन से उधार चुनकर नया उधार बनाएं।",
|
||||
"newCategoryTitle": "नई श्रेणी",
|
||||
"newCategoryPlaceholder": "श्रेणी का नाम",
|
||||
"newSubcategoryTitle": "नई उपश्रेणी",
|
||||
"newSubcategoryPlaceholder": "उपश्रेणी का नाम"
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग्स",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Prestito salvato",
|
||||
"loanDeletedToast": "Prestito eliminato",
|
||||
"loanPaymentAddedToast": "Pagamento registrato",
|
||||
"typeLoan": "Prestito"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "貸付を保存しました",
|
||||
"loanDeletedToast": "貸付を削除しました",
|
||||
"loanPaymentAddedToast": "返済を記録しました",
|
||||
"typeLoan": "貸付"
|
||||
"typeLoan": "貸付",
|
||||
"tabsLabel": "予算セクション",
|
||||
"budgetTab": "予算",
|
||||
"loansTab": "貸付",
|
||||
"filteredTransactions": "絞り込み済み取引",
|
||||
"clearLoanFilter": "フィルター解除",
|
||||
"loanFilterActive": "貸付:{{title}}",
|
||||
"filterLoanTransactions": "この貸付の取引を表示",
|
||||
"loansEmptyDescription": "+ ボタンから貸付を選んで作成します。",
|
||||
"newCategoryTitle": "新しいカテゴリ",
|
||||
"newCategoryPlaceholder": "カテゴリ名",
|
||||
"newSubcategoryTitle": "新しいサブカテゴリ",
|
||||
"newSubcategoryPlaceholder": "サブカテゴリ名"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Empréstimo salvo",
|
||||
"loanDeletedToast": "Empréstimo excluído",
|
||||
"loanPaymentAddedToast": "Pagamento registrado",
|
||||
"typeLoan": "Empréstimo"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Займ сохранён",
|
||||
"loanDeletedToast": "Займ удалён",
|
||||
"loanPaymentAddedToast": "Платёж записан",
|
||||
"typeLoan": "Займ"
|
||||
"typeLoan": "Займ",
|
||||
"tabsLabel": "Разделы бюджета",
|
||||
"budgetTab": "Бюджет",
|
||||
"loansTab": "Займы",
|
||||
"filteredTransactions": "Отфильтрованные операции",
|
||||
"clearLoanFilter": "Сбросить фильтр",
|
||||
"loanFilterActive": "Займ: {{title}}",
|
||||
"filterLoanTransactions": "Показать операции этого займа",
|
||||
"loansEmptyDescription": "Создайте займ кнопкой + и выберите Займ.",
|
||||
"newCategoryTitle": "Новая категория",
|
||||
"newCategoryPlaceholder": "Название категории",
|
||||
"newSubcategoryTitle": "Новая подкатегория",
|
||||
"newSubcategoryPlaceholder": "Название подкатегории"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Lån sparat",
|
||||
"loanDeletedToast": "Lån borttaget",
|
||||
"loanPaymentAddedToast": "Betalning registrerad",
|
||||
"typeLoan": "Lån"
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Borç kaydedildi",
|
||||
"loanDeletedToast": "Borç silindi",
|
||||
"loanPaymentAddedToast": "Ödeme kaydedildi",
|
||||
"typeLoan": "Borç"
|
||||
"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ı"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ayarlar",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "Позику збережено",
|
||||
"loanDeletedToast": "Позику видалено",
|
||||
"loanPaymentAddedToast": "Платіж записано",
|
||||
"typeLoan": "Позика"
|
||||
"typeLoan": "Позика",
|
||||
"tabsLabel": "Розділи бюджету",
|
||||
"budgetTab": "Бюджет",
|
||||
"loansTab": "Позики",
|
||||
"filteredTransactions": "Відфільтровані операції",
|
||||
"clearLoanFilter": "Очистити фільтр",
|
||||
"loanFilterActive": "Позика: {{title}}",
|
||||
"filterLoanTransactions": "Показати операції цієї позики",
|
||||
"loansEmptyDescription": "Створіть позику кнопкою + і виберіть Позика.",
|
||||
"newCategoryTitle": "Нова категорія",
|
||||
"newCategoryPlaceholder": "Назва категорії",
|
||||
"newSubcategoryTitle": "Нова підкатегорія",
|
||||
"newSubcategoryPlaceholder": "Назва підкатегорії"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Налаштування",
|
||||
|
||||
+13
-1
@@ -597,7 +597,19 @@
|
||||
"loanSavedToast": "借款已保存",
|
||||
"loanDeletedToast": "借款已删除",
|
||||
"loanPaymentAddedToast": "还款已记录",
|
||||
"typeLoan": "借款"
|
||||
"typeLoan": "借款",
|
||||
"tabsLabel": "预算分区",
|
||||
"budgetTab": "预算",
|
||||
"loansTab": "借款",
|
||||
"filteredTransactions": "已筛选交易",
|
||||
"clearLoanFilter": "清除筛选",
|
||||
"loanFilterActive": "借款:{{title}}",
|
||||
"filterLoanTransactions": "显示此借款的交易",
|
||||
"loansEmptyDescription": "点击 + 按钮并选择借款来创建。",
|
||||
"newCategoryTitle": "新类别",
|
||||
"newCategoryPlaceholder": "类别名称",
|
||||
"newSubcategoryTitle": "新子类别",
|
||||
"newSubcategoryPlaceholder": "子类别名称"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
|
||||
+171
-28
@@ -125,6 +125,8 @@ let state = {
|
||||
summary: null,
|
||||
prevSummary: null, // Vormonat für Monatsvergleich
|
||||
loans: { loans: [], summary: { active_count: 0, remaining_amount: 0, remaining_installments: 0 } },
|
||||
activeTab: 'budget',
|
||||
loanFilterId: null,
|
||||
currency: 'EUR',
|
||||
meta: { expenseCategories: [], incomeCategories: [], expenseSubcategories: {} },
|
||||
};
|
||||
@@ -161,8 +163,11 @@ function setHtml(element, html) {
|
||||
async function loadMonth(month) {
|
||||
const prevMonth = addMonths(month, -1);
|
||||
try {
|
||||
const entriesPath = state.loanFilterId
|
||||
? `/budget?loan_id=${encodeURIComponent(state.loanFilterId)}`
|
||||
: `/budget?month=${month}`;
|
||||
const [entriesRes, summaryRes, prevSummaryRes, loansRes] = await Promise.all([
|
||||
api.get(`/budget?month=${month}`),
|
||||
api.get(entriesPath),
|
||||
api.get(`/budget/summary?month=${month}`),
|
||||
api.get(`/budget/summary?month=${prevMonth}`),
|
||||
api.get('/budget/loans'),
|
||||
@@ -224,6 +229,14 @@ export async function render(container, { user }) {
|
||||
</button>
|
||||
<button class="budget-nav__today" id="budget-today">${t('budget.currentMonth')}</button>
|
||||
<span class="budget-nav__label" id="budget-label"></span>
|
||||
<div class="budget-tabs" role="tablist" aria-label="${t('budget.tabsLabel')}">
|
||||
<button class="budget-tab" id="budget-tab-budget" type="button" role="tab" aria-selected="true" data-tab="budget">
|
||||
${t('budget.budgetTab')}
|
||||
</button>
|
||||
<button class="budget-tab" id="budget-tab-loans" type="button" role="tab" aria-selected="false" data-tab="loans">
|
||||
${t('budget.loansTab')}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn--primary btn--icon" id="budget-add" aria-label="${t('budget.addEntryLabel')}">
|
||||
<i data-lucide="plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -273,6 +286,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();
|
||||
}
|
||||
|
||||
@@ -292,10 +311,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) : '';
|
||||
|
||||
setHtml(body, `
|
||||
<div class="budget-tab-panel budget-tab-panel--budget">
|
||||
<!-- Zusammenfassung -->
|
||||
<div class="budget-summary">
|
||||
<div class="budget-summary-card budget-summary-card--income">
|
||||
@@ -324,43 +352,41 @@ function renderBody() {
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
${renderLoansDashboard()}
|
||||
|
||||
<!-- Transaktionsliste -->
|
||||
<div class="budget-list-section">
|
||||
<div class="budget-list-header">
|
||||
<span class="budget-list-header__title">${t('budget.transactions')}</span>
|
||||
${state.entries.length ? `
|
||||
<div>
|
||||
<span class="budget-list-header__title">${state.loanFilterId ? t('budget.filteredTransactions') : t('budget.transactions')}</span>
|
||||
${state.loanFilterId ? `<div class="budget-list-header__filter">${esc(activeLoanLabel())}</div>` : ''}
|
||||
</div>
|
||||
<div class="budget-list-header__actions">
|
||||
${state.loanFilterId ? `
|
||||
<button class="btn btn--secondary" id="budget-clear-loan-filter"
|
||||
style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3);">
|
||||
<i data-lucide="x" style="width:14px;height:14px;margin-right:4px;" aria-hidden="true"></i>${t('budget.clearLoanFilter')}
|
||||
</button>` : ''}
|
||||
${state.entries.length && !state.loanFilterId ? `
|
||||
<a href="/api/v1/budget/export?month=${state.month}" class="btn btn--secondary"
|
||||
style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3);">
|
||||
<i data-lucide="download" style="width:14px;height:14px;margin-right:4px;" aria-hidden="true"></i>CSV
|
||||
</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-list" id="budget-list">
|
||||
${renderEntries()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
_container.querySelector('#empty-cta-budget')?.addEventListener('click', () => {
|
||||
document.querySelector('.page-fab')?.click();
|
||||
});
|
||||
_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.querySelector('#budget-clear-loan-filter')?.addEventListener('click', async () => {
|
||||
state.loanFilterId = null;
|
||||
await loadMonth(state.month);
|
||||
renderBody();
|
||||
});
|
||||
stagger(_container.querySelector('#budget-list')?.querySelectorAll('.budget-entry') ?? []);
|
||||
|
||||
@@ -376,6 +402,19 @@ 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 activeLoanLabel() {
|
||||
const loan = state.loans.loans.find((item) => item.id === state.loanFilterId);
|
||||
return loan ? t('budget.loanFilterActive', { title: loan.title }) : '';
|
||||
}
|
||||
|
||||
function renderCategoryBars(byCategory) {
|
||||
const maxAbs = Math.max(...byCategory.map((c) => Math.abs(c.total)), 1);
|
||||
|
||||
@@ -485,6 +524,55 @@ function renderLoansDashboard() {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLoansPage() {
|
||||
const loans = state.loans?.loans ?? [];
|
||||
if (!loans.length) {
|
||||
return `<div class="budget-tab-panel budget-tab-panel--loans">
|
||||
<div class="empty-state">
|
||||
<i data-lucide="hand-coins" class="empty-state__icon" aria-hidden="true"></i>
|
||||
<div class="empty-state__title">${t('budget.loansEmpty')}</div>
|
||||
<div class="empty-state__description">${t('budget.loansEmptyDescription')}</div>
|
||||
<button class="btn btn--primary empty-state__cta" id="budget-empty-loan">
|
||||
<i data-lucide="plus" aria-hidden="true" class="icon-base"></i>
|
||||
${t('budget.newLoan')}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `<div class="budget-tab-panel budget-tab-panel--loans">
|
||||
${renderLoansDashboard()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function wireLoansPage() {
|
||||
_container.querySelector('#budget-empty-loan')?.addEventListener('click', () => openBudgetModal({ mode: 'create', initialType: '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', async () => {
|
||||
state.loanFilterId = parseInt(btn.dataset.id, 10);
|
||||
state.activeTab = 'budget';
|
||||
await loadMonth(state.month);
|
||||
renderBody();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -493,7 +581,12 @@ function renderLoanCard(loan) {
|
||||
return `
|
||||
<article class="budget-loan-card">
|
||||
<div class="budget-loan-card__main">
|
||||
<div class="budget-loan-card__title-row">
|
||||
<div class="budget-loan-card__title">${esc(loan.title)}</div>
|
||||
<button class="budget-loan-card__filter" data-action="loan-filter" data-id="${loan.id}" aria-label="${t('budget.filterLoanTransactions')}">
|
||||
<i data-lucide="filter" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="budget-loan-card__meta">${esc(loan.borrower)} · ${t('budget.loanInstallmentMeta', {
|
||||
paid: loan.paid_installments,
|
||||
total: loan.installment_count,
|
||||
@@ -554,7 +647,7 @@ function formatEntryDate(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);
|
||||
@@ -671,7 +764,7 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
content,
|
||||
size: 'sm',
|
||||
onSave(panel) {
|
||||
let currentType = isExpense ? 'expense' : 'income';
|
||||
let currentType = !isEdit && initialType === 'loan' ? 'loan' : (isExpense ? 'expense' : 'income');
|
||||
|
||||
const setType = (type) => {
|
||||
currentType = type;
|
||||
@@ -724,7 +817,11 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
};
|
||||
|
||||
const addCategory = async () => {
|
||||
const name = window.prompt(t('budget.newCategoryPrompt'));
|
||||
const name = await requestNameInPanel(panel, {
|
||||
title: t('budget.newCategoryTitle'),
|
||||
label: t('budget.newCategoryPrompt'),
|
||||
placeholder: t('budget.newCategoryPlaceholder'),
|
||||
});
|
||||
if (!name?.trim()) return;
|
||||
try {
|
||||
const res = await api.post('/budget/categories', { name: name.trim(), type: currentType });
|
||||
@@ -740,7 +837,11 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
if (currentType !== 'expense') return;
|
||||
const category = panel.querySelector('#bm-category').value;
|
||||
if (!category) return;
|
||||
const name = window.prompt(t('budget.newSubcategoryPrompt'));
|
||||
const name = await requestNameInPanel(panel, {
|
||||
title: t('budget.newSubcategoryTitle'),
|
||||
label: t('budget.newSubcategoryPrompt'),
|
||||
placeholder: t('budget.newSubcategoryPlaceholder'),
|
||||
});
|
||||
if (!name?.trim()) return;
|
||||
try {
|
||||
const res = await api.post(`/budget/categories/${encodeURIComponent(category)}/subcategories`, { name: name.trim() });
|
||||
@@ -812,8 +913,7 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
const idx = state.entries.findIndex((e) => e.id === entry.id);
|
||||
if (idx !== -1) state.entries[idx] = res.data;
|
||||
}
|
||||
const sumRes = await api.get(`/budget/summary?month=${state.month}`);
|
||||
state.summary = sumRes.data;
|
||||
await loadMonth(state.month);
|
||||
|
||||
closeModal({ force: true });
|
||||
renderBody();
|
||||
@@ -829,6 +929,50 @@ function openBudgetModal({ mode, entry = null }) {
|
||||
});
|
||||
}
|
||||
|
||||
function requestNameInPanel(panel, { title, label, placeholder }) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'budget-inline-modal';
|
||||
setHtml(overlay, `
|
||||
<div class="budget-inline-modal__panel" role="dialog" aria-modal="true" aria-label="${esc(title)}">
|
||||
<div class="budget-inline-modal__header">
|
||||
<strong>${esc(title)}</strong>
|
||||
<button class="btn btn--icon" type="button" data-action="inline-cancel" aria-label="${t('common.cancel')}">
|
||||
<i data-lucide="x" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="budget-inline-name">${esc(label)}</label>
|
||||
<input class="form-input" id="budget-inline-name" type="text" placeholder="${esc(placeholder)}">
|
||||
</div>
|
||||
<div class="budget-inline-modal__footer">
|
||||
<button class="btn btn--secondary" type="button" data-action="inline-cancel">${t('common.cancel')}</button>
|
||||
<button class="btn btn--primary" type="button" data-action="inline-save">${t('common.add')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
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();
|
||||
@@ -976,8 +1120,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) {
|
||||
|
||||
+171
-1
@@ -25,6 +25,21 @@
|
||||
.budget-page { height: 100dvh; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.budget-nav {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.budget-nav__label {
|
||||
order: 3;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.budget-tabs {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Monat-Navigation
|
||||
* -------------------------------------------------------- */
|
||||
@@ -58,6 +73,49 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.budget-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: 2px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
|
||||
.budget-tab {
|
||||
min-height: 34px;
|
||||
padding: 0 var(--space-3);
|
||||
border: 0;
|
||||
border-radius: var(--radius-xs);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.budget-tab--active {
|
||||
background: var(--module-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
}
|
||||
|
||||
.budget-tab-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.budget-tab-panel--budget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.budget-tab-panel--loans {
|
||||
overflow-y: auto;
|
||||
padding-top: var(--space-3);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Zusammenfassungs-Karten
|
||||
* -------------------------------------------------------- */
|
||||
@@ -252,8 +310,9 @@
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
max-height: 260px;
|
||||
max-height: min(58dvh, 640px);
|
||||
overflow-y: auto;
|
||||
padding-right: var(--space-1);
|
||||
}
|
||||
|
||||
.budget-loans__empty {
|
||||
@@ -283,6 +342,37 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.budget-loan-card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.budget-loan-card__filter {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.budget-loan-card__filter:hover {
|
||||
color: var(--module-accent);
|
||||
border-color: var(--module-accent);
|
||||
}
|
||||
|
||||
.budget-loan-card__filter i {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.budget-loan-card__amounts {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -356,6 +446,7 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.budget-list-header {
|
||||
@@ -367,6 +458,20 @@
|
||||
border-top: 3px solid var(--module-accent);
|
||||
}
|
||||
|
||||
.budget-list-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.budget-list-header__filter {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.budget-list-header__title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
@@ -377,10 +482,39 @@
|
||||
|
||||
.budget-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.budget-list,
|
||||
.budget-loans__list,
|
||||
.budget-tab-panel--loans {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--module-accent) transparent;
|
||||
}
|
||||
|
||||
.budget-list::-webkit-scrollbar,
|
||||
.budget-loans__list::-webkit-scrollbar,
|
||||
.budget-tab-panel--loans::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.budget-list::-webkit-scrollbar-track,
|
||||
.budget-loans__list::-webkit-scrollbar-track,
|
||||
.budget-tab-panel--loans::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.budget-list::-webkit-scrollbar-thumb,
|
||||
.budget-loans__list::-webkit-scrollbar-thumb,
|
||||
.budget-tab-panel--loans::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--module-accent) 55%, transparent);
|
||||
border: 3px solid transparent;
|
||||
border-radius: var(--radius-full);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.budget-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -528,3 +662,39 @@
|
||||
padding: 2px var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.budget-inline-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-4);
|
||||
background: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.budget-inline-modal__panel {
|
||||
width: min(100%, 380px);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.budget-inline-modal__header,
|
||||
.budget-inline-modal__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.budget-inline-modal__header {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.budget-inline-modal__footer {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
+72
-14
@@ -287,6 +287,22 @@ function refreshLoanStatus(loanId) {
|
||||
return loan;
|
||||
}
|
||||
|
||||
function entryWithLoanMeta(id) {
|
||||
return db.get().prepare(`
|
||||
SELECT b.*, u.display_name AS creator_name,
|
||||
p.id AS loan_payment_id,
|
||||
p.loan_id AS loan_id,
|
||||
p.installment_number AS loan_installment_number,
|
||||
l.title AS loan_title,
|
||||
l.borrower AS loan_borrower
|
||||
FROM budget_entries b
|
||||
LEFT JOIN users u ON u.id = b.created_by
|
||||
LEFT JOIN budget_loan_payments p ON p.budget_entry_id = b.id
|
||||
LEFT JOIN budget_loans l ON l.id = p.loan_id
|
||||
WHERE b.id = ?
|
||||
`).get(id);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Statische Routen vor /:id
|
||||
// --------------------------------------------------------
|
||||
@@ -761,21 +777,36 @@ router.get('/', (req, res) => {
|
||||
try {
|
||||
const today = new Date().toISOString().slice(0, 7);
|
||||
const month = req.query.month || today;
|
||||
const loanId = req.query.loan_id ? parseInt(req.query.loan_id, 10) : null;
|
||||
|
||||
if (!MONTH_RE.test(month))
|
||||
if (!loanId && !MONTH_RE.test(month))
|
||||
return res.status(400).json({ error: 'month muss YYYY-MM sein', code: 400 });
|
||||
|
||||
generateRecurringInstances(db.get(), month);
|
||||
if (!loanId) generateRecurringInstances(db.get(), month);
|
||||
|
||||
const from = `${month}-01`;
|
||||
const to = `${month}-31`;
|
||||
let sql = `
|
||||
SELECT b.*, u.display_name AS creator_name
|
||||
SELECT b.*, u.display_name AS creator_name,
|
||||
p.id AS loan_payment_id,
|
||||
p.loan_id AS loan_id,
|
||||
p.installment_number AS loan_installment_number,
|
||||
l.title AS loan_title,
|
||||
l.borrower AS loan_borrower
|
||||
FROM budget_entries b
|
||||
LEFT JOIN users u ON u.id = b.created_by
|
||||
WHERE b.date BETWEEN ? AND ?
|
||||
LEFT JOIN budget_loan_payments p ON p.budget_entry_id = b.id
|
||||
LEFT JOIN budget_loans l ON l.id = p.loan_id
|
||||
`;
|
||||
const params = [from, to];
|
||||
const params = [];
|
||||
|
||||
if (loanId) {
|
||||
sql += ' WHERE p.loan_id = ?';
|
||||
params.push(loanId);
|
||||
} else {
|
||||
sql += ' WHERE b.date BETWEEN ? AND ?';
|
||||
params.push(from, to);
|
||||
}
|
||||
|
||||
if (req.query.category && validCategoryKeys().includes(req.query.category)) {
|
||||
sql += ' AND b.category = ?';
|
||||
@@ -822,11 +853,7 @@ router.post('/', (req, res) => {
|
||||
req.session.userId
|
||||
);
|
||||
|
||||
const entry = db.get().prepare(`
|
||||
SELECT b.*, u.display_name AS creator_name
|
||||
FROM budget_entries b LEFT JOIN users u ON u.id = b.created_by
|
||||
WHERE b.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
const entry = entryWithLoanMeta(result.lastInsertRowid);
|
||||
|
||||
res.status(201).json({ data: entry });
|
||||
} catch (err) {
|
||||
@@ -856,6 +883,12 @@ router.put('/:id', (req, res) => {
|
||||
const errors = collectErrors(checks);
|
||||
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||
const { title, amount, category, subcategory: requestedSubcategory, date, is_recurring, recurrence_rule } = req.body;
|
||||
const linkedPayment = db.get().prepare(`
|
||||
SELECT * FROM budget_loan_payments WHERE budget_entry_id = ?
|
||||
`).get(id);
|
||||
if (linkedPayment && amount !== undefined && Number(amount) <= 0) {
|
||||
return res.status(400).json({ error: 'Loan repayment entries must remain income.', code: 400 });
|
||||
}
|
||||
const nextCategory = category ?? entry.category;
|
||||
const subcategory = requestedSubcategory !== undefined || category !== undefined
|
||||
? validateSubcategory(nextCategory, requestedSubcategory ?? entry.subcategory)
|
||||
@@ -864,6 +897,7 @@ router.put('/:id', (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid subcategory.', code: 400 });
|
||||
}
|
||||
|
||||
const tx = db.get().transaction(() => {
|
||||
db.get().prepare(`
|
||||
UPDATE budget_entries
|
||||
SET title = COALESCE(?, title),
|
||||
@@ -885,10 +919,23 @@ router.put('/:id', (req, res) => {
|
||||
id
|
||||
);
|
||||
|
||||
const updated = db.get().prepare(`
|
||||
SELECT b.*, u.display_name AS creator_name
|
||||
FROM budget_entries b LEFT JOIN users u ON u.id = b.created_by WHERE b.id = ?
|
||||
`).get(id);
|
||||
if (linkedPayment) {
|
||||
db.get().prepare(`
|
||||
UPDATE budget_loan_payments
|
||||
SET amount = COALESCE(?, amount),
|
||||
paid_date = COALESCE(?, paid_date)
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
amount !== undefined ? cents(amount) : null,
|
||||
date ?? null,
|
||||
linkedPayment.id
|
||||
);
|
||||
refreshLoanStatus(linkedPayment.loan_id);
|
||||
}
|
||||
});
|
||||
tx();
|
||||
|
||||
const updated = entryWithLoanMeta(id);
|
||||
|
||||
res.json({ data: updated });
|
||||
} catch (err) {
|
||||
@@ -908,7 +955,18 @@ router.delete('/:id', (req, res) => {
|
||||
const entry = db.get().prepare('SELECT * FROM budget_entries WHERE id = ?').get(id);
|
||||
if (!entry) return res.status(404).json({ error: 'Entry not found', code: 404 });
|
||||
|
||||
const linkedPayment = db.get().prepare(`
|
||||
SELECT * FROM budget_loan_payments WHERE budget_entry_id = ?
|
||||
`).get(id);
|
||||
|
||||
const tx = db.get().transaction(() => {
|
||||
if (linkedPayment) {
|
||||
db.get().prepare('DELETE FROM budget_loan_payments WHERE id = ?').run(linkedPayment.id);
|
||||
}
|
||||
db.get().prepare('DELETE FROM budget_entries WHERE id = ?').run(id);
|
||||
if (linkedPayment) refreshLoanStatus(linkedPayment.loan_id);
|
||||
});
|
||||
tx();
|
||||
|
||||
// Wenn eine Instanz gelöscht wird: Monat als übersprungen markieren
|
||||
if (entry.recurrence_parent_id) {
|
||||
|
||||
Reference in New Issue
Block a user