Administration
Administration
Le module admin regroupe les fonctionnalités réservées aux opérateurs de la plateforme : gestion des templates documentaires (CGU, mandat, etc.), validation KYB des organisations et supervision des campagnes.
Les vues tabulaires admin utilisent deux composants selon la page :
<AkDataList>(de@aidalinfo/nuxt-ui-kit) piloté paruseServerAdminListpour les listes paginées côté serveur : organisations, audits de sécurité, imprimeries, faces, pappers-cache, onboarding-signatures et approvals-pending. La propcard-viewforce le rendu en cartes (slot#card) pour les pages sans table (faces).TableTemplate(app/mypromo/app/components/table/TableTemplate.vue) pour les vues héritées ou les contextes non couverts parAkDataList(templates documentaires, etc.).
Chemins :
- Module serveur :
app/mypromo/server/modules/admin/ - Routes API :
app/mypromo/server/api/admin/ - Composant table générique :
app/mypromo/app/components/table/TableTemplate.vue
Templates documentaires
Principe
Les templates documentaires permettent à l'admin de gérer le contenu markdown des documents obligatoires (CGU, mandat de prélèvement, etc.) et leur style PDF global. Chaque template est identifié par une templateKey stable et suit un cycle de vie brouillon / publié / archivé.
Le catalogue (métadonnées des templates : titre, variables, catégorie) est déclaré dans des fichiers JSON versionnés dans le dépôt. Le markdown (contenu réel) est stocké en base de données.
Route admin : /admin/documents-templates
Domaine
Source de vérité : app/mypromo/server/modules/admin/domain/
Kinds (DocumentTemplateKind) : markdown | pdf_css
Statuts (DocumentTemplateStatus) : draft | published | archived
templateKey : chaîne [a-z0-9_]+, identifiant stable.
Structure d'un enregistrement
type DocumentTemplateRecord = {
templateKey: string
kind: DocumentTemplateKind // 'markdown' | 'pdf_css'
status: DocumentTemplateStatus // 'draft' | 'published' | 'archived'
version: number // entier croissant pour le publié
titleSnapshot?: string
catalogVersion?: string
content: string // markdown ou CSS
variablesSnapshot?: DocumentTemplateVariableSnapshot[]
createdByUserId?: string
updatedByUserId?: string
publishedAt?: Date
}
Catalogue JSON
Chaque entrée de catalogue contient :
| Champ | Type | Description |
|---|---|---|
templateKey | string | Identifiant stable |
title | string | Titre affiché dans l'admin |
description | string? | Description optionnelle |
category | string | legal, billing, onboarding, … |
variables | DocumentTemplateCatalogVariable[] | Variables injectables |
isRequiredForKyb | boolean? | Requis dans le flux KYB |
renderContext | string | Type de contexte de données attendu |
Types de variables : string | number | boolean | date | currency | array | object
Règles métier
- Une seule version
draftactive partemplateKeyetkind. - Les versions publiées sont immuables.
- La suppression d'une version publiée n'est autorisée que si ce n'est pas la dernière version active (garde-fou code dans
assertCanDeletePublishedTemplateVersion). - Toute suppression exige une justification textuelle et est journalisée.
Positionnement de signature Yousign
Les ancres de signature dans le markdown suivent la convention Smart Anchors Yousign :
{{s1|signature|180|60}}
{{s2|signature|180|60}}
L'upload du document vers Yousign active l'analyse d'ancres (parse_anchors: true). Ces ancres remplacent le positionnement par coordonnées X/Y.
API — templates documentaires
Toutes les routes nécessitent la permission kyb.review (rôle admin).
Catalogue et liste
GET /api/admin/document-templates/catalog
Retourne le catalogue JSON (titres, variables, catégories).
GET /api/admin/document-templates
Liste les templates avec statut et version courante.
GET /api/admin/document-templates/{templateKey}/versions
Historique des versions brouillon et publiées d'un template.
Brouillon markdown
POST /api/admin/document-templates/{templateKey}/draft
Content-Type: application/json
{ "content": "# CGU\n\n..." }
Crée ou met à jour le brouillon markdown d'un template.
DELETE /api/admin/document-templates/{templateKey}/draft
Supprime le brouillon courant.
Publication et suppression de version
POST /api/admin/document-templates/{templateKey}/publish
Publie le brouillon courant en nouvelle version.
DELETE /api/admin/document-templates/{templateKey}/versions/{version}
Content-Type: application/json
{ "reason": "Correction CGU 2026" }
Supprime une version publiée (hors dernière version active).
Preview PDF
POST /api/admin/document-templates/{templateKey}/preview
Content-Type: application/json
{
"context": {
"recipientName": "Acme SAS",
"siret": "12345678900012"
}
}
Génère un aperçu PDF à partir du brouillon ou d'une version publiée avec les variables de test injectées. Le rendu exploite @aidalinfo/template-renderer via l'endpoint /api/documents/preview.
Le rendu PDF applique un habillage commun :
- En-tête discret (masqué sur la première page par défaut)
- Pied de page avec titre documentaire et pagination
page / total - En-tête/pied configurables via
headerLines,footerTextLine1,footerTextLine2
Style PDF global
GET /api/admin/document-templates/pdf-style/versions
POST /api/admin/document-templates/pdf-style/draft
POST /api/admin/document-templates/pdf-style/publish
DELETE /api/admin/document-templates/pdf-style/draft
GET /api/admin/document-templates/pdf-style/source
Le CSS PDF suit le même cycle de vie brouillon/publié que les templates markdown (kind pdf_css).
Validation KYB et modération des organisations
Statuts de revue KYB
Définis dans app/mypromo/server/modules/auth/domain/company-profile.ts :
KYB_REVIEW_STATUSES = ['submitted', 'in_review', 'approved', 'rejected']
| Statut | Signification |
|---|---|
submitted | Dossier soumis par l'organisation |
in_review | En cours d'instruction par un admin |
approved | Validé — l'organisation peut accéder à la plateforme |
rejected | Refusé — une note de motif peut être fournie |
Règles de gating
- Aucune réservation n'est possible tant que le KYB n'est pas
approved. - Pour un industriel, l'accès complet à son workspace est bloqué tant que la revue KYB n'est pas
approved. - Un distributeur suit le même gating.
Règles de modération
- Les visuels peuvent être bloqués pour non-conformité (charte enseigne, légal).
- Une campagne non conforme ne peut pas passer au statut
approved.
API — revue KYB
GET /api/admin/organizations
Liste les organisations avec filtres (statut KYB, actorType, conflicts SIRET, etc.).
GET /api/admin/organizations/{organizationId}
Détails d'une organisation + conflits SIRET associés. Requiert kyb.review.
POST /api/admin/organizations/{organizationId}/review
Content-Type: application/json
{
"status": "approved",
"note": "Dossier complet, SIRET vérifié."
}
Applique une décision de revue KYB (approved ou rejected). Enregistre un événement d'audit de sécurité (kyb.company.reviewed) avec l'acteur, le statut et la note. Requiert kyb.review.
POST /api/admin/organizations/{organizationId}/reset-onboarding
Réinitialise le parcours d'onboarding d'une organisation (usage opérationnel).
API — signature et onboarding
GET /api/admin/onboarding/signature-requests
POST /api/admin/onboarding/signature-requests/relaunch
Consultation et relance des demandes de signature Yousign en attente.
GET /api/admin/approvals/pending
Liste des demandes d'approbation en attente (toutes organisations).
Composants tabulaires
AkDataList (organisations, audits, imprimeries, faces, pappers-cache, onboarding-signatures, approvals-pending)
<AkDataList> (@aidalinfo/nuxt-ui-kit) est le composant de liste paginée pour les vues admin à fort volume. Il opère en deux modes :
mode="server": pagination/recherche/tri délégués àuseServerAdminList(organisations, audits de sécurité, faces, pappers-cache, onboarding-signatures, approvals-pending).mode="client": pagination en mémoire pour les listes statiques (imprimeries).
Quand la page n'expose pas de tri (ex. pappers-cache), on ne câble pas v-model:sort et aucune colonne n'est marquée sortable — le moteur garde son tri interne par défaut côté API. Les filtres select de la page passent par :filters + v-model:filter-values ; la closure fetchPage lit filterValues.value.<key> et un watch(filterValues, () => reload(), { deep: true }) déclenche le rechargement (le coalescing du moteur + le reset interne d'AkDataList garantissent un seul fetch par action). Une action par ligne avec spinner local utilise DataListAction.loading: (row) => … (disponible depuis @aidalinfo/nuxt-ui-kit@0.11.0).
La prop card-view (booléenne, @aidalinfo/nuxt-ui-kit@0.9.0+) force le rendu en cartes à toutes les tailles d'écran — la table n'est jamais rendue. Le slot #card="{ row }" pilote le rendu de chaque carte. Les pages admin/faces utilisent ce mode. Les boutons d'action (edit/delete avec leur loading par ligne) sont rendus dans le slot #card, pas via :actions.
La prop :empty accepte { icon, title, description } et doit toujours être fournie avec des libellés traduits via t() — ne pas laisser le fallback hardcodé du composant.
Tri avec v-model:sort et sort ref
useServerAdminList retourne un ref sort (writable computed) de forme { column: string | null, direction: 'asc' | 'desc' } prêt pour v-model:sort d'AkDataList. Il inclut :
- Clear-sort guard : si
columnestnull, le tri ne change pas (évite un fetch avec état incohérent). - Coalescing : écrire
sort.valuemet à joursortByetsortOrderdans le même tick → un seul fetch. sortColumnKeys(option) : mappings champ API → clé colonne AkDataList quand ils diffèrent. Ex:{ name: 'organizationName' }— legetrenvoie la clé AkDataList, lesetreverse-mappe vers le champ API.
<!-- Pas de mapping : column key === field -->
<AkDataList v-model:sort="sort" ... />
<!-- Avec mapping : { name: 'organizationName' } -->
const { sort } = useServerAdminList({ sortColumnKeys: { name: 'organizationName' }, ... })
Ne pas créer de computed sort local dans les pages : utiliser directement le sort retourné par le composable.
Composant TableTemplate
TableTemplate (app/mypromo/app/components/table/TableTemplate.vue) est le composant de table générique partagé entre tous les workspaces pour les vues héritées (templates documentaires, etc.). La logique de pagination, sélection et helpers est centralisée dans le composable useTableEngine (app/mypromo/app/composables/useTableEngine.ts).
Props principales
| Prop | Type | Description |
|---|---|---|
data | unknown[]? | Données en mode générique |
columns | TableColumn<unknown>[]? | Colonnes UTable en mode générique |
loading | boolean? | État de chargement |
page | number | Page courante |
totalItems | number | Total d'éléments |
itemsPerPage | number | Éléments par page |
selection | Record<string, boolean>? | État de sélection |
selectionMode | 'none' | 'single' | 'multiple'? | Mode de sélection |
tableProps | Record<string, unknown>? | Pass-through vers UTable |
Props historiques conservées : title, itemCount, emptyMessage, pageOfLabel, pagination/footer, trash.
Événements
| Événement | Description |
|---|---|
update:page | Changement de page |
update:itemsPerPage | Changement du nombre d'items par page |
update:selection | Changement de sélection |
update:trashOpen | Ouverture/fermeture de la corbeille |
restore:trash-item | Restauration d'un élément |
restore:trash-all | Restauration de tous les éléments |
Slots
#table: override complet (prioritaire sur le mode générique).#mobile: rendu mobile custom — déclenche la bascule mobile/desktop.#toolbar,#bulk-actions,#empty,#header-actions,#footer-start,#trash-*: extensions opt-in.- En mode générique, les slots non réservés sont forwardés vers
UTable(ex:#status-cell,#actions-header).
Priorité de rendu
- Si
#mobileest fourni : rendu mobile custom sousmd, table au-dessus. - Sinon :
#tablefourni → override legacy ;#tableabsent etcolumnsprésent → rendu génériqueUTable.
Variant naked (mobile)
Supprime uniquement le shell visuel TableTemplate en dessous de md (header/body/footer sans padding de carte) pour s'intégrer au conteneur parent mobile. À partir de md, le shell standard est conservé.
Contraintes shell
Le conteneur borne les slots toolbar et footer (min-w-0, max-w-full) et utilise une pagination mobile compacte pour éviter les débordements horizontaux sur petits écrans.
Référentiel univers & rayons magasin
Référentiel administrable des zones de magasin (univers et rayons) proposé lors du placement d'un emplacement publicitaire. Il remplace la saisie libre par une liste contrôlée par l'admin.
Page admin : /admin/app-settings (hub Paramètres application, onglet Univers & rayons). Section de navigation : Configurations.
Modèle
Deux tables (module catalog) :
CatalogStoreUniverse(catalog_store_universes) :code(unique),label,normalizedLabel,sortOrder,isTransversal(Promotions, Bio, MDD…),archivedAt.CatalogStoreRayon(catalog_store_rayons) :universeId(FKonDelete: Restrict),code(unique),label,normalizedLabel,sortOrder,archivedAt.
Un univers contient N rayons ; chaque rayon appartient à un univers. Le code est dérivé du label (UNIV_… / RAYON_…, sans diacritiques) et reste stable au renommage.
Règles métier
- Domaine :
server/modules/catalog/domain/store-zone.ts(normalisation label/code/clé de recherche). - Application :
server/modules/catalog/application/store-zone.ts. - Unicité : label d'univers unique globalement ; label de rayon unique dans son univers.
- Suppression = soft-archive (
archivedAt) via l'action « Archiver » ; l'archivage d'un univers est répercuté sur ses rayons. La suppression définitive (deleteStoreUniverse/deleteStoreRayon) est refusée en 409 dans trois cas : un univers qui contient encore des rayons ; un univers ou un rayon référencé par un espace publicitaire (AdSpace.placementUniverseId/placementRayonId) ; un univers ou un rayon référencé par une campagne (Campaign.universeIds/rayonIds, ciblage). Ces garde-fous protègent les FK logiques (tableauxString[]non contraints en base) : il faut d'abord retirer la référence côté espace ou campagne, ou archiver plutôt que supprimer.
API
Toutes sous /api/admin/store-zones, permission catalog.manage.all :
GET /api/admin/store-zones # arbre univers + rayons
POST /api/admin/store-zones/universes # { label, isTransversal? }
PATCH /api/admin/store-zones/universes/{id} # { label?, isTransversal?, sortOrder?, archived? }
DELETE /api/admin/store-zones/universes/{id} # 409 si rayons, ad-space ou campagne le référencent
POST /api/admin/store-zones/rayons # { universeId, label }
PATCH /api/admin/store-zones/rayons/{id} # { label?, universeId?, sortOrder?, archived? }
DELETE /api/admin/store-zones/rayons/{id} # 409 si ad-space ou campagne le référencent
Données initiales : pnpm run seed:store-zones (upsert idempotent, additif).
RBAC et observabilité
- Toutes les routes admin requièrent le rôle
admin. - La revue KYB requiert la permission
kyb.review. - La gestion des templates documentaires requiert la permission
kyb.review(permissiondocuments.managerecommandée à terme). - La gestion du référentiel univers/rayons (
/admin/app-settings) requiert la permissioncatalog.manage.all. - Le smoke test email requiert
platform.manage. - Toutes les suppressions (draft, version) sont journalisées avec acteur, templateKey, version et raison.
- Logs structurés avec
requestIdpour la corrélation. - Événements d'audit de sécurité enregistrés via
recordSecurityAuditEventsur les actions KYB sensibles.