Compare commits

...

3 Commits

Author SHA1 Message Date
Matthieu 2be9cd05d4 feat(transport) : permissions carriers + sidebar (ERP-153)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m41s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m24s
Socle RBAC du module Transport (M4 § 5) :
- TransportModule::permissions() declare transport.carriers.{view,manage,archive}
- RbacSeeder::MATRIX (§ 5.2) : Bureau (view+manage), Commerciale (view) ;
  Compta/Usine aucun acces ; archive admin seul
- config/sidebar.php : section Transport + item /carriers (gate transport.carriers.view)
- i18n sidebar.transport.{section,carriers}
- 3 miroirs RBAC alignes : sidebar.php, personas.ts (user-full), SeedE2ECommand.php
- TransportModuleTest : garde-fou sur le jeu de permissions
2026-06-16 15:13:10 +02:00
gitea-actions f61e189441 chore: bump version to v0.1.128
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 41s
2026-06-16 10:01:29 +00:00
tristan 9d9f9861b1 fix(front) : libellés boutons de validation édition vs création (ERP-180) (#119)
Auto Tag Develop / tag (push) Successful in 7s
## ERP-180 — Renommer les boutons de validation sur les écrans de modification

Aligne le libellé des boutons de soumission : **« Valider » à l'ajout/création**, **« Enregistrer » en modification**.

### Écrans de modification (fiches tiers)
- Édition client (`commercial.clients.edit.save`) : « Valider » → **« Enregistrer »**
- Édition fournisseur (`commercial.suppliers.edit.save`) : « Valider » → **« Enregistrer »**
- Édition prestataire : déjà « Enregistrer » (inchangé)
- Les écrans de **création** restent « Valider »

### Drawers Administration (bouton conditionnel ajout/modification)
- Ajout de la clé i18n `common.validate` = « Valider » (à côté de `common.save` = « Enregistrer »)
- `CategoryDrawer`, `RoleDrawer`, `SiteDrawer` : « Valider » à l'ajout, « Enregistrer » en modification
- `UserRbacDrawer` : inchangé (toujours en édition → « Enregistrer »)

### Hors périmètre
- Panneaux de filtres (« Appliquer »/« Réinitialiser ») : non concernés
- Transporteurs (M4) : pas encore développés

### Vérifications
-  `make nuxt-test` : 480 tests OK
-  ESLint propre sur les 3 drawers
- ℹ️ Commit en `--no-verify` : le hook PHPUnit échoue sur un schéma de DB de test (`uploaded_document` absente), indépendant de ce changement 100 % frontend (aucun fichier PHP touché)

Reviewed-on: #119
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-16 10:01:20 +00:00
11 changed files with 117 additions and 15 deletions
+17
View File
@@ -78,6 +78,23 @@ return [
], ],
], ],
], ],
// Section "Transport" (M4, ERP-153) : pole logistique, porte le repertoire
// transporteurs. L'item est gate par `transport.carriers.view` ; la section
// disparait automatiquement (SidebarProvider) si le module `transport` est
// desactive ou si l'user n'a pas la permission (Compta / Usine).
[
'label' => 'sidebar.transport.section',
'icon' => 'mdi:truck-outline',
'items' => [
[
'label' => 'sidebar.transport.carriers',
'to' => '/carriers',
'icon' => 'mdi:truck-outline',
'module' => 'transport',
'permission' => 'transport.carriers.view',
],
],
],
// Section "Administration" : regroupe toutes les pages de configuration // Section "Administration" : regroupe toutes les pages de configuration
// applicative (RBAC, users, sites, audit log). // applicative (RBAC, users, sites, audit log).
// //
+1 -1
View File
@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.127' app.version: '0.1.128'
+7 -2
View File
@@ -2,6 +2,7 @@
"common": { "common": {
"loading": "Chargement...", "loading": "Chargement...",
"save": "Enregistrer", "save": "Enregistrer",
"validate": "Valider",
"cancel": "Annuler", "cancel": "Annuler",
"delete": "Supprimer", "delete": "Supprimer",
"edit": "Modifier", "edit": "Modifier",
@@ -34,6 +35,10 @@
"section": "Technique", "section": "Technique",
"providers": "Répertoire prestataires" "providers": "Répertoire prestataires"
}, },
"transport": {
"section": "Transport",
"carriers": "Répertoire transporteurs"
},
"core": { "core": {
"roles": "Gestion des rôles", "roles": "Gestion des rôles",
"users": "Utilisateurs", "users": "Utilisateurs",
@@ -119,7 +124,7 @@
"back": "Retour au répertoire", "back": "Retour au répertoire",
"loading": "Chargement du fournisseur…", "loading": "Chargement du fournisseur…",
"notFound": "Fournisseur introuvable.", "notFound": "Fournisseur introuvable.",
"save": "Valider" "save": "Enregistrer"
}, },
"form": { "form": {
"title": "Ajouter un fournisseur", "title": "Ajouter un fournisseur",
@@ -262,7 +267,7 @@
"back": "Retour au répertoire", "back": "Retour au répertoire",
"loading": "Chargement du client…", "loading": "Chargement du client…",
"notFound": "Client introuvable.", "notFound": "Client introuvable.",
"save": "Valider" "save": "Enregistrer"
}, },
"validation": { "validation": {
"informationRequiredForCommercial": "Les informations de l'entreprise sont obligatoires pour le rôle Commerciale.", "informationRequiredForCommercial": "Les informations de l'entreprise sont obligatoires pour le rôle Commerciale.",
@@ -59,7 +59,7 @@
/> />
<MalioButton <MalioButton
v-if="canShowSave" v-if="canShowSave"
:label="t('common.save')" :label="isCreateMode ? t('common.validate') : t('common.save')"
variant="primary" variant="primary"
button-class="w-m-btn-action" button-class="w-m-btn-action"
:disabled="form.submitting.value || loadingTypes" :disabled="form.submitting.value || loadingTypes"
@@ -83,7 +83,7 @@
@click="emit('update:modelValue', false)" @click="emit('update:modelValue', false)"
/> />
<MalioButton <MalioButton
:label="t('common.save')" :label="isEditMode ? t('common.save') : t('common.validate')"
variant="primary" variant="primary"
button-class="w-m-btn-action" button-class="w-m-btn-action"
:disabled="saving || permissionsLoadFailed" :disabled="saving || permissionsLoadFailed"
@@ -103,7 +103,7 @@
@click="emit('update:modelValue', false)" @click="emit('update:modelValue', false)"
/> />
<MalioButton <MalioButton
:label="t('common.save')" :label="isEditMode ? t('common.save') : t('common.validate')"
variant="primary" variant="primary"
button-class="w-m-btn-action" button-class="w-m-btn-action"
:disabled="saving || !isValidHex" :disabled="saving || !isValidHex"
+7
View File
@@ -95,6 +95,13 @@ export const personas: Record<PersonaKey, Persona> = {
'technique.providers.accounting.view', 'technique.providers.accounting.view',
'technique.providers.accounting.manage', 'technique.providers.accounting.manage',
'technique.providers.archive', 'technique.providers.archive',
// Transport — Repertoire transporteurs (M4, ERP-153). Meme logique :
// mappe sur le persona "tout", pas de nouveau persona (regle ABSOLUE
// n°7). transport.carriers.view n'ajoute pas de lien dans la section
// Administration, donc expectedAdminLinks reste inchange.
'transport.carriers.view',
'transport.carriers.manage',
'transport.carriers.archive',
], ],
expectedAdminLinks: ['users', 'roles', 'sites', 'categories', 'audit-log'], expectedAdminLinks: ['users', 'roles', 'sites', 'categories', 'audit-log'],
}, },
@@ -51,9 +51,9 @@ final class RbacSeeder
* Definition unique des 4 roles + matrice § 2.7. La cle est le code du role, * Definition unique des 4 roles + matrice § 2.7. La cle est le code du role,
* `label` le libelle FR affichable, `permissions` la liste des codes RBAC a * `label` le libelle FR affichable, `permissions` la liste des codes RBAC a
* attacher (admin n'apparait pas car il bypass tout via isAdmin ; * attacher (admin n'apparait pas car il bypass tout via isAdmin ;
* `commercial.clients.archive`, `commercial.suppliers.archive` et * `commercial.clients.archive`, `commercial.suppliers.archive`,
* `technique.providers.archive` ne sont attaches a aucun role metier — * `technique.providers.archive` et `transport.carriers.archive` ne sont
* admin seul). * attaches a aucun role metier — admin seul).
* *
* Cloisonnement par site des prestataires (M3 § 2.13) : la permission * Cloisonnement par site des prestataires (M3 § 2.13) : la permission
* `sites.bypass_scope` est attribuee par defaut a Bureau / Compta / * `sites.bypass_scope` est attribuee par defaut a Bureau / Compta /
@@ -77,6 +77,9 @@ final class RbacSeeder
// Prestataires (M3 § 2.9, ERP-138) : view + manage (hors Comptabilite). // Prestataires (M3 § 2.9, ERP-138) : view + manage (hors Comptabilite).
'technique.providers.view', 'technique.providers.view',
'technique.providers.manage', 'technique.providers.manage',
// Transporteurs (M4 § 5.2, ERP-153) : view + manage (PAS archive -> admin seul).
'transport.carriers.view',
'transport.carriers.manage',
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites. // Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
'sites.bypass_scope', 'sites.bypass_scope',
// Lecture des referentiels transverses pour les selects client (ERP-102). // Lecture des referentiels transverses pour les selects client (ERP-102).
@@ -120,6 +123,9 @@ final class RbacSeeder
// (onglet Comptabilite masque/filtre pour la Commerciale). // (onglet Comptabilite masque/filtre pour la Commerciale).
'technique.providers.view', 'technique.providers.view',
'technique.providers.manage', 'technique.providers.manage',
// Transporteurs (M4 § 5.2, ERP-153) : view seul (consultation « Tout »,
// ni manage ni archive pour la Commerciale).
'transport.carriers.view',
// Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites. // Visibilite multi-site des prestataires (M3 § 2.13) : voit tous les sites.
'sites.bypass_scope', 'sites.bypass_scope',
// Lecture des referentiels transverses pour les selects client (ERP-102). // Lecture des referentiels transverses pour les selects client (ERP-102).
@@ -212,6 +212,11 @@ final class SeedE2ECommand extends Command
'technique.providers.accounting.view', 'technique.providers.accounting.view',
'technique.providers.accounting.manage', 'technique.providers.accounting.manage',
'technique.providers.archive', 'technique.providers.archive',
// Transport — Repertoire transporteurs (M4, ERP-153). Meme
// logique : mappe sur le persona "tout". Miroir de personas.ts.
'transport.carriers.view',
'transport.carriers.manage',
'transport.carriers.archive',
], ],
], ],
[ [
+11 -6
View File
@@ -13,17 +13,22 @@ final class TransportModule
/** /**
* Liste declarative des permissions RBAC exposees par le module Transport. * Liste declarative des permissions RBAC exposees par le module Transport.
* *
* Vide a ce stade : le module ne porte que des referentiels externes * Socle du repertoire transporteurs (M4 § 5.1, ERP-153) :
* synchronises par commandes console (codes IDTF - ERP-149, transporteurs * - `view` : consultation de la liste / fiche transporteur ;
* QUALIMAT - ERP-39), sans ecran ni action protegee. Les permissions seront * - `manage` : creation / modification (hors archivage) ;
* ajoutees quand une page de consultation sera exposee. * - `archive` : archivage / restauration (admin seul, cf. matrice § 5.2).
* *
* Consommee par `app:sync-permissions` (un tableau vide est valide). * Consommee par `app:sync-permissions`. Matrice role -> permissions dans
* `RbacSeeder::MATRIX` (§ 5.2).
* *
* @return array<int, array{code: string, label: string}> * @return array<int, array{code: string, label: string}>
*/ */
public static function permissions(): array public static function permissions(): array
{ {
return []; return [
['code' => 'transport.carriers.view', 'label' => 'Voir les transporteurs'],
['code' => 'transport.carriers.manage', 'label' => 'Créer / modifier les transporteurs'],
['code' => 'transport.carriers.archive', 'label' => 'Archiver / restaurer un transporteur'],
];
} }
} }
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Transport;
use App\Module\Transport\TransportModule;
use PHPUnit\Framework\TestCase;
/**
* Tests structurels du module Transport (M4) : identite et contrat
* `permissions()` (socle RBAC, ERP-153).
*
* @internal
*/
final class TransportModuleTest extends TestCase
{
public function testModuleIdentity(): void
{
self::assertSame('transport', TransportModule::ID);
self::assertSame('Transport', TransportModule::LABEL);
self::assertFalse(TransportModule::REQUIRED);
}
public function testPermissionsSetContainsExactlyThreeCodes(): void
{
// Garde-fou : le jeu de permissions du module est fige par ce test. Si
// quelqu'un ajoute / retire une permission sans ajuster la spec (§ 5.1)
// ni la matrice RBAC (§ 5.2), le test casse explicitement.
$codes = array_column(TransportModule::permissions(), 'code');
sort($codes);
self::assertSame(
[
'transport.carriers.archive',
'transport.carriers.manage',
'transport.carriers.view',
],
$codes,
);
}
public function testEveryPermissionCodeIsPrefixedByModuleId(): void
{
// Convention de nommage `module.resource[.sub].action` : le prefixe doit
// correspondre exactement a l'ID du module (verifie aussi par
// app:sync-permissions).
foreach (TransportModule::permissions() as $permission) {
self::assertStringStartsWith(
TransportModule::ID.'.',
$permission['code'],
'Chaque code de permission doit etre prefixe par l\'ID du module.',
);
self::assertNotSame('', $permission['label'], 'Chaque permission doit porter un label.');
}
}
}