Commit db6152db authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Exercices : suppression admin après archivage + personnalisation à la création

- Suppression d'exercice réservée au rôle TENANT_ADMIN et autorisée
  uniquement lorsque l'exercice est ARCHIVED (garde-fou côté service,
  bouton dédié côté UI avec confirmation).
- Parcours de personnalisation à la création (à blanc et à l'installation
  d'un kit) : nom d'organisation, ville, secteur, thème visuel joueur,
  EDR, partenaire, média, date de démarrage. Stockés en contexte
  (Exercise.context) + moteur de variables {{...}} substituables dans le
  contenu des kits (prêt pour les futurs kits).

Ajoute aussi, dans les types partagés et le client API, les champs
d'inject presse/appel utilisés par la couche kit (voir commit suivant).

Co-Authored-By: Claude (RCA)
parent d4c5805d
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
-- Personnalisation d'exercice : contexte de mise en situation (organisation,
-- ville, EDR, partenaire, média, date de démarrage) + support des variables
-- {{...}} substituées à l'installation d'un kit.
ALTER TABLE "Exercise" ADD COLUMN "context" JSONB;
+8 −0
Original line number Diff line number Diff line
@@ -92,6 +92,8 @@ model Exercise {
  sector      String?
  status      ExerciseStatus @default(DRAFT)
  playerSkin  PlayerSkin     @default(DEFAULT)
  /// Personnalisation (mise en situation) : { organisation, ville, edr, partenaire, media, startAt }.
  context     Json?

  // Horloge d'exercice pilotable (pilotage au lot 3, cf. spec-technique §1.4).
  clockRunning       Boolean   @default(false)
@@ -577,6 +579,12 @@ model KitInject {
  authorName     String?
  authorHandle   String?
  likes          Int?
  /// Appel (CALL) : qui appeler.
  to             String?
  /// Presse (NEWS) : media, source, rubrique.
  outlet         String?
  newsSource     String?
  newsCategory   String?

  @@index([kitId])
}
+39 −0
Original line number Diff line number Diff line
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
import { PlayerSkin } from '@prisma/client';

/** Champs de personnalisation communs à la création d'exercice et à l'installation d'un kit. */
export class PersonalizationDto {
  @IsOptional()
  @IsEnum(PlayerSkin)
  playerSkin?: PlayerSkin;

  @IsOptional()
  @IsString()
  @MaxLength(160)
  organisation?: string;

  @IsOptional()
  @IsString()
  @MaxLength(160)
  ville?: string;

  @IsOptional()
  @IsString()
  @MaxLength(160)
  edr?: string;

  @IsOptional()
  @IsString()
  @MaxLength(160)
  partenaire?: string;

  @IsOptional()
  @IsString()
  @MaxLength(160)
  media?: string;

  @IsOptional()
  @IsString()
  @MaxLength(40)
  startAt?: string;
}
+73 −0
Original line number Diff line number Diff line
/**
 * Personnalisation d'un exercice (parcours de création / installation de kit) :
 * champs de mise en situation + moteur de variables {{...}} substituées dans le
 * contenu d'un kit à l'installation.
 *
 * Variables disponibles pour les (futurs) kits : {{organisation}}, {{ville}},
 * {{secteur}}, {{edr}}, {{partenaire}}, {{media}}.
 */

export interface Personalization {
  name?: string;
  sector?: string;
  playerSkin?: string;
  organisation?: string;
  ville?: string;
  edr?: string;
  partenaire?: string;
  media?: string;
  startAt?: string;
}

export interface ExerciseContext {
  organisation?: string;
  ville?: string;
  edr?: string;
  partenaire?: string;
  media?: string;
  startAt?: string;
}

const CONTEXT_KEYS = ['organisation', 'ville', 'edr', 'partenaire', 'media', 'startAt'] as const;

/** Contexte (mise en situation) nettoyé, ou undefined si aucun champ renseigné. */
export function buildContext(p: Personalization | undefined): ExerciseContext | undefined {
  if (!p) return undefined;
  const ctx: Record<string, string> = {};
  for (const k of CONTEXT_KEYS) {
    const v = p[k];
    if (typeof v === 'string' && v.trim() !== '') ctx[k] = v.trim();
  }
  return Object.keys(ctx).length ? (ctx as ExerciseContext) : undefined;
}

/** Table des variables {{...}} disponibles pour la substitution. */
export function buildVars(
  sector: string | null | undefined,
  ctx: ExerciseContext | undefined,
): Record<string, string> {
  const vars: Record<string, string> = {};
  if (ctx?.organisation) vars.organisation = ctx.organisation;
  if (ctx?.ville) vars.ville = ctx.ville;
  if (sector && sector.trim()) vars.secteur = sector.trim();
  if (ctx?.edr) vars.edr = ctx.edr;
  if (ctx?.partenaire) vars.partenaire = ctx.partenaire;
  if (ctx?.media) vars.media = ctx.media;
  return vars;
}

const TOKEN = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;

/**
 * Remplace les variables {{cle}} par leur valeur. Un jeton dont la variable
 * n'est pas fournie est laissé intact (utile pour repérer les oublis).
 */
export function applyVars<T extends string | null | undefined>(
  text: T,
  vars: Record<string, string>,
): T {
  if (typeof text !== 'string') return text;
  return text.replace(TOKEN, (match, key: string) =>
    Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match,
  ) as T;
}
+2 −1
Original line number Diff line number Diff line
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import { PersonalizationDto } from '../../common/personalization.dto';

export class CreateExerciseDto {
export class CreateExerciseDto extends PersonalizationDto {
  @IsString()
  @MinLength(1)
  @MaxLength(200)
Loading