Accumulated work on the dispatch/legacy-writeback branch: - Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard, PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs; hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js. - Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js, sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental migration scripts; LegacySyncPage, SupplierInvoices + negative-billing / terminated-active reports. - Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit, olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers, legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets, ClientDetail updates. - docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
191 lines
5.5 KiB
JavaScript
191 lines
5.5 KiB
JavaScript
/**
|
|
* 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)
|
|
}
|
|
|
|
// ═══ Canonical helpers (consolidation target — see docs/UI_AND_OPTIMIZATION.md §D) ═══
|
|
|
|
/**
|
|
* Email → short agent handle (local part). Canonical replacement for the ~5 private
|
|
* `shortAgent` copies across conversation/board components.
|
|
* @param {string|null} email
|
|
* @returns {string}
|
|
*/
|
|
export function shortAgent (email) {
|
|
return String(email || '').split('@')[0]
|
|
}
|
|
|
|
/**
|
|
* Compact time of day "9h05" (fr). Used by tech list/cards.
|
|
* @param {string|number|Date|null} t
|
|
* @returns {string}
|
|
*/
|
|
export function fmtTimeHHhMM (t) {
|
|
if (!t) return ''
|
|
const d = new Date(t)
|
|
if (isNaN(d.getTime())) return ''
|
|
return `${d.getHours()}h${String(d.getMinutes()).padStart(2, '0')}`
|
|
}
|
|
|
|
/**
|
|
* Relative timestamp: today → "14:32", otherwise → "13/06" (fr-CA).
|
|
* Canonical target for the divergent inbox/board `formatTime` copies.
|
|
* @param {string|number|Date|null} ts
|
|
* @returns {string}
|
|
*/
|
|
export function relTime (ts) {
|
|
if (!ts) return ''
|
|
const dt = new Date(ts)
|
|
if (isNaN(dt.getTime())) return ''
|
|
const now = new Date()
|
|
return dt.toDateString() === now.toDateString()
|
|
? dt.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' })
|
|
: dt.toLocaleDateString('fr-CA', { day: '2-digit', month: '2-digit' })
|
|
}
|
|
|
|
/**
|
|
* Short date+time, e.g. "2026-06-13 14:32" (fr-CA). Accepts ISO or "YYYY-MM-DD HH:MM:SS".
|
|
* Canonical target for the ~6 `fmtTime`/`fmt`/`fmtTs` copies. Returns '' on empty/invalid.
|
|
* @param {string|number|Date|null} d
|
|
* @returns {string}
|
|
*/
|
|
export function formatDateTimeShort (d) {
|
|
if (!d) return ''
|
|
const dt = new Date(String(d).replace(' ', 'T'))
|
|
if (isNaN(dt.getTime())) return ''
|
|
return dt.toLocaleString('fr-CA', { dateStyle: 'short', timeStyle: 'short' })
|
|
}
|