feat: migrate remaining 5 modules to shared modal system

Migrate budget, contacts, notes, meals, calendar to use the shared
openModal/closeModal from components/modal.js. Each module now gets
focus-trap, escape-handler, overlay-click, focus-restore, scroll-lock.

Removed ~460 lines of duplicate modal CSS (.budget-modal-overlay,
.contact-modal-overlay, .note-modal-overlay, .meal-modal-overlay,
.event-modal-overlay and their children). Content-specific styles
(color-picker, autocomplete, ingredient-list, etc.) are preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ulsklyc
2026-03-26 12:22:21 +01:00
parent 7e718e2422
commit c796d3ad6f
10 changed files with 552 additions and 1097 deletions
+35 -51
View File
@@ -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,32 +277,18 @@ 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) =>
`<option value="${c}" ${isEdit && entry.category === c ? 'selected' : ''}>${c}</option>`
).join('');
const overlay = document.createElement('div');
overlay.id = 'budget-modal-overlay';
overlay.className = 'budget-modal-overlay';
overlay.innerHTML = `
<div class="budget-modal" role="dialog" aria-modal="true">
<div class="budget-modal__header">
<h2 class="budget-modal__title">${isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag'}</h2>
<button class="budget-modal__close" id="bm-close" aria-label="Schließen">
<i data-lucide="x" style="width:16px;height:16px;"></i>
</button>
</div>
<div class="budget-modal__body">
<!-- Einnahme / Ausgabe Toggle -->
const content = `
<div class="amount-type-toggle">
<button class="amount-type-btn amount-type-btn--expenses ${isExpense ? 'amount-type-btn--active' : ''}"
id="type-expense" type="button">Ausgabe</button>
@@ -339,52 +326,50 @@ function openModal({ mode, entry = null }) {
<span class="allday-toggle__label">Wiederkehrend</span>
</label>
</div>
</div>
<div class="budget-modal__footer">
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
${isEdit ? `<button class="btn btn--danger btn--icon" id="bm-delete" title="Löschen">
<i data-lucide="trash-2" style="width:16px;height:16px;"></i>
</button>` : '<div></div>'}
<div class="budget-modal__footer-actions">
<div style="display:flex;gap:var(--space-3)">
<button class="btn btn--secondary" id="bm-cancel">Abbrechen</button>
<button class="btn btn--primary" id="bm-save">${isEdit ? 'Speichern' : 'Hinzufügen'}</button>
</div>
</div>
</div>
`;
</div>`;
document.body.appendChild(overlay);
if (window.lucide) lucide.createIcons();
// Typ-Toggle
openSharedModal({
title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag',
content,
size: 'sm',
onSave(panel) {
let currentType = isExpense ? 'expense' : 'income';
overlay.querySelector('#type-expense').addEventListener('click', () => {
panel.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');
panel.querySelector('#type-expense').classList.add('amount-type-btn--active');
panel.querySelector('#type-income').classList.remove('amount-type-btn--active');
});
overlay.querySelector('#type-income').addEventListener('click', () => {
panel.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');
panel.querySelector('#type-income').classList.add('amount-type-btn--active');
panel.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(); });
panel.querySelector('#bm-cancel').addEventListener('click', closeModal);
overlay.querySelector('#bm-delete')?.addEventListener('click', async () => {
panel.querySelector('#bm-delete')?.addEventListener('click', async () => {
if (!confirm(`"${entry.title}" wirklich löschen?`)) return;
overlay.remove();
closeModal();
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;
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; }
@@ -405,11 +390,10 @@ function openModal({ mode, entry = null }) {
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();
closeModal();
renderBody();
window.oikos?.showToast(mode === 'create' ? 'Eintrag hinzugefügt' : 'Eintrag gespeichert', 'success');
} catch (err) {
@@ -418,8 +402,8 @@ function openModal({ mode, entry = null }) {
saveBtn.textContent = isEdit ? 'Speichern' : 'Hinzufügen';
}
});
overlay.querySelector('#bm-title').focus();
},
});
}
// --------------------------------------------------------
+35 -66
View File
@@ -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 overlay = document.createElement('div');
overlay.id = 'event-modal-overlay';
overlay.className = 'event-modal-overlay';
overlay.innerHTML = buildEventModalHTML({ mode, event, date });
document.body.appendChild(overlay);
if (window.lucide) lucide.createIcons();
// RRULE-Events binden
bindRRuleEvents(overlay, 'event');
const isEdit = mode === 'edit';
const content = buildEventModalContent({ mode, event, date });
openSharedModal({
title: isEdit ? 'Termin bearbeiten' : 'Neuer Termin',
content,
size: 'md',
onSave(panel) {
// RRULE-Events binden
bindRRuleEvents(panel, 'event');
const selectedColor = isEdit ? (event?.color || EVENT_COLORS[0]) : EVENT_COLORS[0];
// Farb-Auswahl
overlay.querySelectorAll('.color-swatch').forEach((sw) => {
panel.querySelectorAll('.color-swatch').forEach((sw) => {
sw.addEventListener('click', () => {
overlay.querySelectorAll('.color-swatch').forEach((s) => s.classList.remove('color-swatch--active'));
panel.querySelectorAll('.color-swatch').forEach((s) => s.classList.remove('color-swatch--active'));
sw.classList.add('color-swatch--active');
});
});
// Initial aktive Farbe markieren
overlay.querySelectorAll('.color-swatch').forEach((sw) => {
panel.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');
const alldayCheck = panel.querySelector('#modal-allday');
const timeFields = panel.querySelector('#time-fields');
const alldayFields = panel.querySelector('#allday-fields');
alldayCheck.addEventListener('change', () => {
timeFields.style.display = alldayCheck.checked ? 'none' : '';
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';
if (isEdit && event?.all_day) { timeFields.style.display = 'none'; alldayFields.style.display = ''; }
// 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(); });
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
// Löschen (nur Edit)
overlay.querySelector('#modal-delete')?.addEventListener('click', async () => {
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
if (!confirm(`"${event.title}" wirklich löschen?`)) return;
closeEventModal();
closeModal();
await deleteEvent(event.id);
});
// Speichern
overlay.querySelector('#modal-save').addEventListener('click', () => saveEvent(overlay, mode, event?.id));
overlay.querySelector('#modal-title').focus();
panel.querySelector('#modal-save').addEventListener('click', () => saveEvent(panel, mode, event?.id));
},
});
}
function buildEventModalHTML({ mode, event, date }) {
function buildEventModalContent({ mode, event, date }) {
const isEdit = mode === 'edit';
const today = date || state.today;
@@ -776,14 +770,6 @@ function buildEventModalHTML({ mode, event, date }) {
].join('');
return `
<div class="event-modal" role="dialog" aria-modal="true">
<div class="event-modal__header">
<h2 class="event-modal__title">${isEdit ? 'Termin bearbeiten' : 'Neuer Termin'}</h2>
<button class="event-modal__close" id="modal-close" aria-label="Schließen">
<i data-lucide="x" style="width:16px;height:16px;"></i>
</button>
</div>
<div class="event-modal__body">
<div class="form-group">
<label class="form-label" for="modal-title">Titel *</label>
<input type="text" class="form-input" id="modal-title"
@@ -798,7 +784,7 @@ function buildEventModalHTML({ mode, event, date }) {
</div>
<div id="time-fields">
<div class="event-modal__row">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label" for="modal-start-date">Startdatum</label>
<input type="date" class="form-input" id="modal-start-date" value="${startDate}">
@@ -808,7 +794,7 @@ function buildEventModalHTML({ mode, event, date }) {
<input type="time" class="form-input" id="modal-start-time" value="${startTime}">
</div>
</div>
<div class="event-modal__row">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label" for="modal-end-date">Enddatum</label>
<input type="date" class="form-input" id="modal-end-date" value="${endDate}">
@@ -820,9 +806,8 @@ function buildEventModalHTML({ mode, event, date }) {
</div>
</div>
<!-- Ganztägig: nur Datum -->
<div id="allday-fields" style="display:none;">
<div class="event-modal__row">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label" for="modal-allday-start">Von</label>
<input type="date" class="form-input" id="modal-allday-start" value="${startDate}">
@@ -862,32 +847,16 @@ function buildEventModalHTML({ mode, event, date }) {
</div>
${renderRRuleFields('event', isEdit ? event.recurrence_rule : null)}
</div>
<div class="event-modal__footer">
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
${isEdit ? `<button class="btn btn--danger btn--icon" id="modal-delete" title="Löschen">
<i data-lucide="trash-2" style="width:16px;height:16px;"></i>
</button>` : '<div></div>'}
<div class="event-modal__footer-actions">
<div style="display:flex;gap:var(--space-3)">
<button class="btn btn--secondary" id="modal-cancel">Abbrechen</button>
<button class="btn btn--primary" id="modal-save">${isEdit ? 'Speichern' : 'Erstellen'}</button>
</div>
</div>
</div>
`;
}
// Allday-Toggle: Felder umschalten
document.addEventListener('change', (e) => {
if (e.target.id !== 'modal-allday') return;
const tf = document.querySelector('#time-fields');
const af = document.querySelector('#allday-fields');
if (!tf || !af) return;
if (e.target.checked) { tf.style.display = 'none'; af.style.display = ''; }
else { tf.style.display = ''; af.style.display = 'none'; }
});
function closeEventModal() {
document.querySelector('#event-modal-overlay')?.remove();
</div>`;
}
async function saveEvent(overlay, mode, eventId) {
@@ -943,7 +912,7 @@ async function saveEvent(overlay, mode, eventId) {
if (idx !== -1) state.events[idx] = res.data;
}
closeEventModal();
closeModal();
renderView();
window.oikos?.showToast(mode === 'create' ? 'Termin erstellt' : 'Termin gespeichert', 'success');
} catch (err) {
+27 -41
View File
@@ -5,6 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
// --------------------------------------------------------
// Konstanten
@@ -95,7 +96,7 @@ export async function render(container, { user }) {
});
// Neu
const addHandler = () => openModal({ mode: 'create' });
const addHandler = () => openContactModal({ mode: 'create' });
_container.querySelector('#contacts-add-btn').addEventListener('click', addHandler);
_container.querySelector('#fab-new-contact').addEventListener('click', addHandler);
}
@@ -168,7 +169,7 @@ function renderList() {
const item = e.target.closest('.contact-item[data-id]');
if (item && !e.target.closest('a') && !e.target.closest('[data-action]')) {
const c = state.contacts.find((c) => c.id === parseInt(item.dataset.id, 10));
if (c) openModal({ mode: 'edit', contact: c });
if (c) openContactModal({ mode: 'edit', contact: c });
}
});
}
@@ -200,9 +201,7 @@ function renderContactItem(c) {
// Modal
// --------------------------------------------------------
function openModal({ mode, contact = null }) {
document.querySelector('#contact-modal-overlay')?.remove();
function openContactModal({ mode, contact = null }) {
const isEdit = mode === 'edit';
const v = (field) => escHtml(isEdit && contact[field] ? contact[field] : '');
@@ -210,18 +209,7 @@ function openModal({ mode, contact = null }) {
`<option value="${c}" ${isEdit && contact.category === c ? 'selected' : ''}>${c}</option>`
).join('');
const overlay = document.createElement('div');
overlay.id = 'contact-modal-overlay';
overlay.className = 'contact-modal-overlay';
overlay.innerHTML = `
<div class="contact-modal" role="dialog" aria-modal="true">
<div class="contact-modal__header">
<h2 class="contact-modal__title">${isEdit ? 'Kontakt bearbeiten' : 'Neuer Kontakt'}</h2>
<button class="contact-modal__close" id="cm-close" aria-label="Schließen">
<i data-lucide="x" style="width:16px;height:16px;"></i>
</button>
</div>
<div class="contact-modal__body">
const content = `
<div class="form-group">
<label class="form-label" for="cm-name">Name *</label>
<input type="text" class="form-input" id="cm-name" placeholder="Vollständiger Name" value="${v('name')}">
@@ -246,8 +234,8 @@ function openModal({ mode, contact = null }) {
<label class="form-label" for="cm-notes">Notizen</label>
<textarea class="form-input" id="cm-notes" rows="2" placeholder="Optional…">${v('notes')}</textarea>
</div>
</div>
<div class="contact-modal__footer">
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
${isEdit ? `<button class="btn btn--danger btn--icon" id="cm-delete" title="Löschen">
<i data-lucide="trash-2" style="width:16px;height:16px;"></i>
</button>` : '<div></div>'}
@@ -255,31 +243,29 @@ function openModal({ mode, contact = null }) {
<button class="btn btn--secondary" id="cm-cancel">Abbrechen</button>
<button class="btn btn--primary" id="cm-save">${isEdit ? 'Speichern' : 'Erstellen'}</button>
</div>
</div>
</div>
`;
</div>`;
document.body.appendChild(overlay);
if (window.lucide) lucide.createIcons();
openSharedModal({
title: isEdit ? 'Kontakt bearbeiten' : 'Neuer Kontakt',
content,
size: 'md',
onSave(panel) {
panel.querySelector('#cm-cancel').addEventListener('click', closeModal);
overlay.querySelector('#cm-close').addEventListener('click', () => overlay.remove());
overlay.querySelector('#cm-cancel').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
overlay.querySelector('#cm-delete')?.addEventListener('click', async () => {
panel.querySelector('#cm-delete')?.addEventListener('click', async () => {
if (!confirm(`"${contact.name}" wirklich löschen?`)) return;
overlay.remove();
closeModal();
await deleteContact(contact.id);
});
overlay.querySelector('#cm-save').addEventListener('click', async () => {
const saveBtn = overlay.querySelector('#cm-save');
const name = overlay.querySelector('#cm-name').value.trim();
const category = overlay.querySelector('#cm-category').value;
const phone = overlay.querySelector('#cm-phone').value.trim() || null;
const email = overlay.querySelector('#cm-email').value.trim() || null;
const address = overlay.querySelector('#cm-address').value.trim() || null;
const notes = overlay.querySelector('#cm-notes').value.trim() || null;
panel.querySelector('#cm-save').addEventListener('click', async () => {
const saveBtn = panel.querySelector('#cm-save');
const name = panel.querySelector('#cm-name').value.trim();
const category = panel.querySelector('#cm-category').value;
const phone = panel.querySelector('#cm-phone').value.trim() || null;
const email = panel.querySelector('#cm-email').value.trim() || null;
const address = panel.querySelector('#cm-address').value.trim() || null;
const notes = panel.querySelector('#cm-notes').value.trim() || null;
if (!name) { window.oikos?.showToast('Name ist erforderlich', 'error'); return; }
@@ -300,7 +286,7 @@ function openModal({ mode, contact = null }) {
const idx = state.contacts.findIndex((c) => c.id === contact.id);
if (idx !== -1) state.contacts[idx] = res.data;
}
overlay.remove();
closeModal();
renderList();
window.oikos?.showToast(mode === 'create' ? 'Kontakt gespeichert' : 'Kontakt aktualisiert', 'success');
} catch (err) {
@@ -309,8 +295,8 @@ function openModal({ mode, contact = null }) {
saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen';
}
});
overlay.querySelector('#cm-name').focus();
},
});
}
async function deleteContact(id) {
+32 -46
View File
@@ -5,6 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
// --------------------------------------------------------
// Konstanten
@@ -248,14 +249,14 @@ function wireGrid(grid) {
const action = btn.dataset.action;
if (action === 'add-meal') {
openModal({ mode: 'create', date: btn.dataset.date, mealType: btn.dataset.type });
openMealModal({ mode: 'create', date: btn.dataset.date, mealType: btn.dataset.type });
return;
}
if (action === 'edit-meal') {
const mealId = parseInt(btn.dataset.mealId, 10);
const meal = state.meals.find((m) => m.id === mealId);
if (meal) openModal({ mode: 'edit', meal, date: meal.date, mealType: meal.meal_type });
if (meal) openMealModal({ mode: 'edit', meal, date: meal.date, mealType: meal.meal_type });
return;
}
@@ -281,21 +282,21 @@ function wireGrid(grid) {
// Modal
// --------------------------------------------------------
function openModal(opts) {
function openMealModal(opts) {
state.modal = opts;
document.querySelector('#meal-modal-overlay')?.remove();
const { mode, date, mealType, meal } = opts;
const isEdit = mode === 'edit';
const overlay = document.createElement('div');
overlay.id = 'meal-modal-overlay';
overlay.className = 'meal-modal-overlay';
overlay.innerHTML = buildModalHTML(opts);
document.body.appendChild(overlay);
if (window.lucide) lucide.createIcons();
const content = buildModalContent(opts);
openSharedModal({
title: isEdit ? 'Mahlzeit bearbeiten' : 'Mahlzeit hinzufügen',
content,
size: 'md',
onSave(panel) {
// Autocomplete
const titleInput = overlay.querySelector('#modal-title');
const acDropdown = overlay.querySelector('#modal-autocomplete');
const titleInput = panel.querySelector('#modal-title');
const acDropdown = panel.querySelector('#modal-autocomplete');
let acIndex = -1;
let acTimer;
@@ -331,8 +332,8 @@ function openModal(opts) {
});
// Zutaten
const ingList = overlay.querySelector('#ingredient-list');
const addIngBtn = overlay.querySelector('#add-ingredient-btn');
const ingList = panel.querySelector('#ingredient-list');
const addIngBtn = panel.querySelector('#add-ingredient-btn');
addIngBtn.addEventListener('click', () => {
const tmp = document.createElement('div');
@@ -348,12 +349,12 @@ function openModal(opts) {
if (btn) btn.closest('.ingredient-row').remove();
});
// Einkaufslisten-Transfer Button im Modal
overlay.querySelector('#transfer-btn')?.addEventListener('click', async () => {
const selectEl = overlay.querySelector('#transfer-list-select');
// Einkaufslisten-Transfer
panel.querySelector('#transfer-btn')?.addEventListener('click', async () => {
const selectEl = panel.querySelector('#transfer-list-select');
const listId = parseInt(selectEl?.value, 10);
if (!listId || !state.modal?.meal) return;
const btn = overlay.querySelector('#transfer-btn');
const btn = panel.querySelector('#transfer-btn');
btn.disabled = true;
try {
const res = await api.post(`/meals/${state.modal.meal.id}/to-shopping-list`, { listId });
@@ -372,18 +373,13 @@ function openModal(opts) {
}
});
// Schließen
overlay.querySelector('#modal-close').addEventListener('click', closeModal);
overlay.querySelector('#modal-cancel').addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
// Speichern
overlay.querySelector('#modal-save').addEventListener('click', () => saveModal(overlay));
titleInput.focus();
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
panel.querySelector('#modal-save').addEventListener('click', () => saveModal(panel));
},
});
}
function buildModalHTML({ mode, date, mealType, meal }) {
function buildModalContent({ mode, date, mealType, meal }) {
const isEdit = mode === 'edit';
const typeOpts = MEAL_TYPES.map((t) =>
`<option value="${t.key}" ${t.key === mealType ? 'selected' : ''}>${t.label}</option>`
@@ -400,20 +396,12 @@ function buildModalHTML({ mode, date, mealType, meal }) {
const hasIngOpen = isEdit && meal.ingredients?.some((i) => !i.on_shopping_list);
return `
<div class="meal-modal" role="dialog" aria-modal="true">
<div class="meal-modal__header">
<h2 class="meal-modal__title">${isEdit ? 'Mahlzeit bearbeiten' : 'Mahlzeit hinzufügen'}</h2>
<button class="meal-modal__close" id="modal-close" aria-label="Schließen">
<i data-lucide="x" style="width:16px;height:16px;"></i>
</button>
</div>
<div class="meal-modal__body">
<div class="meal-modal__row">
<div class="form-group">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group" style="margin-bottom:0">
<label class="form-label" for="modal-date">Datum</label>
<input type="date" class="form-input" id="modal-date" value="${date}">
</div>
<div class="form-group">
<div class="form-group" style="margin-bottom:0">
<label class="form-label" for="modal-type">Mahlzeit</label>
<select class="form-input" id="modal-type">${typeOpts}</select>
</div>
@@ -454,13 +442,11 @@ function buildModalHTML({ mode, date, mealType, meal }) {
Jetzt übertragen
</button>
</div>` : ''}
</div>
<div class="meal-modal__footer">
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
<button class="btn btn--secondary" id="modal-cancel">Abbrechen</button>
<button class="btn btn--primary" id="modal-save">${isEdit ? 'Speichern' : 'Hinzufügen'}</button>
</div>
</div>
`;
</div>`;
}
function ingredientRowHTML(name, qty, id) {
@@ -476,7 +462,7 @@ function ingredientRowHTML(name, qty, id) {
}
function closeModal() {
document.querySelector('#meal-modal-overlay')?.remove();
closeSharedModal();
state.modal = null;
}
+28 -44
View File
@@ -5,6 +5,7 @@
*/
import { api } from '/api.js';
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
// --------------------------------------------------------
// Konstanten
@@ -71,7 +72,7 @@ export async function render(container, { user }) {
}
renderGrid();
const addHandler = () => openModal({ mode: 'create' });
const addHandler = () => openNoteModal({ mode: 'create' });
_container.querySelector('#notes-add-btn').addEventListener('click', addHandler);
_container.querySelector('#fab-new-note').addEventListener('click', addHandler);
}
@@ -112,7 +113,7 @@ function renderGrid() {
const card = e.target.closest('.note-card[data-id]');
if (card) {
const note = state.notes.find((n) => n.id === parseInt(card.dataset.id, 10));
if (note) openModal({ mode: 'edit', note });
if (note) openNoteModal({ mode: 'edit', note });
}
});
}
@@ -152,25 +153,11 @@ function renderNoteCard(note) {
// Modal
// --------------------------------------------------------
function openModal({ mode, note = null }) {
document.querySelector('#note-modal-overlay')?.remove();
const overlay = document.createElement('div');
overlay.id = 'note-modal-overlay';
overlay.className = 'note-modal-overlay';
function openNoteModal({ mode, note = null }) {
const isEdit = mode === 'edit';
const selColor = isEdit ? note.color : NOTE_COLORS[0];
overlay.innerHTML = `
<div class="note-modal" role="dialog" aria-modal="true">
<div class="note-modal__header">
<h2 class="note-modal__title">${isEdit ? 'Notiz bearbeiten' : 'Neue Notiz'}</h2>
<button class="note-modal__close" id="note-modal-close" aria-label="Schließen">
<i data-lucide="x" style="width:16px;height:16px;"></i>
</button>
</div>
<div class="note-modal__body">
const content = `
<div class="form-group">
<label class="form-label" for="note-title">Titel (optional)</label>
<input type="text" class="form-input" id="note-title"
@@ -199,53 +186,50 @@ function openModal({ mode, note = null }) {
<span class="allday-toggle__label">Anpinnen (erscheint auf Dashboard)</span>
</label>
</div>
</div>
<div class="note-modal__footer">
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
<button class="btn btn--secondary" id="note-modal-cancel">Abbrechen</button>
<button class="btn btn--primary" id="note-modal-save">${isEdit ? 'Speichern' : 'Erstellen'}</button>
</div>
</div>
`;
document.body.appendChild(overlay);
if (window.lucide) lucide.createIcons();
</div>`;
openSharedModal({
title: isEdit ? 'Notiz bearbeiten' : 'Neue Notiz',
content,
size: 'md',
onSave(panel) {
// Farb-Swatch
overlay.querySelectorAll('.note-color-swatch').forEach((sw) => {
panel.querySelectorAll('.note-color-swatch').forEach((sw) => {
sw.addEventListener('click', () => {
overlay.querySelectorAll('.note-color-swatch').forEach((s) => s.classList.remove('note-color-swatch--active'));
panel.querySelectorAll('.note-color-swatch').forEach((s) => s.classList.remove('note-color-swatch--active'));
sw.classList.add('note-color-swatch--active');
});
});
overlay.querySelector('#note-modal-close').addEventListener('click', () => overlay.remove());
overlay.querySelector('#note-modal-cancel').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
panel.querySelector('#note-modal-cancel').addEventListener('click', closeModal);
overlay.querySelector('#note-modal-save').addEventListener('click', async () => {
const saveBtn = overlay.querySelector('#note-modal-save');
const title = overlay.querySelector('#note-title').value.trim() || null;
const content = overlay.querySelector('#note-content').value.trim();
const color = overlay.querySelector('.note-color-swatch--active')?.dataset.color || NOTE_COLORS[0];
const pinned = overlay.querySelector('#note-pinned').checked ? 1 : 0;
panel.querySelector('#note-modal-save').addEventListener('click', async () => {
const saveBtn = panel.querySelector('#note-modal-save');
const title = panel.querySelector('#note-title').value.trim() || null;
const cnt = panel.querySelector('#note-content').value.trim();
const color = panel.querySelector('.note-color-swatch--active')?.dataset.color || NOTE_COLORS[0];
const pinned = panel.querySelector('#note-pinned').checked ? 1 : 0;
if (!content) { window.oikos?.showToast('Inhalt ist erforderlich', 'error'); return; }
if (!cnt) { window.oikos?.showToast('Inhalt ist erforderlich', 'error'); return; }
saveBtn.disabled = true;
saveBtn.textContent = '…';
try {
if (mode === 'create') {
const res = await api.post('/notes', { title, content, color, pinned });
const res = await api.post('/notes', { title, content: cnt, color, pinned });
state.notes.unshift(res.data);
} else {
const res = await api.put(`/notes/${note.id}`, { title, content, color, pinned });
const res = await api.put(`/notes/${note.id}`, { title, content: cnt, color, pinned });
const idx = state.notes.findIndex((n) => n.id === note.id);
if (idx !== -1) state.notes[idx] = res.data;
// Angepinnte nach oben sortieren
state.notes.sort((a, b) => b.pinned - a.pinned);
}
overlay.remove();
closeModal();
renderGrid();
window.oikos?.showToast(mode === 'create' ? 'Notiz erstellt' : 'Notiz gespeichert', 'success');
} catch (err) {
@@ -254,8 +238,8 @@ function openModal({ mode, note = null }) {
saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen';
}
});
overlay.querySelector('#note-content').focus();
},
});
}
// --------------------------------------------------------
+1 -87
View File
@@ -285,81 +285,8 @@
}
/* --------------------------------------------------------
* Modal
* Budget-Modal Content-Styles (Overlay/Panel via shared modal.js)
* -------------------------------------------------------- */
.budget-modal-overlay {
position: fixed;
inset: 0;
background-color: var(--color-overlay);
z-index: var(--z-modal);
display: flex;
align-items: flex-end;
justify-content: center;
animation: fadeIn var(--transition-fast) ease;
}
@media (min-width: 768px) {
.budget-modal-overlay {
align-items: center;
padding: var(--space-4);
}
}
.budget-modal {
background-color: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
max-height: 90dvh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp var(--transition-base) ease;
}
@media (min-width: 768px) {
.budget-modal {
border-radius: var(--radius-lg);
max-width: 460px;
max-height: 80dvh;
}
}
.budget-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.budget-modal__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
}
.budget-modal__close {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-border);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
min-height: unset;
}
.budget-modal__body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
/* Einnahme/Ausgabe-Toggle */
.amount-type-toggle {
@@ -394,16 +321,3 @@
color: #ffffff;
}
.budget-modal__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
.budget-modal__footer-actions {
display: flex;
gap: var(--space-3);
}
+1 -97
View File
@@ -513,90 +513,8 @@
}
/* --------------------------------------------------------
* Termin-Modal
* Calendar-Modal Content-Styles (Overlay/Panel via shared modal.js)
* -------------------------------------------------------- */
.event-modal-overlay {
position: fixed;
inset: 0;
background-color: var(--color-overlay);
z-index: var(--z-modal);
display: flex;
align-items: flex-end;
justify-content: center;
animation: fadeIn var(--transition-fast) ease;
}
@media (min-width: 768px) {
.event-modal-overlay {
align-items: center;
padding: var(--space-4);
}
}
.event-modal {
background-color: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
max-height: 90dvh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp var(--transition-base) ease;
}
@media (min-width: 768px) {
.event-modal {
border-radius: var(--radius-lg);
max-width: 560px;
max-height: 85dvh;
}
}
.event-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
gap: var(--space-2);
}
.event-modal__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
flex: 1;
}
.event-modal__close {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-border);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
min-height: unset;
flex-shrink: 0;
}
.event-modal__body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.event-modal__row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
/* Farbauswahl */
.color-picker {
@@ -636,20 +554,6 @@
color: var(--color-text-primary);
}
.event-modal__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
.event-modal__footer-actions {
display: flex;
gap: var(--space-3);
}
/* --------------------------------------------------------
* Termin-Detailansicht (Popup beim Klick)
* -------------------------------------------------------- */
-85
View File
@@ -230,88 +230,3 @@
color: var(--color-text-secondary);
}
/* --------------------------------------------------------
* Modal (gleiche Struktur wie andere Module)
* -------------------------------------------------------- */
.contact-modal-overlay {
position: fixed;
inset: 0;
background-color: var(--color-overlay);
z-index: var(--z-modal);
display: flex;
align-items: flex-end;
justify-content: center;
animation: fadeIn var(--transition-fast) ease;
}
@media (min-width: 768px) {
.contact-modal-overlay {
align-items: center;
padding: var(--space-4);
}
}
.contact-modal {
background-color: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
max-height: 90dvh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp var(--transition-base) ease;
}
@media (min-width: 768px) {
.contact-modal {
border-radius: var(--radius-lg);
max-width: 480px;
max-height: 85dvh;
}
}
.contact-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.contact-modal__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
}
.contact-modal__close {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-border);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
min-height: unset;
}
.contact-modal__body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.contact-modal__footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
+1 -106
View File
@@ -244,102 +244,8 @@
}
/* --------------------------------------------------------
* Modal — Mahlzeit erstellen / bearbeiten
* Meals-Modal Content-Styles (Overlay/Panel via shared modal.js)
* -------------------------------------------------------- */
.meal-modal-overlay {
position: fixed;
inset: 0;
background-color: var(--color-overlay);
z-index: var(--z-modal);
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0;
animation: fadeIn var(--transition-fast) ease;
}
@media (min-width: 768px) {
.meal-modal-overlay {
align-items: center;
padding: var(--space-4);
}
}
.meal-modal {
background-color: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
max-height: 90dvh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp var(--transition-base) ease;
}
@media (min-width: 768px) {
.meal-modal {
border-radius: var(--radius-lg);
max-width: 520px;
max-height: 80dvh;
}
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (min-width: 768px) {
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
}
.meal-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.meal-modal__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
}
.meal-modal__close {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-border);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
min-height: unset;
flex-shrink: 0;
}
.meal-modal__body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
/* Formular-Zeile: Datum + Typ */
.meal-modal__row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
/* Autocomplete im Modal */
.meal-modal__autocomplete {
@@ -459,17 +365,6 @@
align-self: flex-start;
}
/* Modal-Footer */
.meal-modal__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
/* --------------------------------------------------------
* Mahlzeit-Typ Labels (DE)
* -------------------------------------------------------- */
+1 -83
View File
@@ -214,81 +214,8 @@
}
/* --------------------------------------------------------
* Modal
* Notes-Modal Content-Styles (Overlay/Panel via shared modal.js)
* -------------------------------------------------------- */
.note-modal-overlay {
position: fixed;
inset: 0;
background-color: var(--color-overlay);
z-index: var(--z-modal);
display: flex;
align-items: flex-end;
justify-content: center;
animation: fadeIn var(--transition-fast) ease;
}
@media (min-width: 768px) {
.note-modal-overlay {
align-items: center;
padding: var(--space-4);
}
}
.note-modal {
background-color: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
width: 100%;
max-height: 90dvh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp var(--transition-base) ease;
}
@media (min-width: 768px) {
.note-modal {
border-radius: var(--radius-lg);
max-width: 520px;
max-height: 80dvh;
}
}
.note-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.note-modal__title {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
}
.note-modal__close {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: var(--color-border);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
min-height: unset;
}
.note-modal__body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
/* Farb-Auswahl */
.note-color-picker {
@@ -309,12 +236,3 @@
.note-color-swatch:hover { transform: scale(1.15); }
.note-color-swatch--active { border-color: var(--color-text-primary); transform: scale(1.1); }
.note-modal__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-4);
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}