diff --git a/frontend/modules/transport/pages/carriers/[id]/edit.vue b/frontend/modules/transport/pages/carriers/[id]/edit.vue
index 2857975..3f3d9ce 100644
--- a/frontend/modules/transport/pages/carriers/[id]/edit.vue
+++ b/frontend/modules/transport/pages/carriers/[id]/edit.vue
@@ -63,7 +63,15 @@
/>
-
+ main.indexationRate = clampPercent(v)"
+ />
main.containerType = v === null ? null : String(v)"
/>
-
+ main.volumeM3 = sanitizeDecimal(v)"
+ />
@@ -173,6 +187,7 @@ import CarrierContactBlock from '~/modules/transport/components/CarrierContactBl
import CarrierPriceBlock from '~/modules/transport/components/CarrierPriceBlock.vue'
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
import { useCarrier } from '~/modules/transport/composables/useCarrier'
+import { clampPercent, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
interface SelectOption {
value: string
diff --git a/frontend/modules/transport/pages/carriers/new.vue b/frontend/modules/transport/pages/carriers/new.vue
index afdd702..130f54e 100644
--- a/frontend/modules/transport/pages/carriers/new.vue
+++ b/frontend/modules/transport/pages/carriers/new.vue
@@ -86,12 +86,16 @@
« Affreter ». La ligne 1 étant pleine (4 colonnes), ils démarrent
naturellement en colonne 1 de la ligne 2. -->
-
+ main.indexationRate = clampPercent(v)"
/>
@@ -106,12 +110,14 @@
@update:model-value="(v: string | number | null) => main.containerType = v === null ? null : String(v)"
/>
-
+ main.volumeM3 = sanitizeDecimal(v)"
/>
@@ -344,6 +350,7 @@ import CarrierContactBlock from '~/modules/transport/components/CarrierContactBl
import CarrierPriceBlock from '~/modules/transport/components/CarrierPriceBlock.vue'
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
+import { clampPercent, sanitizeDecimal } from '~/modules/transport/utils/forms/numberInput'
interface SelectOption {
value: string
diff --git a/frontend/modules/transport/utils/forms/__tests__/numberInput.test.ts b/frontend/modules/transport/utils/forms/__tests__/numberInput.test.ts
new file mode 100644
index 0000000..e64a7df
--- /dev/null
+++ b/frontend/modules/transport/utils/forms/__tests__/numberInput.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import { clampPercent, sanitizeDecimal } from '../numberInput'
+
+describe('numberInput — saisie volume / indexation (ERP-170)', () => {
+ it('sanitizeDecimal : ne garde que chiffres + un seul point', () => {
+ expect(sanitizeDecimal('30')).toBe('30')
+ expect(sanitizeDecimal('30.5')).toBe('30.5')
+ expect(sanitizeDecimal('30,5 kg')).toBe('305') // virgule + espace + lettres retirés
+ expect(sanitizeDecimal('1.2.3')).toBe('1.23') // un seul point conservé
+ expect(sanitizeDecimal('abc12.3x')).toBe('12.3')
+ expect(sanitizeDecimal('')).toBe('')
+ })
+
+ it('clampPercent : plafonne à 100, laisse le reste tel quel', () => {
+ expect(clampPercent('50')).toBe('50')
+ expect(clampPercent('100')).toBe('100')
+ expect(clampPercent('150')).toBe('100')
+ expect(clampPercent('100.01')).toBe('100')
+ expect(clampPercent('12,5')).toBe('12,5') // ≤ 100 → inchangé
+ expect(clampPercent('')).toBe('')
+ })
+})
diff --git a/frontend/modules/transport/utils/forms/numberInput.ts b/frontend/modules/transport/utils/forms/numberInput.ts
new file mode 100644
index 0000000..6e845c7
--- /dev/null
+++ b/frontend/modules/transport/utils/forms/numberInput.ts
@@ -0,0 +1,27 @@
+/**
+ * Helpers de saisie numérique du formulaire principal transporteur (ERP-170).
+ * Champs texte restreints (volume m³ décimal, indexation plafonnée). Purs / testables.
+ */
+
+/**
+ * Restreint une saisie à un nombre décimal : chiffres + UN seul point (RG volume m³,
+ * « nombres avec des points » comme les autres modules). Supprime tout autre caractère.
+ */
+export function sanitizeDecimal(value: string): string {
+ let cleaned = (value ?? '').replace(/[^0-9.]/g, '')
+ const dot = cleaned.indexOf('.')
+ if (dot !== -1) {
+ // Conserve le 1er point, retire les suivants.
+ cleaned = cleaned.slice(0, dot + 1) + cleaned.slice(dot + 1).replace(/\./g, '')
+ }
+ return cleaned
+}
+
+/**
+ * Plafonne un pourcentage à 100 (contrainte FRONT : l'indexation n'a pas de max back).
+ * Renvoie « 100 » si la valeur saisie dépasse 100, sinon la valeur telle quelle.
+ */
+export function clampPercent(value: string): string {
+ const n = Number(String(value ?? '').replace(',', '.').replace(/\s/g, ''))
+ return (!Number.isNaN(n) && n > 100) ? '100' : value
+}