diff --git a/apps/ops/src/components/shared/RouteMap.vue b/apps/ops/src/components/shared/RouteMap.vue
index aa6db71..0e22235 100644
--- a/apps/ops/src/components/shared/RouteMap.vue
+++ b/apps/ops/src/components/shared/RouteMap.vue
@@ -12,6 +12,7 @@
import { watch, onMounted, onBeforeUnmount } from 'vue'
import { MAPBOX_TOKEN } from 'src/config/erpnext'
import * as roster from 'src/api/roster'
+import { initials } from 'src/composables/useFormatters' // initiales (source unique, ex-locale dé-dupliquée)
const props = defineProps({
routes: { type: Array, default: () => [] },
@@ -72,7 +73,7 @@ async function realLine (r) { // géométrie routière réelle d'une tournée (O
function clearMarkers () { for (const m of stopMk) m.mk.remove(); for (const m of homeMk) m.mk.remove(); stopMk = []; homeMk = [] }
// ── Positions GPS LIVE des techs (marqueur distinct des arrêts : pastille ronde à initiales + halo pulsé) ──
-function initials (n) { return String(n || '?').trim().split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() }
+// initials → composables/useFormatters (source unique)
function clearLive () { for (const m of liveMk) m.mk.remove(); liveMk = [] }
function clearPins () { for (const m of pinMk) m.mk.remove(); pinMk = [] }
function renderLive (list) {
diff --git a/apps/ops/src/composables/useFormatters.js b/apps/ops/src/composables/useFormatters.js
index 516c4fd..7b16788 100644
--- a/apps/ops/src/composables/useFormatters.js
+++ b/apps/ops/src/composables/useFormatters.js
@@ -197,3 +197,16 @@ export function formatDateTimeShort (d) {
if (isNaN(dt.getTime())) return ''
return dt.toLocaleString('fr-CA', { dateStyle: 'short', timeStyle: 'short' })
}
+
+// ── Formateurs canoniques dé-dupliqués (audit T1 2026-07-04) — remplacent des copies locales IDENTIQUES ──
+// Initiales = 2 PREMIERS mots (« Jean Bourdon » → « JB »), '?' si vide. ⚠ DIFFÉRENT de staffInitials (1er + DERNIER mot).
+// Version « 2 premiers » = celle de la grille Planif + carte des tournées.
+export function initials (name) {
+ return String(name || '').split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join('').toUpperCase() || '?'
+}
+// Heure d'horloge : chaîne « 8:30 » → « 8h30 ». ⚠ DIFFÉRENT de fmtTimeHHhMM (qui prend un Date).
+export function fmtClock (t) { if (!t) return ''; const [h, m] = String(t).split(':'); return `${h}h${m}` }
+// Date « longue » : « 3 juillet 2026 » ; '' si absent.
+export function formatDateLong (d) {
+ return d ? new Date(d).toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' }) : ''
+}
diff --git a/apps/ops/src/modules/tech/components/JobCard.vue b/apps/ops/src/modules/tech/components/JobCard.vue
index d052ca7..3dc99c3 100644
--- a/apps/ops/src/modules/tech/components/JobCard.vue
+++ b/apps/ops/src/modules/tech/components/JobCard.vue
@@ -36,6 +36,7 @@