Files
Starseed/frontend/modules/transport/utils/forms/carrierMappers.ts
T
tristan 68236956f1
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m20s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m54s
feat(transport) : consultation + modification transporteur (ERP-170)
2026-06-17 11:35:14 +02:00

191 lines
6.8 KiB
TypeScript

/**
* Helpers purs des écrans Consultation / Modification transporteur (M4, ERP-170) —
* miroir de `providerDetail.ts` (M3). Mappent le payload `GET /api/carriers/{id}`
* (relations embarquées via les groupes `carrier:item:read` + `qualimat:read` +
* read-groups cross-module client/supplier/site/adresses) vers les brouillons
* « plats » partagés avec les blocs Adresse / Contact / Prix.
*
* Ne touchent ni à l'API ni à l'état réactif (testables unitairement). Les champs
* nuls peuvent être OMIS (skip_null_values) → toujours lire avec `?? null`.
*/
import { formatPhoneFR } from '~/shared/utils/phone'
import type {
CarrierAddressFormDraft,
CarrierContactFormDraft,
CarrierMainDraft,
CarrierPriceFormDraft,
} from '~/modules/transport/types/carrierForm'
/** Référence Hydra embarquée minimale (@id toujours présent). */
export interface HydraRef {
'@id': string
[key: string]: unknown
}
/** Une relation peut être embarquée (objet), un IRI nu (chaîne) ou absente. */
export type Relation = HydraRef | string | null | undefined
/** Adresse embarquée (groupe carrier:item:read). */
export interface CarrierAddressRead extends HydraRef {
id: number
country?: string | null
postalCode?: string | null
city?: string | null
street?: string | null
streetComplement?: string | null
}
/** Contact embarqué (groupe carrier:item:read). */
export interface CarrierContactRead extends HydraRef {
id: number
firstName?: string | null
lastName?: string | null
jobTitle?: string | null
phonePrimary?: string | null
phoneSecondary?: string | null
email?: string | null
}
/** Prix embarqué (groupe carrier:item:read + relations cross-module). */
export interface CarrierPriceRead extends HydraRef {
id: number
direction?: string | null
client?: Relation
clientDeliveryAddress?: Relation
departureSite?: Relation
supplier?: Relation
supplierSupplyAddress?: Relation
deliverySite?: Relation
containerType?: string | null
pricingUnit?: string | null
price?: string | null
priceState?: string | null
}
/**
* Détail d'un transporteur (`GET /api/carriers/{id}`). Tous les champs optionnels :
* skip_null_values peut omettre n'importe quelle clé.
*/
export interface CarrierDetail extends HydraRef {
id: number
name?: string | null
certificationType?: string | null
isChartered?: boolean
indexationRate?: string | null
containerType?: string | null
volumeM3?: string | null
liotPlates?: string | null
dischargeDocument?: Relation
qualimatCarrier?: Relation
isArchived?: boolean
addresses?: CarrierAddressRead[]
contacts?: CarrierContactRead[]
prices?: CarrierPriceRead[]
}
/** Extrait l'IRI d'une relation (objet embarqué, IRI nu, ou null si absente). */
export function iriOf(relation: Relation): string | null {
if (relation === null || relation === undefined) {
return null
}
if (typeof relation === 'string') {
return relation
}
return relation['@id'] ?? null
}
/**
* Libellé d'affichage d'une relation embarquée : `name` (site) à défaut une adresse
* condensée (voie · CP · ville). Chaîne vide si la relation est un IRI nu / absente.
*/
export function labelOfRelation(relation: Relation): string {
if (!relation || typeof relation === 'string') {
return ''
}
const name = relation.name as string | undefined
if (name) {
return name
}
const parts = [relation.street, relation.postalCode, relation.city].filter(Boolean)
return parts.join(' · ')
}
/** Mappe le détail vers le brouillon du formulaire principal. */
export function mapMainToDraft(detail: CarrierDetail): CarrierMainDraft {
return {
name: detail.name ?? '',
certificationType: detail.certificationType ?? null,
isChartered: detail.isChartered ?? false,
indexationRate: detail.indexationRate ?? '',
containerType: detail.containerType ?? null,
volumeM3: detail.volumeM3 ?? '',
liotPlates: detail.liotPlates ?? '',
dischargeDocumentIri: iriOf(detail.dischargeDocument),
qualimatCarrierIri: iriOf(detail.qualimatCarrier),
}
}
/** Mappe une adresse embarquée vers un brouillon. */
export function mapAddressToDraft(address: CarrierAddressRead): CarrierAddressFormDraft {
return {
id: address.id,
country: address.country ?? 'France',
postalCode: address.postalCode ?? null,
city: address.city ?? null,
street: address.street ?? null,
streetComplement: address.streetComplement ?? null,
}
}
/** Mappe un contact embarqué vers un brouillon (téléphones formatés XX XX XX XX XX). */
export function mapContactToDraft(contact: CarrierContactRead): CarrierContactFormDraft {
const secondary = contact.phoneSecondary ?? null
return {
id: contact.id,
firstName: contact.firstName ?? null,
lastName: contact.lastName ?? null,
jobTitle: contact.jobTitle ?? null,
phonePrimary: contact.phonePrimary ? formatPhoneFR(contact.phonePrimary) : null,
phoneSecondary: secondary ? formatPhoneFR(secondary) : null,
email: contact.email ?? null,
hasSecondaryPhone: secondary !== null && secondary !== '',
}
}
/** Mappe un prix embarqué vers un brouillon (relations en IRI). */
export function mapPriceToDraft(price: CarrierPriceRead): CarrierPriceFormDraft {
const direction = price.direction === 'CLIENT' || price.direction === 'FOURNISSEUR'
? price.direction
: null
return {
id: price.id,
direction,
clientIri: iriOf(price.client),
clientDeliveryAddressIri: iriOf(price.clientDeliveryAddress),
departureSiteIri: iriOf(price.departureSite),
supplierIri: iriOf(price.supplier),
supplierSupplyAddressIri: iriOf(price.supplierSupplyAddress),
deliverySiteIri: iriOf(price.deliverySite),
containerType: price.containerType ?? null,
pricingUnit: price.pricingUnit ?? null,
price: price.price ?? null,
priceState: price.priceState ?? null,
}
}
/** Bouton « Modifier » : visible avec la permission `manage` (Admin / Bureau). */
export function canEditCarrier(can: (code: string) => boolean): boolean {
return can('transport.carriers.manage')
}
/** Bouton « Archiver » : permission archive ET transporteur encore actif (Admin seul). */
export function showArchiveAction(can: (code: string) => boolean, isArchived: boolean): boolean {
return can('transport.carriers.archive') && !isArchived
}
/** Bouton « Restaurer » : permission archive ET transporteur déjà archivé (Admin seul). */
export function showRestoreAction(can: (code: string) => boolean, isArchived: boolean): boolean {
return can('transport.carriers.archive') && isArchived
}