/** * Shared formatting utilities for the ops app. * Used in ClientDetailPage, TicketsPage, and future pages. */ const ERP_BASE = 'https://erp.gigafibre.ca' /** * Format a date string to fr-CA short format (e.g. "30 mars 2026") * @param {string|null} d - Date string (ISO or YYYY-MM-DD) * @returns {string} */ export function formatDate (d) { if (!d) return '—' return new Date(d).toLocaleDateString('fr-CA', { year: 'numeric', month: 'short', day: 'numeric' }) } /** * Format a date string to DD/MM/YYYY * @param {string|null} d - Date string * @returns {string} */ export function formatDateShort (d) { if (!d) return '—' const str = String(d).slice(0, 10) if (str.length < 10) return str const [y, m, day] = str.split('-') return `${day}/${m}/${y}` } /** * Format a number as CAD currency * @param {number} v * @returns {string} */ export function formatMoney (v) { return (v || 0).toLocaleString('fr-CA', { style: 'currency', currency: 'CAD' }) } /** * Build a link to the ERPNext desk for a doctype + name * @param {string} doctype - e.g. 'Sales Invoice', 'Issue' * @param {string} name - Document name/ID * @returns {string} */ export function erpLink (doctype, name) { if (!doctype || !name) return '#' return ERP_BASE + '/app/' + doctype.toLowerCase().replace(/ /g, '-') + '/' + encodeURIComponent(name) } /** * Build a full URL for an ERPNext file attachment * @param {string} url - Relative or absolute file URL * @returns {string} */ export function erpFileUrl (url) { if (!url) return '#' if (url.startsWith('http')) return url return ERP_BASE + url } export function formatDateTime (dt) { if (!dt) return '' const d = new Date(dt) if (isNaN(d)) return dt return d.toLocaleDateString('fr-CA', { year: 'numeric', month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' }) } // ═══ Staff avatar helpers ═══ const AVATAR_COLORS = ['#4f46e5','#0891b2','#059669','#d97706','#dc2626','#7c3aed','#be185d','#0d9488','#6366f1','#ea580c'] /** * Generate a consistent color from a name string (for avatar backgrounds). * @param {string|null} name * @returns {string} Hex color */ export function staffColor (name) { if (!name) return '#9e9e9e' let h = 0 for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h) return AVATAR_COLORS[Math.abs(h) % AVATAR_COLORS.length] } /** * Extract initials from a full name string. * @param {string|null} name * @returns {string} */ export function staffInitials (name) { if (!name) return '' const parts = name.trim().split(/\s+/) if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() return parts[0].substring(0, 2).toUpperCase() } /** * Decode HTML entities (' -> ', & -> &, etc.) * @param {string|null} str * @returns {string} */ export function decodeHtml (str) { if (!str) return str const el = document.createElement('textarea') el.innerHTML = str return el.value } /** * Extract display name from email (user@domain -> User) * @param {string|null} email * @returns {string} */ export function noteAuthorName (email) { if (!email) return 'Système' const local = email.split('@')[0] return local.charAt(0).toUpperCase() + local.slice(1).replace(/[._-]/g, ' ') } /** * Relative time display for notes/comments. * @param {string|null} d - Date/datetime string * @returns {string} */ export function noteTimeAgo (d) { if (!d) return '' const diff = Date.now() - new Date(d).getTime() const mins = Math.floor(diff / 60000) if (mins < 1) return "À l'instant" if (mins < 60) return `Il y a ${mins} min` const hours = Math.floor(mins / 60) if (hours < 24) return `Il y a ${hours}h` const days = Math.floor(hours / 24) if (days < 7) return `Il y a ${days}j` return formatDate(d) }