// Small time/date helpers shared by popup, dashboard and planner. // Times of day are handled as "minutes since midnight", durations as minutes. var U = (() => { const pad = (n) => String(n).padStart(2, '0'); const toMin = (hhmm) => { const m = /^(\d{1,2})[:.h](\d{2})$/.exec(String(hhmm || '').trim()); return m ? +m[1] * 60 + +m[2] : null; }; const fmtTime = (min) => `${pad(Math.floor(min / 60))}:${pad(min % 60)}`; const fmtDur = (min) => { const sign = min < 0 ? '-' : ''; min = Math.abs(Math.round(min)); return `${sign}${Math.floor(min / 60)}:${pad(min % 60)}`; }; // "1", "1.5", "1,5", "1:30", "90m", "1h30" -> minutes. Empty -> null. const parseHours = (s) => { s = String(s ?? '').trim().toLowerCase(); if (!s) return null; let m; if ((m = /^(\d+)\s*m(in)?$/.exec(s))) return +m[1]; if ((m = /^(\d+)\s*[:h]\s*(\d{1,2})?\s*m?(in)?$/.exec(s))) return +m[1] * 60 + +(m[2] || 0); if ((m = /^(\d+(?:[.,]\d+)?)$/.exec(s))) return Math.round(parseFloat(m[1].replace(',', '.')) * 60); return null; }; const parseDate = (iso) => { const [y, m, d] = iso.split('-').map(Number); return new Date(y, m - 1, d); }; const isoDate = (dt) => `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`; const addDays = (iso, n) => { const dt = parseDate(iso); dt.setDate(dt.getDate() + n); return isoDate(dt); }; // ISO weekday: 1 = Monday ... 7 = Sunday const weekday = (iso) => parseDate(iso).getDay() || 7; const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const weekdayName = (iso) => WEEKDAYS[weekday(iso) - 1]; // Monday of the week containing iso const monday = (iso) => addDays(iso, 1 - weekday(iso)); const weeksBetween = (a, b) => Math.round((parseDate(monday(b)) - parseDate(monday(a))) / (7 * 864e5)); // Kimai wants HTML5 local datetime: 2026-10-07T08:00:00 const kimaiDateTime = (iso, min) => `${iso}T${fmtTime(min)}:00`; const uid = () => Math.random().toString(36).slice(2, 10); /* Swiss display: 01/09/2026, 13:30. Internally always ISO dates + HH:MM. */ const fmtDate = (iso) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : ''); // "1/9/2026", "01.09.2026", "01-09-26", "2026-09-01" -> "2026-09-01" (null if invalid) const parseUserDate = (s) => { s = String(s || '').trim(); let m, y, mo, d; if ((m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s))) [, y, mo, d] = m; else if ((m = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4}|\d{2})$/.exec(s))) [, d, mo, y] = m; else return null; if (y.length === 2) y = '20' + y; const dt = new Date(+y, +mo - 1, +d); return dt.getMonth() === +mo - 1 && dt.getDate() === +d ? isoDate(dt) : null; }; const fmtDateTime = (dt) => `${fmtDate(isoDate(dt))} ${fmtTime(dt.getHours() * 60 + dt.getMinutes())}`; // "930", "9:30", "9.30", "9h30", "9" -> "09:30" (null if invalid) const parseUserTime = (s) => { s = String(s || '').trim(); let m; if ((m = /^(\d{1,2})$/.exec(s))) m = [s, m[1], '0']; else if (!(m = /^(\d{1,2})[:.h]?(\d{2})$/.exec(s))) return null; const min = +m[1] * 60 + +m[2]; return +m[1] < 24 && +m[2] < 60 ? fmtTime(min) : null; }; // Text inputs with data-kind="date" | "time": read/write ISO / HH:MM, show Swiss format const getField = (el) => (el.dataset.kind === 'date' ? parseUserDate(el.value) : parseUserTime(el.value)) || ''; const setField = (el, v) => { el.value = el.dataset.kind === 'date' ? fmtDate(v) : v || ''; }; const setupFields = (root = document) => { root.querySelectorAll('input[data-kind]').forEach((el) => { el.placeholder ||= el.dataset.kind === 'date' ? 'dd/mm/yyyy' : 'hh:mm'; el.size = el.dataset.kind === 'date' ? 10 : 5; el.addEventListener('blur', () => { const v = getField(el); el.classList.toggle('invalid', !!el.value.trim() && !v); if (v) setField(el, v); }); }); }; return { pad, toMin, fmtTime, fmtDur, parseHours, parseDate, isoDate, addDays, weekday, weekdayName, WEEKDAYS, monday, weeksBetween, kimaiDateTime, uid, fmtDate, parseUserDate, fmtDateTime, parseUserTime, getField, setField, setupFields, }; })();