feat: i18n shopping, meals, calendar pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+74
-56
@@ -8,17 +8,35 @@ import { api } from '/api.js';
|
|||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
import { stagger } from '/utils/ux.js';
|
import { stagger } from '/utils/ux.js';
|
||||||
|
import { t } from '/i18n.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Konstanten
|
// Konstanten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
const VIEWS = ['month', 'week', 'day', 'agenda'];
|
const VIEWS = ['month', 'week', 'day', 'agenda'];
|
||||||
const VIEW_LABELS = { month: 'Monat', week: 'Woche', day: 'Tag', agenda: 'Agenda' };
|
const VIEW_LABELS = () => ({
|
||||||
const DAY_NAMES_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
month: t('calendar.viewMonth'),
|
||||||
const DAY_NAMES_LONG = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
week: t('calendar.viewWeek'),
|
||||||
const MONTH_NAMES = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
day: t('calendar.viewDay'),
|
||||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
agenda: t('calendar.viewAgenda'),
|
||||||
|
});
|
||||||
|
const DAY_NAMES_SHORT = () => [
|
||||||
|
t('calendar.dayShortSunday'), t('calendar.dayShortMonday'), t('calendar.dayShortTuesday'),
|
||||||
|
t('calendar.dayShortWednesday'), t('calendar.dayShortThursday'), t('calendar.dayShortFriday'),
|
||||||
|
t('calendar.dayShortSaturday'),
|
||||||
|
];
|
||||||
|
const DAY_NAMES_LONG = () => [
|
||||||
|
t('calendar.dayLongSunday'), t('calendar.dayLongMonday'), t('calendar.dayLongTuesday'),
|
||||||
|
t('calendar.dayLongWednesday'), t('calendar.dayLongThursday'), t('calendar.dayLongFriday'),
|
||||||
|
t('calendar.dayLongSaturday'),
|
||||||
|
];
|
||||||
|
const MONTH_NAMES = () => [
|
||||||
|
t('calendar.monthJanuary'), t('calendar.monthFebruary'), t('calendar.monthMarch'),
|
||||||
|
t('calendar.monthApril'), t('calendar.monthMay'), t('calendar.monthJune'),
|
||||||
|
t('calendar.monthJuly'), t('calendar.monthAugust'), t('calendar.monthSeptember'),
|
||||||
|
t('calendar.monthOctober'), t('calendar.monthNovember'), t('calendar.monthDecember'),
|
||||||
|
];
|
||||||
|
|
||||||
const EVENT_COLORS = [
|
const EVENT_COLORS = [
|
||||||
'#007AFF', '#34C759', '#FF9500', '#FF3B30',
|
'#007AFF', '#34C759', '#FF9500', '#FF3B30',
|
||||||
@@ -73,9 +91,9 @@ function getMondayOf(dateStr) {
|
|||||||
function formatDate(dateStr, { long = false, weekday = false } = {}) {
|
function formatDate(dateStr, { long = false, weekday = false } = {}) {
|
||||||
const d = new Date(dateStr + 'T00:00:00');
|
const d = new Date(dateStr + 'T00:00:00');
|
||||||
const day = d.getDate();
|
const day = d.getDate();
|
||||||
const mon = MONTH_NAMES[d.getMonth()];
|
const mon = MONTH_NAMES()[d.getMonth()];
|
||||||
if (weekday) {
|
if (weekday) {
|
||||||
const wd = long ? DAY_NAMES_LONG[d.getDay()] : DAY_NAMES_SHORT[d.getDay()];
|
const wd = long ? DAY_NAMES_LONG()[d.getDay()] : DAY_NAMES_SHORT()[d.getDay()];
|
||||||
return `${wd}, ${day}. ${mon}`;
|
return `${wd}, ${day}. ${mon}`;
|
||||||
}
|
}
|
||||||
return `${day}. ${mon} ${d.getFullYear()}`;
|
return `${day}. ${mon} ${d.getFullYear()}`;
|
||||||
@@ -132,7 +150,7 @@ async function loadRange(from, to) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Calendar] loadRange Fehler:', err);
|
console.error('[Calendar] loadRange Fehler:', err);
|
||||||
state.events = [];
|
state.events = [];
|
||||||
window.oikos?.showToast('Termine konnten nicht geladen werden.', 'danger');
|
window.oikos?.showToast(t('calendar.loadError'), 'danger');
|
||||||
}
|
}
|
||||||
state.rangeFrom = from;
|
state.rangeFrom = from;
|
||||||
state.rangeTo = to;
|
state.rangeTo = to;
|
||||||
@@ -161,7 +179,7 @@ export async function render(container, { user }) {
|
|||||||
<div class="calendar-page" id="calendar-page">
|
<div class="calendar-page" id="calendar-page">
|
||||||
<div class="cal-toolbar" id="cal-toolbar"></div>
|
<div class="cal-toolbar" id="cal-toolbar"></div>
|
||||||
<div id="cal-body" style="flex:1;display:flex;flex-direction:column;overflow:hidden;"></div>
|
<div id="cal-body" style="flex:1;display:flex;flex-direction:column;overflow:hidden;"></div>
|
||||||
<button class="page-fab" id="fab-new-event" aria-label="Neuer Termin">
|
<button class="page-fab" id="fab-new-event" aria-label="${t('calendar.newEvent')}">
|
||||||
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
|
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,26 +203,26 @@ function renderToolbar() {
|
|||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
|
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<h1 class="sr-only">Kalender</h1>
|
<h1 class="sr-only">${t('calendar.title')}</h1>
|
||||||
<div class="cal-toolbar__nav">
|
<div class="cal-toolbar__nav">
|
||||||
<button class="btn btn--icon" id="cal-prev" aria-label="Zurück">
|
<button class="btn btn--icon" id="cal-prev" aria-label="${t('calendar.back')}">
|
||||||
<i data-lucide="chevron-left" aria-hidden="true"></i>
|
<i data-lucide="chevron-left" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="cal-toolbar__today" id="cal-today">Heute</button>
|
<button class="cal-toolbar__today" id="cal-today">${t('calendar.today')}</button>
|
||||||
<span class="cal-toolbar__label" id="cal-label"></span>
|
<span class="cal-toolbar__label" id="cal-label"></span>
|
||||||
<div class="cal-toolbar__views">
|
<div class="cal-toolbar__views">
|
||||||
${VIEWS.map((v) => `
|
${VIEWS.map((v) => `
|
||||||
<button class="cal-toolbar__view-btn ${v === state.view ? 'cal-toolbar__view-btn--active' : ''}"
|
<button class="cal-toolbar__view-btn ${v === state.view ? 'cal-toolbar__view-btn--active' : ''}"
|
||||||
data-view="${v}">${VIEW_LABELS[v]}</button>
|
data-view="${v}">${VIEW_LABELS()[v]}</button>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--primary btn--icon" id="cal-add" aria-label="Termin hinzufügen"
|
<button class="btn btn--primary btn--icon" id="cal-add" aria-label="${t('calendar.addEvent')}"
|
||||||
style="margin-left:auto;">
|
style="margin-left:auto;">
|
||||||
<i data-lucide="plus" aria-hidden="true"></i>
|
<i data-lucide="plus" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="cal-toolbar__nav">
|
<div class="cal-toolbar__nav">
|
||||||
<button class="btn btn--icon" id="cal-next" aria-label="Weiter">
|
<button class="btn btn--icon" id="cal-next" aria-label="${t('calendar.forward')}">
|
||||||
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,12 +255,12 @@ function updateLabel() {
|
|||||||
if (!lbl) return;
|
if (!lbl) return;
|
||||||
const d = new Date(state.cursor + 'T00:00:00');
|
const d = new Date(state.cursor + 'T00:00:00');
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const mon = MONTH_NAMES[d.getMonth()];
|
const mon = MONTH_NAMES()[d.getMonth()];
|
||||||
|
|
||||||
if (state.view === 'month') lbl.textContent = `${mon} ${year}`;
|
if (state.view === 'month') lbl.textContent = `${mon} ${year}`;
|
||||||
if (state.view === 'week') lbl.textContent = `KW ${getWeekNumber(state.cursor)} · ${mon} ${year}`;
|
if (state.view === 'week') lbl.textContent = t('calendar.weekNumberLabel', { week: getWeekNumber(state.cursor), month: mon, year });
|
||||||
if (state.view === 'day') lbl.textContent = formatDate(state.cursor, { weekday: true, long: true });
|
if (state.view === 'day') lbl.textContent = formatDate(state.cursor, { weekday: true, long: true });
|
||||||
if (state.view === 'agenda') lbl.textContent = `Ab ${formatDate(state.cursor)}`;
|
if (state.view === 'agenda') lbl.textContent = t('calendar.agendaFrom', { date: formatDate(state.cursor) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekNumber(dateStr) {
|
function getWeekNumber(dateStr) {
|
||||||
@@ -328,7 +346,7 @@ function renderMonthView(container) {
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="month-view">
|
<div class="month-view">
|
||||||
<div class="month-weekdays">
|
<div class="month-weekdays">
|
||||||
${['Mo','Di','Mi','Do','Fr','Sa','So'].map((n) => `<div class="month-weekday">${n}</div>`).join('')}
|
${[t('calendar.dayShortMonday'),t('calendar.dayShortTuesday'),t('calendar.dayShortWednesday'),t('calendar.dayShortThursday'),t('calendar.dayShortFriday'),t('calendar.dayShortSaturday'),t('calendar.dayShortSunday')].map((n) => `<div class="month-weekday">${n}</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
<div class="month-grid" id="month-grid">
|
<div class="month-grid" id="month-grid">
|
||||||
${days.map(({ date, inMonth }) => renderMonthDay(date, inMonth)).join('')}
|
${days.map(({ date, inMonth }) => renderMonthDay(date, inMonth)).join('')}
|
||||||
@@ -376,7 +394,7 @@ function renderMonthDay(date, inMonth) {
|
|||||||
<div class="${classes}" data-date="${date}">
|
<div class="${classes}" data-date="${date}">
|
||||||
<div class="month-day__number">${new Date(date + 'T00:00:00').getDate()}</div>
|
<div class="month-day__number">${new Date(date + 'T00:00:00').getDate()}</div>
|
||||||
${evHtml}
|
${evHtml}
|
||||||
${extra > 0 ? `<div class="month-day__more">+${extra} weitere</div>` : ''}
|
${extra > 0 ? `<div class="month-day__more">${t('calendar.moreEvents', { count: extra })}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -404,14 +422,14 @@ function renderWeekView(container) {
|
|||||||
${days.map((d) => {
|
${days.map((d) => {
|
||||||
const dt = new Date(d + 'T00:00:00');
|
const dt = new Date(d + 'T00:00:00');
|
||||||
return `<div class="week-view__day-header">
|
return `<div class="week-view__day-header">
|
||||||
<div class="week-view__day-name">${DAY_NAMES_SHORT[(dt.getDay())]}</div>
|
<div class="week-view__day-name">${DAY_NAMES_SHORT()[dt.getDay()]}</div>
|
||||||
<div class="week-view__day-num ${d === state.today ? 'week-view__day-num--today' : ''}">${dt.getDate()}</div>
|
<div class="week-view__day-num ${d === state.today ? 'week-view__day-num--today' : ''}">${dt.getDate()}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
<!-- Ganztägige Ereignisse -->
|
<!-- Ganztägige Ereignisse -->
|
||||||
<div class="allday-row" style="display:grid;grid-template-columns:48px repeat(7,1fr);">
|
<div class="allday-row" style="display:grid;grid-template-columns:48px repeat(7,1fr);">
|
||||||
<div style="width:48px;padding:2px;font-size:10px;color:var(--color-text-disabled);text-align:right;padding-right:4px;line-height:24px;">ganztg.</div>
|
<div style="width:48px;padding:2px;font-size:10px;color:var(--color-text-disabled);text-align:right;padding-right:4px;line-height:24px;">${t('calendar.allDayShort')}</div>
|
||||||
${days.map((d, i) => `
|
${days.map((d, i) => `
|
||||||
<div class="allday-cell">
|
<div class="allday-cell">
|
||||||
${alldayEvs[i].map((ev) => `
|
${alldayEvs[i].map((ev) => `
|
||||||
@@ -523,7 +541,7 @@ function renderDayView(container) {
|
|||||||
</div>
|
</div>
|
||||||
${allday.length ? `
|
${allday.length ? `
|
||||||
<div class="allday-row" style="display:grid;grid-template-columns:48px 1fr;">
|
<div class="allday-row" style="display:grid;grid-template-columns:48px 1fr;">
|
||||||
<div style="padding:2px 4px 2px 0;font-size:10px;color:var(--color-text-disabled);text-align:right;line-height:24px;">ganztg.</div>
|
<div style="padding:2px 4px 2px 0;font-size:10px;color:var(--color-text-disabled);text-align:right;line-height:24px;">${t('calendar.allDayShort')}</div>
|
||||||
<div class="allday-cell">
|
<div class="allday-cell">
|
||||||
${allday.map((ev) => `
|
${allday.map((ev) => `
|
||||||
<div class="allday-event" data-id="${ev.id}" style="background-color:${escHtml(ev.color)};">
|
<div class="allday-event" data-id="${ev.id}" style="background-color:${escHtml(ev.color)};">
|
||||||
@@ -584,12 +602,12 @@ function renderAgendaView(container) {
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="agenda-view" id="agenda-view">
|
<div class="agenda-view" id="agenda-view">
|
||||||
${groups.length === 0
|
${groups.length === 0
|
||||||
? `<div class="agenda-empty">Keine Termine im gewählten Zeitraum.</div>`
|
? `<div class="agenda-empty">${t('calendar.noEvents')}</div>`
|
||||||
: groups.map(({ date, events }) => `
|
: groups.map(({ date, events }) => `
|
||||||
<div class="agenda-day">
|
<div class="agenda-day">
|
||||||
<div class="agenda-day__header ${date === state.today ? 'agenda-day__header--today' : ''}">
|
<div class="agenda-day__header ${date === state.today ? 'agenda-day__header--today' : ''}">
|
||||||
<span class="agenda-day__date">${formatDate(date)}</span>
|
<span class="agenda-day__date">${formatDate(date)}</span>
|
||||||
<span class="agenda-day__weekday">${DAY_NAMES_LONG[new Date(date + 'T00:00:00').getDay()]}</span>
|
<span class="agenda-day__weekday">${DAY_NAMES_LONG()[new Date(date + 'T00:00:00').getDay()]}</span>
|
||||||
</div>
|
</div>
|
||||||
${events.map((ev) => renderAgendaEvent(ev)).join('')}
|
${events.map((ev) => renderAgendaEvent(ev)).join('')}
|
||||||
</div>
|
</div>
|
||||||
@@ -611,7 +629,7 @@ function renderAgendaView(container) {
|
|||||||
|
|
||||||
function renderAgendaEvent(ev) {
|
function renderAgendaEvent(ev) {
|
||||||
const timeStr = ev.all_day
|
const timeStr = ev.all_day
|
||||||
? 'Ganztägig'
|
? t('calendar.allDay')
|
||||||
: formatTime(ev.start_datetime)
|
: formatTime(ev.start_datetime)
|
||||||
+ (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : ' Uhr');
|
+ (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : ' Uhr');
|
||||||
|
|
||||||
@@ -650,7 +668,7 @@ function showEventPopup(ev, anchor) {
|
|||||||
popup.className = 'event-popup';
|
popup.className = 'event-popup';
|
||||||
|
|
||||||
const timeStr = ev.all_day
|
const timeStr = ev.all_day
|
||||||
? 'Ganztägig'
|
? t('calendar.allDay')
|
||||||
: formatDateTime(ev.start_datetime)
|
: formatDateTime(ev.start_datetime)
|
||||||
+ (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : '');
|
+ (ev.end_datetime ? ` – ${formatTime(ev.end_datetime)} Uhr` : '');
|
||||||
|
|
||||||
@@ -664,7 +682,7 @@ function showEventPopup(ev, anchor) {
|
|||||||
${ev.assigned_name ? `<div>👤 ${escHtml(ev.assigned_name)}</div>` : ''}
|
${ev.assigned_name ? `<div>👤 ${escHtml(ev.assigned_name)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-popup__actions">
|
<div class="event-popup__actions">
|
||||||
<button class="btn btn--secondary" style="flex:1;" id="popup-edit">Bearbeiten</button>
|
<button class="btn btn--secondary" style="flex:1;" id="popup-edit">${t('calendar.popupEdit')}</button>
|
||||||
<button class="btn btn--danger" id="popup-delete">
|
<button class="btn btn--danger" id="popup-delete">
|
||||||
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -687,7 +705,7 @@ function showEventPopup(ev, anchor) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
popup.querySelector('#popup-delete').addEventListener('click', async () => {
|
popup.querySelector('#popup-delete').addEventListener('click', async () => {
|
||||||
if (!confirm(`"${ev.title}" wirklich löschen?`)) return;
|
if (!confirm(t('calendar.deleteConfirm', { title: ev.title }))) return;
|
||||||
popup.remove();
|
popup.remove();
|
||||||
await deleteEvent(ev.id);
|
await deleteEvent(ev.id);
|
||||||
});
|
});
|
||||||
@@ -712,7 +730,7 @@ function openEventModal({ mode, event = null, date = null }) {
|
|||||||
const content = buildEventModalContent({ mode, event, date });
|
const content = buildEventModalContent({ mode, event, date });
|
||||||
|
|
||||||
openSharedModal({
|
openSharedModal({
|
||||||
title: isEdit ? 'Termin bearbeiten' : 'Neuer Termin',
|
title: isEdit ? t('calendar.editEvent') : t('calendar.newEvent'),
|
||||||
content,
|
content,
|
||||||
size: 'md',
|
size: 'md',
|
||||||
onSave(panel) {
|
onSave(panel) {
|
||||||
@@ -745,7 +763,7 @@ function openEventModal({ mode, event = null, date = null }) {
|
|||||||
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
||||||
|
|
||||||
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
||||||
if (!confirm(`"${event.title}" wirklich löschen?`)) return;
|
if (!confirm(t('calendar.deleteConfirm', { title: event.title }))) return;
|
||||||
closeModal();
|
closeModal();
|
||||||
await deleteEvent(event.id);
|
await deleteEvent(event.id);
|
||||||
});
|
});
|
||||||
@@ -767,7 +785,7 @@ function buildEventModalContent({ mode, event, date }) {
|
|||||||
? event.end_datetime.slice(11, 16) : '10:00';
|
? event.end_datetime.slice(11, 16) : '10:00';
|
||||||
|
|
||||||
const userOpts = [
|
const userOpts = [
|
||||||
'<option value="">— Niemand —</option>',
|
`<option value="">${t('calendar.assignedNobody')}</option>`,
|
||||||
...state.users.map((u) =>
|
...state.users.map((u) =>
|
||||||
`<option value="${u.id}" ${isEdit && event.assigned_to === u.id ? 'selected' : ''}>${escHtml(u.display_name)}</option>`
|
`<option value="${u.id}" ${isEdit && event.assigned_to === u.id ? 'selected' : ''}>${escHtml(u.display_name)}</option>`
|
||||||
),
|
),
|
||||||
@@ -775,36 +793,36 @@ function buildEventModalContent({ mode, event, date }) {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-title">Titel *</label>
|
<label class="form-label" for="modal-title">${t('calendar.titleLabel')}</label>
|
||||||
<input type="text" class="form-input" id="modal-title"
|
<input type="text" class="form-input" id="modal-title"
|
||||||
placeholder="z.B. Zahnarzt" value="${escHtml(isEdit ? event.title : '')}">
|
placeholder="${t('calendar.titlePlaceholder')}" value="${escHtml(isEdit ? event.title : '')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="allday-toggle">
|
<label class="allday-toggle">
|
||||||
<input type="checkbox" id="modal-allday" ${isEdit && event.all_day ? 'checked' : ''}>
|
<input type="checkbox" id="modal-allday" ${isEdit && event.all_day ? 'checked' : ''}>
|
||||||
<span class="allday-toggle__label">Ganztägig</span>
|
<span class="allday-toggle__label">${t('calendar.allDayToggle')}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="time-fields">
|
<div id="time-fields">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-start-date">Startdatum</label>
|
<label class="form-label" for="modal-start-date">${t('calendar.startDateLabel')}</label>
|
||||||
<input type="date" class="form-input" id="modal-start-date" value="${startDate}">
|
<input type="date" class="form-input" id="modal-start-date" value="${startDate}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-start-time">Startzeit</label>
|
<label class="form-label" for="modal-start-time">${t('calendar.startTimeLabel')}</label>
|
||||||
<input type="time" class="form-input" id="modal-start-time" value="${startTime}">
|
<input type="time" class="form-input" id="modal-start-time" value="${startTime}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-end-date">Enddatum</label>
|
<label class="form-label" for="modal-end-date">${t('calendar.endDateLabel')}</label>
|
||||||
<input type="date" class="form-input" id="modal-end-date" value="${endDate}">
|
<input type="date" class="form-input" id="modal-end-date" value="${endDate}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-end-time">Endzeit</label>
|
<label class="form-label" for="modal-end-time">${t('calendar.endTimeLabel')}</label>
|
||||||
<input type="time" class="form-input" id="modal-end-time" value="${endTime}">
|
<input type="time" class="form-input" id="modal-end-time" value="${endTime}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -813,29 +831,29 @@ function buildEventModalContent({ mode, event, date }) {
|
|||||||
<div id="allday-fields" style="display:none;">
|
<div id="allday-fields" style="display:none;">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-allday-start">Von</label>
|
<label class="form-label" for="modal-allday-start">${t('calendar.fromLabel')}</label>
|
||||||
<input type="date" class="form-input" id="modal-allday-start" value="${startDate}">
|
<input type="date" class="form-input" id="modal-allday-start" value="${startDate}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-allday-end">Bis</label>
|
<label class="form-label" for="modal-allday-end">${t('calendar.toLabel')}</label>
|
||||||
<input type="date" class="form-input" id="modal-allday-end" value="${endDate}">
|
<input type="date" class="form-input" id="modal-allday-end" value="${endDate}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-location">Ort</label>
|
<label class="form-label" for="modal-location">${t('calendar.locationLabel')}</label>
|
||||||
<input type="text" class="form-input" id="modal-location"
|
<input type="text" class="form-input" id="modal-location"
|
||||||
placeholder="Optional" value="${escHtml(isEdit && event.location ? event.location : '')}">
|
placeholder="${t('calendar.locationPlaceholder')}" value="${escHtml(isEdit && event.location ? event.location : '')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-assigned">Zugewiesen an</label>
|
<label class="form-label" for="modal-assigned">${t('calendar.assignedLabel')}</label>
|
||||||
<select class="form-input" id="modal-assigned">${userOpts}</select>
|
<select class="form-input" id="modal-assigned">${userOpts}</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Farbe</label>
|
<label class="form-label">${t('calendar.colorLabel')}</label>
|
||||||
<div class="color-picker">
|
<div class="color-picker">
|
||||||
${EVENT_COLORS.map((c) => `
|
${EVENT_COLORS.map((c) => `
|
||||||
<div class="color-swatch" data-color="${c}" style="background-color:${c};"
|
<div class="color-swatch" data-color="${c}" style="background-color:${c};"
|
||||||
@@ -845,20 +863,20 @@ function buildEventModalContent({ mode, event, date }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-description">Beschreibung</label>
|
<label class="form-label" for="modal-description">${t('calendar.descriptionLabel')}</label>
|
||||||
<textarea class="form-input" id="modal-description" rows="2"
|
<textarea class="form-input" id="modal-description" rows="2"
|
||||||
placeholder="Optional…">${escHtml(isEdit && event.description ? event.description : '')}</textarea>
|
placeholder="${t('calendar.descriptionPlaceholder')}">${escHtml(isEdit && event.description ? event.description : '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${renderRRuleFields('event', isEdit ? event.recurrence_rule : null)}
|
${renderRRuleFields('event', isEdit ? event.recurrence_rule : null)}
|
||||||
|
|
||||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
<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" aria-label="Termin löschen">
|
${isEdit ? `<button class="btn btn--danger btn--icon" id="modal-delete" aria-label="${t('calendar.deleteEvent')}">
|
||||||
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
</button>` : '<div></div>'}
|
</button>` : '<div></div>'}
|
||||||
<div style="display:flex;gap:var(--space-3)">
|
<div style="display:flex;gap:var(--space-3)">
|
||||||
<button class="btn btn--secondary" id="modal-cancel">Abbrechen</button>
|
<button class="btn btn--secondary" id="modal-cancel">${t('common.cancel')}</button>
|
||||||
<button class="btn btn--primary" id="modal-save">${isEdit ? 'Speichern' : 'Erstellen'}</button>
|
<button class="btn btn--primary" id="modal-save">${isEdit ? t('common.save') : t('common.create')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -868,7 +886,7 @@ async function saveEvent(overlay, mode, eventId) {
|
|||||||
const title = overlay.querySelector('#modal-title').value.trim();
|
const title = overlay.querySelector('#modal-title').value.trim();
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
window.oikos?.showToast('Titel ist erforderlich', 'error');
|
window.oikos?.showToast(t('calendar.titleRequired'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,11 +936,11 @@ async function saveEvent(overlay, mode, eventId) {
|
|||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
renderView();
|
renderView();
|
||||||
window.oikos?.showToast(mode === 'create' ? 'Termin erstellt' : 'Termin gespeichert', 'success');
|
window.oikos?.showToast(mode === 'create' ? t('calendar.createdToast') : t('calendar.savedToast'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? 'Fehler beim Speichern', 'error');
|
window.oikos?.showToast(err.data?.error ?? t('calendar.saveError'), 'error');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.textContent = mode === 'edit' ? 'Speichern' : 'Erstellen';
|
saveBtn.textContent = mode === 'edit' ? t('common.save') : t('common.create');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,9 +949,9 @@ async function deleteEvent(id) {
|
|||||||
await api.delete(`/calendar/${id}`);
|
await api.delete(`/calendar/${id}`);
|
||||||
state.events = state.events.filter((e) => e.id !== id);
|
state.events = state.events.filter((e) => e.id !== id);
|
||||||
renderView();
|
renderView();
|
||||||
window.oikos?.showToast('Termin gelöscht', 'success');
|
window.oikos?.showToast(t('calendar.deletedToast'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? 'Fehler beim Löschen', 'error');
|
window.oikos?.showToast(err.data?.error ?? t('calendar.deleteError'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
-51
@@ -7,19 +7,23 @@
|
|||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
||||||
import { stagger } from '/utils/ux.js';
|
import { stagger } from '/utils/ux.js';
|
||||||
|
import { t } from '/i18n.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Konstanten
|
// Konstanten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
const MEAL_TYPES = [
|
const MEAL_TYPES = () => [
|
||||||
{ key: 'breakfast', label: 'Frühstück', icon: 'sunrise' },
|
{ key: 'breakfast', label: t('meals.typeBreakfast'), icon: 'sunrise' },
|
||||||
{ key: 'lunch', label: 'Mittagessen', icon: 'sun' },
|
{ key: 'lunch', label: t('meals.typeLunch'), icon: 'sun' },
|
||||||
{ key: 'dinner', label: 'Abendessen', icon: 'moon' },
|
{ key: 'dinner', label: t('meals.typeDinner'), icon: 'moon' },
|
||||||
{ key: 'snack', label: 'Snack', icon: 'cookie' },
|
{ key: 'snack', label: t('meals.typeSnack'), icon: 'cookie' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const DAY_NAMES = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
const DAY_NAMES = () => [
|
||||||
|
t('meals.dayMo'), t('meals.dayDi'), t('meals.dayMi'), t('meals.dayDo'),
|
||||||
|
t('meals.dayFr'), t('meals.daySa'), t('meals.daySo'),
|
||||||
|
];
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// State
|
// State
|
||||||
@@ -84,7 +88,7 @@ async function loadWeek(week) {
|
|||||||
console.error('[Meals] loadWeek Fehler:', err);
|
console.error('[Meals] loadWeek Fehler:', err);
|
||||||
state.meals = [];
|
state.meals = [];
|
||||||
state.currentWeek = getMondayOf(week);
|
state.currentWeek = getMondayOf(week);
|
||||||
window.oikos?.showToast('Essensplan konnte nicht geladen werden.', 'danger');
|
window.oikos?.showToast(t('meals.loadError'), 'danger');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,19 +109,19 @@ export async function render(container, { user }) {
|
|||||||
_container = container;
|
_container = container;
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="meals-page">
|
<div class="meals-page">
|
||||||
<h1 class="sr-only">Essensplan</h1>
|
<h1 class="sr-only">${t('meals.title')}</h1>
|
||||||
<div class="week-nav">
|
<div class="week-nav">
|
||||||
<button class="btn btn--icon" id="week-prev" aria-label="Vorherige Woche">
|
<button class="btn btn--icon" id="week-prev" aria-label="${t('meals.prevWeek')}">
|
||||||
<i data-lucide="chevron-left" aria-hidden="true"></i>
|
<i data-lucide="chevron-left" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="week-nav__label" id="week-label"></span>
|
<span class="week-nav__label" id="week-label"></span>
|
||||||
<button class="week-nav__today" id="week-today">Heute</button>
|
<button class="week-nav__today" id="week-today">${t('meals.today')}</button>
|
||||||
<button class="btn btn--icon" id="week-next" aria-label="Nächste Woche">
|
<button class="btn btn--icon" id="week-next" aria-label="${t('meals.nextWeek')}">
|
||||||
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="week-grid" id="week-grid">
|
<div class="week-grid" id="week-grid">
|
||||||
<div style="margin:auto;padding:2rem;text-align:center;color:var(--color-text-disabled)">Lade…</div>
|
<div style="margin:auto;padding:2rem;text-align:center;color:var(--color-text-disabled)">${t('meals.loadingIndicator')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -144,6 +148,7 @@ function renderWeekGrid() {
|
|||||||
formatWeekLabel(state.currentWeek);
|
formatWeekLabel(state.currentWeek);
|
||||||
|
|
||||||
const days = Array.from({ length: 7 }, (_, i) => addDays(state.currentWeek, i));
|
const days = Array.from({ length: 7 }, (_, i) => addDays(state.currentWeek, i));
|
||||||
|
const dayNames = DAY_NAMES();
|
||||||
|
|
||||||
grid.innerHTML = days.map((date, idx) => {
|
grid.innerHTML = days.map((date, idx) => {
|
||||||
const mealsForDay = state.meals.filter((m) => m.date === date);
|
const mealsForDay = state.meals.filter((m) => m.date === date);
|
||||||
@@ -152,11 +157,11 @@ function renderWeekGrid() {
|
|||||||
return `
|
return `
|
||||||
<div class="day-column">
|
<div class="day-column">
|
||||||
<div class="day-header ${todayClass}">
|
<div class="day-header ${todayClass}">
|
||||||
<span class="day-header__name">${DAY_NAMES[idx]}</span>
|
<span class="day-header__name">${dayNames[idx]}</span>
|
||||||
<span class="day-header__date">${formatDayDate(date)}</span>
|
<span class="day-header__date">${formatDayDate(date)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="day-slots">
|
<div class="day-slots">
|
||||||
${MEAL_TYPES.map((type) => renderSlot(date, type, mealsForDay)).join('')}
|
${MEAL_TYPES().map((type) => renderSlot(date, type, mealsForDay)).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -175,14 +180,14 @@ function renderSlot(date, type, mealsForDay) {
|
|||||||
<div class="meal-slot meal-slot--empty" data-date="${date}" data-type="${type.key}">
|
<div class="meal-slot meal-slot--empty" data-date="${date}" data-type="${type.key}">
|
||||||
<div class="meal-slot__type-label">${type.label}</div>
|
<div class="meal-slot__type-label">${type.label}</div>
|
||||||
<div class="empty-state empty-state--compact">
|
<div class="empty-state empty-state--compact">
|
||||||
<div class="empty-state__description">Kein Essen geplant</div>
|
<div class="empty-state__description">${t('meals.noMealPlanned')}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="meal-slot__add-btn"
|
class="meal-slot__add-btn"
|
||||||
data-action="add-meal"
|
data-action="add-meal"
|
||||||
data-date="${date}"
|
data-date="${date}"
|
||||||
data-type="${type.key}"
|
data-type="${type.key}"
|
||||||
aria-label="${type.label} hinzufügen"
|
aria-label="${t('meals.addMeal', { type: type.label })}"
|
||||||
>
|
>
|
||||||
<i data-lucide="plus" style="width:16px;height:16px;" aria-hidden="true"></i>
|
<i data-lucide="plus" style="width:16px;height:16px;" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -192,7 +197,7 @@ function renderSlot(date, type, mealsForDay) {
|
|||||||
|
|
||||||
const ingCount = meal.ingredients?.length ?? 0;
|
const ingCount = meal.ingredients?.length ?? 0;
|
||||||
const ingDone = meal.ingredients?.filter((i) => i.on_shopping_list).length ?? 0;
|
const ingDone = meal.ingredients?.filter((i) => i.on_shopping_list).length ?? 0;
|
||||||
const ingLabel = ingCount > 0 ? `${ingCount} Zutat${ingCount !== 1 ? 'en' : ''}` : '';
|
const ingLabel = ingCount > 0 ? (ingCount !== 1 ? t('meals.ingredientCountPlural', { count: ingCount }) : t('meals.ingredientCount', { count: ingCount })) : '';
|
||||||
const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : '';
|
const ingDoneLabel = ingCount > 0 && ingDone === ingCount ? ' ✓' : '';
|
||||||
const canTransfer = ingCount > 0 && ingDone < ingCount;
|
const canTransfer = ingCount > 0 && ingDone < ingCount;
|
||||||
|
|
||||||
@@ -211,12 +216,12 @@ function renderSlot(date, type, mealsForDay) {
|
|||||||
${canTransfer ? `<button class="meal-card__action-btn meal-card__action-btn--shopping"
|
${canTransfer ? `<button class="meal-card__action-btn meal-card__action-btn--shopping"
|
||||||
data-action="transfer-meal"
|
data-action="transfer-meal"
|
||||||
data-meal-id="${meal.id}"
|
data-meal-id="${meal.id}"
|
||||||
aria-label="Zutaten auf Einkaufsliste"
|
aria-label="${t('meals.transferToShoppingList')}"
|
||||||
><i data-lucide="shopping-cart" style="width:14px;height:14px;" aria-hidden="true"></i></button>` : ''}
|
><i data-lucide="shopping-cart" style="width:14px;height:14px;" aria-hidden="true"></i></button>` : ''}
|
||||||
<button class="meal-card__action-btn"
|
<button class="meal-card__action-btn"
|
||||||
data-action="delete-meal"
|
data-action="delete-meal"
|
||||||
data-meal-id="${meal.id}"
|
data-meal-id="${meal.id}"
|
||||||
aria-label="Mahlzeit löschen"
|
aria-label="${t('meals.deleteMeal')}"
|
||||||
><i data-lucide="trash-2" style="width:14px;height:14px;" aria-hidden="true"></i></button>
|
><i data-lucide="trash-2" style="width:14px;height:14px;" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,7 +434,7 @@ function openMealModal(opts) {
|
|||||||
const content = buildModalContent(opts);
|
const content = buildModalContent(opts);
|
||||||
|
|
||||||
openSharedModal({
|
openSharedModal({
|
||||||
title: isEdit ? 'Mahlzeit bearbeiten' : 'Mahlzeit hinzufügen',
|
title: isEdit ? t('meals.editMeal') : t('meals.addMealTitle'),
|
||||||
content,
|
content,
|
||||||
size: 'md',
|
size: 'md',
|
||||||
onSave(panel) {
|
onSave(panel) {
|
||||||
@@ -498,12 +503,12 @@ function openMealModal(opts) {
|
|||||||
try {
|
try {
|
||||||
const res = await api.post(`/meals/${state.modal.meal.id}/to-shopping-list`, { listId });
|
const res = await api.post(`/meals/${state.modal.meal.id}/to-shopping-list`, { listId });
|
||||||
if (res.data.transferred > 0) {
|
if (res.data.transferred > 0) {
|
||||||
window.oikos?.showToast(`${res.data.transferred} Zutat${res.data.transferred !== 1 ? 'en' : ''} übertragen`, 'success');
|
window.oikos?.showToast(res.data.transferred !== 1 ? t('meals.transferSuccessPlural', { count: res.data.transferred }) : t('meals.transferSuccess', { count: res.data.transferred }), 'success');
|
||||||
await loadWeek(state.currentWeek);
|
await loadWeek(state.currentWeek);
|
||||||
closeModal();
|
closeModal();
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
} else {
|
} else {
|
||||||
window.oikos?.showToast('Alle Zutaten bereits übertragen', 'info');
|
window.oikos?.showToast(t('meals.transferAlreadyDone'), 'info');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -520,13 +525,13 @@ function openMealModal(opts) {
|
|||||||
|
|
||||||
function buildModalContent({ mode, date, mealType, meal }) {
|
function buildModalContent({ mode, date, mealType, meal }) {
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
const typeOpts = MEAL_TYPES.map((t) =>
|
const typeOpts = MEAL_TYPES().map((mt) =>
|
||||||
`<option value="${t.key}" ${t.key === mealType ? 'selected' : ''}>${t.label}</option>`
|
`<option value="${mt.key}" ${mt.key === mealType ? 'selected' : ''}>${mt.label}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const listOpts = state.lists.length
|
const listOpts = state.lists.length
|
||||||
? state.lists.map((l) => `<option value="${l.id}">${escHtml(l.name)}</option>`).join('')
|
? state.lists.map((l) => `<option value="${l.id}">${escHtml(l.name)}</option>`).join('')
|
||||||
: '<option value="" disabled>Keine Einkaufslisten vorhanden</option>';
|
: `<option value="" disabled>${t('meals.noShoppingLists')}</option>`;
|
||||||
|
|
||||||
const ingRows = isEdit && meal.ingredients?.length
|
const ingRows = isEdit && meal.ingredients?.length
|
||||||
? meal.ingredients.map((ing) => ingredientRowHTML(ing.name, ing.quantity ?? '', ing.id)).join('')
|
? meal.ingredients.map((ing) => ingredientRowHTML(ing.name, ing.quantity ?? '', ing.id)).join('')
|
||||||
@@ -537,36 +542,36 @@ function buildModalContent({ mode, date, mealType, meal }) {
|
|||||||
return `
|
return `
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
<div class="form-group" style="margin-bottom:0">
|
<div class="form-group" style="margin-bottom:0">
|
||||||
<label class="form-label" for="modal-date">Datum</label>
|
<label class="form-label" for="modal-date">${t('meals.dateLabel')}</label>
|
||||||
<input type="date" class="form-input" id="modal-date" value="${date}">
|
<input type="date" class="form-input" id="modal-date" value="${date}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom:0">
|
<div class="form-group" style="margin-bottom:0">
|
||||||
<label class="form-label" for="modal-type">Mahlzeit</label>
|
<label class="form-label" for="modal-type">${t('meals.mealTypeLabel')}</label>
|
||||||
<select class="form-input" id="modal-type">${typeOpts}</select>
|
<select class="form-input" id="modal-type">${typeOpts}</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="position:relative;">
|
<div class="form-group" style="position:relative;">
|
||||||
<label class="form-label" for="modal-title">Titel *</label>
|
<label class="form-label" for="modal-title">${t('meals.titleLabel')}</label>
|
||||||
<input type="text" class="form-input" id="modal-title"
|
<input type="text" class="form-input" id="modal-title"
|
||||||
placeholder="z.B. Spaghetti Bolognese"
|
placeholder="${t('meals.titlePlaceholder')}"
|
||||||
value="${escHtml(isEdit ? meal.title : '')}"
|
value="${escHtml(isEdit ? meal.title : '')}"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
<div id="modal-autocomplete" class="meal-modal__autocomplete" hidden></div>
|
<div id="modal-autocomplete" class="meal-modal__autocomplete" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-notes">Notizen</label>
|
<label class="form-label" for="modal-notes">${t('meals.notesLabel')}</label>
|
||||||
<textarea class="form-input" id="modal-notes" rows="2"
|
<textarea class="form-input" id="modal-notes" rows="2"
|
||||||
placeholder="Optional…">${escHtml(isEdit && meal.notes ? meal.notes : '')}</textarea>
|
placeholder="${t('meals.notesPlaceholder')}">${escHtml(isEdit && meal.notes ? meal.notes : '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Zutaten</label>
|
<label class="form-label">${t('meals.ingredientsLabel')}</label>
|
||||||
<div class="ingredient-list" id="ingredient-list">${ingRows}</div>
|
<div class="ingredient-list" id="ingredient-list">${ingRows}</div>
|
||||||
<button class="add-ingredient-btn" id="add-ingredient-btn" type="button">
|
<button class="add-ingredient-btn" id="add-ingredient-btn" type="button">
|
||||||
<i data-lucide="plus" style="width:14px;height:14px;" aria-hidden="true"></i>
|
<i data-lucide="plus" style="width:14px;height:14px;" aria-hidden="true"></i>
|
||||||
Zutat hinzufügen
|
${t('meals.addIngredient')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -574,26 +579,26 @@ function buildModalContent({ mode, date, mealType, meal }) {
|
|||||||
<div class="shopping-transfer">
|
<div class="shopping-transfer">
|
||||||
<div class="shopping-transfer__label">
|
<div class="shopping-transfer__label">
|
||||||
<i data-lucide="shopping-cart" style="width:14px;height:14px;" aria-hidden="true"></i>
|
<i data-lucide="shopping-cart" style="width:14px;height:14px;" aria-hidden="true"></i>
|
||||||
Zutaten auf Einkaufsliste übertragen
|
${t('meals.transferLabel')}
|
||||||
</div>
|
</div>
|
||||||
<select class="shopping-transfer__select" id="transfer-list-select">${listOpts}</select>
|
<select class="shopping-transfer__select" id="transfer-list-select">${listOpts}</select>
|
||||||
<button class="btn btn--secondary shopping-transfer__btn" id="transfer-btn" type="button">
|
<button class="btn btn--secondary shopping-transfer__btn" id="transfer-btn" type="button">
|
||||||
Jetzt übertragen
|
${t('meals.transferNow')}
|
||||||
</button>
|
</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
<div class="modal-panel__footer" style="border:none;padding:0;margin-top:var(--space-4)">
|
<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--secondary" id="modal-cancel">${t('common.cancel')}</button>
|
||||||
<button class="btn btn--primary" id="modal-save">${isEdit ? 'Speichern' : 'Hinzufügen'}</button>
|
<button class="btn btn--primary" id="modal-save">${isEdit ? t('common.save') : t('common.add')}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ingredientRowHTML(name, qty, id) {
|
function ingredientRowHTML(name, qty, id) {
|
||||||
return `
|
return `
|
||||||
<div class="ingredient-row" data-ing-id="${id ?? ''}">
|
<div class="ingredient-row" data-ing-id="${id ?? ''}">
|
||||||
<input type="text" class="form-input ingredient-row__name" placeholder="Zutat" value="${escHtml(name)}">
|
<input type="text" class="form-input ingredient-row__name" placeholder="${t('meals.ingredientNamePlaceholder')}" value="${escHtml(name)}">
|
||||||
<input type="text" class="form-input ingredient-row__qty" placeholder="Menge" value="${escHtml(qty)}">
|
<input type="text" class="form-input ingredient-row__qty" placeholder="${t('meals.ingredientQtyPlaceholder')}" value="${escHtml(qty)}">
|
||||||
<button class="ingredient-row__remove" data-action="remove-ingredient" type="button" aria-label="Zutat entfernen">
|
<button class="ingredient-row__remove" data-action="remove-ingredient" type="button" aria-label="${t('meals.removeIngredient')}">
|
||||||
<i data-lucide="x" style="width:14px;height:14px;" aria-hidden="true"></i>
|
<i data-lucide="x" style="width:14px;height:14px;" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,7 +618,7 @@ async function saveModal(overlay) {
|
|||||||
const notes = overlay.querySelector('#modal-notes').value.trim() || null;
|
const notes = overlay.querySelector('#modal-notes').value.trim() || null;
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
window.oikos?.showToast('Titel ist erforderlich', 'error');
|
window.oikos?.showToast(t('meals.titleRequired'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,11 +661,11 @@ async function saveModal(overlay) {
|
|||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
window.oikos?.showToast(mode === 'create' ? 'Mahlzeit hinzugefügt' : 'Mahlzeit gespeichert', 'success');
|
window.oikos?.showToast(mode === 'create' ? t('meals.addMealTitle') : t('meals.editMeal'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? 'Fehler beim Speichern', 'error');
|
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.textContent = state.modal?.mode === 'edit' ? 'Speichern' : 'Hinzufügen';
|
saveBtn.textContent = state.modal?.mode === 'edit' ? t('common.save') : t('common.add');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,14 +674,14 @@ async function saveModal(overlay) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
async function deleteMeal(mealId) {
|
async function deleteMeal(mealId) {
|
||||||
if (!confirm('Mahlzeit wirklich löschen?')) return;
|
if (!confirm(t('meals.deleteMeal') + '?')) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/meals/${mealId}`);
|
await api.delete(`/meals/${mealId}`);
|
||||||
state.meals = state.meals.filter((m) => m.id !== mealId);
|
state.meals = state.meals.filter((m) => m.id !== mealId);
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
window.oikos?.showToast('Mahlzeit gelöscht', 'success');
|
window.oikos?.showToast(t('meals.deleteMeal'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? 'Fehler beim Löschen', 'error');
|
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +691,7 @@ async function deleteMeal(mealId) {
|
|||||||
|
|
||||||
async function transferMeal(mealId) {
|
async function transferMeal(mealId) {
|
||||||
if (!state.lists.length) {
|
if (!state.lists.length) {
|
||||||
window.oikos?.showToast('Keine Einkaufslisten vorhanden', 'error');
|
window.oikos?.showToast(t('meals.noShoppingLists'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,14 +708,14 @@ async function transferMeal(mealId) {
|
|||||||
try {
|
try {
|
||||||
const res = await api.post(`/meals/${mealId}/to-shopping-list`, { listId });
|
const res = await api.post(`/meals/${mealId}/to-shopping-list`, { listId });
|
||||||
if (res.data.transferred > 0) {
|
if (res.data.transferred > 0) {
|
||||||
window.oikos?.showToast(`${res.data.transferred} Zutat${res.data.transferred !== 1 ? 'en' : ''} übertragen`, 'success');
|
window.oikos?.showToast(res.data.transferred !== 1 ? t('meals.transferSuccessPlural', { count: res.data.transferred }) : t('meals.transferSuccess', { count: res.data.transferred }), 'success');
|
||||||
await loadWeek(state.currentWeek);
|
await loadWeek(state.currentWeek);
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
} else {
|
} else {
|
||||||
window.oikos?.showToast('Alle Zutaten bereits übertragen', 'info');
|
window.oikos?.showToast(t('meals.transferAlreadyDone'), 'info');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos?.showToast(err.data?.error ?? 'Fehler beim Übertragen', 'error');
|
window.oikos?.showToast(err.data?.error ?? t('common.errorGeneric'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
-28
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
|
import { t } from '/i18n.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Konstanten
|
// Konstanten
|
||||||
@@ -21,6 +22,18 @@ const ITEM_CATEGORIES = [
|
|||||||
'Tiefkühl', 'Getränke', 'Haushalt', 'Drogerie', 'Sonstiges',
|
'Tiefkühl', 'Getränke', 'Haushalt', 'Drogerie', 'Sonstiges',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CATEGORY_LABELS = () => ({
|
||||||
|
'Obst & Gemüse': t('shopping.catFruitVeg'),
|
||||||
|
'Backwaren': t('shopping.catBakery'),
|
||||||
|
'Milchprodukte': t('shopping.catDairy'),
|
||||||
|
'Fleisch & Fisch': t('shopping.catMeatFish'),
|
||||||
|
'Tiefkühl': t('shopping.catFrozen'),
|
||||||
|
'Getränke': t('shopping.catDrinks'),
|
||||||
|
'Haushalt': t('shopping.catHousehold'),
|
||||||
|
'Drogerie': t('shopping.catDrugstore'),
|
||||||
|
'Sonstiges': t('shopping.catMisc'),
|
||||||
|
});
|
||||||
|
|
||||||
const CATEGORY_ICONS = {
|
const CATEGORY_ICONS = {
|
||||||
'Obst & Gemüse': 'apple',
|
'Obst & Gemüse': 'apple',
|
||||||
'Backwaren': 'wheat',
|
'Backwaren': 'wheat',
|
||||||
@@ -95,9 +108,9 @@ function renderListContent(container) {
|
|||||||
content.innerHTML = `
|
content.innerHTML = `
|
||||||
<div class="no-lists">
|
<div class="no-lists">
|
||||||
<i data-lucide="shopping-cart" style="width:56px;height:56px;color:var(--color-text-disabled)" aria-hidden="true"></i>
|
<i data-lucide="shopping-cart" style="width:56px;height:56px;color:var(--color-text-disabled)" aria-hidden="true"></i>
|
||||||
<div style="font-size:var(--text-lg);font-weight:var(--font-weight-semibold)">Keine Listen</div>
|
<div style="font-size:var(--text-lg);font-weight:var(--font-weight-semibold)">${t('shopping.noLists')}</div>
|
||||||
<div style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
<div style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
||||||
Erstelle eine Liste mit dem + Button.
|
${t('shopping.noListsDescription')}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
@@ -110,7 +123,7 @@ function renderListContent(container) {
|
|||||||
<!-- Liste-Header -->
|
<!-- Liste-Header -->
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<span class="list-header__name" data-action="rename-list" data-id="${state.activeList.id}"
|
<span class="list-header__name" data-action="rename-list" data-id="${state.activeList.id}"
|
||||||
role="button" tabindex="0" aria-label="Liste umbenennen">
|
role="button" tabindex="0" aria-label="${t('shopping.renameListLabel')}">
|
||||||
${state.activeList.name}
|
${state.activeList.name}
|
||||||
<i data-lucide="pencil" class="list-header__edit-icon" aria-hidden="true"></i>
|
<i data-lucide="pencil" class="list-header__edit-icon" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -119,10 +132,10 @@ function renderListContent(container) {
|
|||||||
<button class="btn btn--ghost" data-action="clear-checked"
|
<button class="btn btn--ghost" data-action="clear-checked"
|
||||||
style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
||||||
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
||||||
Abgehakt löschen (${checkedCount})
|
${t('shopping.clearChecked', { count: checkedCount })}
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
<button class="btn btn--ghost btn--icon" data-action="delete-list"
|
<button class="btn btn--ghost btn--icon" data-action="delete-list"
|
||||||
data-id="${state.activeList.id}" aria-label="Liste löschen"
|
data-id="${state.activeList.id}" aria-label="${t('shopping.deleteListLabel')}"
|
||||||
style="color:var(--color-text-secondary)">
|
style="color:var(--color-text-secondary)">
|
||||||
<i data-lucide="trash" style="width:18px;height:18px" aria-hidden="true"></i>
|
<i data-lucide="trash" style="width:18px;height:18px" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -134,17 +147,17 @@ function renderListContent(container) {
|
|||||||
<form class="quick-add__form" id="quick-add-form" novalidate autocomplete="off">
|
<form class="quick-add__form" id="quick-add-form" novalidate autocomplete="off">
|
||||||
<div class="quick-add__input-wrap">
|
<div class="quick-add__input-wrap">
|
||||||
<input class="quick-add__input" type="text" id="item-name-input"
|
<input class="quick-add__input" type="text" id="item-name-input"
|
||||||
placeholder="Artikel hinzufügen…" aria-label="Artikelname" autocomplete="off">
|
placeholder="${t('shopping.itemNamePlaceholder')}" aria-label="${t('shopping.itemNameLabel')}" autocomplete="off">
|
||||||
<input class="quick-add__qty" type="text" id="item-qty-input"
|
<input class="quick-add__qty" type="text" id="item-qty-input"
|
||||||
placeholder="Menge" aria-label="Menge" autocomplete="off">
|
placeholder="${t('shopping.itemQtyPlaceholder')}" aria-label="${t('shopping.itemQtyLabel')}" autocomplete="off">
|
||||||
<div class="autocomplete-dropdown" id="autocomplete-dropdown" hidden></div>
|
<div class="autocomplete-dropdown" id="autocomplete-dropdown" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<select class="quick-add__cat" id="item-cat-select" aria-label="Kategorie">
|
<select class="quick-add__cat" id="item-cat-select" aria-label="${t('shopping.categoryLabel')}">
|
||||||
${ITEM_CATEGORIES.map((c) =>
|
${ITEM_CATEGORIES.map((c) =>
|
||||||
`<option value="${c}">${c}</option>`
|
`<option value="${c}">${c}</option>`
|
||||||
).join('')}
|
).join('')}
|
||||||
</select>
|
</select>
|
||||||
<button class="quick-add__btn" type="submit" aria-label="Artikel hinzufügen">
|
<button class="quick-add__btn" type="submit" aria-label="${t('shopping.addItemLabel')}">
|
||||||
<i data-lucide="plus" style="width:20px;height:20px" aria-hidden="true"></i>
|
<i data-lucide="plus" style="width:20px;height:20px" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -170,17 +183,18 @@ function renderItems() {
|
|||||||
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
|
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
|
||||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="empty-state__title">Die Liste ist leer</div>
|
<div class="empty-state__title">${t('shopping.emptyList')}</div>
|
||||||
<div class="empty-state__description">Artikel über das Eingabefeld oben hinzufügen.</div>
|
<div class="empty-state__description">${t('shopping.emptyListDescription')}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const catLabels = CATEGORY_LABELS();
|
||||||
const groups = groupItemsByCategory(state.items);
|
const groups = groupItemsByCategory(state.items);
|
||||||
return groups.map(([cat, items]) => `
|
return groups.map(([cat, items]) => `
|
||||||
<div class="item-category">
|
<div class="item-category">
|
||||||
<div class="item-category__header">
|
<div class="item-category__header">
|
||||||
<i data-lucide="${CATEGORY_ICONS[cat] ?? 'tag'}" class="item-category__icon" aria-hidden="true"></i>
|
<i data-lucide="${CATEGORY_ICONS[cat] ?? 'tag'}" class="item-category__icon" aria-hidden="true"></i>
|
||||||
${cat}
|
${catLabels[cat] || cat}
|
||||||
</div>
|
</div>
|
||||||
${items.map(renderItem).join('')}
|
${items.map(renderItem).join('')}
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
@@ -192,17 +206,17 @@ function renderItem(item) {
|
|||||||
<div class="swipe-row" data-swipe-id="${item.id}" data-swipe-checked="${item.is_checked}">
|
<div class="swipe-row" data-swipe-id="${item.id}" data-swipe-checked="${item.is_checked}">
|
||||||
<div class="swipe-reveal swipe-reveal--done" aria-hidden="true">
|
<div class="swipe-reveal swipe-reveal--done" aria-hidden="true">
|
||||||
<i data-lucide="${isDone ? 'rotate-ccw' : 'check'}" style="width:22px;height:22px" aria-hidden="true"></i>
|
<i data-lucide="${isDone ? 'rotate-ccw' : 'check'}" style="width:22px;height:22px" aria-hidden="true"></i>
|
||||||
<span>${isDone ? 'Zurück' : 'Abhaken'}</span>
|
<span>${isDone ? t('shopping.swipeBack') : t('shopping.swipeCheck')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="swipe-reveal swipe-reveal--delete" aria-hidden="true">
|
<div class="swipe-reveal swipe-reveal--delete" aria-hidden="true">
|
||||||
<i data-lucide="trash-2" style="width:22px;height:22px" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:22px;height:22px" aria-hidden="true"></i>
|
||||||
<span>Löschen</span>
|
<span>${t('shopping.swipeDelete')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="shopping-item ${isDone ? 'shopping-item--checked' : ''}"
|
<div class="shopping-item ${isDone ? 'shopping-item--checked' : ''}"
|
||||||
data-item-id="${item.id}">
|
data-item-id="${item.id}">
|
||||||
<button class="item-check ${isDone ? 'item-check--checked' : ''}"
|
<button class="item-check ${isDone ? 'item-check--checked' : ''}"
|
||||||
data-action="toggle-item" data-id="${item.id}" data-checked="${item.is_checked}"
|
data-action="toggle-item" data-id="${item.id}" data-checked="${item.is_checked}"
|
||||||
aria-label="${escHtml(item.name)} ${isDone ? 'als nicht erledigt markieren' : 'abhaken'}">
|
aria-label="${isDone ? t('shopping.markUndoneLabel', { name: escHtml(item.name) }) : t('shopping.markDoneLabel', { name: escHtml(item.name) })}">
|
||||||
<i data-lucide="check" class="item-check__icon" aria-hidden="true"></i>
|
<i data-lucide="check" class="item-check__icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
@@ -210,7 +224,7 @@ function renderItem(item) {
|
|||||||
${item.quantity ? `<div class="item-quantity">${escHtml(item.quantity)}</div>` : ''}
|
${item.quantity ? `<div class="item-quantity">${escHtml(item.quantity)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<button class="item-delete" data-action="delete-item" data-id="${item.id}"
|
<button class="item-delete" data-action="delete-item" data-id="${item.id}"
|
||||||
aria-label="${escHtml(item.name)} löschen">
|
aria-label="${t('shopping.deleteItemLabel', { name: escHtml(item.name) })}">
|
||||||
<i data-lucide="x" style="width:16px;height:16px" aria-hidden="true"></i>
|
<i data-lucide="x" style="width:16px;height:16px" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -474,7 +488,7 @@ function updateItemsList(container) {
|
|||||||
<button class="btn btn--ghost" data-action="clear-checked"
|
<button class="btn btn--ghost" data-action="clear-checked"
|
||||||
style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
style="font-size:var(--text-sm);color:var(--color-text-secondary)">
|
||||||
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
||||||
Abgehakt löschen (${checkedCount})
|
${t('shopping.clearChecked', { count: checkedCount })}
|
||||||
</button>`);
|
</button>`);
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
} else if (clearBtn) {
|
} else if (clearBtn) {
|
||||||
@@ -483,7 +497,7 @@ function updateItemsList(container) {
|
|||||||
} else {
|
} else {
|
||||||
clearBtn.innerHTML = `
|
clearBtn.innerHTML = `
|
||||||
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
<i data-lucide="trash-2" style="width:15px;height:15px" aria-hidden="true"></i>
|
||||||
Abgehakt löschen (${checkedCount})`;
|
${t('shopping.clearChecked', { count: checkedCount })}`;
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,7 +523,7 @@ async function loadLists() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Shopping] loadLists Fehler:', err);
|
console.error('[Shopping] loadLists Fehler:', err);
|
||||||
state.lists = [];
|
state.lists = [];
|
||||||
window.oikos?.showToast('Listen konnten nicht geladen werden.', 'danger');
|
window.oikos?.showToast(t('shopping.listsLoadError'), 'danger');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +542,7 @@ async function switchList(listId, container) {
|
|||||||
console.error('[Shopping] loadItems Fehler:', err);
|
console.error('[Shopping] loadItems Fehler:', err);
|
||||||
state.items = [];
|
state.items = [];
|
||||||
state.activeList = state.lists.find((l) => l.id === listId) ?? null;
|
state.activeList = state.lists.find((l) => l.id === listId) ?? null;
|
||||||
window.oikos?.showToast('Artikel konnten nicht geladen werden.', 'danger');
|
window.oikos?.showToast(t('shopping.itemsLoadError'), 'danger');
|
||||||
}
|
}
|
||||||
renderListContent(container);
|
renderListContent(container);
|
||||||
wireListContentEvents(container);
|
wireListContentEvents(container);
|
||||||
@@ -548,7 +562,7 @@ function wireTabBar(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.dataset.action === 'new-list') {
|
if (target.dataset.action === 'new-list') {
|
||||||
const name = prompt('Name der neuen Liste:');
|
const name = prompt(t('shopping.newListPrompt'));
|
||||||
if (!name?.trim()) return;
|
if (!name?.trim()) return;
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/shopping', { name: name.trim() });
|
const data = await api.post('/shopping', { name: name.trim() });
|
||||||
@@ -621,7 +635,7 @@ function wireListContentEvents(container) {
|
|||||||
updateItemsList(container);
|
updateItemsList(container);
|
||||||
updateListCounter(state.activeListId, -count, -count);
|
updateListCounter(state.activeListId, -count, -count);
|
||||||
renderTabs(container);
|
renderTabs(container);
|
||||||
window.oikos.showToast(`${count} Artikel entfernt.`);
|
window.oikos.showToast(t('shopping.itemsRemovedToast', { count }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast(err.message, 'danger');
|
window.oikos.showToast(err.message, 'danger');
|
||||||
}
|
}
|
||||||
@@ -629,7 +643,7 @@ function wireListContentEvents(container) {
|
|||||||
|
|
||||||
// ---- Liste umbenennen ----
|
// ---- Liste umbenennen ----
|
||||||
if (action === 'rename-list') {
|
if (action === 'rename-list') {
|
||||||
const newName = prompt('Neuer Listen-Name:', state.activeList?.name);
|
const newName = prompt(t('shopping.renameListPrompt'), state.activeList?.name);
|
||||||
if (!newName?.trim() || newName.trim() === state.activeList?.name) return;
|
if (!newName?.trim() || newName.trim() === state.activeList?.name) return;
|
||||||
try {
|
try {
|
||||||
const data = await api.put(`/shopping/${state.activeListId}`, { name: newName.trim() });
|
const data = await api.put(`/shopping/${state.activeListId}`, { name: newName.trim() });
|
||||||
@@ -646,7 +660,7 @@ function wireListContentEvents(container) {
|
|||||||
|
|
||||||
// ---- Liste löschen ----
|
// ---- Liste löschen ----
|
||||||
if (action === 'delete-list') {
|
if (action === 'delete-list') {
|
||||||
if (!confirm(`Liste "${state.activeList?.name}" und alle Artikel löschen?`)) return;
|
if (!confirm(t('shopping.deleteListConfirm', { name: state.activeList?.name }))) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/shopping/${state.activeListId}`);
|
await api.delete(`/shopping/${state.activeListId}`);
|
||||||
state.lists = state.lists.filter((l) => l.id !== state.activeListId);
|
state.lists = state.lists.filter((l) => l.id !== state.activeListId);
|
||||||
@@ -659,7 +673,7 @@ function wireListContentEvents(container) {
|
|||||||
renderTabs(container);
|
renderTabs(container);
|
||||||
renderListContent(container);
|
renderListContent(container);
|
||||||
}
|
}
|
||||||
window.oikos.showToast('Liste gelöscht.');
|
window.oikos.showToast(t('shopping.deletedListToast'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast(err.message, 'danger');
|
window.oikos.showToast(err.message, 'danger');
|
||||||
}
|
}
|
||||||
@@ -701,15 +715,15 @@ export async function render(container, { user }) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Shopping] Ladefehler:', err.message);
|
console.error('[Shopping] Ladefehler:', err.message);
|
||||||
window.oikos.showToast('Einkaufslisten konnten nicht geladen werden.', 'danger');
|
window.oikos.showToast(t('shopping.listsLoadError'), 'danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="shopping-page">
|
<div class="shopping-page">
|
||||||
<h1 class="sr-only">Einkaufslisten</h1>
|
<h1 class="sr-only">${t('shopping.title')}</h1>
|
||||||
<div class="list-tabs-bar" id="list-tabs-bar"></div>
|
<div class="list-tabs-bar" id="list-tabs-bar"></div>
|
||||||
<div id="list-content" style="flex:1;display:flex;flex-direction:column;overflow:hidden"></div>
|
<div id="list-content" style="flex:1;display:flex;flex-direction:column;overflow:hidden"></div>
|
||||||
<button class="page-fab" id="fab-new-item" aria-label="Artikel hinzufügen">
|
<button class="page-fab" id="fab-new-item" aria-label="${t('shopping.addItemLabel')}">
|
||||||
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
|
<i data-lucide="plus" style="width:24px;height:24px" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user