fix(front) : masque les onglets vides en consultation des 4 repertoires (ERP-193)
This commit is contained in:
@@ -84,7 +84,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Onglets (navigation libre, tout en lecture seule) ─────────── -->
|
<!-- ── Onglets (navigation libre, tout en lecture seule) ─────────── -->
|
||||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<!-- ERP-193 : on n'affiche la barre que s'il reste au moins un onglet
|
||||||
|
non vide (sinon seul le bloc principal est visible). -->
|
||||||
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Information -->
|
<!-- Onglet Information -->
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
<div class="mt-12 grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||||
@@ -241,12 +243,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- ERP-193 : les onglets « a venir » (Transport / Statistiques /
|
||||||
<!-- Onglets non encore implementes : frame vide (navigation libre). -->
|
Rapports / Echanges) ne sont plus rendus en consultation
|
||||||
<template #transport><ComingSoonPlaceholder /></template>
|
(masquage des onglets vides) — slots supprimes. -->
|
||||||
<template #statistics><ComingSoonPlaceholder /></template>
|
|
||||||
<template #reports><ComingSoonPlaceholder /></template>
|
|
||||||
<template #exchanges><ComingSoonPlaceholder /></template>
|
|
||||||
</MalioTabList>
|
</MalioTabList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -278,13 +277,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useClient } from '~/modules/commercial/composables/useClient'
|
import { useClient } from '~/modules/commercial/composables/useClient'
|
||||||
import { buildClientFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/clientFormRules'
|
import { isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/clientFormRules'
|
||||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||||
import {
|
import {
|
||||||
canEditClient,
|
canEditClient,
|
||||||
categoryOptionsOf,
|
categoryOptionsOf,
|
||||||
|
clientConsultationVisibleTabs,
|
||||||
contactOptionsOf,
|
contactOptionsOf,
|
||||||
mapAccountingDraft,
|
mapAccountingDraft,
|
||||||
mapAddressView,
|
mapAddressView,
|
||||||
@@ -412,9 +412,11 @@ const paymentTypeOptions = computed(() => referentialOptionOf(client.value?.paym
|
|||||||
const bankOptions = computed(() => referentialOptionOf(client.value?.bank))
|
const bankOptions = computed(() => referentialOptionOf(client.value?.bank))
|
||||||
|
|
||||||
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
||||||
// 4 onglets actifs (Information, Contact, Adresse, + Comptabilite si droit) et
|
// ERP-193 (retour metier) : on masque les coquilles non implementees ET tout
|
||||||
// 4 coquilles (Transport, Statistiques, Rapports, Echanges).
|
// onglet de donnees vide. La liste depend donc du payload charge.
|
||||||
const tabKeys = computed(() => buildClientFormTabKeys(canAccountingView.value, { includeEditOnlyTabs: true }))
|
const visibleTabKeys = computed(() => clientConsultationVisibleTabs(client.value, {
|
||||||
|
canAccountingView: canAccountingView.value,
|
||||||
|
}))
|
||||||
|
|
||||||
const TAB_ICONS: Record<string, string> = {
|
const TAB_ICONS: Record<string, string> = {
|
||||||
information: 'mdi:account-outline',
|
information: 'mdi:account-outline',
|
||||||
@@ -427,14 +429,26 @@ const TAB_ICONS: Record<string, string> = {
|
|||||||
exchanges: 'mdi:account-group-outline',
|
exchanges: 'mdi:account-group-outline',
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = computed(() => tabKeys.value.map(key => ({
|
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
||||||
key,
|
key,
|
||||||
label: t(`commercial.clients.tab.${key}`),
|
label: t(`commercial.clients.tab.${key}`),
|
||||||
icon: TAB_ICONS[key],
|
icon: TAB_ICONS[key],
|
||||||
})))
|
})))
|
||||||
|
|
||||||
// Onglet initial : repris de l'edition au retour (history.state), sinon Information.
|
// Onglet initial : vide tant que le client n'est pas charge. Des que la liste
|
||||||
const activeTab = ref(readHistoryTab(tabKeys.value) ?? 'information')
|
// des onglets visibles est connue, on cale sur l'onglet repris de l'edition
|
||||||
|
// (history.state) s'il est encore visible, sinon le premier onglet visible.
|
||||||
|
// Un watcher recale aussi si l'onglet courant disparait (ex: changement de droit).
|
||||||
|
const activeTab = ref('')
|
||||||
|
watch(visibleTabKeys, (keys) => {
|
||||||
|
if (keys.length === 0) {
|
||||||
|
activeTab.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keys.includes(activeTab.value)) {
|
||||||
|
activeTab.value = readHistoryTab(keys) ?? keys[0]
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||||
function goBack(): void {
|
function goBack(): void {
|
||||||
|
|||||||
@@ -224,12 +224,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- ERP-193 : les onglets « a venir » (Transport / Statistiques /
|
||||||
<!-- Onglets non encore implementes : frame vide (navigation libre). -->
|
Rapports / Echanges) ne sont plus rendus en consultation
|
||||||
<template #transport><ComingSoonPlaceholder /></template>
|
(masquage des onglets vides) — slots supprimes. -->
|
||||||
<template #statistics><ComingSoonPlaceholder /></template>
|
|
||||||
<template #reports><ComingSoonPlaceholder /></template>
|
|
||||||
<template #exchanges><ComingSoonPlaceholder /></template>
|
|
||||||
</MalioTabList>
|
</MalioTabList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -261,9 +258,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useSupplier } from '~/modules/commercial/composables/useSupplier'
|
import { useSupplier } from '~/modules/commercial/composables/useSupplier'
|
||||||
import { buildSupplierFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/supplierFormRules'
|
import { isRibRequiredForPaymentType } from '~/modules/commercial/utils/forms/supplierFormRules'
|
||||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||||
import {
|
import {
|
||||||
canEditSupplier,
|
canEditSupplier,
|
||||||
@@ -278,6 +275,7 @@ import {
|
|||||||
referentialOptionOf,
|
referentialOptionOf,
|
||||||
showArchiveAction,
|
showArchiveAction,
|
||||||
showRestoreAction,
|
showRestoreAction,
|
||||||
|
supplierConsultationVisibleTabs,
|
||||||
type SelectOption,
|
type SelectOption,
|
||||||
type SupplierDetail,
|
type SupplierDetail,
|
||||||
} from '~/modules/commercial/utils/forms/supplierConsultation'
|
} from '~/modules/commercial/utils/forms/supplierConsultation'
|
||||||
@@ -387,9 +385,11 @@ const paymentTypeOptions = computed(() => referentialOptionOf(supplier.value?.pa
|
|||||||
const bankOptions = computed(() => referentialOptionOf(supplier.value?.bank))
|
const bankOptions = computed(() => referentialOptionOf(supplier.value?.bank))
|
||||||
|
|
||||||
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
// ── Onglets : navigation LIBRE (pas de sequence forcee en consultation) ────
|
||||||
// 3 onglets actifs (Information, Contacts, Adresses, + Comptabilite si droit) et
|
// ERP-193 (retour metier) : on masque les coquilles non implementees ET tout
|
||||||
// 4 coquilles (Transport, Statistiques, Rapports, Echanges).
|
// onglet de donnees vide. La liste depend donc du payload charge.
|
||||||
const tabKeys = computed(() => buildSupplierFormTabKeys(canAccountingView.value, { includeEditOnlyTabs: true }))
|
const visibleTabKeys = computed(() => supplierConsultationVisibleTabs(supplier.value, {
|
||||||
|
canAccountingView: canAccountingView.value,
|
||||||
|
}))
|
||||||
|
|
||||||
const TAB_ICONS: Record<string, string> = {
|
const TAB_ICONS: Record<string, string> = {
|
||||||
information: 'mdi:account-outline',
|
information: 'mdi:account-outline',
|
||||||
@@ -402,14 +402,25 @@ const TAB_ICONS: Record<string, string> = {
|
|||||||
exchanges: 'mdi:account-group-outline',
|
exchanges: 'mdi:account-group-outline',
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = computed(() => tabKeys.value.map(key => ({
|
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
||||||
key,
|
key,
|
||||||
label: t(`commercial.suppliers.tab.${key}`),
|
label: t(`commercial.suppliers.tab.${key}`),
|
||||||
icon: TAB_ICONS[key],
|
icon: TAB_ICONS[key],
|
||||||
})))
|
})))
|
||||||
|
|
||||||
// Onglet initial : repris de l'edition au retour (history.state), sinon Information.
|
// Onglet initial : vide tant que le fournisseur n'est pas charge. Des que la
|
||||||
const activeTab = ref(readHistoryTab(tabKeys.value) ?? 'information')
|
// liste des onglets visibles est connue, on cale sur l'onglet repris de
|
||||||
|
// l'edition (history.state) s'il est encore visible, sinon le premier visible.
|
||||||
|
const activeTab = ref('')
|
||||||
|
watch(visibleTabKeys, (keys) => {
|
||||||
|
if (keys.length === 0) {
|
||||||
|
activeTab.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keys.includes(activeTab.value)) {
|
||||||
|
activeTab.value = readHistoryTab(keys) ?? keys[0]
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||||
function goBack(): void {
|
function goBack(): void {
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
canEditClient,
|
canEditClient,
|
||||||
categoryOptionsOf,
|
categoryOptionsOf,
|
||||||
|
clientConsultationVisibleTabs,
|
||||||
contactOptionsOf,
|
contactOptionsOf,
|
||||||
|
hasAccountingData,
|
||||||
|
hasInformationData,
|
||||||
iriOf,
|
iriOf,
|
||||||
mapAccountingDraft,
|
mapAccountingDraft,
|
||||||
mapAddressToDraft,
|
mapAddressToDraft,
|
||||||
@@ -248,3 +251,73 @@ describe('paymentTypeCodeOf (ERP-121 : RIB masques hors-LCR en consultation)', (
|
|||||||
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('hasInformationData', () => {
|
||||||
|
it('faux si tous les champs Information sont vides/absents', () => {
|
||||||
|
expect(hasInformationData({ '@id': '/api/clients/1', id: 1 })).toBe(false)
|
||||||
|
expect(hasInformationData({ '@id': '/api/clients/1', id: 1, description: ' ' })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai des qu\'un champ Information porte une donnee', () => {
|
||||||
|
expect(hasInformationData({ '@id': '/api/clients/1', id: 1, directorName: 'Dupont' })).toBe(true)
|
||||||
|
expect(hasInformationData({ '@id': '/api/clients/1', id: 1, employeesCount: 0 })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hasAccountingData', () => {
|
||||||
|
it('faux sans champ comptable ni RIB', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/clients/1', id: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai avec un champ comptable scalaire', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/clients/1', id: 1, siren: '123456789' })).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai avec une relation comptable embarquee (paymentType)', () => {
|
||||||
|
expect(hasAccountingData({
|
||||||
|
'@id': '/api/clients/1', id: 1,
|
||||||
|
paymentType: { '@id': '/api/payment_types/1', code: 'LCR' },
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai avec au moins un RIB', () => {
|
||||||
|
expect(hasAccountingData({
|
||||||
|
'@id': '/api/clients/1', id: 1,
|
||||||
|
ribs: [{ '@id': '/api/ribs/1', id: 1, iban: 'FR76...' }],
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clientConsultationVisibleTabs', () => {
|
||||||
|
it('retourne [] tant que le client n\'est pas charge', () => {
|
||||||
|
expect(clientConsultationVisibleTabs(null, { canAccountingView: true })).toEqual([])
|
||||||
|
expect(clientConsultationVisibleTabs(undefined, { canAccountingView: true })).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque les coquilles et les onglets vides (client minimal)', () => {
|
||||||
|
const client: ClientDetail = { '@id': '/api/clients/1', id: 1, companyName: 'ACME' }
|
||||||
|
expect(clientConsultationVisibleTabs(client, { canAccountingView: true })).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche les onglets non vides dans l\'ordre information/contact/address/accounting', () => {
|
||||||
|
const client: ClientDetail = {
|
||||||
|
'@id': '/api/clients/1', id: 1,
|
||||||
|
directorName: 'Dupont',
|
||||||
|
contacts: [{ '@id': '/api/client_contacts/1', id: 1 }],
|
||||||
|
addresses: [{ '@id': '/api/client_addresses/1', id: 1 }],
|
||||||
|
siren: '123456789',
|
||||||
|
}
|
||||||
|
expect(clientConsultationVisibleTabs(client, { canAccountingView: true }))
|
||||||
|
.toEqual(['information', 'contact', 'address', 'accounting'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque Comptabilite sans le droit accounting.view meme si des donnees existent', () => {
|
||||||
|
const client: ClientDetail = {
|
||||||
|
'@id': '/api/clients/1', id: 1,
|
||||||
|
contacts: [{ '@id': '/api/client_contacts/1', id: 1 }],
|
||||||
|
siren: '123456789',
|
||||||
|
}
|
||||||
|
expect(clientConsultationVisibleTabs(client, { canAccountingView: false }))
|
||||||
|
.toEqual(['contact'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
canEditSupplier,
|
canEditSupplier,
|
||||||
categoryOptionsOf,
|
categoryOptionsOf,
|
||||||
contactOptionsOf,
|
contactOptionsOf,
|
||||||
|
hasAccountingData,
|
||||||
|
hasInformationData,
|
||||||
iriOf,
|
iriOf,
|
||||||
mapAccountingDraft,
|
mapAccountingDraft,
|
||||||
mapAddressToDraft,
|
mapAddressToDraft,
|
||||||
@@ -14,6 +16,7 @@ import {
|
|||||||
showArchiveAction,
|
showArchiveAction,
|
||||||
showRestoreAction,
|
showRestoreAction,
|
||||||
siteOptionsOf,
|
siteOptionsOf,
|
||||||
|
supplierConsultationVisibleTabs,
|
||||||
type SupplierDetail,
|
type SupplierDetail,
|
||||||
} from '../supplierConsultation'
|
} from '../supplierConsultation'
|
||||||
|
|
||||||
@@ -237,3 +240,60 @@ describe('paymentTypeCodeOf (ERP-121 : RIB masques hors-LCR en consultation)', (
|
|||||||
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('hasInformationData (fournisseur)', () => {
|
||||||
|
it('faux si tous les champs Information (volume previsionnel inclus) sont vides', () => {
|
||||||
|
expect(hasInformationData({ '@id': '/api/suppliers/1', id: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai des qu\'un champ Information porte une donnee', () => {
|
||||||
|
expect(hasInformationData({ '@id': '/api/suppliers/1', id: 1, directorName: 'Martin' })).toBe(true)
|
||||||
|
expect(hasInformationData({ '@id': '/api/suppliers/1', id: 1, volumeForecast: 1200 })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hasAccountingData (fournisseur)', () => {
|
||||||
|
it('faux sans champ comptable ni RIB', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/suppliers/1', id: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai avec un champ comptable ou un RIB', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/suppliers/1', id: 1, siren: '123456789' })).toBe(true)
|
||||||
|
expect(hasAccountingData({
|
||||||
|
'@id': '/api/suppliers/1', id: 1,
|
||||||
|
ribs: [{ '@id': '/api/supplier_ribs/1', id: 1, iban: 'FR76...' }],
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('supplierConsultationVisibleTabs', () => {
|
||||||
|
it('retourne [] tant que le fournisseur n\'est pas charge', () => {
|
||||||
|
expect(supplierConsultationVisibleTabs(null, { canAccountingView: true })).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque les coquilles et les onglets vides (fournisseur minimal)', () => {
|
||||||
|
expect(supplierConsultationVisibleTabs(
|
||||||
|
{ '@id': '/api/suppliers/1', id: 1, companyName: 'ACME' },
|
||||||
|
{ canAccountingView: true },
|
||||||
|
)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche information/contacts/addresses/accounting (cles plurielles) dans l\'ordre', () => {
|
||||||
|
const supplier: SupplierDetail = {
|
||||||
|
'@id': '/api/suppliers/1', id: 1,
|
||||||
|
volumeForecast: 1000,
|
||||||
|
contacts: [{ '@id': '/api/supplier_contacts/1', id: 1 }],
|
||||||
|
addresses: [{ '@id': '/api/supplier_addresses/1', id: 1 }],
|
||||||
|
siren: '123456789',
|
||||||
|
}
|
||||||
|
expect(supplierConsultationVisibleTabs(supplier, { canAccountingView: true }))
|
||||||
|
.toEqual(['information', 'contacts', 'addresses', 'accounting'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque Comptabilite sans le droit accounting.view', () => {
|
||||||
|
expect(supplierConsultationVisibleTabs(
|
||||||
|
{ '@id': '/api/suppliers/1', id: 1, siren: '123456789' },
|
||||||
|
{ canAccountingView: false },
|
||||||
|
)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -317,6 +317,77 @@ export function mapAddressView(address: AddressRead): AddressView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si une valeur scalaire porte une donnee affichable (non null/undefined,
|
||||||
|
* et non chaine vide apres trim). Sert aux predicats « onglet vide » ci-dessous.
|
||||||
|
*/
|
||||||
|
function hasValue(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() !== ''
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'onglet Information porte au moins une donnee. ERP-193 : en
|
||||||
|
* consultation on masque les onglets vides ; Information n'echappe pas a la
|
||||||
|
* regle malgre son statut d'onglet d'atterrissage par defaut.
|
||||||
|
*/
|
||||||
|
export function hasInformationData(client: ClientDetail): boolean {
|
||||||
|
return [
|
||||||
|
client.description,
|
||||||
|
client.competitors,
|
||||||
|
client.foundedAt,
|
||||||
|
client.employeesCount,
|
||||||
|
client.revenueAmount,
|
||||||
|
client.profitAmount,
|
||||||
|
client.directorName,
|
||||||
|
].some(hasValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'onglet Comptabilite porte au moins un champ comptable OU un RIB.
|
||||||
|
* (Le gating permission `accounting.view` reste applique en amont par l'appelant.)
|
||||||
|
*/
|
||||||
|
export function hasAccountingData(client: ClientDetail): boolean {
|
||||||
|
const draft = mapAccountingDraft(client)
|
||||||
|
const hasField = Object.values(draft).some(hasValue)
|
||||||
|
const hasRib = (client.ribs ?? []).length > 0
|
||||||
|
return hasField || hasRib
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onglets visibles en consultation (ERP-193, retour metier) : on masque les
|
||||||
|
* coquilles non implementees (Transport / Statistiques / Rapports / Echanges)
|
||||||
|
* ET tout onglet de donnees vide. L'ordre reproduit `buildClientFormTabKeys`.
|
||||||
|
* Retourne `[]` tant que le client n'est pas charge.
|
||||||
|
*/
|
||||||
|
export function clientConsultationVisibleTabs(
|
||||||
|
client: ClientDetail | null | undefined,
|
||||||
|
options: { canAccountingView: boolean },
|
||||||
|
): string[] {
|
||||||
|
if (!client) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const visible: string[] = []
|
||||||
|
if (hasInformationData(client)) {
|
||||||
|
visible.push('information')
|
||||||
|
}
|
||||||
|
if ((client.contacts ?? []).length > 0) {
|
||||||
|
visible.push('contact')
|
||||||
|
}
|
||||||
|
if ((client.addresses ?? []).length > 0) {
|
||||||
|
visible.push('address')
|
||||||
|
}
|
||||||
|
if (options.canAccountingView && hasAccountingData(client)) {
|
||||||
|
visible.push('accounting')
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet
|
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet
|
||||||
* — `manage` (formulaire/onglets metier) OU `accounting.manage` (le role Compta
|
* — `manage` (formulaire/onglets metier) OU `accounting.manage` (le role Compta
|
||||||
|
|||||||
@@ -292,6 +292,78 @@ export function mapAddressView(address: AddressRead): AddressView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si une valeur scalaire porte une donnee affichable (non null/undefined,
|
||||||
|
* et non chaine vide apres trim). Sert aux predicats « onglet vide » ci-dessous.
|
||||||
|
*/
|
||||||
|
function hasValue(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() !== ''
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'onglet Information porte au moins une donnee (volume previsionnel
|
||||||
|
* inclus, specifique fournisseur). ERP-193 : en consultation on masque les
|
||||||
|
* onglets vides, Information comprise.
|
||||||
|
*/
|
||||||
|
export function hasInformationData(supplier: SupplierDetail): boolean {
|
||||||
|
return [
|
||||||
|
supplier.description,
|
||||||
|
supplier.competitors,
|
||||||
|
supplier.foundedAt,
|
||||||
|
supplier.employeesCount,
|
||||||
|
supplier.revenueAmount,
|
||||||
|
supplier.profitAmount,
|
||||||
|
supplier.directorName,
|
||||||
|
supplier.volumeForecast,
|
||||||
|
].some(hasValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'onglet Comptabilite porte au moins un champ comptable OU un RIB.
|
||||||
|
* (Le gating permission `accounting.view` reste applique en amont par l'appelant.)
|
||||||
|
*/
|
||||||
|
export function hasAccountingData(supplier: SupplierDetail): boolean {
|
||||||
|
const draft = mapAccountingDraft(supplier)
|
||||||
|
const hasField = Object.values(draft).some(hasValue)
|
||||||
|
const hasRib = (supplier.ribs ?? []).length > 0
|
||||||
|
return hasField || hasRib
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onglets visibles en consultation (ERP-193, retour metier) : on masque les
|
||||||
|
* coquilles non implementees (Transport / Statistiques / Rapports / Echanges)
|
||||||
|
* ET tout onglet de donnees vide. L'ordre reproduit `buildSupplierFormTabKeys`.
|
||||||
|
* Retourne `[]` tant que le fournisseur n'est pas charge.
|
||||||
|
*/
|
||||||
|
export function supplierConsultationVisibleTabs(
|
||||||
|
supplier: SupplierDetail | null | undefined,
|
||||||
|
options: { canAccountingView: boolean },
|
||||||
|
): string[] {
|
||||||
|
if (!supplier) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const visible: string[] = []
|
||||||
|
if (hasInformationData(supplier)) {
|
||||||
|
visible.push('information')
|
||||||
|
}
|
||||||
|
if ((supplier.contacts ?? []).length > 0) {
|
||||||
|
visible.push('contacts')
|
||||||
|
}
|
||||||
|
if ((supplier.addresses ?? []).length > 0) {
|
||||||
|
visible.push('addresses')
|
||||||
|
}
|
||||||
|
if (options.canAccountingView && hasAccountingData(supplier)) {
|
||||||
|
visible.push('accounting')
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet
|
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet
|
||||||
* — `manage` (formulaire/onglets metier) OU `accounting.manage` (le role Compta
|
* — `manage` (formulaire/onglets metier) OU `accounting.manage` (le role Compta
|
||||||
|
|||||||
@@ -68,7 +68,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Onglets (navigation libre, tout en lecture seule) ──────────── -->
|
<!-- ── Onglets (navigation libre, tout en lecture seule) ──────────── -->
|
||||||
<MalioTabList v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
<!-- ERP-193 : barre masquee s'il ne reste aucun onglet non vide. -->
|
||||||
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" :max-visible-tabs="5" :max-width="1100" class="mt-[60px]">
|
||||||
<!-- Onglet Contacts -->
|
<!-- Onglet Contacts -->
|
||||||
<template #contacts>
|
<template #contacts>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
@@ -96,10 +97,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- ERP-193 : les onglets « a venir » (Rapports / Echanges) ne sont
|
||||||
<!-- Onglets placeholder « A venir » (comme les autres modules). -->
|
plus rendus en consultation (masquage des onglets vides). -->
|
||||||
<template #reports><ComingSoonPlaceholder /></template>
|
|
||||||
<template #exchanges><ComingSoonPlaceholder /></template>
|
|
||||||
|
|
||||||
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
<!-- Onglet Comptabilite (present uniquement si accounting.view). -->
|
||||||
<template v-if="canAccountingView" #accounting>
|
<template v-if="canAccountingView" #accounting>
|
||||||
@@ -158,7 +157,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useProvider } from '~/modules/technique/composables/useProvider'
|
import { useProvider } from '~/modules/technique/composables/useProvider'
|
||||||
import {
|
import {
|
||||||
canEditProvider,
|
canEditProvider,
|
||||||
@@ -170,6 +169,7 @@ import {
|
|||||||
mapContactToDraft,
|
mapContactToDraft,
|
||||||
mapRibToDraft,
|
mapRibToDraft,
|
||||||
paymentTypeCodeOf,
|
paymentTypeCodeOf,
|
||||||
|
providerConsultationVisibleTabs,
|
||||||
referentialOptionOf,
|
referentialOptionOf,
|
||||||
showArchiveAction,
|
showArchiveAction,
|
||||||
showRestoreAction,
|
showRestoreAction,
|
||||||
@@ -197,7 +197,6 @@ const headerTitle = computed(() => provider.value?.companyName || t('technique.p
|
|||||||
useHead({ title: t('technique.providers.consultation.title') })
|
useHead({ title: t('technique.providers.consultation.title') })
|
||||||
|
|
||||||
// ── Onglets (ordre spec : Contacts · Adresse · Rapports · Échanges · Comptabilité) ──
|
// ── Onglets (ordre spec : Contacts · Adresse · Rapports · Échanges · Comptabilité) ──
|
||||||
const activeTab = ref('contacts')
|
|
||||||
const TAB_ICONS: Record<string, string> = {
|
const TAB_ICONS: Record<string, string> = {
|
||||||
contacts: 'mdi:account-box-plus-outline',
|
contacts: 'mdi:account-box-plus-outline',
|
||||||
address: 'mdi:map-marker-outline',
|
address: 'mdi:map-marker-outline',
|
||||||
@@ -205,11 +204,27 @@ const TAB_ICONS: Record<string, string> = {
|
|||||||
exchanges: 'mdi:swap-horizontal',
|
exchanges: 'mdi:swap-horizontal',
|
||||||
accounting: 'mdi:bank-circle-outline',
|
accounting: 'mdi:bank-circle-outline',
|
||||||
}
|
}
|
||||||
const tabs = computed(() => {
|
// ERP-193 (retour metier) : on masque les coquilles (Rapports / Echanges) ET
|
||||||
const keys = ['contacts', 'address', 'reports', 'exchanges']
|
// tout onglet de donnees vide. La liste depend donc du payload charge.
|
||||||
if (canAccountingView.value) keys.push('accounting')
|
const visibleTabKeys = computed(() => providerConsultationVisibleTabs(provider.value, {
|
||||||
return keys.map(key => ({ key, label: t(`technique.providers.tab.${key}`), icon: TAB_ICONS[key] }))
|
canAccountingView: canAccountingView.value,
|
||||||
})
|
}))
|
||||||
|
const tabs = computed(() => visibleTabKeys.value.map(
|
||||||
|
key => ({ key, label: t(`technique.providers.tab.${key}`), icon: TAB_ICONS[key] }),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Onglet initial : vide tant que le prestataire n'est pas charge, puis premier
|
||||||
|
// onglet visible. Un watcher recale si l'onglet courant disparait.
|
||||||
|
const activeTab = ref('')
|
||||||
|
watch(visibleTabKeys, (keys) => {
|
||||||
|
if (keys.length === 0) {
|
||||||
|
activeTab.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keys.includes(activeTab.value)) {
|
||||||
|
activeTab.value = keys[0]
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// ── Donnees mappees depuis la SEULE reponse detail ─────────────────────────────
|
// ── Donnees mappees depuis la SEULE reponse detail ─────────────────────────────
|
||||||
const mainCategoryIris = computed(() => irisOf(provider.value?.categories))
|
const mainCategoryIris = computed(() => irisOf(provider.value?.categories))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const {
|
|||||||
canEditProvider,
|
canEditProvider,
|
||||||
categoryOptionsOf,
|
categoryOptionsOf,
|
||||||
contactOptionsOf,
|
contactOptionsOf,
|
||||||
|
hasAccountingData,
|
||||||
iriOf,
|
iriOf,
|
||||||
irisOf,
|
irisOf,
|
||||||
mapAccountingDraft,
|
mapAccountingDraft,
|
||||||
@@ -17,6 +18,7 @@ const {
|
|||||||
mapContactToDraft,
|
mapContactToDraft,
|
||||||
mapRibToDraft,
|
mapRibToDraft,
|
||||||
paymentTypeCodeOf,
|
paymentTypeCodeOf,
|
||||||
|
providerConsultationVisibleTabs,
|
||||||
referentialOptionOf,
|
referentialOptionOf,
|
||||||
showArchiveAction,
|
showArchiveAction,
|
||||||
showRestoreAction,
|
showRestoreAction,
|
||||||
@@ -165,3 +167,48 @@ describe('providerDetail helpers', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('hasAccountingData (prestataire)', () => {
|
||||||
|
it('faux sans champ comptable ni RIB', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/providers/1', id: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai avec un champ comptable ou un RIB', () => {
|
||||||
|
expect(hasAccountingData({ '@id': '/api/providers/1', id: 1, siren: '123456789' })).toBe(true)
|
||||||
|
expect(hasAccountingData({
|
||||||
|
'@id': '/api/providers/1', id: 1,
|
||||||
|
ribs: [{ '@id': '/api/provider_ribs/1', id: 1, iban: 'FR76...' }],
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('providerConsultationVisibleTabs', () => {
|
||||||
|
it('retourne [] tant que le prestataire n\'est pas charge', () => {
|
||||||
|
expect(providerConsultationVisibleTabs(null, { canAccountingView: true })).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque les coquilles (reports/exchanges) et les onglets vides', () => {
|
||||||
|
expect(providerConsultationVisibleTabs(
|
||||||
|
{ '@id': '/api/providers/1', id: 1, companyName: 'ACME' },
|
||||||
|
{ canAccountingView: true },
|
||||||
|
)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche contacts/address/accounting dans l\'ordre (pas d\'onglet information)', () => {
|
||||||
|
const provider = {
|
||||||
|
'@id': '/api/providers/1', id: 1,
|
||||||
|
contacts: [{ '@id': '/api/provider_contacts/1', id: 1 }],
|
||||||
|
addresses: [{ '@id': '/api/provider_addresses/1', id: 1 }],
|
||||||
|
siren: '123456789',
|
||||||
|
}
|
||||||
|
expect(providerConsultationVisibleTabs(provider, { canAccountingView: true }))
|
||||||
|
.toEqual(['contacts', 'address', 'accounting'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque Comptabilite sans le droit accounting.view', () => {
|
||||||
|
expect(providerConsultationVisibleTabs(
|
||||||
|
{ '@id': '/api/providers/1', id: 1, siren: '123456789' },
|
||||||
|
{ canAccountingView: false },
|
||||||
|
)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -224,6 +224,58 @@ export function paymentTypeCodeOf(relation: Relation): string | null {
|
|||||||
return (relation.code as string | undefined) ?? null
|
return (relation.code as string | undefined) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si une valeur scalaire porte une donnee affichable (non null/undefined,
|
||||||
|
* et non chaine vide apres trim). Sert au predicat « onglet vide » ci-dessous.
|
||||||
|
*/
|
||||||
|
function hasValue(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() !== ''
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'onglet Comptabilite porte au moins un champ comptable OU un RIB.
|
||||||
|
* (Le gating permission `accounting.view` reste applique en amont par l'appelant.)
|
||||||
|
*/
|
||||||
|
export function hasAccountingData(provider: ProviderDetail): boolean {
|
||||||
|
const draft = mapAccountingDraft(provider)
|
||||||
|
const hasField = Object.values(draft).some(hasValue)
|
||||||
|
const hasRib = (provider.ribs ?? []).length > 0
|
||||||
|
return hasField || hasRib
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onglets visibles en consultation (ERP-193, retour metier) : on masque les
|
||||||
|
* coquilles non implementees (Rapports / Echanges) ET tout onglet de donnees
|
||||||
|
* vide. Le prestataire n'a pas d'onglet Information (bloc principal au-dessus
|
||||||
|
* des onglets). Ordre : Contacts · Adresse · Comptabilite. Retourne `[]` tant
|
||||||
|
* que le prestataire n'est pas charge.
|
||||||
|
*/
|
||||||
|
export function providerConsultationVisibleTabs(
|
||||||
|
provider: ProviderDetail | null | undefined,
|
||||||
|
options: { canAccountingView: boolean },
|
||||||
|
): string[] {
|
||||||
|
if (!provider) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const visible: string[] = []
|
||||||
|
if ((provider.contacts ?? []).length > 0) {
|
||||||
|
visible.push('contacts')
|
||||||
|
}
|
||||||
|
if ((provider.addresses ?? []).length > 0) {
|
||||||
|
visible.push('address')
|
||||||
|
}
|
||||||
|
if (options.canAccountingView && hasAccountingData(provider)) {
|
||||||
|
visible.push('accounting')
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet —
|
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet —
|
||||||
* `manage` (onglets metier) OU `accounting.manage` (le role Compta doit pouvoir
|
* `manage` (onglets metier) OU `accounting.manage` (le role Compta doit pouvoir
|
||||||
|
|||||||
@@ -113,7 +113,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Onglets (Adresses · Contacts · Prix) — ouvre sur Adresses ──── -->
|
<!-- ── Onglets (Adresses · Contacts · Prix) — ouvre sur Adresses ──── -->
|
||||||
<MalioTabList v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
<!-- ERP-193 : barre masquee s'il ne reste aucun onglet non vide. -->
|
||||||
|
<MalioTabList v-if="visibleTabKeys.length" v-model="activeTab" :tabs="tabs" class="mt-[60px]">
|
||||||
<template #addresses>
|
<template #addresses>
|
||||||
<div class="mt-12 flex flex-col gap-6">
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
<!-- Adresse UNIQUE (ERP-172). -->
|
<!-- Adresse UNIQUE (ERP-172). -->
|
||||||
@@ -241,12 +242,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
import CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
||||||
import CarrierContactBlock from '~/modules/transport/components/CarrierContactBlock.vue'
|
import CarrierContactBlock from '~/modules/transport/components/CarrierContactBlock.vue'
|
||||||
import { useCarrier } from '~/modules/transport/composables/useCarrier'
|
import { useCarrier } from '~/modules/transport/composables/useCarrier'
|
||||||
import {
|
import {
|
||||||
canEditCarrier,
|
canEditCarrier,
|
||||||
|
carrierConsultationVisibleTabs,
|
||||||
labelOfRelation,
|
labelOfRelation,
|
||||||
mapAddressToDraft,
|
mapAddressToDraft,
|
||||||
mapContactToDraft,
|
mapContactToDraft,
|
||||||
@@ -300,18 +302,32 @@ const dischargeLabel = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ── Onglets : Adresses · Contacts · Prix (ouvre sur Adresses, pas de Qualimat) ──
|
// ── Onglets : Adresses · Contacts · Prix (ouvre sur Adresses, pas de Qualimat) ──
|
||||||
const activeTab = ref('addresses')
|
// ERP-193 (retour metier) : on masque tout onglet de donnees vide.
|
||||||
const TAB_ICONS: Record<string, string> = {
|
const TAB_ICONS: Record<string, string> = {
|
||||||
addresses: 'mdi:map-marker-outline',
|
addresses: 'mdi:map-marker-outline',
|
||||||
contacts: 'mdi:account-box-plus-outline',
|
contacts: 'mdi:account-box-plus-outline',
|
||||||
prices: 'mdi:payment',
|
prices: 'mdi:payment',
|
||||||
}
|
}
|
||||||
const tabs = computed(() => ['addresses', 'contacts', 'prices'].map(key => ({
|
const visibleTabKeys = computed(() => carrierConsultationVisibleTabs(carrier.value))
|
||||||
|
const tabs = computed(() => visibleTabKeys.value.map(key => ({
|
||||||
key,
|
key,
|
||||||
label: t(`transport.carriers.tab.${key}`),
|
label: t(`transport.carriers.tab.${key}`),
|
||||||
icon: TAB_ICONS[key],
|
icon: TAB_ICONS[key],
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
// Onglet initial : vide tant que le transporteur n'est pas charge, puis premier
|
||||||
|
// onglet visible. Un watcher recale si l'onglet courant disparait.
|
||||||
|
const activeTab = ref('')
|
||||||
|
watch(visibleTabKeys, (keys) => {
|
||||||
|
if (keys.length === 0) {
|
||||||
|
activeTab.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!keys.includes(activeTab.value)) {
|
||||||
|
activeTab.value = keys[0]
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// Adresse UNIQUE (ERP-172) : un seul bloc en lecture seule (vide si pas d'adresse).
|
// Adresse UNIQUE (ERP-172) : un seul bloc en lecture seule (vide si pas d'adresse).
|
||||||
const address = computed(() => carrier.value?.address
|
const address = computed(() => carrier.value?.address
|
||||||
? mapAddressToDraft(carrier.value.address)
|
? mapAddressToDraft(carrier.value.address)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import {
|
import {
|
||||||
canEditCarrier,
|
canEditCarrier,
|
||||||
|
carrierConsultationVisibleTabs,
|
||||||
|
hasAddressData,
|
||||||
iriOf,
|
iriOf,
|
||||||
labelOfRelation,
|
labelOfRelation,
|
||||||
mapAddressToDraft,
|
mapAddressToDraft,
|
||||||
@@ -118,3 +120,47 @@ describe('carrierMappers', () => {
|
|||||||
expect(showRestoreAction(noArchive, true)).toBe(false)
|
expect(showRestoreAction(noArchive, true)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('hasAddressData', () => {
|
||||||
|
it('faux pour une adresse absente ou entièrement vide', () => {
|
||||||
|
expect(hasAddressData(null)).toBe(false)
|
||||||
|
expect(hasAddressData(undefined)).toBe(false)
|
||||||
|
expect(hasAddressData({ '@id': '/api/carrier_addresses/1', id: 1 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('vrai dès qu\'un champ adresse est rempli', () => {
|
||||||
|
expect(hasAddressData({ '@id': '/api/carrier_addresses/1', id: 1, city: 'Poitiers' })).toBe(true)
|
||||||
|
expect(hasAddressData({ '@id': '/api/carrier_addresses/1', id: 1, country: 'France' })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('carrierConsultationVisibleTabs', () => {
|
||||||
|
it('retourne [] tant que le transporteur n\'est pas chargé', () => {
|
||||||
|
expect(carrierConsultationVisibleTabs(null)).toEqual([])
|
||||||
|
expect(carrierConsultationVisibleTabs(undefined)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque les onglets vides (transporteur minimal)', () => {
|
||||||
|
expect(carrierConsultationVisibleTabs({ '@id': '/api/carriers/1', id: 1, name: 'LIOT' })).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche addresses/contacts/prices dans l\'ordre quand renseignés', () => {
|
||||||
|
const carrier: CarrierDetail = {
|
||||||
|
'@id': '/api/carriers/1', id: 1,
|
||||||
|
address: { '@id': '/api/carrier_addresses/1', id: 1, city: 'Poitiers' },
|
||||||
|
contacts: [{ '@id': '/api/carrier_contacts/1', id: 1 }],
|
||||||
|
prices: [{ '@id': '/api/carrier_prices/1', id: 1 }],
|
||||||
|
}
|
||||||
|
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['addresses', 'contacts', 'prices'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne garde que les onglets non vides (contacts seulement)', () => {
|
||||||
|
const carrier: CarrierDetail = {
|
||||||
|
'@id': '/api/carriers/1', id: 1,
|
||||||
|
address: { '@id': '/api/carrier_addresses/1', id: 1 },
|
||||||
|
contacts: [{ '@id': '/api/carrier_contacts/1', id: 1 }],
|
||||||
|
prices: [],
|
||||||
|
}
|
||||||
|
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['contacts'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -175,6 +175,62 @@ export function mapPriceToDraft(price: CarrierPriceRead): CarrierPriceFormDraft
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si une valeur scalaire porte une donnée affichable (non null/undefined,
|
||||||
|
* et non chaîne vide après trim). Sert aux prédicats « onglet vide » ci-dessous.
|
||||||
|
*/
|
||||||
|
function hasValue(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() !== ''
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vrai si l'adresse unique (OneToOne, ERP-172) porte au moins un champ rempli.
|
||||||
|
* Un objet adresse vide (toutes clés nulles) est considéré comme « pas d'adresse ».
|
||||||
|
*/
|
||||||
|
export function hasAddressData(address: CarrierAddressRead | null | undefined): boolean {
|
||||||
|
if (!address) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
address.postalCode,
|
||||||
|
address.city,
|
||||||
|
address.street,
|
||||||
|
address.streetComplement,
|
||||||
|
address.country,
|
||||||
|
].some(hasValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onglets visibles en consultation (ERP-193, retour métier) : on masque tout
|
||||||
|
* onglet de données vide. Le transporteur n'a pas de coquille « à venir ».
|
||||||
|
* Ordre : Adresses · Contacts · Prix. Retourne `[]` tant que le transporteur
|
||||||
|
* n'est pas chargé.
|
||||||
|
*/
|
||||||
|
export function carrierConsultationVisibleTabs(
|
||||||
|
carrier: CarrierDetail | null | undefined,
|
||||||
|
): string[] {
|
||||||
|
if (!carrier) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const visible: string[] = []
|
||||||
|
if (hasAddressData(carrier.address)) {
|
||||||
|
visible.push('addresses')
|
||||||
|
}
|
||||||
|
if ((carrier.contacts ?? []).length > 0) {
|
||||||
|
visible.push('contacts')
|
||||||
|
}
|
||||||
|
if ((carrier.prices ?? []).length > 0) {
|
||||||
|
visible.push('prices')
|
||||||
|
}
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
|
||||||
/** Bouton « Modifier » : visible avec la permission `manage` (Admin / Bureau). */
|
/** Bouton « Modifier » : visible avec la permission `manage` (Admin / Bureau). */
|
||||||
export function canEditCarrier(can: (code: string) => boolean): boolean {
|
export function canEditCarrier(can: (code: string) => boolean): boolean {
|
||||||
return can('transport.carriers.manage')
|
return can('transport.carriers.manage')
|
||||||
|
|||||||
Reference in New Issue
Block a user