Administration

Module admin — templates documentaires versionnés, AkDataList/TableTemplate, validation KYB et modération des organisations.

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é par useServerAdminList pour les listes paginées côté serveur : organisations, audits de sécurité, imprimeries, faces, pappers-cache, onboarding-signatures et approvals-pending. La prop card-view force 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 par AkDataList (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 :

ChampTypeDescription
templateKeystringIdentifiant stable
titlestringTitre affiché dans l'admin
descriptionstring?Description optionnelle
categorystringlegal, billing, onboarding, …
variablesDocumentTemplateCatalogVariable[]Variables injectables
isRequiredForKybboolean?Requis dans le flux KYB
renderContextstringType de contexte de données attendu

Types de variables : string | number | boolean | date | currency | array | object

Règles métier

  • Une seule version draft active par templateKey et kind.
  • 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']
StatutSignification
submittedDossier soumis par l'organisation
in_reviewEn cours d'instruction par un admin
approvedValidé — l'organisation peut accéder à la plateforme
rejectedRefusé — 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 column est null, le tri ne change pas (évite un fetch avec état incohérent).
  • Coalescing : écrire sort.value met à jour sortBy et sortOrder dans le même tick → un seul fetch.
  • sortColumnKeys (option) : mappings champ API → clé colonne AkDataList quand ils diffèrent. Ex: { name: 'organizationName' } — le get renvoie la clé AkDataList, le set reverse-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

PropTypeDescription
dataunknown[]?Données en mode générique
columnsTableColumn<unknown>[]?Colonnes UTable en mode générique
loadingboolean?État de chargement
pagenumberPage courante
totalItemsnumberTotal d'éléments
itemsPerPagenumberÉléments par page
selectionRecord<string, boolean>?État de sélection
selectionMode'none' | 'single' | 'multiple'?Mode de sélection
tablePropsRecord<string, unknown>?Pass-through vers UTable

Props historiques conservées : title, itemCount, emptyMessage, pageOfLabel, pagination/footer, trash.

Événements

ÉvénementDescription
update:pageChangement de page
update:itemsPerPageChangement du nombre d'items par page
update:selectionChangement de sélection
update:trashOpenOuverture/fermeture de la corbeille
restore:trash-itemRestauration d'un élément
restore:trash-allRestauration 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

  1. Si #mobile est fourni : rendu mobile custom sous md, table au-dessus.
  2. Sinon : #table fourni → override legacy ; #table absent et columns présent → rendu générique UTable.

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 (FK onDelete: 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 (tableaux String[] 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 (permission documents.manage recommandée à terme).
  • La gestion du référentiel univers/rayons (/admin/app-settings) requiert la permission catalog.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 requestId pour la corrélation.
  • Événements d'audit de sécurité enregistrés via recordSecurityAuditEvent sur les actions KYB sensibles.