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:
parent
112ed0ea2b
commit
1017cab649
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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' }) : ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user