stage 1
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user