refactor(ui): T1 audit — dédup formatters → useFormatters (0 changement d'affichage)

3 formateurs canoniques ajoutés (initials [2 premiers mots, '?'], fmtClock [«8:30»→«8h30»], formatDateLong)
+ remplacement des copies locales IDENTIQUES (vérifiées à l'impl) :
- initials : PlanificationPage + RouteMap
- fmtTime → fmtClock : JobCard + TechTasksPage
- formatDate → formatDateLong : ReportRevenueExplorer + ReportInternetCher
Les VRAIES variantes laissées telles quelles (staffInitials 1er+dernier ; fmtDate string ; fmtDur RendezVous ;
fmtMoney multi-devise ; formatDate Settings/ConversationPanel/Gifts date+heure). Build vert, leak 0.
Première tranche de l'audit workflow 26 pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-04 18:31:18 -04:00
parent 112ed0ea2b
commit 1017cab649
7 changed files with 25 additions and 21 deletions

View File

@ -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) {

View File

@ -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' }) : ''
}

View File

@ -36,6 +36,7 @@
<script setup>
import { computed } from 'vue'
import { fmtClock as fmtTime } from 'src/composables/useFormatters' // « 8:30 »« 8h30 » (source unique)
const props = defineProps({
job: { type: Object, required: true },
@ -66,11 +67,7 @@ const chipLabel = computed(() => {
return 'À venir'
})
function fmtTime (t) {
if (!t) return ''
const [h, m] = t.split(':')
return `${h}h${m}`
}
// fmtTime composables/useFormatters (fmtClock)
</script>
<style lang="scss" scoped>

View File

@ -249,6 +249,7 @@ import { useRouter } from 'vue-router'
import { Notify } from 'quasar'
import { BASE_URL } from 'src/config/erpnext'
import { HUB_URL } from 'src/config/hub'
import { fmtClock as fmtTime } from 'src/composables/useFormatters' // « 8:30 »« 8h30 » (source unique)
import JobCard from '../components/JobCard.vue'
const props = defineProps({
@ -334,11 +335,7 @@ function canTerminateDirect (j) {
return import.meta.env.DEV && j.status && j.status !== 'Completed' && j.status !== 'Cancelled'
}
function fmtTime (t) {
if (!t) return ''
const [h, m] = t.split(':')
return `${h}h${m}`
}
// fmtTime composables/useFormatters (fmtClock)
function fmtDate (iso) {
if (!iso) return ''
try {

View File

@ -1817,7 +1817,7 @@ import { useSSE, sendSmsViaHub } from 'src/composables/useSSE'
import { MAPBOX_TOKEN } from 'src/config/erpnext' // routage routier réel (API Mapbox Matrix), déjà utilisé par le Dispatch
import { legacyDeptColor, heatColor } from 'src/composables/useHelpers' // coloriage par type « comme legacy » (partagé) + heatColor (source unique, aussi <OccupancyStrip>)
import { useUserPrefs } from 'src/composables/useUserPrefs' // préférences d'affichage par utilisateur (serveur)
import { relTime } from 'src/composables/useFormatters' // temps relatif COHÉRENT (même formateur que la Boîte)
import { relTime, initials } from 'src/composables/useFormatters' // temps relatif + initiales (source unique, ex-locales dé-dupliquées)
import { messageIdentity, priorityMeta, PRIORITY_LEVELS } from 'src/composables/useConversationDisplay' // resolvers partagés + priorité (drapeau) réutilisée des conversations
import { useSla, SLA_BADGE } from 'src/composables/useSla' // SLA existant (politiques éditables) réutilisé sur les jobs (échéance = création + résolution)
import { useAuthStore } from 'src/stores/auth' // email de l'agent (X-Authentik-Email) pour l'envoi de réponses client
@ -4875,7 +4875,7 @@ const poolJobs = computed(() => {
})
// Comptes par secteur (municipalité) pour les en-têtes du regroupement « Secteur ».
const poolSectorCounts = computed(() => { const m = {}; for (const j of poolJobs.value) { const k = jobCity(j) || 'Sans secteur'; const e = m[k] || (m[k] = { n: 0, mins: 0 }); e.n++; e.mins += (+j.est_min || (+j.duration_h || 1) * 60) } return m })
function initials (name) { return String(name || '').split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join('').toUpperCase() || '?' }
// initials composables/useFormatters (source unique ; 2 premiers mots · '?')
function toggleSel (j) {
if (j.status === 'On Hold') { $q.notify({ type: 'warning', message: 'En attente d\'une tâche précédente — non assignable', timeout: 2500 }); return }
if (selectedJobs[j.name]) delete selectedJobs[j.name]; else selectedJobs[j.name] = true

View File

@ -196,7 +196,7 @@ import { ref, computed } from 'vue'
import { useQuasar, copyToClipboard } from 'quasar'
import { fetchOverpricedInternet, overpricedInternetCsvUrl, lookupServiceabilityBatch } from 'src/api/reports'
import DataTable from 'src/components/shared/DataTable.vue'
import { formatMoney } from 'src/composables/useFormatters'
import { formatMoney, formatDateLong as formatDate } from 'src/composables/useFormatters'
const $q = useQuasar()
const threshold = ref(90)
@ -256,9 +256,7 @@ const isStale = computed(() => daysOld.value > 7)
const staleClass = computed(() => isStale.value ? 'bg-orange-2 text-warning' : 'bg-blue-1 text-info')
// formatMoney unifié via useFormatters (import ci-dessus) même format fr-CA
function formatDate (iso) {
return iso ? new Date(iso).toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' }) : ''
}
// formatDate composables/useFormatters (formatDateLong, source unique)
function shorten (s) {
if (!s) return ''
return s.length > 48 ? s.slice(0, 48) + '…' : s

View File

@ -137,7 +137,7 @@ import { ref, computed, watch, onMounted } from 'vue'
import { useQuasar } from 'quasar'
import { fetchRevenueExplorer, revenueExplorerCsvUrl } from 'src/api/reports'
import DataTable from 'src/components/shared/DataTable.vue'
import { formatMoney } from 'src/composables/useFormatters'
import { formatMoney, formatDateLong as formatDate } from 'src/composables/useFormatters'
const $q = useQuasar()
@ -207,9 +207,7 @@ function sharePct (net) {
const daysOld = computed(() => dataAsOf.value ? Math.floor((Date.now() - new Date(dataAsOf.value)) / 86400000) : 0)
const isStale = computed(() => daysOld.value > 7)
const staleClass = computed(() => isStale.value ? 'bg-orange-2 text-warning' : 'bg-blue-1 text-info')
function formatDate (iso) {
return iso ? new Date(iso).toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' }) : ''
}
// formatDate composables/useFormatters (formatDateLong, source unique)
async function loadReport () {
loading.value = true