'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 ``
}
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,
}