gigafibre-fsm/apps/ops/src/composables/useFormatters.js
louispaulb 0f65c02d83 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
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>
2026-06-15 06:12:12 -04:00

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 (&#039; -> ', &amp; -> &, 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' })
}