/* * Utility functions used across multiple frontend modules. These are pure * functions with no side effects and no dependency on application state. */ /* ── Formatting ─────────────────────────────────────────────────────────── */ export function prettifyStatus(status) { return status .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } export function formatDateTime(value) { return new Date(value).toLocaleString(); } export function formatTime(value) { return new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } export function formatRelativeTime(value) { const diffMs = Date.now() - new Date(value).getTime(); const diffMinutes = Math.round(diffMs / 60000); if (diffMinutes < 1) { return 'just now'; } if (diffMinutes < 60) { return `${diffMinutes} minute(s) ago`; } const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) { return `${diffHours} hour(s) ago`; } return `${Math.round(diffHours / 24)} day(s) ago`; } export function formatFileSize(bytes) { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /* ── Naming & Sanitization ──────────────────────────────────────────────── */ export function sanitizeForFilename(value) { return String(value || 'report') .trim() .replace(/\s+/g, '-') .replace(/[^a-zA-Z0-9-_]/g, '') .slice(0, 40); } export function generateReportNumber() { const now = new Date(); const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String( now.getDate() ).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String( now.getMinutes() ).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; return `POC-${stamp}`; } export function buildGeneratedFilename(report, field, sequence, extension) { const reportNumber = sanitizeForFilename(report.answers.reportNumber || report.reportNumber); const sectionCode = sanitizeForFilename(field.id).slice(0, 10).toUpperCase(); return `${reportNumber}_${sectionCode}_${String(sequence).padStart(3, '0')}.${extension}`; } /* ── Badge helpers ──────────────────────────────────────────────────────── */ export function badgeClassForTone(tone) { if (tone === 'success') { return 'badge-online'; } if (tone === 'error') { return 'badge-error'; } if (tone === 'warning') { return 'badge-offline'; } return 'badge-neutral'; } /* ── Template helpers ───────────────────────────────────────────────────── */ export function makeTemplateKey(code, version) { return `${code}::${version}`; } /* * Multiple versions of the same template can exist in local cache because old * drafts remain bound to the version they started with. The catalog therefore * picks the newest version per template code for the creation UI while keeping * older records available in the version lookup map. */ export function deriveTemplateCatalog(templateRows) { const byCode = new Map(); for (const row of templateRows) { const existing = byCode.get(row.code); const shouldReplace = !existing || Number(row.version) > Number(existing.version); if (shouldReplace) { byCode.set(row.code, row); } } return Array.from(byCode.values()) .sort((left, right) => left.name.localeCompare(right.name)) .map((item) => ({ code: item.code, name: item.name, description: item.description, activeVersion: item.version, publishedAt: item.publishedAt })); } /* ── General ────────────────────────────────────────────────────────────── */ /* * Debounce helper. Returns a wrapper that delays invocation until `ms` * milliseconds of inactivity have passed, preventing expensive operations * (such as a full form re-render) from running on every keystroke. */ export function debounce(fn, ms) { let timer = null; return function debounced(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), ms); }; }