feat(catalog) : M6 — écran consultation produit + onglets conditionnés + édition sans redirection
Auto Tag Develop / tag (push) Successful in 37s

- Nouvel écran de consultation lecture seule /admin/products/{id} (calque
  client/fournisseur) : clic sur une ligne ouvre la consultation (plus l'édition
  directe), bouton « Modifier » → édition.
- Règle ERP-193 en consultation : champs vides / checkbox non cochées masqués
  (isFilled) ; onglets vides masqués → les coquilles Fournisseurs/Clients
  (placeholder, module Contrat inexistant) ne sont pas rendues en consultation.
- Onglets Fournisseurs/Clients : non affichés à l'ajout (avant validation du
  formulaire principal) ; visibilité conditionnée par l'état (spec C3, « Aucun »
  = OTHER) : Fournisseurs si Achat/Aucun, Clients si Vendu/Aucun.
- Édition : après « Enregistrer » on reste sur l'écran (l'utilisateur garde la
  main, calque client/fournisseur) ; réaffichage des valeurs normalisées serveur
  (RG-6.07) via re-prefill, plus de redirection.
- i18n consultation + tests (consultation, onglets, no-redirect) ; spec écran 8.bis.
This commit is contained in:
2026-06-27 17:18:17 +02:00
parent 58d0c499d4
commit eb94204c55
14 changed files with 464 additions and 37 deletions
@@ -4,24 +4,51 @@
V0 (HP-M6-01, RG-6.10) : ils dependent d'un module Contrat inexistant.
Rendu en placeholder « en cours de développement » (meme composant que les
onglets non-dev des fiches M1→M4). AUCUN appel API, AUCUN champ saisissable.
Affiches sans condition d'etat (a raffiner avec le module Contrat).
Visibilite conditionnee par l'etat du produit (cf. spec C3, « Aucun » = OTHER) :
- « Fournisseurs » : visible si l'etat contient Achat (PURCHASE) ou Aucun (OTHER) ;
- « Clients » : visible si l'etat contient Vendu (SALE) ou Aucun (OTHER).
Si aucun onglet n'est applicable (etat vide), rien n'est rendu.
-->
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
<MalioTabList v-if="tabs.length" v-model="activeTab" :tabs="tabs" class="mt-[60px]">
<template #suppliers><ComingSoonPlaceholder :title="t('admin.products.tab.placeholder')" /></template>
<template #clients><ComingSoonPlaceholder :title="t('admin.products.tab.placeholder')" /></template>
</MalioTabList>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
const props = defineProps<{
/** Etats du produit (codes enum PURCHASE / SALE / OTHER) pilotant la visibilite. */
states: string[]
}>()
const { t } = useI18n()
const activeTab = ref('suppliers')
// RG (spec C3) : « Fournisseurs » si Achat ou Aucun ; « Clients » si Vendu ou Aucun.
const showSuppliers = computed(() => props.states.includes('PURCHASE') || props.states.includes('OTHER'))
const showClients = computed(() => props.states.includes('SALE') || props.states.includes('OTHER'))
// Icone (Iconify) par onglet, alignee sur la convention des fiches existantes.
const tabs = computed(() => [
{ key: 'suppliers', label: t('admin.products.tab.suppliers'), icon: 'mdi:truck-outline' },
{ key: 'clients', label: t('admin.products.tab.clients'), icon: 'mdi:account-group-outline' },
])
const tabs = computed(() => {
const list: { key: string, label: string, icon: string }[] = []
if (showSuppliers.value) {
list.push({ key: 'suppliers', label: t('admin.products.tab.suppliers'), icon: 'mdi:truck-outline' })
}
if (showClients.value) {
list.push({ key: 'clients', label: t('admin.products.tab.clients'), icon: 'mdi:account-group-outline' })
}
return list
})
const activeTab = ref('suppliers')
// Si l'onglet actif disparait suite a un changement d'etat, retombe sur le premier
// onglet encore disponible (evite un onglet actif fantome).
watch(tabs, (list) => {
if (list.length && !list.some(tab => tab.key === activeTab.value)) {
activeTab.value = list[0].key
}
}, { immediate: true })
</script>
@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h, nextTick } from 'vue'
import ProductPlaceholderTabs from '../ProductPlaceholderTabs.vue'
// i18n auto-import : retourne la cle telle quelle.
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
// Stub de MalioTabList : expose les `key` des onglets recus (data-tab) pour
// verifier la visibilite conditionnee par l'etat, sans dependre de la lib UI.
const TabListStub = defineComponent({
props: { tabs: { type: Array, default: () => [] }, modelValue: { type: String, default: '' } },
setup(props) {
return () => h(
'div',
{ 'data-testid': 'tablist' },
(props.tabs as { key: string }[]).map(tab => h('span', { 'data-tab': tab.key })),
)
},
})
const PlaceholderStub = defineComponent({ setup() { return () => h('div') } })
function mountTabs(states: string[]) {
return mount(ProductPlaceholderTabs, {
props: { states },
global: { stubs: { MalioTabList: TabListStub, ComingSoonPlaceholder: PlaceholderStub } },
})
}
const tabKeys = (wrapper: ReturnType<typeof mountTabs>): string[] =>
wrapper.findAll('[data-tab]').map(node => node.attributes('data-tab') ?? '')
describe('ProductPlaceholderTabs — visibilite conditionnee par l\'etat', () => {
it('Achat (PURCHASE) : affiche uniquement « Fournisseurs »', () => {
expect(tabKeys(mountTabs(['PURCHASE']))).toEqual(['suppliers'])
})
it('Vendu (SALE) : affiche uniquement « Clients »', () => {
expect(tabKeys(mountTabs(['SALE']))).toEqual(['clients'])
})
it('Aucun (OTHER) : affiche les deux onglets', () => {
expect(tabKeys(mountTabs(['OTHER']))).toEqual(['suppliers', 'clients'])
})
it('Achat + Vendu : affiche les deux onglets', () => {
expect(tabKeys(mountTabs(['PURCHASE', 'SALE']))).toEqual(['suppliers', 'clients'])
})
it('etat vide : ne rend aucun onglet (MalioTabList absent)', () => {
const wrapper = mountTabs([])
expect(wrapper.find('[data-testid="tablist"]').exists()).toBe(false)
})
it('retombe sur le premier onglet visible si l\'actif disparait', async () => {
// OTHER -> suppliers actif par defaut ; passage a SALE retire « Fournisseurs ».
const wrapper = mountTabs(['OTHER'])
await wrapper.setProps({ states: ['SALE'] })
await nextTick()
// Seul « Clients » subsiste : pas d'onglet actif fantome (verifie via le modelValue).
const tablist = wrapper.findComponent(TabListStub)
expect(tablist.props('modelValue')).toBe('clients')
})
})