Files
CLProject/public/js/utils.js
T
2026-04-19 21:14:16 +02:00

151 lines
4.6 KiB
JavaScript

/*
* 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);
};
}