fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s

Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).

## Contenu

- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).

## Tests

- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #139.
This commit is contained in:
2026-06-22 09:40:40 +00:00
committed by Autin
parent 6c938756cc
commit 5e15c1f69f
93 changed files with 2791 additions and 803 deletions
@@ -97,13 +97,18 @@ export function iriOf(relation: Relation): string | 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.
* Libellé d'affichage d'une relation embarquée : `companyName` (client/fournisseur)
* à défaut `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 companyName = relation.companyName as string | undefined
if (companyName) {
return companyName
}
const name = relation.name as string | undefined
if (name) {
return name
@@ -175,6 +180,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). */
export function canEditCarrier(can: (code: string) => boolean): boolean {
return can('transport.carriers.manage')