From 7cd0a90c8eb4b28afb1fe8c7e075781c6da1cc17 Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 28 Jun 2026 14:15:50 +0200 Subject: [PATCH 01/15] =?UTF-8?q?feat(front)=20:=20tags=20multiselect=20?= =?UTF-8?q?=E2=80=94=20couleur=20des=20sites=20+=20limite=20d'affichage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sites : les tags du multiselect prennent la couleur de fond du site (champ color, groupe site:read) avec texte blanc, en saisie comme en consultation (clients, fournisseurs, prestataires, produits). - Autres multiselects : 3 tags affichés au maximum (surplus condensé en « +N »). - Bump @malio/layer-ui 1.7.15 → 1.7.17 (support color/textColor et maxTags sur les options). --- .../catalog/components/CategoryDrawer.vue | 1 + .../catalog/composables/useProductOptions.ts | 20 +++++- .../pages/admin/products/[id]/edit.vue | 2 + .../catalog/pages/admin/products/new.vue | 2 + .../components/ClientAddressBlock.vue | 2 + .../components/SupplierAddressBlock.vue | 2 + .../__tests__/useClientReferentials.spec.ts | 9 +-- .../composables/useClientReferentials.ts | 10 ++- .../composables/useSupplierReferentials.ts | 10 ++- .../commercial/pages/clients/[id]/edit.vue | 1 + .../commercial/pages/clients/[id]/index.vue | 1 + .../modules/commercial/pages/clients/new.vue | 1 + .../commercial/pages/suppliers/[id]/edit.vue | 1 + .../commercial/pages/suppliers/[id]/index.vue | 1 + .../commercial/pages/suppliers/new.vue | 1 + .../__tests__/clientConsultation.spec.ts | 6 +- .../__tests__/supplierConsultation.spec.ts | 6 +- .../utils/forms/clientConsultation.ts | 8 ++- .../utils/forms/supplierConsultation.ts | 8 ++- .../components/ProviderAddressBlock.vue | 1 + .../composables/useProviderReferentials.ts | 10 ++- .../technique/pages/providers/[id]/edit.vue | 1 + .../technique/pages/providers/[id]/index.vue | 1 + .../modules/technique/pages/providers/new.vue | 1 + .../forms/__tests__/providerDetail.spec.ts | 4 +- .../technique/utils/forms/providerDetail.ts | 2 +- frontend/package-lock.json | 63 +++++++++++-------- frontend/package.json | 2 +- 28 files changed, 130 insertions(+), 47 deletions(-) diff --git a/frontend/modules/catalog/components/CategoryDrawer.vue b/frontend/modules/catalog/components/CategoryDrawer.vue index ebe3473..c941bb8 100644 --- a/frontend/modules/catalog/components/CategoryDrawer.vue +++ b/frontend/modules/catalog/components/CategoryDrawer.vue @@ -30,6 +30,7 @@ , toLabel: (member: HydraMember) => string, + toColor?: (member: HydraMember) => string | undefined, ): Promise { const res = await useApi().get<{ member?: HydraMember[] }>( url, { pagination: 'false', ...query }, { headers: LD_JSON_HEADERS, toast: false }, ) - return (res.member ?? []).map(m => ({ value: m['@id'], label: toLabel(m) })) + return (res.member ?? []).map(m => ({ + value: m['@id'], + label: toLabel(m), + // Couleur reportee uniquement si un extracteur est fourni (ex: sites). + ...(toColor ? { color: toColor(m) } : {}), + })) } /** Sites de disponibilite (libelle = nom du site). */ @@ -49,7 +63,9 @@ export function useSiteOptions() { const options = ref([]) async function load(): Promise { - options.value = await fetchOptions('/sites', {}, s => s.name ?? '') + // Sites : couleur de fond depuis l'embed + texte blanc pour rester lisible. + const sites = await fetchOptions('/sites', {}, s => s.name ?? '', s => s.color) + options.value = sites.map(o => ({ ...o, textColor: '#FFFFFF' })) } return { options, load } diff --git a/frontend/modules/catalog/pages/admin/products/[id]/edit.vue b/frontend/modules/catalog/pages/admin/products/[id]/edit.vue index 6fbb767..fde56e1 100644 --- a/frontend/modules/catalog/pages/admin/products/[id]/edit.vue +++ b/frontend/modules/catalog/pages/admin/products/[id]/edit.vue @@ -25,6 +25,7 @@ { // Resilience : les referentiels OK sont peuples malgre l'echec de /categories. // Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal). - expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }]) + expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86', textColor: '#FFFFFF' }]) expect(refs.tvaModes.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }]) expect(refs.banks.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }]) // Pays : value = nom du pays (et non l'IRI). @@ -63,7 +63,7 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { }) } if (url === '/sites') { - return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault', postalCode: '86100' }] }) + return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault', postalCode: '86100', color: '#FF0000' }] }) } return Promise.resolve({ member: [] }) }) @@ -74,8 +74,9 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => { expect(refs.categories.value).toEqual([ { value: '/api/categories/1', label: 'Secteur', code: 'SECTEUR' }, ]) - // Le libelle d'un site est son numero de departement (2 premiers chiffres du code postal). - expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }]) + // Le libelle d'un site est son numero de departement (2 premiers chiffres du + // code postal) ; la couleur du site est reportee (fond) avec un texte blanc. + expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86', color: '#FF0000', textColor: '#FFFFFF' }]) }) it('separe les categories CLIENT (formulaire) des categories ADRESSE (blocs adresse)', async () => { diff --git a/frontend/modules/commercial/composables/useClientReferentials.ts b/frontend/modules/commercial/composables/useClientReferentials.ts index 6799f79..b4f83eb 100644 --- a/frontend/modules/commercial/composables/useClientReferentials.ts +++ b/frontend/modules/commercial/composables/useClientReferentials.ts @@ -19,6 +19,13 @@ import { ref } from 'vue' export interface RefOption { value: string label: string + // Couleur de fond optionnelle de l'option (hex #RRGGBB). Aujourd'hui + // alimentee par le referentiel sites (couleur d'identification du site, + // affichee sur les tags selectionnes du multiselect). + color?: string + // Couleur de texte optionnelle (hex). Sites : blanc, pour rester lisible + // sur le fond colore du tag. + textColor?: string } /** Option de type de reglement enrichie de son code stable (RG-1.12 / RG-1.13). */ @@ -46,6 +53,7 @@ interface CategoryMember extends HydraMember { interface SiteMember extends HydraMember { name: string postalCode: string + color?: string } interface ReferentialMember extends HydraMember { @@ -119,7 +127,7 @@ export function useClientReferentials() { // Libelle = numero de departement (2 premiers chiffres du code // postal du site), ex: 86100 -> « 86 ». Le code postal est deja // expose par /sites (groupe site:read) — aucune colonne a ajouter. - .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2) })) }), + .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2), color: s.color, textColor: '#FFFFFF' })) }), fetchAll('/tva_modes') .then((tva) => { tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label })) }), fetchAll('/payment_delays') diff --git a/frontend/modules/commercial/composables/useSupplierReferentials.ts b/frontend/modules/commercial/composables/useSupplierReferentials.ts index 01ef814..849d7eb 100644 --- a/frontend/modules/commercial/composables/useSupplierReferentials.ts +++ b/frontend/modules/commercial/composables/useSupplierReferentials.ts @@ -20,6 +20,13 @@ import { ref } from 'vue' export interface RefOption { value: string label: string + // Couleur de fond optionnelle de l'option (hex #RRGGBB). Alimentee par le + // referentiel sites (couleur d'identification du site, affichee sur les tags + // selectionnes du multiselect). + color?: string + // Couleur de texte optionnelle (hex). Sites : blanc, pour rester lisible + // sur le fond colore du tag. + textColor?: string } /** Option de type de reglement enrichie de son code stable (RG-2.07 / RG-2.08). */ @@ -44,6 +51,7 @@ interface CategoryMember extends HydraMember { interface SiteMember extends HydraMember { name: string postalCode: string + color?: string } interface ReferentialMember extends HydraMember { @@ -106,7 +114,7 @@ export function useSupplierReferentials() { fetchAll('/sites') // Libelle = numero de departement (2 premiers chiffres du code // postal du site), ex: 86100 -> « 86 ». - .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2) })) }), + .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2), color: s.color, textColor: '#FFFFFF' })) }), fetchAll('/tva_modes') .then((tva) => { tvaModes.value = tva.map(t => ({ value: t['@id'], label: t.label })) }), fetchAll('/payment_delays') diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 5a67243..a05c5e1 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -35,6 +35,7 @@ { ]) }) - it('siteOptionsOf expose value=IRI, label=nom', () => { + it('siteOptionsOf expose value=IRI, label=nom, color, textColor', () => { expect(siteOptionsOf([{ '@id': '/api/sites/4', name: 'Chatellerault', color: '#000' }])).toEqual([ - { value: '/api/sites/4', label: 'Chatellerault' }, + { value: '/api/sites/4', label: 'Chatellerault', color: '#000', textColor: '#FFFFFF' }, ]) }) @@ -201,7 +201,7 @@ describe('options construites depuis l\'embed (role-independantes)', () => { categories: [{ '@id': '/api/categories/3', name: 'Secteur', code: 'SECTEUR' }], }) expect(view.draft.id).toBe(18) - expect(view.siteOptions).toEqual([{ value: '/api/sites/4', label: 'Chatellerault' }]) + expect(view.siteOptions).toEqual([{ value: '/api/sites/4', label: 'Chatellerault', textColor: '#FFFFFF' }]) expect(view.categoryOptions).toEqual([{ value: '/api/categories/3', label: 'Secteur', code: 'SECTEUR' }]) }) }) diff --git a/frontend/modules/commercial/utils/forms/__tests__/supplierConsultation.spec.ts b/frontend/modules/commercial/utils/forms/__tests__/supplierConsultation.spec.ts index 2882efb..c8cb4f8 100644 --- a/frontend/modules/commercial/utils/forms/__tests__/supplierConsultation.spec.ts +++ b/frontend/modules/commercial/utils/forms/__tests__/supplierConsultation.spec.ts @@ -155,9 +155,9 @@ describe('options construites depuis l\'embed (role-independantes)', () => { ]) }) - it('siteOptionsOf expose value=IRI, label=nom', () => { + it('siteOptionsOf expose value=IRI, label=nom, color, textColor', () => { expect(siteOptionsOf([{ '@id': '/api/sites/87', name: 'Chatellerault', color: '#000' }])).toEqual([ - { value: '/api/sites/87', label: 'Chatellerault' }, + { value: '/api/sites/87', label: 'Chatellerault', color: '#000', textColor: '#FFFFFF' }, ]) }) @@ -190,7 +190,7 @@ describe('options construites depuis l\'embed (role-independantes)', () => { }) expect(view.draft.id).toBe(33) expect(view.draft.addressType).toBe('RENDU') - expect(view.siteOptions).toEqual([{ value: '/api/sites/87', label: 'Chatellerault' }]) + expect(view.siteOptions).toEqual([{ value: '/api/sites/87', label: 'Chatellerault', textColor: '#FFFFFF' }]) expect(view.categoryOptions).toEqual([{ value: '/api/categories/2279', label: 'Negociant', code: 'NEGOCIANT' }]) }) }) diff --git a/frontend/modules/commercial/utils/forms/clientConsultation.ts b/frontend/modules/commercial/utils/forms/clientConsultation.ts index dfe35df..658efd2 100644 --- a/frontend/modules/commercial/utils/forms/clientConsultation.ts +++ b/frontend/modules/commercial/utils/forms/clientConsultation.ts @@ -143,6 +143,12 @@ export interface ClientRelation { export interface SelectOption { value: string label: string + // Couleur de fond optionnelle (hex #RRGGBB), reportee pour les sites afin + // de colorer les tags selectionnes en consultation comme en edition. + color?: string + // Couleur de texte optionnelle (hex). Sites : blanc, pour rester lisible + // sur le fond colore du tag. + textColor?: string } /** Option de categorie enrichie de son code (compatible CategoryOption des blocs). */ @@ -266,7 +272,7 @@ export function categoryOptionsOf(categories: CategoryRead[] | undefined): Categ /** Options de sites (value=IRI, label=nom) construites depuis l'embed d'une adresse. */ export function siteOptionsOf(sites: SiteRead[] | undefined): SelectOption[] { - return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'] })) + return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'], color: s.color, textColor: '#FFFFFF' })) } /** Options de contacts (value=IRI, label=nom complet ou email) depuis l'embed client. */ diff --git a/frontend/modules/commercial/utils/forms/supplierConsultation.ts b/frontend/modules/commercial/utils/forms/supplierConsultation.ts index 5257f32..9f96a8d 100644 --- a/frontend/modules/commercial/utils/forms/supplierConsultation.ts +++ b/frontend/modules/commercial/utils/forms/supplierConsultation.ts @@ -138,6 +138,12 @@ export interface AccountingDraft { export interface SelectOption { value: string label: string + // Couleur de fond optionnelle (hex #RRGGBB), reportee pour les sites afin + // de colorer les tags selectionnes en consultation comme en edition. + color?: string + // Couleur de texte optionnelle (hex). Sites : blanc, pour rester lisible + // sur le fond colore du tag. + textColor?: string } /** Option de categorie enrichie de son code (compatible CategoryOption des blocs). */ @@ -241,7 +247,7 @@ export function categoryOptionsOf(categories: CategoryRead[] | undefined): Categ /** Options de sites (value=IRI, label=nom) construites depuis l'embed d'une adresse. */ export function siteOptionsOf(sites: SiteRead[] | undefined): SelectOption[] { - return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'] })) + return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'], color: s.color, textColor: '#FFFFFF' })) } /** Options de contacts (value=IRI, label=nom complet ou email) depuis l'embed fournisseur. */ diff --git a/frontend/modules/technique/components/ProviderAddressBlock.vue b/frontend/modules/technique/components/ProviderAddressBlock.vue index 24faeef..ac773f0 100644 --- a/frontend/modules/technique/components/ProviderAddressBlock.vue +++ b/frontend/modules/technique/components/ProviderAddressBlock.vue @@ -37,6 +37,7 @@ v-if="!hideEmpty || isFilled(model.contactIris)" :model-value="model.contactIris" :options="contactOptions" + :max-tags="3" :label="t('technique.providers.form.address.contacts')" :display-tag="true" :readonly="readonly" diff --git a/frontend/modules/technique/composables/useProviderReferentials.ts b/frontend/modules/technique/composables/useProviderReferentials.ts index 3b56d8c..37ae76d 100644 --- a/frontend/modules/technique/composables/useProviderReferentials.ts +++ b/frontend/modules/technique/composables/useProviderReferentials.ts @@ -26,6 +26,13 @@ import { ref } from 'vue' export interface RefOption { value: string label: string + // Couleur de fond optionnelle de l'option (hex #RRGGBB). Alimentee par le + // referentiel sites (couleur d'identification du site, affichee sur les tags + // selectionnes du multiselect). + color?: string + // Couleur de texte optionnelle (hex). Sites : blanc, pour rester lisible + // sur le fond colore du tag. + textColor?: string } /** Option de type de reglement enrichie de son code stable (RG-3.07 / RG-3.08). */ @@ -50,6 +57,7 @@ interface CategoryMember extends HydraMember { interface SiteMember extends HydraMember { name: string postalCode: string + color?: string } interface CountryMember extends HydraMember { @@ -94,7 +102,7 @@ export function useProviderReferentials() { // Sites (RG-3.03) : libelle = numero de departement (2 premiers chiffres // du code postal du site), ex: 86100 -> « 86 », 17400 -> « 17 ». fetchAll('/sites') - .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2) })) }), + .then((sitesList) => { sites.value = sitesList.map(s => ({ value: s['@id'], label: (s.postalCode ?? '').slice(0, 2), color: s.color, textColor: '#FFFFFF' })) }), // Pays (ERP-116) : la valeur d'option est le NOM du pays (l'adresse stocke // `country` en chaine libre, « France »...). value === label. Aligne sur // les ecrans client/fournisseur. Sert le select Pays de l'onglet Adresse. diff --git a/frontend/modules/technique/pages/providers/[id]/edit.vue b/frontend/modules/technique/pages/providers/[id]/edit.vue index a7cabdd..abc159f 100644 --- a/frontend/modules/technique/pages/providers/[id]/edit.vue +++ b/frontend/modules/technique/pages/providers/[id]/edit.vue @@ -31,6 +31,7 @@ { it('categoryOptionsOf / siteOptionsOf / contactOptionsOf', () => { expect(categoryOptionsOf([{ '@id': '/api/categories/7', name: 'Maintenance', code: 'MAINT' }])) .toEqual([{ value: '/api/categories/7', label: 'Maintenance' }]) - expect(siteOptionsOf([{ '@id': '/api/sites/1', name: 'Châtellerault' }])) - .toEqual([{ value: '/api/sites/1', label: 'Châtellerault' }]) + expect(siteOptionsOf([{ '@id': '/api/sites/1', name: 'Châtellerault', color: '#000' }])) + .toEqual([{ value: '/api/sites/1', label: 'Châtellerault', color: '#000', textColor: '#FFFFFF' }]) expect(contactOptionsOf([{ '@id': '/api/provider_contacts/5', id: 5, firstName: 'Jean', lastName: 'Dupont' }])) .toEqual([{ value: '/api/provider_contacts/5', label: 'Jean Dupont' }]) }) diff --git a/frontend/modules/technique/utils/forms/providerDetail.ts b/frontend/modules/technique/utils/forms/providerDetail.ts index 8904a8f..44d5d14 100644 --- a/frontend/modules/technique/utils/forms/providerDetail.ts +++ b/frontend/modules/technique/utils/forms/providerDetail.ts @@ -187,7 +187,7 @@ export function categoryOptionsOf(categories: CategoryRead[] | undefined): RefOp /** Options de sites (value=IRI, label=nom) construites depuis un embed. */ export function siteOptionsOf(sites: SiteRead[] | undefined): RefOption[] { - return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'] })) + return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'], color: s.color, textColor: '#FFFFFF' })) } /** Options de contacts (value=IRI, label=nom complet ou email) depuis l'embed prestataire. */ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index af5ee32..6777d94 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,7 @@ "name": "starseed-frontend", "hasInstallScript": true, "dependencies": { - "@malio/layer-ui": "^1.7.15", + "@malio/layer-ui": "^1.7.18", "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.3", "@nuxtjs/tailwindcss": "^6.14.0", @@ -85,6 +85,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -582,27 +583,6 @@ "integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==", "license": "MIT" }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1303,6 +1283,7 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" @@ -1866,9 +1847,9 @@ "license": "MIT" }, "node_modules/@malio/layer-ui": { - "version": "1.7.15", - "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.15/layer-ui-1.7.15.tgz", - "integrity": "sha512-CgEC0l2pkR6rlzpi1zZqswHs+/yGTSd861tdT678/wSKtQPQ6JxUIf63ugFDItyvyLW+nbcNWuHTFC2Bimp1EQ==", + "version": "1.7.18", + "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.18/layer-ui-1.7.18.tgz", + "integrity": "sha512-A+YcnEzzucsAz0FqkhVmN41uvtEHjy4ZbbHK8POjqNCkhuy7aTnisMUiYGlZUaEcu5lRjzw6RvjAavRTGzTNvQ==", "dependencies": { "@nuxt/icon": "^2.2.1", "@nuxtjs/tailwindcss": "^6.14.0", @@ -2221,6 +2202,7 @@ "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz", "integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==", "license": "MIT", + "peer": true, "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", @@ -2323,6 +2305,7 @@ "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.2.tgz", "integrity": "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w==", "license": "MIT", + "peer": true, "dependencies": { "@vue/shared": "^3.5.30", "defu": "^6.1.4", @@ -4638,6 +4621,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.2.tgz", "integrity": "sha512-yjv2N7gaQMbIVfsSZHBMscLoybgetcTraXsSMrELAerl/jfRipg5S1dBXMFvgRy8Kh48+TGoH+5nqshxdOEGoQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4886,6 +4870,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.2.tgz", "integrity": "sha512-tRbbjpOPrY4ApIHtn3ctnKIhkkioewMsZa5gJzqVB47LJFNyzLXLo/aID4sJRKTIMi1wd1fA9TiBKPe6KqczPA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4991,6 +4976,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.23.2.tgz", "integrity": "sha512-K2o1gMwn09nrd5ewftSy08U6LMC1cW3Cmml5+vHT9P/VeMtYwkbNg+9Mt1uFh7VfAZmlkj8d3u7RYqfl8xMVJA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5017,6 +5003,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.2.tgz", "integrity": "sha512-kRHQ3nSbAfkFdxj9FtDdr4hpREndGgWFA6ZEAwlLeGUxf8QYTpuF9zb2yxdBPBlTc5+JsbPcskNt+u1PazGKYw==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5031,6 +5018,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.2.tgz", "integrity": "sha512-1kvsBqGNu2ZJ0P/lkxN0pAMqSyUcpkMIzE4xwGUIyAiD0pZV6dr+OCMwGWOTLllSyrn91xI5K7OLk3pYeCPKqA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", @@ -5174,6 +5162,7 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -5236,6 +5225,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -6015,6 +6005,7 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.32", @@ -6258,6 +6249,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6645,6 +6637,7 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6842,6 +6835,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6956,6 +6950,7 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7150,7 +7145,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/clean-regexp": { "version": "1.0.0", @@ -8203,6 +8199,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9361,6 +9358,7 @@ "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -11807,6 +11805,7 @@ "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.4.2.tgz", "integrity": "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==", "license": "MIT", + "peer": true, "dependencies": { "@dxup/nuxt": "^0.4.0", "@nuxt/cli": "^3.34.0", @@ -12865,6 +12864,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -12922,6 +12922,7 @@ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz", "integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==", "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "^0.112.0" }, @@ -13188,6 +13189,7 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -13313,6 +13315,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13856,6 +13859,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14646,6 +14650,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15549,6 +15554,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16228,6 +16234,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -16494,6 +16501,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17412,6 +17420,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/compiler-sfc": "3.5.32", @@ -17456,6 +17465,7 @@ "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", @@ -17492,6 +17502,7 @@ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.1.tgz", "integrity": "sha512-azq8fhVnCwJAw0iXW7i44h9P+Bj+snNuevBAaJ9bxn0I3YVsRU3deVFPNnTfZ2uxVJefGp83JUmL68ddCPw5Pw==", "license": "MIT", + "peer": true, "dependencies": { "@intlify/core-base": "11.3.1", "@intlify/devtools-types": "11.3.1", diff --git a/frontend/package.json b/frontend/package.json index 1afb5be..d0cd673 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@malio/layer-ui": "^1.7.15", + "@malio/layer-ui": "^1.7.18", "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.3", "@nuxtjs/tailwindcss": "^6.14.0", -- 2.39.5 From 4b9382df3f70392f2b6cf7146c43c6070653a348 Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 28 Jun 2026 14:24:20 +0200 Subject: [PATCH 02/15] =?UTF-8?q?fix(front)=20:=20resync=20package-lock.js?= =?UTF-8?q?on=20=E2=80=94=20noeuds=20@emnapi=20manquants=20(npm=20ci=20CI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 55 +++++++++++++++----------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6777d94..221dc91 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -85,7 +85,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -583,6 +582,27 @@ "integrity": "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==", "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1283,7 +1303,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" @@ -2202,7 +2221,6 @@ "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz", "integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==", "license": "MIT", - "peer": true, "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", @@ -2305,7 +2323,6 @@ "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.2.tgz", "integrity": "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w==", "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "^3.5.30", "defu": "^6.1.4", @@ -4621,7 +4638,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.2.tgz", "integrity": "sha512-yjv2N7gaQMbIVfsSZHBMscLoybgetcTraXsSMrELAerl/jfRipg5S1dBXMFvgRy8Kh48+TGoH+5nqshxdOEGoQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4870,7 +4886,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.2.tgz", "integrity": "sha512-tRbbjpOPrY4ApIHtn3ctnKIhkkioewMsZa5gJzqVB47LJFNyzLXLo/aID4sJRKTIMi1wd1fA9TiBKPe6KqczPA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4976,7 +4991,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.23.2.tgz", "integrity": "sha512-K2o1gMwn09nrd5ewftSy08U6LMC1cW3Cmml5+vHT9P/VeMtYwkbNg+9Mt1uFh7VfAZmlkj8d3u7RYqfl8xMVJA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5003,7 +5017,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.2.tgz", "integrity": "sha512-kRHQ3nSbAfkFdxj9FtDdr4hpREndGgWFA6ZEAwlLeGUxf8QYTpuF9zb2yxdBPBlTc5+JsbPcskNt+u1PazGKYw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5018,7 +5031,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.2.tgz", "integrity": "sha512-1kvsBqGNu2ZJ0P/lkxN0pAMqSyUcpkMIzE4xwGUIyAiD0pZV6dr+OCMwGWOTLllSyrn91xI5K7OLk3pYeCPKqA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", @@ -5162,7 +5174,6 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -5225,7 +5236,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -6005,7 +6015,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.32", @@ -6249,7 +6258,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6637,7 +6645,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6835,7 +6842,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6950,7 +6956,6 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7145,8 +7150,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/clean-regexp": { "version": "1.0.0", @@ -8199,7 +8203,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9358,7 +9361,6 @@ "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -11805,7 +11807,6 @@ "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.4.2.tgz", "integrity": "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==", "license": "MIT", - "peer": true, "dependencies": { "@dxup/nuxt": "^0.4.0", "@nuxt/cli": "^3.34.0", @@ -12864,7 +12865,6 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -12922,7 +12922,6 @@ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.112.0.tgz", "integrity": "sha512-7rQ3QdJwobMQLMZwQaPuPYMEF2fDRZwf51lZ//V+bA37nejjKW5ifMHbbCwvA889Y4RLhT+/wLJpPRhAoBaZYw==", "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "^0.112.0" }, @@ -13189,7 +13188,6 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -13315,7 +13313,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13859,7 +13856,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14650,7 +14646,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15554,7 +15549,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16234,7 +16228,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -16501,7 +16494,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17420,7 +17412,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/compiler-sfc": "3.5.32", @@ -17465,7 +17456,6 @@ "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", @@ -17502,7 +17492,6 @@ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.1.tgz", "integrity": "sha512-azq8fhVnCwJAw0iXW7i44h9P+Bj+snNuevBAaJ9bxn0I3YVsRU3deVFPNnTfZ2uxVJefGp83JUmL68ddCPw5Pw==", "license": "MIT", - "peer": true, "dependencies": { "@intlify/core-base": "11.3.1", "@intlify/devtools-types": "11.3.1", -- 2.39.5 From f8f8f53b4cb66ac70f97af4fe8d6314deed67e72 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 08:21:12 +0200 Subject: [PATCH 03/15] =?UTF-8?q?fix(front)=20:=20types=20referentiels=20p?= =?UTF-8?q?artages=20=E2=80=94=20supprime=20le=20warning=20Duplicated=20im?= =?UTF-8?q?ports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ClientAddressBlock.vue | 2 +- .../components/SupplierAddressBlock.vue | 2 +- .../composables/useClientReferentials.ts | 27 +------------- .../composables/useSupplierReferentials.ts | 24 +----------- .../commercial/pages/clients/[id]/edit.vue | 3 +- .../modules/commercial/pages/clients/new.vue | 3 +- .../commercial/pages/suppliers/[id]/edit.vue | 3 +- .../commercial/pages/suppliers/new.vue | 3 +- .../modules/commercial/types/referentials.ts | 37 +++++++++++++++++++ 9 files changed, 49 insertions(+), 55 deletions(-) create mode 100644 frontend/modules/commercial/types/referentials.ts diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue index 4847057..b82fdf0 100644 --- a/frontend/modules/commercial/components/ClientAddressBlock.vue +++ b/frontend/modules/commercial/components/ClientAddressBlock.vue @@ -219,7 +219,7 @@ import { type AddressType, } from '~/modules/commercial/utils/forms/clientFormRules' import { useAddressAutocomplete, type AddressSuggestion } from '~/shared/composables/useAddressAutocomplete' -import type { CategoryOption, RefOption } from '~/modules/commercial/composables/useClientReferentials' +import type { CategoryOption, RefOption } from '~/modules/commercial/types/referentials' import type { AddressFormDraft } from '~/modules/commercial/types/clientForm' import { ADDRESS_MASK } from '~/shared/utils/textSanitize' import { isFilled } from '~/shared/utils/consultationDisplay' diff --git a/frontend/modules/commercial/components/SupplierAddressBlock.vue b/frontend/modules/commercial/components/SupplierAddressBlock.vue index 96fd9da..2b37916 100644 --- a/frontend/modules/commercial/components/SupplierAddressBlock.vue +++ b/frontend/modules/commercial/components/SupplierAddressBlock.vue @@ -200,7 +200,7 @@ diff --git a/frontend/modules/sites/composables/useCurrentSite.ts b/frontend/modules/sites/composables/useCurrentSite.ts index da196d2..a2035fb 100644 --- a/frontend/modules/sites/composables/useCurrentSite.ts +++ b/frontend/modules/sites/composables/useCurrentSite.ts @@ -6,8 +6,8 @@ * rollback si la requete PATCH `/api/me/current-site` echoue. * * Garantie d'unicite : le flag `switching` bloque les double-clicks - * concurrents. Le reset explicite est appele au logout - * (voir `modules/core/pages/logout.vue`). + * concurrents. Le state est purge au logout via `onAuthSessionCleared` + * (declenche par `clearSession()`, cf. `useLogout` et l'intercepteur 401). * * Auto-select : aucun. Le backend (`UserRbacProcessor::ensureCurrentSiteConsistency`) * garantit deja l'invariant "user avec sites non vide => currentSite non null" @@ -30,8 +30,8 @@ const availableSites = ref([]) const switching = ref(false) // Enregistrement unique au niveau module (singleton) : quand clearSession() -// est appelee par l'intercepteur 401 de useApi, le state local est purgé -// de la meme facon qu'au logout explicite (logout.vue). +// est appelee (logout volontaire via useLogout, ou intercepteur 401 de useApi), +// le state local est purgé. onAuthSessionCleared(() => { currentSite.value = null availableSites.value = [] diff --git a/frontend/shared/composables/useLogout.ts b/frontend/shared/composables/useLogout.ts new file mode 100644 index 0000000..4ff444a --- /dev/null +++ b/frontend/shared/composables/useLogout.ts @@ -0,0 +1,21 @@ +/** + * Déconnexion centralisée — déclenchée directement par un handler (ex: lien du + * footer de la sidebar), sans passer par une page de redirection dédiée. + * + * `authStore.logout()` invalide la session serveur (POST /api/logout), vide + * l'état auth, et appelle `clearSession()` qui notifie tous les composables + * singletons (sidebar, modules, currentSite, auditLog, categoriesAdmin) via + * `onAuthSessionCleared` — leurs états sont donc réinitialisés ici sans aucun + * reset manuel. La redirection vers `/login` (inévitable : un utilisateur + * déconnecté ne peut pas rester sur une page protégée) est la seule navigation. + */ +export function useLogout() { + const auth = useAuthStore() + + async function logout(): Promise { + await auth.logout() + await navigateTo('/login') + } + + return { logout } +} diff --git a/frontend/shared/stores/auth.ts b/frontend/shared/stores/auth.ts index d448947..ed6da1a 100644 --- a/frontend/shared/stores/auth.ts +++ b/frontend/shared/stores/auth.ts @@ -77,9 +77,11 @@ export const useAuthStore = defineStore('auth', { } catch { // Ignore logout errors so we can still clear local auth state. } finally { - this.user = null - this.checked = true - this.isLoading = false + // clearSession() vide l'etat auth ET notifie les composables + // singletons (sidebar, modules, currentSite, auditLog, + // categoriesAdmin) via onAuthSessionCleared : plus besoin de + // resets manuels au logout — meme chemin que l'intercepteur 401. + this.clearSession() } }, async refreshUser() { diff --git a/frontend/tests/e2e/auth/login.spec.ts b/frontend/tests/e2e/auth/login.spec.ts index 0cfca7f..9bab667 100644 --- a/frontend/tests/e2e/auth/login.spec.ts +++ b/frontend/tests/e2e/auth/login.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test' import { LoginPage } from '../helpers/pages/LoginPage' +import { SidebarComponent } from '../helpers/pages/SidebarComponent' import { getPersona } from '../_fixtures/personas' /** @@ -53,8 +54,12 @@ test.describe('Login', () => { await loginPage.fillAndSubmit(superAdmin.username, superAdmin.password) await page.waitForURL('/') - // 2. Navigation vers /logout (il y a un lien "Deconnexion" dans la sidebar) - await page.goto('/logout') + // 2. Deconnexion via le footer de la sidebar : survol du bloc compte + // (revele le bouton) puis clic. Le handler appelle useLogout() qui POST + // /api/logout, reset les stores, et redirige vers /login (sans page /logout). + const sidebar = new SidebarComponent(page) + await sidebar.accountBlock().hover() + await sidebar.logoutButton().click() await page.waitForURL(/\/login$/) // 3. Le cookie BEARER doit avoir ete supprime par le firewall de logout diff --git a/frontend/tests/e2e/helpers/pages/SidebarComponent.ts b/frontend/tests/e2e/helpers/pages/SidebarComponent.ts index 9e7a130..6c64fab 100644 --- a/frontend/tests/e2e/helpers/pages/SidebarComponent.ts +++ b/frontend/tests/e2e/helpers/pages/SidebarComponent.ts @@ -27,7 +27,21 @@ export class SidebarComponent { return this.page.locator('a[href="/"]').first() } - logoutLink(): Locator { - return this.page.locator('a[href="/logout"]') + /** + * Bloc « compte connecte » du footer de la sidebar. Cible de survol qui + * revele le bouton de deconnexion (la deconnexion n'est plus un item de nav + * `/logout` mais un lien du footer, cf. default.vue + useLogout). + */ + accountBlock(): Locator { + return this.page.locator('[data-test="sidebar-account"]') + } + + /** + * Bouton de deconnexion du footer (revele au survol du bloc compte en mode + * deplie, ou directement la pastille en mode replie). Selecteur par + * `data-test` : stable au renommage/retraduction du label. + */ + logoutButton(): Locator { + return this.page.locator('[data-test="sidebar-logout"]') } } diff --git a/frontend/tests/e2e/permissions/sidebar-visibility.spec.ts b/frontend/tests/e2e/permissions/sidebar-visibility.spec.ts index 01323df..17187c0 100644 --- a/frontend/tests/e2e/permissions/sidebar-visibility.spec.ts +++ b/frontend/tests/e2e/permissions/sidebar-visibility.spec.ts @@ -72,7 +72,10 @@ test.describe('Sidebar visibility', () => { // Meme strategie que ci-dessus : ancrage semantique plutot que // `networkidle` pour eviter les faux timeouts en CI. await expect(sidebar.accountDashboardLink()).toBeVisible({ timeout: 10000 }) - await expect(sidebar.logoutLink()).toBeVisible() + // La deconnexion vit dans le footer (rendu sans condition de permission). + // Le bouton est revele au survol du bloc compte. + await sidebar.accountBlock().hover() + await expect(sidebar.logoutButton()).toBeVisible() }) test('la liste des personas dans personas.ts couvre toutes les combinaisons admin attendues', () => { -- 2.39.5 From b93737391d927bca0caedb04f23a907de526f8d2 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 10:25:03 +0200 Subject: [PATCH 10/15] fix(core) : logout API renvoie 204 sans redirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le firewall répondait par une 302 (target /login). Le fetch front suivait le Location absolu (host upstream du proxy « nginx » en dev) → ERR_NAME_NOT_RESOLVED + ~3s de timeout DNS. ApiLogoutSuccessListener rétrograde la réponse en 204 en conservant le Set-Cookie qui efface BEARER. --- config/packages/security.yaml | 7 ++- .../Security/ApiLogoutSuccessListener.php | 55 +++++++++++++++++++ tests/Module/Core/Api/LogoutApiTest.php | 48 ++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php create mode 100644 tests/Module/Core/Api/LogoutApiTest.php diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f440a21..0152ffc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -33,9 +33,14 @@ security: stateless: true provider: app_user_provider jwt: ~ + # API JWT stateless : pas de `target` (redirection 302) — le logout + # renvoie 204 via ApiLogoutSuccessListener. Une redirection generait + # une URL absolue basee sur le Host (en dev : l'upstream proxy + # « nginx », non resolvable par le navigateur => ERR_NAME_NOT_RESOLVED + # + ~3 s de timeout DNS). Le cookie BEARER reste efface par + # delete_cookies. logout: path: /api/logout - target: /login enable_csrf: false delete_cookies: BEARER: diff --git a/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php b/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php new file mode 100644 index 0000000..14671f9 --- /dev/null +++ b/src/Module/Core/Infrastructure/Security/ApiLogoutSuccessListener.php @@ -0,0 +1,55 @@ + `ERR_NAME_NOT_RESOLVED` apres ~3 s de timeout DNS avant l'echec + * de la promesse (en prod, c'est un GET parasite de la page cible). Une API + * consommee en fetch ne doit pas rediriger : 204 suffit. + * + * On s'enregistre a une priorite NEGATIVE pour passer APRES les listeners par + * defaut (DefaultLogoutListener priorite 64, CookieClearingLogoutListener + * priorite 0) : la reponse et les Set-Cookie de suppression du BEARER sont alors + * deja en place, on se contente de retrograder la redirection en 204 en + * conservant les en-tetes (donc le cookie BEARER reste efface). + */ +final class ApiLogoutSuccessListener implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -255], + ]; + } + + public function onLogout(LogoutEvent $event): void + { + $response = $event->getResponse(); + + // Aucun listener par defaut n'a pose de reponse : on cree directement la 204. + if (null === $response) { + $event->setResponse(new Response(null, Response::HTTP_NO_CONTENT)); + + return; + } + + // Retrograde la redirection (ou toute autre reponse) en 204 sans toucher + // aux en-tetes Set-Cookie deja poses (suppression du BEARER). + $response->setStatusCode(Response::HTTP_NO_CONTENT); + $response->setContent(null); + $response->headers->remove('Location'); + } +} diff --git a/tests/Module/Core/Api/LogoutApiTest.php b/tests/Module/Core/Api/LogoutApiTest.php new file mode 100644 index 0000000..5f3fdff --- /dev/null +++ b/tests/Module/Core/Api/LogoutApiTest.php @@ -0,0 +1,48 @@ + + * `ERR_NAME_NOT_RESOLVED` + ~3 s de timeout DNS. Cf. ApiLogoutSuccessListener. + * + * @internal + */ +final class LogoutApiTest extends AbstractApiTestCase +{ + public function testLogoutReturns204WithoutRedirectAndClearsBearerCookie(): void + { + $client = $this->authenticatedClient('admin', 'admin'); + + $response = $client->request('POST', '/api/logout'); + + self::assertSame(204, $response->getStatusCode(), 'Le logout API doit renvoyer 204 No Content.'); + + $headers = $response->getHeaders(false); + + // Aucune redirection : un fetch ne doit pas avoir de Location a suivre. + self::assertArrayNotHasKey( + 'location', + $headers, + 'Le logout API ne doit pas rediriger (fetch suivrait un Location absolu => ERR_NAME_NOT_RESOLVED).', + ); + + // Le cookie BEARER est efface (Set-Cookie expire / supprime). + $clearsBearer = false; + foreach ($headers['set-cookie'] ?? [] as $cookie) { + if (str_starts_with($cookie, 'BEARER=') + && (str_contains($cookie, 'BEARER=deleted') || str_contains($cookie, 'Max-Age=0')) + ) { + $clearsBearer = true; + } + } + self::assertTrue($clearsBearer, 'Le cookie BEARER doit etre efface au logout.'); + } +} -- 2.39.5 From 4ce1bafb2f9778f0d0f18aa922e236a13b038abf Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 10:41:57 +0200 Subject: [PATCH 11/15] =?UTF-8?q?fix(logistique)=20:=20bloque=20la=20saisi?= =?UTF-8?q?e=20manuelle=20poids/DSD=20=C3=A0=205=20chiffres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Masque front 5 chiffres sur la modale manuelle + Assert\LessThanOrEqual(99999) sur WeighbridgeReadingResource (weight/dsd, mode MANUAL) et backstop entité (validateManualEntryDigits). Le DSD auto (compteur de site) n'est pas contraint. --- .../pages/weighing-tickets/[id]/edit.vue | 6 +-- .../logistique/pages/weighing-tickets/new.vue | 6 +-- .../modules/logistique/utils/weighingMasks.ts | 11 +++++ .../Domain/Entity/WeighingTicket.php | 32 ++++++++++++++ .../Resource/WeighbridgeReadingResource.php | 3 ++ .../Api/WeighbridgeReadingApiTest.php | 43 +++++++++++++++++++ 6 files changed, 95 insertions(+), 6 deletions(-) diff --git a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue index 98fc7ec..d42b2bd 100644 --- a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue +++ b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue @@ -161,14 +161,14 @@
chaque + * 422 est mappee inline sous le champ via useFormErrors (ERP-101). + */ + #[Assert\Callback] + public function validateManualEntryDigits(ExecutionContextInterface $context): void + { + $this->assertManualDigitCap($context, $this->emptyMode, $this->emptyWeight, 'emptyWeight', 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).'); + $this->assertManualDigitCap($context, $this->emptyMode, $this->emptyDsd, 'emptyDsd', 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).'); + $this->assertManualDigitCap($context, $this->fullMode, $this->fullWeight, 'fullWeight', 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).'); + $this->assertManualDigitCap($context, $this->fullMode, $this->fullDsd, 'fullDsd', 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).'); + } + /** * Date du ticket affichee en LISTE (§ 4.0) : date de la pesee a plein si * disponible, sinon date de la pesee a vide. Getter calcule (jamais @@ -646,4 +668,14 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface return $this; } + + private function assertManualDigitCap(ExecutionContextInterface $context, ?string $mode, ?int $value, string $path, string $message): void + { + if ('MANUAL' === $mode && null !== $value && $value > self::MANUAL_VALUE_MAX) { + $context->buildViolation($message) + ->atPath($path) + ->addViolation() + ; + } + } } diff --git a/src/Module/Logistique/Infrastructure/ApiPlatform/Resource/WeighbridgeReadingResource.php b/src/Module/Logistique/Infrastructure/ApiPlatform/Resource/WeighbridgeReadingResource.php index 8840f0b..f7f2bcf 100644 --- a/src/Module/Logistique/Infrastructure/ApiPlatform/Resource/WeighbridgeReadingResource.php +++ b/src/Module/Logistique/Infrastructure/ApiPlatform/Resource/WeighbridgeReadingResource.php @@ -6,6 +6,7 @@ namespace App\Module\Logistique\Infrastructure\ApiPlatform\Resource; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Post; +use App\Module\Logistique\Domain\Entity\WeighingTicket; use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighbridgeReadingProcessor; use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -59,6 +60,7 @@ final class WeighbridgeReadingResource * fournit le poids). En sortie : poids effectif de la pesee. */ #[Assert\Positive(message: 'Le poids doit être un entier positif (kg).')] + #[Assert\LessThanOrEqual(value: WeighingTicket::MANUAL_VALUE_MAX, message: 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).')] #[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])] public ?int $weight = null; @@ -68,6 +70,7 @@ final class WeighbridgeReadingResource * (l'obligation en MANUAL est portee par le Callback ci-dessous). */ #[Assert\Positive(message: 'Le DSD doit être un entier positif.')] + #[Assert\LessThanOrEqual(value: WeighingTicket::MANUAL_VALUE_MAX, message: 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).')] #[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])] public ?int $dsd = null; diff --git a/tests/Module/Logistique/Api/WeighbridgeReadingApiTest.php b/tests/Module/Logistique/Api/WeighbridgeReadingApiTest.php index b6b34d7..dd871a4 100644 --- a/tests/Module/Logistique/Api/WeighbridgeReadingApiTest.php +++ b/tests/Module/Logistique/Api/WeighbridgeReadingApiTest.php @@ -133,6 +133,49 @@ final class WeighbridgeReadingApiTest extends AbstractApiTestCase self::assertViolationOnPath($response, 'dsd'); } + public function testManualWeighingRejectsWeightOverFiveDigits(): void + { + $client = $this->manageClientWithCurrentSite(); + + $response = $client->request('POST', '/api/weighbridge_readings', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + // 100000 = 6 chiffres → au-dela du plafond 5 chiffres (99999). + 'json' => ['mode' => 'MANUAL', 'weight' => 100000, 'dsd' => 16619], + ]); + + self::assertResponseStatusCodeSame(422); + self::assertViolationOnPath($response, 'weight'); + } + + public function testManualWeighingRejectsDsdOverFiveDigits(): void + { + $client = $this->manageClientWithCurrentSite(); + + $response = $client->request('POST', '/api/weighbridge_readings', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => ['mode' => 'MANUAL', 'weight' => 23187, 'dsd' => 100000], + ]); + + self::assertResponseStatusCodeSame(422); + self::assertViolationOnPath($response, 'dsd'); + } + + public function testManualWeighingAcceptsFiveDigitBoundary(): void + { + $client = $this->manageClientWithCurrentSite(); + + $response = $client->request('POST', '/api/weighbridge_readings', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + // 99999 = exactement 5 chiffres → derniere valeur acceptee. + 'json' => ['mode' => 'MANUAL', 'weight' => 99999, 'dsd' => 99999], + ]); + + self::assertResponseStatusCodeSame(200); + $data = $response->toArray(); + self::assertSame(99999, $data['weight']); + self::assertSame(99999, $data['dsd']); + } + /** * Garde-fou ERP-101 (miroir AbstractWeighingTicketApiTestCase) : une 422 doit * porter une violation sur le `propertyPath` attendu, consommable inline par -- 2.39.5 From 8a042fb578937db031f0178aa593c3f1b95e7a87 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 10:52:02 +0200 Subject: [PATCH 12/15] =?UTF-8?q?fix(logistique)=20:=20bloque=20les=20cara?= =?UTF-8?q?ct=C3=A8res=20sp=C3=A9ciaux=20dans=20le=20champ=20=C2=AB=20Autr?= =?UTF-8?q?e=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assert\Regex(FREE_TEXT) sur otherLabel (miroir companyName/competitors) + masque FREE_TEXT_MASK sur l'input « Autre » (new.vue / edit.vue). --- .../pages/weighing-tickets/[id]/edit.vue | 2 + .../logistique/pages/weighing-tickets/new.vue | 2 + .../Domain/Entity/WeighingTicket.php | 2 + .../Api/WeighingTicketLifecycleTest.php | 37 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue index d42b2bd..bb8bf6d 100644 --- a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue +++ b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue @@ -58,6 +58,7 @@ authManageOnSite($this->siteByCode('86')); + + // Le back reste l'autorite (le masque front FREE_TEXT_MASK filtre deja a la + // frappe) : un libelle « Autre » avec des caracteres parasites -> 422 sur + // otherLabel (Assert\Regex FREE_TEXT), mappee inline cote front (ERP-101). + $response = $this->postTicket($http, [ + 'counterpartyType' => 'AUTRE', + 'otherLabel' => 'Chantier ~#|<>{}', + 'emptyDate' => '2026-06-17T09:00:00+02:00', + 'emptyWeight' => 7150, + 'emptyMode' => 'AUTO', + ]); + + self::assertResponseStatusCodeSame(422); + self::assertViolationOnPath($response, 'otherLabel'); + } + + public function testOtherLabelLegitimateIsAccepted(): void + { + $http = $this->authManageOnSite($this->siteByCode('86')); + + // Lettres accentuees, chiffres, espaces, parentheses, °, & : tout autorise + // par FREE_TEXT (miroir des raisons sociales Client/Fournisseur). + $body = $this->postTicket($http, [ + 'counterpartyType' => 'AUTRE', + 'otherLabel' => 'Chantier Léon (Pôle n°2) & Cie', + 'emptyDate' => '2026-06-17T09:00:00+02:00', + 'emptyWeight' => 7150, + 'emptyMode' => 'AUTO', + ])->toArray(); + + self::assertResponseStatusCodeSame(201); + self::assertSame('Chantier Léon (Pôle n°2) & Cie', $body['otherLabel']); + } + public function testValidateRequiresCounterparty(): void { $http = $this->authManageOnSite($this->siteByCode('86')); -- 2.39.5 From dd11cb37ecb72be292330746f586b34f5f999b26 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 29 Jun 2026 10:59:07 +0200 Subject: [PATCH 13/15] =?UTF-8?q?fix(logistique)=20:=20modal=20pes=C3=A9e?= =?UTF-8?q?=20bascule=20=E2=80=94=20titre=20=C2=AB=20Pes=C3=A9e=20bascule?= =?UTF-8?q?=20=C2=BB=20+=20description=20de=20confirmation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/i18n/locales/fr.json | 3 ++- .../modules/logistique/pages/weighing-tickets/[id]/edit.vue | 1 + frontend/modules/logistique/pages/weighing-tickets/new.vue | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 3ca990b..d17f6f8 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -745,7 +745,8 @@ "weighbridge": { "auto": "Pesée bascule", "manual": "Pesée manuelle", - "confirmTitle": "Êtes-vous sûr de vouloir déclencher une pesée ?", + "confirmTitle": "Pesée bascule", + "confirmMessage": "Êtes-vous sûr de vouloir déclencher une pesée ?", "validate": "Valider", "unavailable": "Pont bascule indisponible — passez en pesée manuelle." }, diff --git a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue index bb8bf6d..d870211 100644 --- a/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue +++ b/frontend/modules/logistique/pages/weighing-tickets/[id]/edit.vue @@ -136,6 +136,7 @@ +

{{ t('logistique.weighingTickets.form.weighbridge.confirmMessage') }}

{{ autoModal.error }}

+

{{ t('logistique.weighingTickets.form.weighbridge.confirmMessage') }}

{{ autoModal.error }}