feat(transport) : onglet Qualimat accessible dès le départ, recherche réactive au nom, sélection remplit le formulaire (ERP-166)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 49s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m34s

This commit is contained in:
2026-06-17 08:10:17 +02:00
parent 388d39a379
commit cf645493c1
4 changed files with 31 additions and 38 deletions
-1
View File
@@ -558,7 +558,6 @@
}, },
"qualimat": { "qualimat": {
"empty": "Aucun transporteur QUALIMAT trouvé.", "empty": "Aucun transporteur QUALIMAT trouvé.",
"continue": "Continuer",
"columns": { "columns": {
"name": "Nom", "name": "Nom",
"address": "Adresse", "address": "Adresse",
@@ -162,8 +162,10 @@ describe('useCarrierForm', () => {
// RG-4.13 : réaffiche le nom normalisé (UPPERCASE) renvoyé par le serveur. // RG-4.13 : réaffiche le nom normalisé (UPPERCASE) renvoyé par le serveur.
expect(form.main.name).toBe('TRANSPORTS ACME') expect(form.main.name).toBe('TRANSPORTS ACME')
expect(form.mainLocked.value).toBe(true) expect(form.mainLocked.value).toBe(true)
expect(form.activeTab.value).toBe('qualimat') // L'onglet Qualimat était déjà accessible (saisie assistée) ; le POST
expect(form.unlockedIndex.value).toBe(0) // déverrouille Adresses (index 1) et bascule dessus.
expect(form.activeTab.value).toBe('addresses')
expect(form.unlockedIndex.value).toBe(1)
}) })
it('buildMainPayload : omet certificationType vide, garde isChartered', () => { it('buildMainPayload : omet certificationType vide, garde isChartered', () => {
@@ -213,8 +215,9 @@ describe('useCarrierForm', () => {
expect(CARRIER_TAB_KEYS).toEqual(['qualimat', 'addresses', 'contacts', 'prices']) expect(CARRIER_TAB_KEYS).toEqual(['qualimat', 'addresses', 'contacts', 'prices'])
const form = useCarrierForm() const form = useCarrierForm()
expect(form.tabKeys.value).toEqual(['qualimat', 'addresses', 'contacts', 'prices']) expect(form.tabKeys.value).toEqual(['qualimat', 'addresses', 'contacts', 'prices'])
// Tous verrouillés tant que le formulaire principal n'est pas validé. // L'onglet Qualimat (index 0) est accessible dès le départ (saisie assistée) ;
expect(form.unlockedIndex.value).toBe(-1) // Adresses / Contacts / Prix restent verrouillés jusqu'au POST principal.
expect(form.unlockedIndex.value).toBe(0)
}) })
it('completeTab : déverrouille/avance, et signale le dernier onglet du flux', () => { it('completeTab : déverrouille/avance, et signale le dernier onglet du flux', () => {
@@ -76,8 +76,10 @@ export function useCarrierForm() {
// ── Onglets : ordre + gating progressif ─────────────────────────────────── // ── Onglets : ordre + gating progressif ───────────────────────────────────
const tabKeys = ref<string[]>([...CARRIER_TAB_KEYS]) const tabKeys = ref<string[]>([...CARRIER_TAB_KEYS])
// Index du dernier onglet déverrouillé (-1 tant que le transporteur n'est pas créé). // Index du dernier onglet déverrouillé. L'onglet Qualimat (index 0) est la saisie
const unlockedIndex = ref(-1) // assistée du formulaire principal : accessible DÈS LE DÉPART (≠ Adresses /
// Contacts / Prix, déverrouillés seulement après le POST principal).
const unlockedIndex = ref(0)
const activeTab = ref<string>(CARRIER_TAB_KEYS[0]) const activeTab = ref<string>(CARRIER_TAB_KEYS[0])
// Onglets validés (passent en lecture seule). // Onglets validés (passent en lecture seule).
const validated = reactive<Record<string, boolean>>({}) const validated = reactive<Record<string, boolean>>({})
@@ -194,9 +196,10 @@ export function useCarrierForm() {
} }
/** /**
* POST /carriers (groupe `carrier:write:main`). Pré-check front (nom), puis * POST /carriers (groupe `carrier:write:main`). Pré-check front, puis création.
* création. Au succès : verrouille le bloc principal, déverrouille le 1er onglet * Au succès : verrouille le bloc principal, déverrouille l'onglet Adresses et
* et bascule sur « Qualimat ». Retourne true si créé, false sinon. * bascule dessus (l'onglet Qualimat, saisie assistée, était déjà accessible).
* Retourne true si créé, false sinon.
*/ */
async function submitMain(): Promise<boolean> { async function submitMain(): Promise<boolean> {
if (mainSubmitting.value) return false if (mainSubmitting.value) return false
@@ -217,8 +220,9 @@ export function useCarrierForm() {
main.certificationType = created.certificationType ?? main.certificationType main.certificationType = created.certificationType ?? main.certificationType
mainLocked.value = true mainLocked.value = true
unlockedIndex.value = 0 // Déverrouille l'onglet suivant (Adresses, index 1) et bascule dessus.
activeTab.value = tabKeys.value[0] ?? CARRIER_TAB_KEYS[0] unlockedIndex.value = Math.max(unlockedIndex.value, 1)
activeTab.value = tabKeys.value[1] ?? CARRIER_TAB_KEYS[1]
toast.success({ title: t('transport.carriers.toast.createSuccess') }) toast.success({ title: t('transport.carriers.toast.createSuccess') })
return true return true
} }
@@ -167,15 +167,6 @@
</span> </span>
</template> </template>
</MalioDataTable> </MalioDataTable>
<div v-if="!isValidated('qualimat')" class="flex justify-center">
<MalioButton
variant="primary"
:label="t('transport.carriers.form.qualimat.continue')"
:disabled="carrierId === null"
@click="onContinueQualimat"
/>
</div>
</div> </div>
</template> </template>
@@ -216,7 +207,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { debounce } from '~/shared/utils/debounce'
import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm' import { useCarrierForm } from '~/modules/transport/composables/useCarrierForm'
import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch' import { useQualimatSearch, type QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
@@ -240,7 +232,6 @@ if (!can('transport.carriers.manage')) {
const { const {
main, main,
carrierId,
mainLocked, mainLocked,
mainSubmitting, mainSubmitting,
mainErrors, mainErrors,
@@ -251,10 +242,8 @@ const {
tabKeys, tabKeys,
activeTab, activeTab,
unlockedIndex, unlockedIndex,
isValidated,
submitMain, submitMain,
applyQualimatSelection, applyQualimatSelection,
completeTab,
} = useCarrierForm() } = useCarrierForm()
const { const {
@@ -335,17 +324,20 @@ const tabs = computed(() => tabKeys.value.map((key, index) => ({
const placeholderTabs = computed(() => tabKeys.value.filter(key => key !== 'qualimat')) const placeholderTabs = computed(() => tabKeys.value.filter(key => key !== 'qualimat'))
// ── Saisie assistee QUALIMAT (onglet Qualimat) ─────────────────────────────── // ── Saisie assistee QUALIMAT (onglet Qualimat) ───────────────────────────────
const qualimatLoaded = ref(false)
const confirmOpen = ref(false) const confirmOpen = ref(false)
const pendingRow = ref<QualimatCarrierRow | null>(null) const pendingRow = ref<QualimatCarrierRow | null>(null)
// Chargement quand l'onglet Qualimat devient actif : la recherche est branchée sur // Le datatable QUALIMAT est filtré par le NOM saisi dans le formulaire principal
// le NOM saisi dans le formulaire principal (RG-4.01) — pas de champ dédié. // (RG-4.01) — pas de champ de recherche dédié. Re-filtrage debouncé à chaque frappe,
watch(activeTab, (tab) => { // plus un chargement initial au montage (liste active complète si le nom est vide).
if (tab === 'qualimat' && !qualimatLoaded.value) { const filterQualimatByName = debounce((term: string) => {
qualimatLoaded.value = true void qualimatSetFilters({ search: term })
}, 300)
watch(() => main.name, term => filterQualimatByName(term))
onMounted(() => {
void qualimatSetFilters({ search: main.name }) void qualimatSetFilters({ search: main.name })
}
}) })
/** Adresse QUALIMAT condensee pour la colonne « Adresse » (voie · CP · ville). */ /** Adresse QUALIMAT condensee pour la colonne « Adresse » (voie · CP · ville). */
@@ -406,11 +398,6 @@ async function confirmIntegrate(): Promise<void> {
} }
} }
/** « Continuer » : valide l'onglet Qualimat et avance a l'onglet Adresses. */
function onContinueQualimat(): void {
completeTab('qualimat')
}
/** Retour vers le repertoire transporteurs (fleche d'en-tete). */ /** Retour vers le repertoire transporteurs (fleche d'en-tete). */
function goBack(): void { function goBack(): void {
router.push('/carriers') router.push('/carriers')