'use strict' // ───────────────────────────────────────────────────────────────────────────── // Server-side HTML fragment builders for magic-link pages. // Pure functions returning strings. Pairs with ui/design.css classes. // ───────────────────────────────────────────────────────────────────────────── function esc (s) { return (s == null ? '' : String(s)) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, ''') } // Safely inline JSON in a + --> breakouts) function jsonScript (value) { return JSON.stringify(value).replace(//g, '--\\u003e') } // ── Canonical status metadata ───────────────────────────────────────────── // One source of truth for FR labels + colors. Both tech mobile and detail // views read from this so a status rename propagates everywhere. const STATUS_META = { Scheduled: { label: 'Planifié', color: '#818cf8' }, assigned: { label: 'Assigné', color: '#818cf8' }, open: { label: 'Ouvert', color: '#818cf8' }, 'In Progress': { label: 'En cours', color: '#f59e0b' }, in_progress: { label: 'En cours', color: '#f59e0b' }, Completed: { label: 'Terminé', color: '#22c55e' }, Cancelled: { label: 'Annulé', color: '#94a3b8' }, } function statusMeta (s) { return STATUS_META[s] || { label: s || '—', color: '#94a3b8' } } function badge (status) { const m = statusMeta(status) return `${esc(m.label)}` } // ── Section with count pill ─────────────────────────────────────────────── // title: human heading, e.g. "En retard" // items: array of pre-rendered HTML strings (from jobCard() or similar) // modifier: 'danger' | '' — adds .sec.danger for red accent function section (title, items, modifier = '') { if (!items.length) return '' const mod = modifier ? ' ' + modifier : '' return `
${esc(title)} ${items.length}
${items.join('')}` } // ── Primitive wrappers ──────────────────────────────────────────────────── function card (inner, { onclick, extraClass = '', style = '' } = {}) { const cls = 'card' + (extraClass ? ' ' + extraClass : '') const click = onclick ? ` onclick="${onclick}"` : '' const st = style ? ` style="${style}"` : '' return `
${inner}
` } function panel (title, bodyHtml, { extra = '' } = {}) { return `

${esc(title)}${extra ? ` ${extra}` : ''}

${bodyHtml}
` } function emptyState (emoji, message) { return `
${esc(emoji)}

${message}

` } function placeholder (emoji, title, body) { return `
${esc(emoji)}

${esc(title)}

${body}

` } function button (label, { kind = 'pri', onclick, id, block = false, extraClass = '', disabled = false, style = '' } = {}) { const cls = ['btn', 'btn-' + kind, block ? 'btn-block' : '', extraClass].filter(Boolean).join(' ') return `` } // ── Stat row (inside header) ────────────────────────────────────────────── // items: [{ value, label, onclick? }] function statRow (items) { return `
${items.map(it => { const click = it.onclick ? ` onclick="${it.onclick}"` : '' const id = it.id ? ` id="${it.id}"` : '' return `
${esc(it.value)}${esc(it.label)}
` }).join('')}
` } // ── Tab bar ─────────────────────────────────────────────────────────────── // tabs: [{ id, label, icon, active? }] function tabBar (tabs) { return `
${tabs.map(t => { const cls = 'tab' + (t.active ? ' on' : '') return `` }).join('')}
` } // ── Date / time helpers ─────────────────────────────────────────────────── function fmtTime (t) { if (!t) return '' const [h, m] = String(t).split(':') return `${h}h${m || '00'}` } function fmtDate (d) { if (!d) return '' try { return new Date(d + 'T00:00:00').toLocaleDateString('fr-CA', { weekday: 'short', day: 'numeric', month: 'short' }) } catch { return d } } // "Aujourd'hui" / "Hier" / "Demain" / weekday / date function dateLabelFr (d, today) { if (!d) return 'Sans date' if (d === today) return "Aujourd'hui" try { const diff = Math.round((new Date(d + 'T00:00:00') - new Date(today + 'T00:00:00')) / 86400000) if (diff === -1) return 'Hier' if (diff === 1) return 'Demain' if (diff > 1 && diff <= 6) return new Date(d + 'T00:00:00').toLocaleDateString('fr-CA', { weekday: 'long' }) return fmtDate(d) } catch { return fmtDate(d) } } function todayFr () { return new Date().toLocaleDateString('fr-CA', { weekday: 'long', day: 'numeric', month: 'long' }) } // YYYY-MM-DD in America/Montreal regardless of server TZ function montrealDate (d = new Date()) { return d.toLocaleDateString('en-CA', { timeZone: 'America/Montreal', year: 'numeric', month: '2-digit', day: '2-digit' }) } module.exports = { esc, jsonScript, STATUS_META, statusMeta, badge, section, card, panel, emptyState, placeholder, button, statRow, tabBar, fmtTime, fmtDate, dateLabelFr, todayFr, montrealDate, }