feat(transport) : onglet Prix affiche uniquement si transporteur affrete
This commit is contained in:
@@ -216,7 +216,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 { extractApiErrorMessage } from '~/shared/utils/api'
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||||
import { isRowRemovable } from '~/shared/utils/collectionRow'
|
import { isRowRemovable } from '~/shared/utils/collectionRow'
|
||||||
import CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
import CarrierAddressBlock from '~/modules/transport/components/CarrierAddressBlock.vue'
|
||||||
@@ -304,11 +304,30 @@ const TAB_KEYS = ['qualimat', 'addresses', 'contacts', 'prices']
|
|||||||
// consultation) pour retomber sur le meme onglet ; defaut « addresses ».
|
// consultation) pour retomber sur le meme onglet ; defaut « addresses ».
|
||||||
const requestedTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
const requestedTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||||
const activeTab = ref(TAB_KEYS.includes(requestedTab) ? requestedTab : 'addresses')
|
const activeTab = ref(TAB_KEYS.includes(requestedTab) ? requestedTab : 'addresses')
|
||||||
const tabs = computed(() => TAB_KEYS.map(key => ({
|
// État affrété SAUVEGARDÉ (≠ brouillon `main.isChartered`) : pilote la visibilité
|
||||||
key,
|
// de l'onglet « Prix ». On ne se base PAS sur la checkbox, mais sur le dernier
|
||||||
label: t(`transport.carriers.tab.${key}`),
|
// PATCH principal réussi — sinon, en cas d'erreur back, l'onglet apparaîtrait
|
||||||
icon: TAB_ICONS[key],
|
// alors que l'affrètement n'est pas persisté. Initialisé au chargement, remis à
|
||||||
})))
|
// jour uniquement après un `updateMain()` réussi.
|
||||||
|
const savedIsChartered = ref(false)
|
||||||
|
// L'onglet « Prix » n'est visible que si le transporteur est affrété ET validé.
|
||||||
|
// Les prix existants restent en base même après retrait du statut affrété (jamais
|
||||||
|
// supprimés) : on masque seulement l'onglet tant que le transporteur n'est pas affrété.
|
||||||
|
const tabs = computed(() => TAB_KEYS
|
||||||
|
.filter(key => key !== 'prices' || savedIsChartered.value)
|
||||||
|
.map(key => ({
|
||||||
|
key,
|
||||||
|
label: t(`transport.carriers.tab.${key}`),
|
||||||
|
icon: TAB_ICONS[key],
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Si l'affrètement validé est retiré alors que l'onglet Prix (qui disparait) est
|
||||||
|
// actif, on bascule sur un onglet visible pour éviter un contenu d'onglet vide.
|
||||||
|
watch(savedIsChartered, (chartered) => {
|
||||||
|
if (!chartered && activeTab.value === 'prices') {
|
||||||
|
activeTab.value = 'addresses'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ── Référentiels (pays + clients / fournisseurs / sites pour l'onglet Prix) ───
|
// ── Référentiels (pays + clients / fournisseurs / sites pour l'onglet Prix) ───
|
||||||
const countryOptions = ref<SelectOption[]>([{ value: 'France', label: 'France' }])
|
const countryOptions = ref<SelectOption[]>([{ value: 'France', label: 'France' }])
|
||||||
@@ -340,12 +359,19 @@ onMounted(async () => {
|
|||||||
await load()
|
await load()
|
||||||
if (carrier.value) {
|
if (carrier.value) {
|
||||||
prefillFrom(carrier.value)
|
prefillFrom(carrier.value)
|
||||||
|
// État affrété persisté à l'ouverture (pilote la visibilité de l'onglet Prix).
|
||||||
|
savedIsChartered.value = main.isChartered
|
||||||
// Pré-affiche le nom du fichier de décharge déjà rattaché (s'il existe).
|
// Pré-affiche le nom du fichier de décharge déjà rattaché (s'il existe).
|
||||||
const doc = carrier.value.dischargeDocument
|
const doc = carrier.value.dischargeDocument
|
||||||
if (doc && typeof doc !== 'string') {
|
if (doc && typeof doc !== 'string') {
|
||||||
const meta = doc as Record<string, unknown>
|
const meta = doc as Record<string, unknown>
|
||||||
dischargeFileName.value = String(meta.originalFilename ?? meta.name ?? '')
|
dischargeFileName.value = String(meta.originalFilename ?? meta.name ?? '')
|
||||||
}
|
}
|
||||||
|
// L'onglet « Prix » est masqué si le transporteur n'est pas affrété : si on
|
||||||
|
// arrivait dessus via ?tab=prices, on retombe sur un onglet visible.
|
||||||
|
if (activeTab.value === 'prices' && !savedIsChartered.value) {
|
||||||
|
activeTab.value = 'addresses'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loadCountries().catch(() => {})
|
loadCountries().catch(() => {})
|
||||||
// Exclut les courtiers (catégorie COURTIER) du select clients du module Transport.
|
// Exclut les courtiers (catégorie COURTIER) du select clients du module Transport.
|
||||||
@@ -391,6 +417,10 @@ function goBack(): void {
|
|||||||
async function onUpdateMain(): Promise<void> {
|
async function onUpdateMain(): Promise<void> {
|
||||||
const ok = await updateMain()
|
const ok = await updateMain()
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
// L'onglet « Prix » ne (ré)apparaît qu'ici, après PATCH réussi — jamais au
|
||||||
|
// simple clic sur la checkbox (un échec back laisserait l'onglet visible
|
||||||
|
// alors que l'affrètement n'est pas persisté).
|
||||||
|
savedIsChartered.value = main.isChartered
|
||||||
toast.success({ title: t('transport.carriers.toast.updateSuccess') })
|
toast.success({ title: t('transport.carriers.toast.updateSuccess') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,12 +417,17 @@ const TAB_ICONS: Record<string, string> = {
|
|||||||
|
|
||||||
// Onglets desactives tant que le formulaire principal n'est pas valide
|
// Onglets desactives tant que le formulaire principal n'est pas valide
|
||||||
// (unlockedIndex = -1 au depart) ; deverrouillage progressif ensuite.
|
// (unlockedIndex = -1 au depart) ; deverrouillage progressif ensuite.
|
||||||
const tabs = computed(() => tabKeys.value.map((key, index) => ({
|
// L'onglet « Prix » n'apparait que si le transporteur est affrete (isChartered) :
|
||||||
key,
|
// il est en derniere position, le filtrer ne decale pas les index des autres
|
||||||
label: t(`transport.carriers.tab.${key}`),
|
// onglets (donc la logique de deverrouillage progressif reste correcte).
|
||||||
icon: TAB_ICONS[key],
|
const tabs = computed(() => tabKeys.value
|
||||||
disabled: index > unlockedIndex.value,
|
.filter(key => key !== 'prices' || main.isChartered)
|
||||||
})))
|
.map((key, index) => ({
|
||||||
|
key,
|
||||||
|
label: t(`transport.carriers.tab.${key}`),
|
||||||
|
icon: TAB_ICONS[key],
|
||||||
|
disabled: index > unlockedIndex.value,
|
||||||
|
})))
|
||||||
|
|
||||||
// Tous les onglets ont désormais leur contenu (qualimat / addresses / contacts / prices).
|
// Tous les onglets ont désormais leur contenu (qualimat / addresses / contacts / prices).
|
||||||
const placeholderTabs = computed(() => tabKeys.value.filter(
|
const placeholderTabs = computed(() => tabKeys.value.filter(
|
||||||
|
|||||||
@@ -148,9 +148,10 @@ describe('carrierConsultationVisibleTabs', () => {
|
|||||||
expect(carrierConsultationVisibleTabs({ '@id': '/api/carriers/1', id: 1, name: 'LIOT' })).toEqual([])
|
expect(carrierConsultationVisibleTabs({ '@id': '/api/carriers/1', id: 1, name: 'LIOT' })).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('affiche addresses/contacts/prices dans l\'ordre quand renseignés', () => {
|
it('affiche addresses/contacts/prices dans l\'ordre quand renseignés (affrété)', () => {
|
||||||
const carrier: CarrierDetail = {
|
const carrier: CarrierDetail = {
|
||||||
'@id': '/api/carriers/1', id: 1,
|
'@id': '/api/carriers/1', id: 1,
|
||||||
|
isChartered: true,
|
||||||
address: { '@id': '/api/carrier_addresses/1', id: 1, city: 'Poitiers' },
|
address: { '@id': '/api/carrier_addresses/1', id: 1, city: 'Poitiers' },
|
||||||
contacts: [{ '@id': '/api/carrier_contacts/1', id: 1 }],
|
contacts: [{ '@id': '/api/carrier_contacts/1', id: 1 }],
|
||||||
prices: [{ '@id': '/api/carrier_prices/1', id: 1 }],
|
prices: [{ '@id': '/api/carrier_prices/1', id: 1 }],
|
||||||
@@ -167,4 +168,25 @@ describe('carrierConsultationVisibleTabs', () => {
|
|||||||
}
|
}
|
||||||
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['contacts'])
|
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['contacts'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('affiche l\'onglet Prix dès que le transporteur est affrété, même sans prix', () => {
|
||||||
|
const carrier: CarrierDetail = {
|
||||||
|
'@id': '/api/carriers/1', id: 1,
|
||||||
|
isChartered: true,
|
||||||
|
prices: [],
|
||||||
|
}
|
||||||
|
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['prices'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masque l\'onglet Prix d\'un transporteur non affrété même avec des prix historiques', () => {
|
||||||
|
// Retour métier : les prix d'un ancien affrété ne sont jamais supprimés,
|
||||||
|
// mais l'onglet reste masqué tant que le transporteur n'est pas réaffrété.
|
||||||
|
const carrier: CarrierDetail = {
|
||||||
|
'@id': '/api/carriers/1', id: 1,
|
||||||
|
isChartered: false,
|
||||||
|
contacts: [{ '@id': '/api/carrier_contacts/1', id: 1 }],
|
||||||
|
prices: [{ '@id': '/api/carrier_prices/1', id: 1 }],
|
||||||
|
}
|
||||||
|
expect(carrierConsultationVisibleTabs(carrier)).toEqual(['contacts'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ export function hasAddressData(address: CarrierAddressRead | null | undefined):
|
|||||||
* onglet de données vide. Le transporteur n'a pas de coquille « à venir ».
|
* onglet de données vide. Le transporteur n'a pas de coquille « à venir ».
|
||||||
* Ordre : Adresses · Contacts · Prix. Retourne `[]` tant que le transporteur
|
* Ordre : Adresses · Contacts · Prix. Retourne `[]` tant que le transporteur
|
||||||
* n'est pas chargé.
|
* n'est pas chargé.
|
||||||
|
*
|
||||||
|
* Exception « Prix » : l'onglet n'est visible QUE si le transporteur est
|
||||||
|
* affrété (`isChartered`), indépendamment de la présence de prix. Un ancien
|
||||||
|
* affrété repassé non affrété conserve ses prix en base (jamais supprimés) mais
|
||||||
|
* l'onglet reste masqué tant qu'il n'est pas réaffrété — décision métier.
|
||||||
*/
|
*/
|
||||||
export function carrierConsultationVisibleTabs(
|
export function carrierConsultationVisibleTabs(
|
||||||
carrier: CarrierDetail | null | undefined,
|
carrier: CarrierDetail | null | undefined,
|
||||||
@@ -230,7 +235,7 @@ export function carrierConsultationVisibleTabs(
|
|||||||
if ((carrier.contacts ?? []).length > 0) {
|
if ((carrier.contacts ?? []).length > 0) {
|
||||||
visible.push('contacts')
|
visible.push('contacts')
|
||||||
}
|
}
|
||||||
if ((carrier.prices ?? []).length > 0) {
|
if (carrier.isChartered) {
|
||||||
visible.push('prices')
|
visible.push('prices')
|
||||||
}
|
}
|
||||||
return visible
|
return visible
|
||||||
|
|||||||
Reference in New Issue
Block a user