feat(catalog) : M6 — écran consultation produit + onglets conditionnés + édition sans redirection
Auto Tag Develop / tag (push) Successful in 37s
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:
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user