Files
oikos/public/rrule-ui.js
T
Ulas 5b8ab7303f fix(i18n): translate shopping categories and recurrence UI fields
- Shopping category dropdown options now use CATEGORY_LABELS() for
  translated display text instead of raw German internal keys
- rrule-ui.js now imports t() from /i18n.js; all hardcoded German
  strings (freq options, weekday labels, form labels, unit labels)
  replaced with i18n keys under the new 'rrule' namespace
- Added 'rrule' section to de.json and en.json with 22 new keys

Fixes #21
2026-04-05 13:49:44 +02:00

193 lines
7.0 KiB
JavaScript

/**
* Modul: RRULE UI-Helfer
* Zweck: Wiederholungs-Formular (HTML + Logik) für Aufgaben- und Kalender-Modals
* Abhängigkeiten: /i18n.js
*/
import { t } from '/i18n.js';
const FREQ_OPTIONS = () => [
{ value: '', label: t('rrule.freqNone') },
{ value: 'DAILY', label: t('rrule.freqDaily') },
{ value: 'WEEKLY', label: t('rrule.freqWeekly') },
{ value: 'MONTHLY', label: t('rrule.freqMonthly') },
];
const WEEKDAYS = () => [
{ value: 'MO', label: t('rrule.dayMo') },
{ value: 'TU', label: t('rrule.dayTu') },
{ value: 'WE', label: t('rrule.dayWe') },
{ value: 'TH', label: t('rrule.dayTh') },
{ value: 'FR', label: t('rrule.dayFr') },
{ value: 'SA', label: t('rrule.daySa') },
{ value: 'SU', label: t('rrule.daySu') },
];
/**
* Parsed einen RRULE-String in ein Objekt für die UI.
* @param {string|null} rule - z.B. "FREQ=WEEKLY;BYDAY=MO,TH;INTERVAL=2"
* @returns {{ freq: string, interval: number, byday: string[], until: string }}
*/
export function parseRRule(rule) {
const result = { freq: '', interval: 1, byday: [], until: '' };
if (!rule) return result;
for (const segment of rule.split(';')) {
const eq = segment.indexOf('=');
if (eq === -1) continue;
const key = segment.slice(0, eq).toUpperCase();
const val = segment.slice(eq + 1);
if (key === 'FREQ') result.freq = val;
if (key === 'INTERVAL') result.interval = parseInt(val, 10) || 1;
if (key === 'BYDAY') result.byday = val.split(',').map(d => d.trim());
if (key === 'UNTIL') {
// YYYYMMDD → YYYY-MM-DD
const c = val.replace(/[TZ]/g, '');
result.until = `${c.slice(0, 4)}-${c.slice(4, 6)}-${c.slice(6, 8)}`;
}
}
return result;
}
/**
* Baut einen RRULE-String aus den UI-Werten.
* @param {{ freq: string, interval: number, byday: string[], until: string }} opts
* @returns {string|null} - RRULE-String oder null (keine Wiederholung)
*/
export function buildRRule({ freq, interval, byday, until }) {
if (!freq) return null;
const parts = [`FREQ=${freq}`];
if (interval > 1) parts.push(`INTERVAL=${interval}`);
if (freq === 'WEEKLY' && byday.length > 0) {
parts.push(`BYDAY=${byday.join(',')}`);
}
if (until) {
parts.push(`UNTIL=${until.replace(/-/g, '')}T235959Z`);
}
return parts.join(';');
}
/**
* Rendert das HTML für die Wiederholungs-Felder.
* @param {string} prefix - ID-Prefix (z.B. "task" oder "event")
* @param {string|null} existingRule - bestehende RRULE oder null
* @returns {string} HTML-String
*/
export function renderRRuleFields(prefix, existingRule) {
const parsed = parseRRule(existingRule);
const freqOpts = FREQ_OPTIONS().map(o =>
`<option value="${o.value}" ${parsed.freq === o.value ? 'selected' : ''}>${o.label}</option>`
).join('');
const dayBtns = WEEKDAYS().map(d =>
`<button type="button" class="rrule-day ${parsed.byday.includes(d.value) ? 'rrule-day--active' : ''}"
data-day="${d.value}" aria-label="${d.label}" aria-pressed="${parsed.byday.includes(d.value)}">${d.label}</button>`
).join('');
return `
<div class="rrule-fields" id="${prefix}-rrule-fields">
<div class="form-group">
<label class="label form-label" for="${prefix}-rrule-freq">${t('rrule.labelRepeat')}</label>
<select class="input form-input" id="${prefix}-rrule-freq" style="min-height:44px">
${freqOpts}
</select>
</div>
<div class="rrule-details" id="${prefix}-rrule-details" ${parsed.freq ? '' : 'hidden'}>
<div class="rrule-row">
<div class="form-group" style="margin-bottom:0">
<label class="label form-label" for="${prefix}-rrule-interval">${t('rrule.labelEvery')}</label>
<div class="rrule-interval-wrap">
<input class="input form-input" type="number" id="${prefix}-rrule-interval"
min="1" max="99" value="${parsed.interval}" inputmode="numeric" style="width:64px;text-align:center">
<span class="rrule-interval-unit" id="${prefix}-rrule-unit">${unitLabel(parsed.freq, parsed.interval)}</span>
</div>
</div>
</div>
<div class="rrule-weekdays" id="${prefix}-rrule-weekdays" ${parsed.freq === 'WEEKLY' ? '' : 'hidden'}>
<label class="label form-label">${t('rrule.labelOnDays')}</label>
<div class="rrule-day-grid">${dayBtns}</div>
</div>
<div class="form-group" style="margin-top:var(--space-3)">
<label class="label form-label" for="${prefix}-rrule-until">${t('rrule.labelUntil')}</label>
<input class="input form-input" type="date" id="${prefix}-rrule-until" value="${parsed.until}">
</div>
</div>
</div>
`;
}
function unitLabel(freq, interval) {
const n = interval > 1;
if (freq === 'DAILY') return n ? t('rrule.unitDays') : t('rrule.unitDay');
if (freq === 'WEEKLY') return n ? t('rrule.unitWeeks') : t('rrule.unitWeek');
if (freq === 'MONTHLY') return n ? t('rrule.unitMonths') : t('rrule.unitMonth');
return '';
}
/**
* Bindet Events an die RRULE-Felder (Freq-Change, Day-Toggle, etc.)
* @param {HTMLElement} root - Container-Element
* @param {string} prefix - ID-Prefix
*/
export function bindRRuleEvents(root, prefix) {
const freqSelect = root.querySelector(`#${prefix}-rrule-freq`);
const details = root.querySelector(`#${prefix}-rrule-details`);
const weekdays = root.querySelector(`#${prefix}-rrule-weekdays`);
const unitEl = root.querySelector(`#${prefix}-rrule-unit`);
const intervalEl = root.querySelector(`#${prefix}-rrule-interval`);
if (!freqSelect) return;
freqSelect.addEventListener('change', () => {
const freq = freqSelect.value;
if (details) details.hidden = !freq;
if (weekdays) weekdays.hidden = freq !== 'WEEKLY';
updateUnit();
});
intervalEl?.addEventListener('input', updateUnit);
// Day-Toggle
root.querySelectorAll(`#${prefix}-rrule-weekdays .rrule-day`).forEach(btn => {
btn.addEventListener('click', () => {
btn.classList.toggle('rrule-day--active');
btn.setAttribute('aria-pressed', btn.classList.contains('rrule-day--active'));
});
});
function updateUnit() {
if (!unitEl) return;
const interval = parseInt(intervalEl?.value, 10) || 1;
unitEl.textContent = unitLabel(freqSelect.value, interval);
}
}
/**
* Liest die aktuellen RRULE-Werte aus dem Formular.
* @param {HTMLElement} root - Container-Element
* @param {string} prefix - ID-Prefix
* @returns {{ is_recurring: boolean, recurrence_rule: string|null }}
*/
export function getRRuleValues(root, prefix) {
const freq = root.querySelector(`#${prefix}-rrule-freq`)?.value || '';
const interval = parseInt(root.querySelector(`#${prefix}-rrule-interval`)?.value, 10) || 1;
const until = root.querySelector(`#${prefix}-rrule-until`)?.value || '';
const byday = [];
root.querySelectorAll(`#${prefix}-rrule-weekdays .rrule-day--active`).forEach(btn => {
byday.push(btn.dataset.day);
});
const rule = buildRRule({ freq, interval, byday, until });
return {
is_recurring: !!rule,
recurrence_rule: rule,
};
}