fix(front) : pesée horodatée en date+heure, ré-horodatée à la validation (ERP-189)
Le champ Date des blocs de pesée passe de MalioDate (date seule, heure perdue -> 00:00:00 en base) à MalioDateTime (date + heure). Défaut = instant courant (nowIsoDateTime) et ré-horodatage à la validation d'une pesée (bascule ou manuelle) via applyReading : la date du ticket reflète le moment réel de la pesée. L'hydratation en modification conserve l'heure du back (TIMESTAMP).
This commit is contained in:
@@ -32,9 +32,10 @@
|
|||||||
|
|
||||||
<!-- Ligne 2 : Date, Poids, DSD, Immatriculation. -->
|
<!-- Ligne 2 : Date, Poids, DSD, Immatriculation. -->
|
||||||
<div class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
<div class="grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
<!-- Date de la pesée — jour par défaut (RG-5.07). MalioDate (composant
|
<!-- Date/heure de la pesée — date du jour + heure courante par défaut
|
||||||
projet pour le type date, exception tolérée @.claude/rules/frontend.md). -->
|
(RG-5.07). MalioDateTime : on enregistre l'instant réel de la pesée
|
||||||
<MalioDate
|
(jamais 00:00:00), le back stocke un TIMESTAMP. -->
|
||||||
|
<MalioDateTime
|
||||||
:model-value="block.date"
|
:model-value="block.date"
|
||||||
:label="t('logistique.weighingTickets.form.date')"
|
:label="t('logistique.weighingTickets.form.date')"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
|
||||||
// `todayIso` est importé par le composable : on le stubbe pour une date déterministe.
|
// `nowIsoDateTime` est importé par le composable : on le stubbe pour un instant déterministe.
|
||||||
vi.mock('~/shared/utils/date', () => ({ todayIso: () => '2026-06-22' }))
|
vi.mock('~/shared/utils/date', () => ({ nowIsoDateTime: () => '2026-06-22T08:30:00' }))
|
||||||
|
|
||||||
const { useWeighingTicketForm } = await import('../useWeighingTicketForm')
|
const { useWeighingTicketForm } = await import('../useWeighingTicketForm')
|
||||||
|
|
||||||
describe('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()
|
const form = useWeighingTicketForm()
|
||||||
expect(form.empty.date).toBe('2026-06-22')
|
expect(form.empty.date).toBe('2026-06-22T08:30:00')
|
||||||
expect(form.full.date).toBe('2026-06-22')
|
expect(form.full.date).toBe('2026-06-22T08:30:00')
|
||||||
expect(form.empty.weight).toBeNull()
|
expect(form.empty.weight).toBeNull()
|
||||||
expect(form.empty.dsd).toBeNull()
|
expect(form.empty.dsd).toBeNull()
|
||||||
expect(form.counterpartyType.value).toBeNull()
|
expect(form.counterpartyType.value).toBeNull()
|
||||||
@@ -25,8 +25,8 @@ describe('useWeighingTicketForm', () => {
|
|||||||
expect(payload).not.toHaveProperty('counterpartyType')
|
expect(payload).not.toHaveProperty('counterpartyType')
|
||||||
expect(payload).not.toHaveProperty('immatriculation')
|
expect(payload).not.toHaveProperty('immatriculation')
|
||||||
expect(payload).not.toHaveProperty('emptyWeight')
|
expect(payload).not.toHaveProperty('emptyWeight')
|
||||||
// Les non-null restent : date du jour + booléen Tout format.
|
// Les non-null restent : date/heure courante + booléen Tout format.
|
||||||
expect(payload.emptyDate).toBe('2026-06-22')
|
expect(payload.emptyDate).toBe('2026-06-22T08:30:00')
|
||||||
expect(payload.plateFreeFormat).toBe(false)
|
expect(payload.plateFreeFormat).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,9 +96,13 @@ describe('useWeighingTicketForm', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ── Application d'une lecture de pesée ────────────────────────────────────
|
// ── 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()
|
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' })
|
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.weight).toBe(7150)
|
||||||
expect(form.empty.dsd).toBe(1)
|
expect(form.empty.dsd).toBe(1)
|
||||||
expect(form.empty.mode).toBe('AUTO')
|
expect(form.empty.mode).toBe('AUTO')
|
||||||
@@ -129,7 +133,7 @@ describe('useWeighingTicketForm', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ── Pré-remplissage (écran Modification, ERP-190) ─────────────────────────
|
// ── 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()
|
const form = useWeighingTicketForm()
|
||||||
form.hydrate({
|
form.hydrate({
|
||||||
id: 9,
|
id: 9,
|
||||||
@@ -152,9 +156,9 @@ describe('useWeighingTicketForm', () => {
|
|||||||
expect(form.counterpartyField.value).toBe('client')
|
expect(form.counterpartyField.value).toBe('client')
|
||||||
expect(form.clientIri.value).toBe('/api/clients/629')
|
expect(form.clientIri.value).toBe('/api/clients/629')
|
||||||
expect(form.immatriculation.value).toBe('AB-123-CD')
|
expect(form.immatriculation.value).toBe('AB-123-CD')
|
||||||
// Date datetime back -> date seule pour MalioDate.
|
// Datetime back (avec fuseau) -> local sans fuseau, heure conservée pour MalioDateTime.
|
||||||
expect(form.empty.date).toBe('2026-06-17')
|
expect(form.empty.date).toBe('2026-06-17T09:00:00')
|
||||||
expect(form.full.date).toBe('2026-06-17')
|
expect(form.full.date).toBe('2026-06-17T09:12:00')
|
||||||
expect(form.empty.weight).toBe(7150)
|
expect(form.empty.weight).toBe(7150)
|
||||||
expect(form.full.weight).toBe(14300)
|
expect(form.full.weight).toBe(14300)
|
||||||
})
|
})
|
||||||
@@ -165,8 +169,8 @@ describe('useWeighingTicketForm', () => {
|
|||||||
expect(form.otherLabel.value).toBe('Reprise')
|
expect(form.otherLabel.value).toBe('Reprise')
|
||||||
expect(form.supplierIri.value).toBeNull()
|
expect(form.supplierIri.value).toBeNull()
|
||||||
expect(form.plateFreeFormat.value).toBe(false)
|
expect(form.plateFreeFormat.value).toBe(false)
|
||||||
// Pas de date back -> repli sur le jour (stub 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-22')
|
expect(form.empty.date).toBe('2026-06-22T08:30:00')
|
||||||
expect(form.empty.weight).toBeNull()
|
expect(form.empty.weight).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, reactive, ref } from 'vue'
|
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'
|
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). */
|
/** Saisie d'une pesée (bloc vide OU bloc plein). */
|
||||||
export interface WeighingBlockState {
|
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
|
date: string | null
|
||||||
/** Poids en kg — readonly, rempli par la pesée (bascule ou manuelle). */
|
/** Poids en kg — readonly, rempli par la pesée (bascule ou manuelle). */
|
||||||
weight: number | null
|
weight: number | null
|
||||||
@@ -60,9 +60,14 @@ export interface WeighingTicketHydration {
|
|||||||
fullManualNumber?: string | null
|
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 {
|
* Ramène une chaîne ISO datetime du back (`2026-06-17T09:00:00+02:00`) au format
|
||||||
return value ? value.slice(0, 10) : null
|
* 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<string, unknown>): Record<string, unknown> {
|
|||||||
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== null))
|
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). */
|
/** Crée l'état initial d'un bloc de pesée (date/heure = maintenant, RG-5.07). */
|
||||||
function emptyBlock(today: string): WeighingBlockState {
|
function emptyBlock(now: string): WeighingBlockState {
|
||||||
return {
|
return {
|
||||||
date: today,
|
date: now,
|
||||||
weight: null,
|
weight: null,
|
||||||
dsd: null,
|
dsd: null,
|
||||||
mode: null,
|
mode: null,
|
||||||
@@ -90,7 +95,7 @@ function emptyBlock(today: string): WeighingBlockState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useWeighingTicketForm() {
|
export function useWeighingTicketForm() {
|
||||||
const today = todayIso()
|
const now = nowIsoDateTime()
|
||||||
|
|
||||||
// ── Contrepartie (RG-5.03) ───────────────────────────────────────────────
|
// ── Contrepartie (RG-5.03) ───────────────────────────────────────────────
|
||||||
const counterpartyType = ref<CounterpartyType | null>(null)
|
const counterpartyType = ref<CounterpartyType | null>(null)
|
||||||
@@ -116,8 +121,8 @@ export function useWeighingTicketForm() {
|
|||||||
const plateFreeFormat = ref<boolean>(false)
|
const plateFreeFormat = ref<boolean>(false)
|
||||||
|
|
||||||
// ── Les deux pesées ───────────────────────────────────────────────────────
|
// ── Les deux pesées ───────────────────────────────────────────────────────
|
||||||
const empty = reactive<WeighingBlockState>(emptyBlock(today))
|
const empty = reactive<WeighingBlockState>(emptyBlock(now))
|
||||||
const full = reactive<WeighingBlockState>(emptyBlock(today))
|
const full = reactive<WeighingBlockState>(emptyBlock(now))
|
||||||
|
|
||||||
// Id du ticket créé (POST du bloc vide) — pilote le PATCH du bloc plein.
|
// Id du ticket créé (POST du bloc vide) — pilote le PATCH du bloc plein.
|
||||||
const ticketId = ref<number | null>(null)
|
const ticketId = ref<number | null>(null)
|
||||||
@@ -150,11 +155,17 @@ export function useWeighingTicketForm() {
|
|||||||
return missing
|
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(
|
function applyReading(
|
||||||
block: WeighingBlockState,
|
block: WeighingBlockState,
|
||||||
reading: { weight: number, dsd: number, mode: WeighbridgeMode, manualNumber?: string },
|
reading: { weight: number, dsd: number, mode: WeighbridgeMode, manualNumber?: string },
|
||||||
): void {
|
): void {
|
||||||
|
block.date = nowIsoDateTime()
|
||||||
block.weight = reading.weight
|
block.weight = reading.weight
|
||||||
block.dsd = reading.dsd
|
block.dsd = reading.dsd
|
||||||
block.mode = reading.mode
|
block.mode = reading.mode
|
||||||
@@ -195,7 +206,8 @@ export function useWeighingTicketForm() {
|
|||||||
* Pré-remplit le formulaire à partir du détail d'un ticket existant (écran
|
* 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) →
|
* 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).
|
* 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 {
|
function hydrate(detail: WeighingTicketHydration): void {
|
||||||
ticketId.value = detail.id
|
ticketId.value = detail.id
|
||||||
@@ -206,13 +218,13 @@ export function useWeighingTicketForm() {
|
|||||||
immatriculation.value = detail.immatriculation ?? null
|
immatriculation.value = detail.immatriculation ?? null
|
||||||
plateFreeFormat.value = detail.plateFreeFormat ?? false
|
plateFreeFormat.value = detail.plateFreeFormat ?? false
|
||||||
|
|
||||||
empty.date = isoDateOnly(detail.emptyDate) ?? today
|
empty.date = toLocalIsoDateTime(detail.emptyDate) ?? now
|
||||||
empty.weight = detail.emptyWeight ?? null
|
empty.weight = detail.emptyWeight ?? null
|
||||||
empty.dsd = detail.emptyDsd ?? null
|
empty.dsd = detail.emptyDsd ?? null
|
||||||
empty.mode = detail.emptyMode ?? null
|
empty.mode = detail.emptyMode ?? null
|
||||||
empty.manualNumber = detail.emptyManualNumber ?? null
|
empty.manualNumber = detail.emptyManualNumber ?? null
|
||||||
|
|
||||||
full.date = isoDateOnly(detail.fullDate) ?? today
|
full.date = toLocalIsoDateTime(detail.fullDate) ?? now
|
||||||
full.weight = detail.fullWeight ?? null
|
full.weight = detail.fullWeight ?? null
|
||||||
full.dsd = detail.fullDsd ?? null
|
full.dsd = detail.fullDsd ?? null
|
||||||
full.mode = detail.fullMode ?? null
|
full.mode = detail.fullMode ?? null
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const stubs = {
|
|||||||
MalioInputText: InputStub,
|
MalioInputText: InputStub,
|
||||||
MalioInputNumber: InputStub,
|
MalioInputNumber: InputStub,
|
||||||
MalioSelect: InputStub,
|
MalioSelect: InputStub,
|
||||||
MalioDate: InputStub,
|
MalioDateTime: InputStub,
|
||||||
MalioCheckbox: InputStub,
|
MalioCheckbox: InputStub,
|
||||||
MalioModal: ModalStub,
|
MalioModal: ModalStub,
|
||||||
WeighingBlock: BlockStub,
|
WeighingBlock: BlockStub,
|
||||||
|
|||||||
@@ -15,3 +15,18 @@ export function todayIso(now: Date = new Date()): string {
|
|||||||
const day = String(now.getDate()).padStart(2, '0')
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
return `${year}-${month}-${day}`
|
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}`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user