diff --git a/frontend/modules/logistique/components/WeighingBlock.vue b/frontend/modules/logistique/components/WeighingBlock.vue index 1c93628..d8db47e 100644 --- a/frontend/modules/logistique/components/WeighingBlock.vue +++ b/frontend/modules/logistique/components/WeighingBlock.vue @@ -32,9 +32,10 @@
- - + ({ todayIso: () => '2026-06-22' })) +// `nowIsoDateTime` est importé par le composable : on le stubbe pour un instant déterministe. +vi.mock('~/shared/utils/date', () => ({ nowIsoDateTime: () => '2026-06-22T08:30:00' })) const { useWeighingTicketForm } = await import('../useWeighingTicketForm') describe('useWeighingTicketForm', () => { - it('initialise les 2 blocs à la date du jour (RG-5.07), sans poids ni DSD', () => { + it('initialise les 2 blocs à la date/heure courante (RG-5.07), sans poids ni DSD', () => { const form = useWeighingTicketForm() - expect(form.empty.date).toBe('2026-06-22') - expect(form.full.date).toBe('2026-06-22') + expect(form.empty.date).toBe('2026-06-22T08:30:00') + expect(form.full.date).toBe('2026-06-22T08:30:00') expect(form.empty.weight).toBeNull() expect(form.empty.dsd).toBeNull() expect(form.counterpartyType.value).toBeNull() @@ -25,8 +25,8 @@ describe('useWeighingTicketForm', () => { expect(payload).not.toHaveProperty('counterpartyType') expect(payload).not.toHaveProperty('immatriculation') expect(payload).not.toHaveProperty('emptyWeight') - // Les non-null restent : date du jour + booléen Tout format. - expect(payload.emptyDate).toBe('2026-06-22') + // Les non-null restent : date/heure courante + booléen Tout format. + expect(payload.emptyDate).toBe('2026-06-22T08:30:00') expect(payload.plateFreeFormat).toBe(false) }) @@ -96,9 +96,13 @@ describe('useWeighingTicketForm', () => { }) // ── Application d'une lecture de pesée ──────────────────────────────────── - it('applyReading remplit poids / DSD / mode du bloc visé', () => { + it('applyReading remplit poids / DSD / mode et ré-horodate le bloc à l\'instant de la pesée', () => { const form = useWeighingTicketForm() + // Date périmée (ouverture du formulaire bien avant la pesée). + form.empty.date = '2020-01-01T00:00:00' form.applyReading(form.empty, { weight: 7150, dsd: 1, mode: 'AUTO' }) + // La pesée validée ré-horodate le bloc à maintenant (stub 2026-06-22T08:30:00). + expect(form.empty.date).toBe('2026-06-22T08:30:00') expect(form.empty.weight).toBe(7150) expect(form.empty.dsd).toBe(1) expect(form.empty.mode).toBe('AUTO') @@ -129,7 +133,7 @@ describe('useWeighingTicketForm', () => { }) // ── Pré-remplissage (écran Modification, ERP-190) ───────────────────────── - it('hydrate pré-remplit l\'état depuis le détail (dates ISO ramenées à YYYY-MM-DD)', () => { + it('hydrate pré-remplit l\'état depuis le détail (datetime ISO ramené en local, heure conservée)', () => { const form = useWeighingTicketForm() form.hydrate({ id: 9, @@ -152,9 +156,9 @@ describe('useWeighingTicketForm', () => { expect(form.counterpartyField.value).toBe('client') expect(form.clientIri.value).toBe('/api/clients/629') expect(form.immatriculation.value).toBe('AB-123-CD') - // Date datetime back -> date seule pour MalioDate. - expect(form.empty.date).toBe('2026-06-17') - expect(form.full.date).toBe('2026-06-17') + // Datetime back (avec fuseau) -> local sans fuseau, heure conservée pour MalioDateTime. + expect(form.empty.date).toBe('2026-06-17T09:00:00') + expect(form.full.date).toBe('2026-06-17T09:12:00') expect(form.empty.weight).toBe(7150) expect(form.full.weight).toBe(14300) }) @@ -165,8 +169,8 @@ describe('useWeighingTicketForm', () => { expect(form.otherLabel.value).toBe('Reprise') expect(form.supplierIri.value).toBeNull() expect(form.plateFreeFormat.value).toBe(false) - // Pas de date back -> repli sur le jour (stub 2026-06-22). - expect(form.empty.date).toBe('2026-06-22') + // Pas de date back -> repli sur l'instant courant (stub 2026-06-22T08:30:00). + expect(form.empty.date).toBe('2026-06-22T08:30:00') expect(form.empty.weight).toBeNull() }) diff --git a/frontend/modules/logistique/composables/useWeighingTicketForm.ts b/frontend/modules/logistique/composables/useWeighingTicketForm.ts index ffa9a38..c3a0563 100644 --- a/frontend/modules/logistique/composables/useWeighingTicketForm.ts +++ b/frontend/modules/logistique/composables/useWeighingTicketForm.ts @@ -1,5 +1,5 @@ import { computed, reactive, ref } from 'vue' -import { todayIso } from '~/shared/utils/date' +import { nowIsoDateTime } from '~/shared/utils/date' import type { WeighbridgeMode } from '~/modules/logistique/composables/useWeighbridge' /** @@ -27,7 +27,7 @@ export type CounterpartyType = 'CLIENT' | 'FOURNISSEUR' | 'AUTRE' /** Saisie d'une pesée (bloc vide OU bloc plein). */ export interface WeighingBlockState { - /** Date de la pesée (ISO `YYYY-MM-DD`) — jour par défaut (RG-5.07). */ + /** Date/heure de la pesée (ISO local `YYYY-MM-DDTHH:mm:ss`) — date du jour + heure courante par défaut (RG-5.07). */ date: string | null /** Poids en kg — readonly, rempli par la pesée (bascule ou manuelle). */ weight: number | null @@ -60,9 +60,14 @@ export interface WeighingTicketHydration { fullManualNumber?: string | null } -/** Extrait la partie date `YYYY-MM-DD` d'une chaîne ISO (datetime back) — null si absente. */ -function isoDateOnly(value: string | null | undefined): string | null { - return value ? value.slice(0, 10) : null +/** + * Ramène une chaîne ISO datetime du back (`2026-06-17T09:00:00+02:00`) au format + * local `YYYY-MM-DDTHH:mm:ss` attendu par MalioDateTime (secondes, sans fuseau) : + * on garde les 19 premiers caractères (date + heure), on retire l'offset. Null si + * absente. + */ +function toLocalIsoDateTime(value: string | null | undefined): string | null { + return value ? value.slice(0, 19) : null } /** @@ -78,10 +83,10 @@ function compact(payload: Record): Record { return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== null)) } -/** Crée l'état initial d'un bloc de pesée (date = aujourd'hui, RG-5.07). */ -function emptyBlock(today: string): WeighingBlockState { +/** Crée l'état initial d'un bloc de pesée (date/heure = maintenant, RG-5.07). */ +function emptyBlock(now: string): WeighingBlockState { return { - date: today, + date: now, weight: null, dsd: null, mode: null, @@ -90,7 +95,7 @@ function emptyBlock(today: string): WeighingBlockState { } export function useWeighingTicketForm() { - const today = todayIso() + const now = nowIsoDateTime() // ── Contrepartie (RG-5.03) ─────────────────────────────────────────────── const counterpartyType = ref(null) @@ -116,8 +121,8 @@ export function useWeighingTicketForm() { const plateFreeFormat = ref(false) // ── Les deux pesées ─────────────────────────────────────────────────────── - const empty = reactive(emptyBlock(today)) - const full = reactive(emptyBlock(today)) + const empty = reactive(emptyBlock(now)) + const full = reactive(emptyBlock(now)) // Id du ticket créé (POST du bloc vide) — pilote le PATCH du bloc plein. const ticketId = ref(null) @@ -150,11 +155,17 @@ export function useWeighingTicketForm() { return missing } - /** Applique une lecture de pesée (bascule/manuelle) à un bloc. */ + /** + * Applique une lecture de pesée (bascule/manuelle) à un bloc. La pesée étant + * effectuée À CET INSTANT, on (ré)horodate le bloc à maintenant : la date/heure + * du ticket reflète le moment réel de la pesée validée, pas l'ouverture du + * formulaire (RG-5.07). + */ function applyReading( block: WeighingBlockState, reading: { weight: number, dsd: number, mode: WeighbridgeMode, manualNumber?: string }, ): void { + block.date = nowIsoDateTime() block.weight = reading.weight block.dsd = reading.dsd block.mode = reading.mode @@ -195,7 +206,8 @@ export function useWeighingTicketForm() { * Pré-remplit le formulaire à partir du détail d'un ticket existant (écran * Modification, ERP-190). Le numéro et le site sont immuables (RG-5.09) → * non repris dans l'état éditable (affichés en lecture seule par l'écran). - * Les dates ISO du back (datetime) sont ramenées à `YYYY-MM-DD` pour MalioDate. + * Les dates ISO du back (datetime + fuseau) sont ramenées au format local + * `YYYY-MM-DDTHH:mm:ss` attendu par MalioDateTime (heure conservée). */ function hydrate(detail: WeighingTicketHydration): void { ticketId.value = detail.id @@ -206,13 +218,13 @@ export function useWeighingTicketForm() { immatriculation.value = detail.immatriculation ?? null plateFreeFormat.value = detail.plateFreeFormat ?? false - empty.date = isoDateOnly(detail.emptyDate) ?? today + empty.date = toLocalIsoDateTime(detail.emptyDate) ?? now empty.weight = detail.emptyWeight ?? null empty.dsd = detail.emptyDsd ?? null empty.mode = detail.emptyMode ?? null empty.manualNumber = detail.emptyManualNumber ?? null - full.date = isoDateOnly(detail.fullDate) ?? today + full.date = toLocalIsoDateTime(detail.fullDate) ?? now full.weight = detail.fullWeight ?? null full.dsd = detail.fullDsd ?? null full.mode = detail.fullMode ?? null diff --git a/frontend/modules/logistique/pages/__tests__/weighingTicketEdit.spec.ts b/frontend/modules/logistique/pages/__tests__/weighingTicketEdit.spec.ts index 7a60fc9..c3b3b03 100644 --- a/frontend/modules/logistique/pages/__tests__/weighingTicketEdit.spec.ts +++ b/frontend/modules/logistique/pages/__tests__/weighingTicketEdit.spec.ts @@ -64,7 +64,7 @@ const stubs = { MalioInputText: InputStub, MalioInputNumber: InputStub, MalioSelect: InputStub, - MalioDate: InputStub, + MalioDateTime: InputStub, MalioCheckbox: InputStub, MalioModal: ModalStub, WeighingBlock: BlockStub, diff --git a/frontend/shared/utils/date.ts b/frontend/shared/utils/date.ts index 7123ffa..d75cb73 100644 --- a/frontend/shared/utils/date.ts +++ b/frontend/shared/utils/date.ts @@ -15,3 +15,18 @@ export function todayIso(now: Date = new Date()): string { const day = String(now.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } + +/** + * Date-heure courante au format ISO LOCAL `YYYY-MM-DDTHH:mm:ss` (sans fuseau). + * + * C'est le format attendu par `MalioDateTime` (secondes incluses, pas d'offset + * horaire). Comme `todayIso`, on lit les composantes LOCALES (jamais + * `toISOString()`/UTC) pour ne pas décaler l'heure réelle. Paramètre `now` + * injectable pour les tests. + */ +export function nowIsoDateTime(now: Date = new Date()): string { + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + return `${todayIso(now)}T${hours}:${minutes}:${seconds}` +}