const DateTime = { tz: '', hour12: false, ready: false, listeners: [] }; const DT_FALLBACK_TZ = 'Europe/Moscow'; const DT_CACHE = new Map(); function dtValidTz(tz) { if (!tz || typeof tz !== 'string') return false; try { new Intl.DateTimeFormat('ru-RU', { timeZone: tz }); return true; } catch { return false; } } function dtBrowserTz() { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || DT_FALLBACK_TZ; } catch { return DT_FALLBACK_TZ; } } function dtFmt(opts) { const key = DateTime.tz + '|' + (DateTime.hour12 ? '12' : '24') + '|' + JSON.stringify(opts); let f = DT_CACHE.get(key); if (!f) { f = new Intl.DateTimeFormat('ru-RU', Object.assign({ timeZone: DateTime.tz, hour12: DateTime.hour12 }, opts)); DT_CACHE.set(key, f); } return f; } function dtDate(v) { if (!v) return null; const d = v instanceof Date ? v : new Date(v); return Number.isNaN(d.getTime()) ? null : d; } function fmtDt(v, opts) { const d = dtDate(v); return d ? dtFmt(opts).format(d) : ''; } function dtFmtParts() { return { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }; } function fmtFull(v) { return fmtDt(v, dtFmtParts()); } function fmtDateTime(v) { return fmtDt(v, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } function fmtDateTimeY(v) { return fmtDt(v, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function fmtDateFull(v) { return fmtDt(v, { day: '2-digit', month: '2-digit', year: 'numeric' }); } function fmtDayMonth(v) { return fmtDt(v, { day: '2-digit', month: '2-digit' }); } function fmtDateShort(v) { return fmtDt(v, { day: '2-digit', month: '2-digit', year: '2-digit' }); } function fmtDateLong(v) { return fmtDt(v, { day: 'numeric', month: 'long', year: 'numeric' }); } function fmtTimeOnly(v) { return fmtDt(v, { hour: '2-digit', minute: '2-digit' }); } function dtIsoMatch(v) { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v == null ? '' : v)); return m ? { y: m[1], m: m[2], d: m[3] } : null; } function dtPad(n) { return String(n).padStart(2, '0'); } function fmtDateOnlyIso(v) { const p = dtIsoMatch(v); return p ? `${p.d}.${p.m}.${p.y}` : ''; } function fmtDayMonthIso(v) { const p = dtIsoMatch(v); return p ? `${p.d}.${p.m}` : ''; } function fmtShortIso(v) { const p = dtIsoMatch(v); return p ? `${p.d}.${p.m}.${p.y.slice(2)}` : ''; } function fmtHmStr(v) { const m = /^(\d{1,2}):(\d{2})/.exec(String(v == null ? '' : v)); if (!m) return ''; const h = Number(m[1]); const mm = m[2]; if (!DateTime.hour12) return `${dtPad(h)}:${mm}`; const ap = h < 12 ? 'AM' : 'PM'; const h12 = h % 12 === 0 ? 12 : h % 12; return `${h12}:${mm} ${ap}`; } function fmtDateOnlyLongIso(v) { const p = dtIsoMatch(v); if (!p) return ''; const months = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря']; return `${Number(p.d)} ${months[Number(p.m) - 1] || ''} ${p.y}`; } function dtZoneDate(d) { try { return new Intl.DateTimeFormat('en-CA', { timeZone: DateTime.tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(d); } catch { return ''; } } function todayIso() { return dtZoneDate(new Date()); } function todayDow() { const names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; try { return names.indexOf(new Intl.DateTimeFormat('en-US', { timeZone: DateTime.tz, weekday: 'short' }).format(new Date())); } catch { return new Date().getDay(); } } function nowHm() { try { return new Intl.DateTimeFormat('en-GB', { timeZone: DateTime.tz, hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date()); } catch { return ''; } } function nowYear() { try { return new Intl.DateTimeFormat('en-CA', { timeZone: DateTime.tz, year: 'numeric' }).format(new Date()); } catch { return String(new Date().getFullYear()); } } function isoAddDays(iso, n) { const p = dtIsoMatch(iso); if (!p) return ''; const d = new Date(Date.UTC(Number(p.y), Number(p.m) - 1, Number(p.d))); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); } function dtApply(s) { if (!s || typeof s !== 'object') return; if (dtValidTz(s.timezone)) DateTime.tz = s.timezone; DateTime.hour12 = s.time_format === '12h'; DateTime.listeners.forEach(fn => { try { fn(DateTime); } catch {} }); } async function initDateTime() { DateTime.tz = dtBrowserTz(); DateTime.hour12 = false; try { const res = await fetch('/api/public-settings'); if (res.ok) dtApply(await res.json()); } catch {} DateTime.ready = true; return DateTime; } function onDateTimeChange(fn) { if (typeof fn === 'function') DateTime.listeners.push(fn); }