diff --git a/public/pages/budget.js b/public/pages/budget.js
index eefd4c4..b63b8e1 100644
--- a/public/pages/budget.js
+++ b/public/pages/budget.js
@@ -6,6 +6,7 @@
*/
import { api } from '/api.js';
+import { openModal as openSharedModal, closeModal } from '/components/modal.js';
// --------------------------------------------------------
// Konstanten
@@ -134,7 +135,7 @@ function wireNav() {
renderBody();
updateLabel();
});
- const addHandler = () => openModal({ mode: 'create' });
+ const addHandler = () => openBudgetModal({ mode: 'create' });
_container.querySelector('#budget-add').addEventListener('click', addHandler);
_container.querySelector('#fab-new-budget').addEventListener('click', addHandler);
updateLabel();
@@ -208,7 +209,7 @@ function renderBody() {
const item = e.target.closest('.budget-entry[data-id]');
if (item && !e.target.closest('[data-action]')) {
const entry = state.entries.find((e) => e.id === parseInt(item.dataset.id, 10));
- if (entry) openModal({ mode: 'edit', entry });
+ if (entry) openBudgetModal({ mode: 'edit', entry });
}
});
}
@@ -276,150 +277,133 @@ function formatEntryDate(dateStr) {
// Modal
// --------------------------------------------------------
-function openModal({ mode, entry = null }) {
- document.querySelector('#budget-modal-overlay')?.remove();
-
+function openBudgetModal({ mode, entry = null }) {
const isEdit = mode === 'edit';
const today = new Date().toISOString().slice(0, 10);
- const isExpense = isEdit ? entry.amount < 0 : true; // Standard: Ausgabe
+ const isExpense = isEdit ? entry.amount < 0 : true;
const absAmount = isEdit ? Math.abs(entry.amount).toFixed(2) : '';
const catOpts = CATEGORIES.map((c) =>
``
).join('');
- const overlay = document.createElement('div');
- overlay.id = 'budget-modal-overlay';
- overlay.className = 'budget-modal-overlay';
- overlay.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ const content = `
+
+
+
- `;
- document.body.appendChild(overlay);
- if (window.lucide) lucide.createIcons();
+
+
+
+
- // Typ-Toggle
- let currentType = isExpense ? 'expense' : 'income';
- overlay.querySelector('#type-expense').addEventListener('click', () => {
- currentType = 'expense';
- overlay.querySelector('#type-expense').classList.add('amount-type-btn--active');
- overlay.querySelector('#type-income').classList.remove('amount-type-btn--active');
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ openSharedModal({
+ title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag',
+ content,
+ size: 'sm',
+ onSave(panel) {
+ let currentType = isExpense ? 'expense' : 'income';
+
+ panel.querySelector('#type-expense').addEventListener('click', () => {
+ currentType = 'expense';
+ panel.querySelector('#type-expense').classList.add('amount-type-btn--active');
+ panel.querySelector('#type-income').classList.remove('amount-type-btn--active');
+ });
+ panel.querySelector('#type-income').addEventListener('click', () => {
+ currentType = 'income';
+ panel.querySelector('#type-income').classList.add('amount-type-btn--active');
+ panel.querySelector('#type-expense').classList.remove('amount-type-btn--active');
+ });
+
+ panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
+
+ panel.querySelector('#bm-delete')?.addEventListener('click', async () => {
+ if (!confirm(`"${entry.title}" wirklich löschen?`)) return;
+ closeModal();
+ await deleteEntry(entry.id);
+ });
+
+ panel.querySelector('#bm-save').addEventListener('click', async () => {
+ const saveBtn = panel.querySelector('#bm-save');
+ const title = panel.querySelector('#bm-title').value.trim();
+ const absVal = parseFloat(panel.querySelector('#bm-amount').value);
+ const category = panel.querySelector('#bm-category').value;
+ const date = panel.querySelector('#bm-date').value;
+ const recurring = panel.querySelector('#bm-recurring').checked ? 1 : 0;
+
+ if (!title) { window.oikos?.showToast('Titel ist erforderlich', 'error'); return; }
+ if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast('Gültigen Betrag eingeben', 'error'); return; }
+ if (!date) { window.oikos?.showToast('Datum ist erforderlich', 'error'); return; }
+
+ const amount = currentType === 'expense' ? -absVal : absVal;
+
+ saveBtn.disabled = true;
+ saveBtn.textContent = '…';
+
+ try {
+ const body = { title, amount, category, date, is_recurring: recurring };
+ if (mode === 'create') {
+ const res = await api.post('/budget', body);
+ state.entries.unshift(res.data);
+ } else {
+ const res = await api.put(`/budget/${entry.id}`, body);
+ 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;
+
+ closeModal();
+ renderBody();
+ window.oikos?.showToast(mode === 'create' ? 'Eintrag hinzugefügt' : 'Eintrag gespeichert', 'success');
+ } catch (err) {
+ window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
+ saveBtn.disabled = false;
+ saveBtn.textContent = isEdit ? 'Speichern' : 'Hinzufügen';
+ }
+ });
+ },
});
- overlay.querySelector('#type-income').addEventListener('click', () => {
- currentType = 'income';
- overlay.querySelector('#type-income').classList.add('amount-type-btn--active');
- overlay.querySelector('#type-expense').classList.remove('amount-type-btn--active');
- });
-
- overlay.querySelector('#bm-close').addEventListener('click', () => overlay.remove());
- overlay.querySelector('#bm-cancel').addEventListener('click', () => overlay.remove());
- overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
-
- overlay.querySelector('#bm-delete')?.addEventListener('click', async () => {
- if (!confirm(`"${entry.title}" wirklich löschen?`)) return;
- overlay.remove();
- await deleteEntry(entry.id);
- });
-
- overlay.querySelector('#bm-save').addEventListener('click', async () => {
- const saveBtn = overlay.querySelector('#bm-save');
- const title = overlay.querySelector('#bm-title').value.trim();
- const absVal = parseFloat(overlay.querySelector('#bm-amount').value);
- const category = overlay.querySelector('#bm-category').value;
- const date = overlay.querySelector('#bm-date').value;
- const recurring = overlay.querySelector('#bm-recurring').checked ? 1 : 0;
-
- if (!title) { window.oikos?.showToast('Titel ist erforderlich', 'error'); return; }
- if (isNaN(absVal) || absVal <= 0) { window.oikos?.showToast('Gültigen Betrag eingeben', 'error'); return; }
- if (!date) { window.oikos?.showToast('Datum ist erforderlich', 'error'); return; }
-
- const amount = currentType === 'expense' ? -absVal : absVal;
-
- saveBtn.disabled = true;
- saveBtn.textContent = '…';
-
- try {
- const body = { title, amount, category, date, is_recurring: recurring };
- if (mode === 'create') {
- const res = await api.post('/budget', body);
- state.entries.unshift(res.data);
- } else {
- const res = await api.put(`/budget/${entry.id}`, body);
- const idx = state.entries.findIndex((e) => e.id === entry.id);
- if (idx !== -1) state.entries[idx] = res.data;
- }
- // Zusammenfassung neu laden
- const sumRes = await api.get(`/budget/summary?month=${state.month}`);
- state.summary = sumRes.data;
-
- overlay.remove();
- renderBody();
- window.oikos?.showToast(mode === 'create' ? 'Eintrag hinzugefügt' : 'Eintrag gespeichert', 'success');
- } catch (err) {
- window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
- saveBtn.disabled = false;
- saveBtn.textContent = isEdit ? 'Speichern' : 'Hinzufügen';
- }
- });
-
- overlay.querySelector('#bm-title').focus();
}
// --------------------------------------------------------
diff --git a/public/pages/calendar.js b/public/pages/calendar.js
index b84377e..7147ade 100644
--- a/public/pages/calendar.js
+++ b/public/pages/calendar.js
@@ -6,6 +6,7 @@
import { api } from '/api.js';
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
+import { openModal as openSharedModal, closeModal } from '/components/modal.js';
// --------------------------------------------------------
// Konstanten
@@ -703,61 +704,54 @@ function showEventPopup(ev, anchor) {
// --------------------------------------------------------
function openEventModal({ mode, event = null, date = null }) {
- document.querySelector('#event-modal-overlay')?.remove();
+ const isEdit = mode === 'edit';
+ const content = buildEventModalContent({ mode, event, date });
- const overlay = document.createElement('div');
- overlay.id = 'event-modal-overlay';
- overlay.className = 'event-modal-overlay';
- overlay.innerHTML = buildEventModalHTML({ mode, event, date });
- document.body.appendChild(overlay);
+ openSharedModal({
+ title: isEdit ? 'Termin bearbeiten' : 'Neuer Termin',
+ content,
+ size: 'md',
+ onSave(panel) {
+ // RRULE-Events binden
+ bindRRuleEvents(panel, 'event');
- if (window.lucide) lucide.createIcons();
+ const selectedColor = isEdit ? (event?.color || EVENT_COLORS[0]) : EVENT_COLORS[0];
- // RRULE-Events binden
- bindRRuleEvents(overlay, 'event');
+ // Farb-Auswahl
+ panel.querySelectorAll('.color-swatch').forEach((sw) => {
+ sw.addEventListener('click', () => {
+ panel.querySelectorAll('.color-swatch').forEach((s) => s.classList.remove('color-swatch--active'));
+ sw.classList.add('color-swatch--active');
+ });
+ });
+ panel.querySelectorAll('.color-swatch').forEach((sw) => {
+ if (sw.dataset.color === selectedColor) sw.classList.add('color-swatch--active');
+ });
- const isEdit = mode === 'edit';
- const selectedColor = isEdit ? (event?.color || EVENT_COLORS[0]) : EVENT_COLORS[0];
+ // Ganztägig-Toggle
+ const alldayCheck = panel.querySelector('#modal-allday');
+ const timeFields = panel.querySelector('#time-fields');
+ const alldayFields = panel.querySelector('#allday-fields');
+ alldayCheck.addEventListener('change', () => {
+ if (alldayCheck.checked) { timeFields.style.display = 'none'; alldayFields.style.display = ''; }
+ else { timeFields.style.display = ''; alldayFields.style.display = 'none'; }
+ });
+ if (isEdit && event?.all_day) { timeFields.style.display = 'none'; alldayFields.style.display = ''; }
- // Farb-Auswahl
- overlay.querySelectorAll('.color-swatch').forEach((sw) => {
- sw.addEventListener('click', () => {
- overlay.querySelectorAll('.color-swatch').forEach((s) => s.classList.remove('color-swatch--active'));
- sw.classList.add('color-swatch--active');
- });
+ panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
+
+ panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
+ if (!confirm(`"${event.title}" wirklich löschen?`)) return;
+ closeModal();
+ await deleteEvent(event.id);
+ });
+
+ panel.querySelector('#modal-save').addEventListener('click', () => saveEvent(panel, mode, event?.id));
+ },
});
- // Initial aktive Farbe markieren
- overlay.querySelectorAll('.color-swatch').forEach((sw) => {
- if (sw.dataset.color === selectedColor) sw.classList.add('color-swatch--active');
- });
-
- // Ganztägig-Toggle
- const alldayCheck = overlay.querySelector('#modal-allday');
- const timeFields = overlay.querySelector('#time-fields');
- alldayCheck.addEventListener('change', () => {
- timeFields.style.display = alldayCheck.checked ? 'none' : '';
- });
- if (isEdit && event?.all_day) timeFields.style.display = 'none';
-
- // Schließen
- overlay.querySelector('#modal-close').addEventListener('click', closeEventModal);
- overlay.querySelector('#modal-cancel').addEventListener('click', closeEventModal);
- overlay.addEventListener('click', (e) => { if (e.target === overlay) closeEventModal(); });
-
- // Löschen (nur Edit)
- overlay.querySelector('#modal-delete')?.addEventListener('click', async () => {
- if (!confirm(`"${event.title}" wirklich löschen?`)) return;
- closeEventModal();
- await deleteEvent(event.id);
- });
-
- // Speichern
- overlay.querySelector('#modal-save').addEventListener('click', () => saveEvent(overlay, mode, event?.id));
-
- overlay.querySelector('#modal-title').focus();
}
-function buildEventModalHTML({ mode, event, date }) {
+function buildEventModalContent({ mode, event, date }) {
const isEdit = mode === 'edit';
const today = date || state.today;
@@ -776,118 +770,93 @@ function buildEventModalHTML({ mode, event, date }) {
].join('');
return `
-
-