// Generic helpers shared by the main SPA (app.js) and standalone pages // (login_dda.html). Date-picker enhancement is the only DOM init here and // is a no-op when no .dda-date-input fields exist. const API = ''; function escapeHtml(text) { if (text == null) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function formatApiError(detail) { if (!detail) return null; if (typeof detail === 'string') return detail; if (Array.isArray(detail)) { return detail.map((d) => (d && d.msg) || JSON.stringify(d)).join('; '); } return String(detail); } async function ddaApi(method, path, options = {}) { const headers = { ...options.headers }; if (options.body && !(options.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } const res = await fetch(API + path, { method, headers, credentials: 'include', ...options }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (_) {} if (!res.ok) throw new Error(formatApiError(data?.detail) || res.statusText || 'Request failed'); return data; } function unwrapImagePage(data) { if (Array.isArray(data)) { return { items: data, total: data.length, offset: 0, limit: data.length }; } const items = (data && data.items) || []; return { items, total: Number(data && data.total != null ? data.total : items.length), offset: Number((data && data.offset) || 0), limit: Number((data && data.limit) || items.length), }; } function deferIdle(fn, timeoutMs) { const run = () => { try { fn(); } catch (_) { /* page teardown */ } }; if (typeof requestIdleCallback === 'function') { requestIdleCallback(run, { timeout: timeoutMs || 1200 }); } else { setTimeout(run, 0); } } function showDdaError(msg) { const el = document.getElementById('dda-error'); if (!el) return; el.textContent = msg; el.classList.remove('hidden'); } function hideDdaError() { document.getElementById('dda-error')?.classList.add('hidden'); } function showDdaSuccess(msg) { const el = document.getElementById('dda-success'); if (!el) return; el.textContent = msg; el.classList.remove('hidden'); setTimeout(() => el.classList.add('hidden'), 4000); } function formatBytes(n) { if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(1) + ' GB'; if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB'; return (n / 1024).toFixed(0) + ' KB'; } // --- Dynamic role permissions (drives show/hide/disable in the UI so it // always matches whatever's set in the Roles & Users permission matrix, // without hardcoding any role name in a template or JS file). --- let _ddaPermissionsPromise = null; /** Fetch (once, cached for the page's lifetime) the current user's * per-module permissions: { moduleKey: { view, create, edit, delete } }. * Call this during page init, before hasPermission()/applyPermissionGating() * are used. Resolves to {} (nothing permitted) if the request fails, rather * than throwing — a permission check should fail closed, not break the page. */ function loadMyPermissions() { if (!_ddaPermissionsPromise) { _ddaPermissionsPromise = ddaApi('GET', '/api/dda/rbac/me/permissions').catch(() => ({})); } return _ddaPermissionsPromise; } /** Sync check against an already-resolved permissions map (from * loadMyPermissions()). */ function hasPermission(permissions, moduleKey, action) { return !!(permissions && permissions[moduleKey] && permissions[moduleKey][action]); } /** Declaratively hides/disables every element carrying * data-requires-permission="moduleKey:action" based on the resolved * permissions map, e.g. `; } const yearStart = Math.min(1990, today.getFullYear() - 40); const yearEnd = today.getFullYear() + 2; let years = ''; for (let y = yearEnd; y >= yearStart; y--) { years += ``; } const months = _DATE_MONTHS.map((name, i) => `` ).join(''); pop.innerHTML = `
${_DATE_DOW.map((d) => `${d}`).join('')}
${cells}
`; positionCal(); } function openCal() { const cur = _parseIsoDate(input.value); view = new Date((cur || new Date()).getFullYear(), (cur || new Date()).getMonth(), 1); _closeAllDatePickers(wrap); wrap.classList.add('is-open'); renderCal(); } function setValue(iso) { input.value = iso || ''; input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); syncTrigger(); wrap.classList.remove('is-open'); } function shiftMonth(delta) { view = new Date(view.getFullYear(), view.getMonth() + delta, 1); renderCal(); } trigger.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (e.target.closest('.dda-date-clear')) { setValue(''); return; } if (wrap.classList.contains('is-open')) wrap.classList.remove('is-open'); else openCal(); }); wrap.addEventListener('pointerdown', (e) => e.stopPropagation()); wrap.addEventListener('click', (e) => e.stopPropagation()); pop.addEventListener('pointerdown', (e) => e.stopPropagation()); pop.addEventListener('click', (e) => { e.stopPropagation(); const nav = e.target.closest('[data-cal-nav]'); if (nav) { e.preventDefault(); shiftMonth(Number(nav.dataset.calNav)); return; } if (e.target.closest('[data-cal-today]')) { setValue(_fmtIsoDate(new Date())); return; } const day = e.target.closest('[data-date]'); if (day) setValue(day.dataset.date); }); pop.addEventListener('change', (e) => { const monthSel = pop.querySelector('[data-cal-month]'); const yearSel = pop.querySelector('[data-cal-year]'); if (e.target === monthSel || e.target === yearSel) { view = new Date(Number(yearSel.value), Number(monthSel.value), 1); renderCal(); } }); input.addEventListener('change', syncTrigger); syncTrigger(); } function initDatePickers(root = document) { root.querySelectorAll('input.dda-date-input[type="date"]').forEach(enhanceDateInput); } document.addEventListener('pointerdown', (e) => { if (!e.target.closest('.dda-datewrap')) _closeAllDatePickers(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') _closeAllDatePickers(); }); window.addEventListener('resize', () => { document.querySelectorAll('.dda-datewrap.is-open .dda-cal').forEach((pop) => { const wrap = pop.closest('.dda-datewrap'); const trigger = wrap?.querySelector('.dda-date-trigger'); if (!trigger) return; const r = trigger.getBoundingClientRect(); const width = 280; pop.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - width - 8))}px`; pop.style.top = `${r.bottom + 6}px`; }); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => initDatePickers()); } else { initDatePickers(); } window.initDatePickers = initDatePickers; window.enhanceDateInput = enhanceDateInput;