refactor(front) : formatDateFr mutualisé dans shared/utils/date + rendu déterministe (ERP-191)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m33s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 5m19s

Trois copies identiques de formatDateFr (logistique weighingTicketFormat,
transport carriers/index, CarrierQualimatTab) fusionnées en un seul helper
partagé. La nouvelle version lit la date directement dans la chaîne ISO (10
premiers caractères) au lieu de new Date(value).getDate() : un datetime porteur
d'un offset (…+02:00, …Z) ne bascule plus d'un jour selon le fuseau du
navigateur / runner CI, et reste cohérent avec l'écran d'édition (slice) et
l'export serveur (format d/m/Y).

weighingTicketFormat ré-exporte le helper (imports inchangés côté écrans).
Tests de déterminisme fuseau ajoutés dans shared/utils/date.test.ts.
This commit is contained in:
2026-06-24 16:22:28 +02:00
parent 9b476f22bb
commit 87513d130a
5 changed files with 50 additions and 45 deletions
@@ -5,23 +5,9 @@
* mettre en forme la valeur déjà normalisée renvoyée par l'API.
*/
/**
* Date courte française `JJ-MM-AAAA` (spec M5). Chaîne vide si la valeur est
* absente ou invalide. Lit les composantes locales (cohérent avec l'affichage
* des autres répertoires M1→M4).
*/
export function formatDateFr(value: string | null | undefined): string {
if (!value) {
return ''
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${day}-${month}-${date.getFullYear()}`
}
// Date courte française `JJ-MM-AAAA` (spec M5) : helper partagé inter-modules
// (mutualisé avec les répertoires M1→M4). Re-exporté ici pour les écrans M5.
export { formatDateFr } from '~/shared/utils/date'
/**
* Poids en kg avec séparateur de milliers (espace) + suffixe « Kg »
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { debounce } from '~/shared/utils/debounce'
import { formatDateFr } from '~/shared/utils/date'
import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
/**
@@ -92,19 +93,6 @@ function isExpired(value: string): boolean {
return date.getTime() < today.getTime()
}
/** Format court français JJ-MM-AAAA (chaîne vide si date absente / invalide). */
function formatDateFr(value: string | null | undefined): string {
if (!value) {
return ''
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${day}-${month}-${date.getFullYear()}`
}
// ── Confirmation d'intégration ───────────────────────────────────────────────
const confirmOpen = ref(false)
@@ -141,6 +141,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { formatDateFr } from '~/shared/utils/date'
interface FilterOption {
value: string
@@ -235,20 +236,6 @@ function isValidityExpired(item: Record<string, unknown>): boolean {
return date.getTime() < today.getTime()
}
/** Format court francais JJ-MM-AAAA (spec M4). Chaine vide si date absente / invalide. */
function formatDateFr(value: string | null | undefined): string {
if (!value) {
return ''
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${day}-${month}-${date.getFullYear()}`
}
/** Clic sur une ligne → ecran Consultation (route a plat /carriers/{id}). */
function onRowClick(item: Record<string, unknown>): void {
router.push(`/carriers/${item.id}`)
+26 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { todayIso } from '../date'
import { formatDateFr, todayIso } from '../date'
describe('todayIso', () => {
it('formate la date locale en YYYY-MM-DD (zero-pad mois/jour)', () => {
@@ -17,3 +17,28 @@ describe('todayIso', () => {
expect(todayIso(new Date(2026, 11, 31, 12, 0))).toBe('2026-12-31')
})
})
describe('formatDateFr', () => {
it('formate un datetime ISO avec offset en JJ-MM-AAAA', () => {
expect(formatDateFr('2026-06-17T09:12:00+02:00')).toBe('17-06-2026')
})
it('lit la date dans la CHAINE, sans decalage de fuseau (deterministe)', () => {
// Minuit UTC : une lecture via new Date().getDate() basculerait au 4 dans un
// fuseau negatif (ex. America). On lit la chaine -> reste le 05 partout.
expect(formatDateFr('2026-01-05T00:00:00Z')).toBe('05-01-2026')
// Idem juste avant minuit avec offset +02:00 : la date affichee est celle
// portee par la chaine (17), pas le 16 d'un runtime UTC.
expect(formatDateFr('2026-06-17T00:30:00+02:00')).toBe('17-06-2026')
})
it('accepte une date nue YYYY-MM-DD', () => {
expect(formatDateFr('2026-03-07')).toBe('07-03-2026')
})
it('renvoie une chaine vide pour une valeur absente ou non ISO', () => {
expect(formatDateFr(null)).toBe('')
expect(formatDateFr(undefined)).toBe('')
expect(formatDateFr('pas-une-date')).toBe('')
})
})
+19
View File
@@ -30,3 +30,22 @@ export function nowIsoDateTime(now: Date = new Date()): string {
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${todayIso(now)}T${hours}:${minutes}:${seconds}`
}
/**
* Date courte française `JJ-MM-AAAA` à partir d'une valeur ISO (`YYYY-MM-DD` ou
* datetime `YYYY-MM-DDTHH:mm:ss±HH:mm`). Chaîne vide si absente ou non ISO.
*
* On lit les composantes DIRECTEMENT dans la chaîne (10 premiers caractères) au
* lieu de `new Date(value).getDate()` : un datetime porteur d'un offset (ex.
* `…T00:30:00+02:00`, ou `…Z`) basculerait d'un jour selon le fuseau du
* navigateur / du runner CI. Rendu ainsi déterministe et cohérent avec l'écran
* d'édition (slice de la chaîne brute) et l'export serveur (`format('d/m/Y')`).
*/
export function formatDateFr(value: string | null | undefined): string {
const match = value ? /^(\d{4})-(\d{2})-(\d{2})/.exec(value) : null
if (!match) {
return ''
}
const [, year, month, day] = match
return `${day}-${month}-${year}`
}