Mise en place de l'anglais comme deuxième langue pour l'application
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="modal" (click)="$event.stopPropagation()">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2>Créer une nouvelle Campagne</h2>
|
||||
<h2>{{ 'campaignCreate.title' | translate }}</h2>
|
||||
<button class="btn-close" (click)="onCancel()">
|
||||
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
@@ -11,54 +11,51 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-name">Nom de la campagne *</label>
|
||||
<label for="campaign-name">{{ 'campaignCreate.nameLabel' | translate }}</label>
|
||||
<input
|
||||
id="campaign-name"
|
||||
type="text"
|
||||
formControlName="name"
|
||||
placeholder="Ex: L'Ombre du Nord, Les Héritiers Oubliés..."
|
||||
[placeholder]="'campaignCreate.namePlaceholder' | translate"
|
||||
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-description">Description / Pitch</label>
|
||||
<label for="campaign-description">{{ 'campaignCreate.descriptionLabel' | translate }}</label>
|
||||
<textarea
|
||||
id="campaign-description"
|
||||
formControlName="description"
|
||||
placeholder="Résumez l'intrigue principale de votre campagne..."
|
||||
[placeholder]="'campaignCreate.descriptionPlaceholder' | translate"
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-player-count">Nombre de joueurs</label>
|
||||
<label for="campaign-player-count">{{ 'campaignCreate.playerCountLabel' | translate }}</label>
|
||||
<input id="campaign-player-count" type="number" formControlName="playerCount" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-lore">Univers associé</label>
|
||||
<label for="campaign-lore">{{ 'campaignCreate.loreLabel' | translate }}</label>
|
||||
<select id="campaign-lore" formControlName="loreId">
|
||||
<option value="">— Aucun univers (campagne libre) —</option>
|
||||
<option value="">{{ 'campaignCreate.noLoreOption' | translate }}</option>
|
||||
@for (lore of availableLores; track lore) {
|
||||
<option [value]="lore.id">{{ lore.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<p class="hint">
|
||||
Optionnel. Si associée, vous pourrez lier arcs, chapitres et scènes aux pages du Lore.
|
||||
Laissez vide pour un one-shot ou si vous créerez le Lore plus tard.
|
||||
</p>
|
||||
<p class="hint">{{ 'campaignCreate.loreHint' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="campaign-game-system">Système de JDR</label>
|
||||
<label for="campaign-game-system">{{ 'campaignCreate.gameSystemLabel' | translate }}</label>
|
||||
@if (!creatingGameSystem) {
|
||||
<select id="campaign-game-system" formControlName="gameSystemId">
|
||||
<option value="">— Aucun (campagne générique) —</option>
|
||||
<option value="">{{ 'campaignCreate.noGameSystemOption' | translate }}</option>
|
||||
@for (gs of availableGameSystems; track gs) {
|
||||
<option [value]="gs.id">{{ gs.name }}</option>
|
||||
}
|
||||
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
|
||||
<option [value]="CREATE_GAMESYSTEM_SENTINEL">{{ 'campaignCreate.createGameSystemOption' | translate }}</option>
|
||||
</select>
|
||||
}
|
||||
|
||||
@@ -69,7 +66,7 @@
|
||||
type="text"
|
||||
[(ngModel)]="newGameSystemName"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
|
||||
[placeholder]="'campaignCreate.gameSystemNamePlaceholder' | translate"
|
||||
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
|
||||
(keydown.escape)="cancelCreateGameSystem()"
|
||||
autofocus
|
||||
@@ -79,50 +76,39 @@
|
||||
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
|
||||
(click)="submitCreateGameSystem()">
|
||||
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
|
||||
Créer
|
||||
{{ 'common.create' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
|
||||
Annuler
|
||||
{{ 'common.cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Création rapide — vous pourrez ajouter les règles, les templates de fiches PJ/PNJ
|
||||
et le reste depuis la section "Systèmes" plus tard.
|
||||
</p>
|
||||
<p class="hint">{{ 'campaignCreate.gameSystemCreateHint' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!creatingGameSystem) {
|
||||
<p class="hint">
|
||||
Optionnel. Si défini, l'IA injectera les règles du système (classes, combat, lore...)
|
||||
dans ses suggestions pour respecter les mécaniques du JDR.
|
||||
</p>
|
||||
<p class="hint">{{ 'campaignCreate.gameSystemHint' | translate }}</p>
|
||||
}
|
||||
@if (!creatingGameSystem) {
|
||||
<p class="hint hint-warning">
|
||||
⚠️ Le système de jeu choisi détermine aussi le <strong>template des fiches de PJ et PNJ</strong>.
|
||||
Le changer plus tard rendra les champs des fiches existantes invisibles
|
||||
(les données restent stockées mais ne s'afficheront qu'en revenant à l'ancien système).
|
||||
Choisissez bien dès le départ si possible.
|
||||
</p>
|
||||
<p class="hint hint-warning" [innerHTML]="'campaignCreate.gameSystemWarning' | translate"></p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 Organisation :</strong> Votre campagne sera structurée en :</p>
|
||||
<p [innerHTML]="'campaignCreate.orgIntro' | translate"></p>
|
||||
<ul>
|
||||
<li><strong>Arcs</strong> - Les grandes phases narratives</li>
|
||||
<li><strong>Chapitres</strong> - Les segments d'un arc</li>
|
||||
<li><strong>Scènes</strong> - Les moments de jeu individuels</li>
|
||||
<li [innerHTML]="'campaignCreate.orgArcs' | translate"></li>
|
||||
<li [innerHTML]="'campaignCreate.orgChapters' | translate"></li>
|
||||
<li [innerHTML]="'campaignCreate.orgScenes' | translate"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn-primary" [disabled]="form.invalid">
|
||||
<lucide-icon [img]="BookCopy" [size]="16"></lucide-icon>
|
||||
Créer la campagne
|
||||
{{ 'campaignCreate.submit' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="onCancel()">Annuler</button>
|
||||
<button type="button" class="btn-secondary" (click)="onCancel()">{{ 'common.cancel' | translate }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LucideAngularModule, BookCopy, X, Plus, Check } from 'lucide-angular';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { LoreService } from '../../../services/lore.service';
|
||||
import { Lore } from '../../../services/lore.model';
|
||||
import { GameSystemService } from '../../../services/game-system.service';
|
||||
@@ -22,7 +23,7 @@ export interface CampaignCreatePayload {
|
||||
|
||||
@Component({
|
||||
selector: 'app-campaign-create',
|
||||
imports: [ReactiveFormsModule, FormsModule, LucideAngularModule],
|
||||
imports: [ReactiveFormsModule, FormsModule, LucideAngularModule, TranslatePipe],
|
||||
templateUrl: './campaign-create.component.html',
|
||||
styleUrls: ['./campaign-create.component.scss']
|
||||
})
|
||||
|
||||
@@ -7,22 +7,22 @@
|
||||
<h1>{{ campaign.name }}</h1>
|
||||
<p class="description">{{ campaign.description }}</p>
|
||||
<div class="meta">
|
||||
<span class="badge">{{ campaign.playerCount || 0 }} joueurs</span>
|
||||
<span class="badge">{{ 'campaignDetail.players' | translate:{ n: campaign.playerCount || 0 } }}</span>
|
||||
<!-- Badge "Univers" : lien vers le Lore associé si présent -->
|
||||
@if (linkedLore) {
|
||||
<a
|
||||
class="badge badge-lore"
|
||||
[routerLink]="['/lore', linkedLore.id]"
|
||||
title="Ouvrir l'univers associé">
|
||||
[title]="'campaignDetail.openLinkedLore' | translate">
|
||||
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
|
||||
{{ linkedLore.name }}
|
||||
</a>
|
||||
}
|
||||
<!-- Campagne liée à un Lore qui n'existe plus (supprimé ailleurs) -->
|
||||
@if (campaign.loreId && !linkedLore) {
|
||||
<span class="badge badge-lore-missing" title="L'univers associé est introuvable">
|
||||
<span class="badge badge-lore-missing" [title]="'campaignDetail.loreMissingTitle' | translate">
|
||||
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
|
||||
Univers introuvable
|
||||
{{ 'campaignDetail.loreMissing' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@@ -30,11 +30,11 @@
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-secondary" (click)="startEdit()">
|
||||
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
|
||||
Modifier
|
||||
{{ 'common.edit' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-danger" (click)="deleteCampaign()">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
Supprimer
|
||||
{{ 'common.delete' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,34 +43,34 @@
|
||||
@if (editing) {
|
||||
<div class="detail-header edit-mode">
|
||||
<div class="field">
|
||||
<label>Nom</label>
|
||||
<label>{{ 'common.name' | translate }}</label>
|
||||
<input type="text" [(ngModel)]="editName" name="editName" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Description</label>
|
||||
<label>{{ 'common.description' | translate }}</label>
|
||||
<textarea [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Univers associé</label>
|
||||
<label>{{ 'campaignDetail.loreLabel' | translate }}</label>
|
||||
<select [(ngModel)]="editLoreId" name="editLoreId">
|
||||
<option value="">— Aucun univers (campagne libre) —</option>
|
||||
<option value="">{{ 'campaignDetail.noLoreOption' | translate }}</option>
|
||||
@for (lore of availableLores; track lore) {
|
||||
<option [value]="lore.id">{{ lore.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Système de JDR</label>
|
||||
<label>{{ 'campaignDetail.gameSystemLabel' | translate }}</label>
|
||||
@if (!creatingGameSystem) {
|
||||
<select
|
||||
[(ngModel)]="editGameSystemId"
|
||||
name="editGameSystemId"
|
||||
(ngModelChange)="onEditGameSystemChange($event)">
|
||||
<option value="">— Aucun (générique) —</option>
|
||||
<option value="">{{ 'campaignDetail.noGameSystemOption' | translate }}</option>
|
||||
@for (gs of availableGameSystems; track gs) {
|
||||
<option [value]="gs.id">{{ gs.name }}</option>
|
||||
}
|
||||
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
|
||||
<option [value]="CREATE_GAMESYSTEM_SENTINEL">{{ 'campaignDetail.createGameSystemOption' | translate }}</option>
|
||||
</select>
|
||||
}
|
||||
@if (creatingGameSystem) {
|
||||
@@ -79,7 +79,7 @@
|
||||
type="text"
|
||||
[(ngModel)]="newGameSystemName"
|
||||
name="newGameSystemName"
|
||||
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
|
||||
[placeholder]="'campaignDetail.gameSystemNamePlaceholder' | translate"
|
||||
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
|
||||
(keydown.escape)="cancelCreateGameSystem()"
|
||||
autofocus
|
||||
@@ -89,10 +89,10 @@
|
||||
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
|
||||
(click)="submitCreateGameSystem()">
|
||||
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
|
||||
Créer
|
||||
{{ 'common.create' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
|
||||
Annuler
|
||||
{{ 'common.cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,10 +100,10 @@
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
|
||||
Sauvegarder
|
||||
{{ 'common.save' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancelEdit()">
|
||||
Annuler
|
||||
{{ 'common.cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@
|
||||
@if (!editing) {
|
||||
<section class="detail-section personas-section">
|
||||
<div class="section-header">
|
||||
<h2>Personnages</h2>
|
||||
<h2>{{ 'campaignDetail.charactersTitle' | translate }}</h2>
|
||||
</div>
|
||||
<!-- Les PJ ne sont plus rattachés à la campagne mais à une Partie (Playthrough) :
|
||||
ils se gèrent depuis la page d'une Partie. Ici on ne liste que les PNJ
|
||||
@@ -121,14 +121,14 @@
|
||||
<div class="subsection-header">
|
||||
<h3>
|
||||
<lucide-icon [img]="Drama" [size]="16"></lucide-icon>
|
||||
Personnages non-joueurs
|
||||
{{ 'campaignDetail.npcTitle' | translate }}
|
||||
@if (npcs.length > 0) {
|
||||
<span class="count-badge">{{ npcs.length }}</span>
|
||||
}
|
||||
</h3>
|
||||
<button class="btn-add" (click)="createNpc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouveau PNJ
|
||||
{{ 'campaignDetail.newNpc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@if (npcs.length > 0) {
|
||||
@@ -146,10 +146,10 @@
|
||||
}
|
||||
@if (npcs.length === 0) {
|
||||
<div class="empty-state empty-state--compact">
|
||||
<p>Aucun PNJ pour le moment.</p>
|
||||
<p>{{ 'campaignDetail.noNpc' | translate }}</p>
|
||||
<button class="btn-add-first" (click)="createNpc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Créer votre premier PNJ
|
||||
{{ 'campaignDetail.createFirstNpc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -159,11 +159,11 @@
|
||||
@if (!editing) {
|
||||
<section class="detail-section arcs-section">
|
||||
<div class="section-header">
|
||||
<h2>Arcs narratifs</h2>
|
||||
<h2>{{ 'campaignDetail.arcsTitle' | translate }}</h2>
|
||||
<div class="section-header-actions">
|
||||
<button class="btn-add" (click)="createArc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouvel arc
|
||||
{{ 'campaignDetail.newArc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@
|
||||
<div class="arc-card" (click)="openArc(arc)">
|
||||
<lucide-icon [img]="Swords" [size]="24" class="arc-icon"></lucide-icon>
|
||||
<span class="arc-name">{{ arc.name }}</span>
|
||||
<span class="arc-meta">{{ chapterCountByArc[arc.id!] || 0 }} chapitres</span>
|
||||
<span class="arc-meta">{{ 'campaignDetail.chapters' | translate:{ n: chapterCountByArc[arc.id!] || 0 } }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -181,10 +181,10 @@
|
||||
@if (arcs.length === 0) {
|
||||
<div class="empty-state">
|
||||
<lucide-icon [img]="Swords" [size]="40" class="empty-icon"></lucide-icon>
|
||||
<p>Aucun arc narratif pour le moment.</p>
|
||||
<p>{{ 'campaignDetail.noArc' | translate }}</p>
|
||||
<button class="btn-add-first" (click)="createArc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Créer votre premier arc
|
||||
{{ 'campaignDetail.createFirstArc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -196,11 +196,11 @@
|
||||
<div class="section-header">
|
||||
<h2>
|
||||
<lucide-icon [img]="Dices" [size]="18"></lucide-icon>
|
||||
Mes parties
|
||||
{{ 'campaignDetail.playthroughsTitle' | translate }}
|
||||
</h2>
|
||||
<button class="btn-add" [disabled]="newPlaythroughInFlight" (click)="createPlaythrough()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
Nouvelle partie
|
||||
{{ 'campaignDetail.newPlaythrough' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@if (playthroughs.length > 0) {
|
||||
@@ -221,7 +221,7 @@
|
||||
}
|
||||
@if (playthroughs.length === 0) {
|
||||
<div class="empty-state empty-state--compact">
|
||||
<p>Aucune partie. Créez-en une pour démarrer une session avec une table.</p>
|
||||
<p>{{ 'campaignDetail.noPlaythrough' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, Dices, Drama, Check, Play, Upload, Sparkles } from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { catchError, switchMap, filter, map } from 'rxjs/operators';
|
||||
@@ -28,7 +29,7 @@ import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dia
|
||||
|
||||
@Component({
|
||||
selector: 'app-campaign-detail',
|
||||
imports: [FormsModule, LucideAngularModule, RouterLink],
|
||||
imports: [FormsModule, LucideAngularModule, RouterLink, TranslatePipe],
|
||||
templateUrl: './campaign-detail.component.html',
|
||||
styleUrls: ['./campaign-detail.component.scss']
|
||||
})
|
||||
@@ -101,7 +102,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
private playthroughService: PlaythroughService,
|
||||
private layoutService: LayoutService,
|
||||
private pageTitleService: PageTitleService,
|
||||
private confirmDialog: ConfirmDialogService
|
||||
private confirmDialog: ConfirmDialogService,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -216,7 +218,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
createPlaythrough(): void {
|
||||
if (!this.campaign || this.newPlaythroughInFlight) return;
|
||||
this.newPlaythroughInFlight = true;
|
||||
const defaultName = `Nouvelle partie ${this.playthroughs.length + 1}`;
|
||||
const defaultName = this.translate.instant('campaignDetail.defaultPlaythroughName', { n: this.playthroughs.length + 1 });
|
||||
this.playthroughService.create({
|
||||
campaignId: this.campaign.id!,
|
||||
name: defaultName
|
||||
@@ -275,7 +277,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private showLayout(allCampaigns: Campaign[], data: CampaignTreeData): void {
|
||||
this.layoutService.show(buildCampaignSidebarConfig(this.campaign!, allCampaigns, data, this.campaign!.id!));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(this.campaign!, allCampaigns, data, this.campaign!.id!, this.translate));
|
||||
}
|
||||
|
||||
// ─────────────── Édition / suppression de la Campagne ───────────────
|
||||
@@ -352,16 +354,14 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
if (gameSystemChanged && hasSheets) {
|
||||
const count = this.npcs.length;
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Changer le systeme de jeu ?',
|
||||
message:
|
||||
`Vous etes sur le point de changer le systeme de jeu de cette campagne. ` +
|
||||
`Cela change egalement le template des fiches de PJ et PNJ.`,
|
||||
title: this.translate.instant('campaignDetail.gameSystemChange.title'),
|
||||
message: this.translate.instant('campaignDetail.gameSystemChange.message'),
|
||||
details: [
|
||||
`${count} fiche(s) existante(s) sont liees au template du systeme actuel.`,
|
||||
`Leurs champs ne s'afficheront plus avec le nouveau systeme.`,
|
||||
`Les donnees restent stockees : revenir a l'ancien systeme les rendra a nouveau visibles.`
|
||||
this.translate.instant('campaignDetail.gameSystemChange.detailSheets', { n: count }),
|
||||
this.translate.instant('campaignDetail.gameSystemChange.detailFields'),
|
||||
this.translate.instant('campaignDetail.gameSystemChange.detailStored')
|
||||
],
|
||||
confirmLabel: 'Changer quand meme',
|
||||
confirmLabel: this.translate.instant('campaignDetail.gameSystemChange.confirm'),
|
||||
variant: 'warning'
|
||||
}).then(ok => { if (ok) this.persistEdit(newGameSystemId); });
|
||||
return;
|
||||
@@ -400,20 +400,20 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
this.campaignService.getCampaignDeletionImpact(campaign.id!).subscribe({
|
||||
next: impact => {
|
||||
const parts: string[] = [];
|
||||
if (impact.arcs > 0) parts.push(`${impact.arcs} arc${impact.arcs > 1 ? 's' : ''}`);
|
||||
if (impact.chapters > 0) parts.push(`${impact.chapters} chapitre${impact.chapters > 1 ? 's' : ''}`);
|
||||
if (impact.scenes > 0) parts.push(`${impact.scenes} scène${impact.scenes > 1 ? 's' : ''}`);
|
||||
if (impact.playthroughs > 0) parts.push(`${impact.playthroughs} partie${impact.playthroughs > 1 ? 's' : ''} (avec ses PJ, sessions, faits)`);
|
||||
if (impact.arcs > 0) parts.push(this.translate.instant(impact.arcs > 1 ? 'campaignDetail.delete.arcsPlural' : 'campaignDetail.delete.arcs', { n: impact.arcs }));
|
||||
if (impact.chapters > 0) parts.push(this.translate.instant(impact.chapters > 1 ? 'campaignDetail.delete.chaptersPlural' : 'campaignDetail.delete.chapters', { n: impact.chapters }));
|
||||
if (impact.scenes > 0) parts.push(this.translate.instant(impact.scenes > 1 ? 'campaignDetail.delete.scenesPlural' : 'campaignDetail.delete.scenes', { n: impact.scenes }));
|
||||
if (impact.playthroughs > 0) parts.push(this.translate.instant(impact.playthroughs > 1 ? 'campaignDetail.delete.playthroughsPlural' : 'campaignDetail.delete.playthroughs', { n: impact.playthroughs }));
|
||||
|
||||
const details: string[] = [];
|
||||
if (parts.length) details.push(`Sera aussi supprime : ${parts.join(', ')}.`);
|
||||
details.push('Cette action est irreversible.');
|
||||
if (parts.length) details.push(this.translate.instant('campaignDetail.delete.alsoDeletes', { items: parts.join(', ') }));
|
||||
details.push(this.translate.instant('campaignDetail.delete.irreversible'));
|
||||
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer la campagne ?',
|
||||
message: `Supprimer definitivement la campagne "${campaign.name}" ?`,
|
||||
title: this.translate.instant('campaignDetail.delete.title'),
|
||||
message: this.translate.instant('campaignDetail.delete.message', { name: campaign.name }),
|
||||
confirmLabel: this.translate.instant('common.delete'),
|
||||
details,
|
||||
confirmLabel: 'Supprimer',
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<div class="page-header">
|
||||
<button type="button" class="btn-back" (click)="cancel()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
|
||||
Retour à la campagne
|
||||
{{ 'campaignImport.back' | translate }}
|
||||
</button>
|
||||
<h1>Importer un PDF de campagne</h1>
|
||||
<p class="subtitle">
|
||||
L'IA propose une arborescence arc → chapitre → scène. Vous la révisez et l'ajustez
|
||||
avant de créer quoi que ce soit.
|
||||
</p>
|
||||
<h1>{{ 'campaignImport.title' | translate }}</h1>
|
||||
<p class="subtitle">{{ 'campaignImport.subtitle' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Étape 1 : upload (masqué une fois en revue) -->
|
||||
@@ -18,7 +15,7 @@
|
||||
<input #pdfInput type="file" accept="application/pdf,.pdf" hidden (change)="onPdfSelected($event)" />
|
||||
<button type="button" class="btn-primary big" [disabled]="importing" (click)="pdfInput.click()">
|
||||
<lucide-icon [img]="Upload" [size]="16"></lucide-icon>
|
||||
{{ importing ? 'Import en cours…' : 'Choisir un PDF de campagne' }}
|
||||
{{ (importing ? 'campaignImport.importing' : 'campaignImport.choosePdf') | translate }}
|
||||
</button>
|
||||
<!-- Progression live -->
|
||||
@if (importing) {
|
||||
@@ -36,7 +33,7 @@
|
||||
}
|
||||
@if (importCounts) {
|
||||
<p class="import-counts">
|
||||
Trouvé jusqu'ici : {{ importCounts.arcs }} arc(s) · {{ importCounts.chapters }} chapitre(s) · {{ importCounts.scenes }} scène(s) · {{ importCounts.npcs }} PNJ
|
||||
{{ 'campaignImport.foundSoFar' | translate:{ arcs: importCounts.arcs, chapters: importCounts.chapters, scenes: importCounts.scenes, npcs: importCounts.npcs } }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
@@ -52,12 +49,7 @@
|
||||
<section class="review-area">
|
||||
<div class="review-header">
|
||||
<p class="review-summary">
|
||||
À créer : <strong>{{ arcCount }}</strong> nouvel(s) arc(s),
|
||||
<strong>{{ chapterCount }}</strong> chapitre(s),
|
||||
<strong>{{ sceneCount }}</strong> scène(s)@if (npcs.length > 0) {<span>,
|
||||
<strong>{{ npcCount }}</strong> PNJ</span>}.
|
||||
Les éléments <em>« déjà présent »</em> (grisés) sont affichés pour situer les ajouts ;
|
||||
ils ne seront pas recréés. Modifiez les nouveaux, puis créez.
|
||||
<span [innerHTML]="'campaignImport.summaryToCreate' | translate:{ arcs: arcCount, chapters: chapterCount, scenes: sceneCount }"></span>@if (npcs.length > 0) {<span [innerHTML]="'campaignImport.summaryNpcs' | translate:{ npcs: npcCount }"></span>}<span [innerHTML]="'campaignImport.summaryHint' | translate"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tree">
|
||||
@@ -70,12 +62,12 @@
|
||||
</button>
|
||||
<lucide-icon [img]="Swords" [size]="16" class="node-icon"></lucide-icon>
|
||||
<input type="text" class="node-name" [(ngModel)]="arc.name" [name]="'arc-' + ai"
|
||||
[readonly]="arc.existing" placeholder="Nom de l'arc" />
|
||||
[readonly]="arc.existing" [placeholder]="'campaignImport.arcNamePlaceholder' | translate" />
|
||||
@if (arc.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!arc.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeArc(ai)" title="Supprimer l'arc">
|
||||
<button type="button" class="btn-remove" (click)="removeArc(ai)" [title]="'campaignImport.removeArc' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -84,16 +76,16 @@
|
||||
<div class="node-body">
|
||||
@if (!arc.existing) {
|
||||
<div class="arc-type-toggle">
|
||||
<span class="toggle-label">Type :</span>
|
||||
<span class="toggle-label">{{ 'campaignImport.typeLabel' | translate }}</span>
|
||||
<button type="button" class="type-btn" [class.active]="arc.type === 'LINEAR'"
|
||||
(click)="setArcType(arc, 'LINEAR')">Linéaire</button>
|
||||
(click)="setArcType(arc, 'LINEAR')">{{ 'campaignImport.typeLinear' | translate }}</button>
|
||||
<button type="button" class="type-btn" [class.active]="arc.type === 'HUB'"
|
||||
(click)="setArcType(arc, 'HUB')">Hub (quêtes)</button>
|
||||
(click)="setArcType(arc, 'HUB')">{{ 'campaignImport.typeHub' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
@if (!arc.existing) {
|
||||
<textarea class="node-desc" [(ngModel)]="arc.description" [name]="'arc-desc-' + ai"
|
||||
rows="2" placeholder="Synopsis de l'arc (optionnel)"></textarea>
|
||||
rows="2" [placeholder]="'campaignImport.arcSynopsisPlaceholder' | translate"></textarea>
|
||||
}
|
||||
<!-- Chapitres -->
|
||||
@for (chapter of arc.chapters; track chapter; let ci = $index) {
|
||||
@@ -104,12 +96,12 @@
|
||||
</button>
|
||||
<lucide-icon [img]="BookOpen" [size]="14" class="node-icon"></lucide-icon>
|
||||
<input type="text" class="node-name" [(ngModel)]="chapter.name" [name]="'chap-' + ai + '-' + ci"
|
||||
[readonly]="chapter.existing" placeholder="Nom du chapitre" />
|
||||
[readonly]="chapter.existing" [placeholder]="'campaignImport.chapterNamePlaceholder' | translate" />
|
||||
@if (chapter.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!chapter.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeChapter(arc, ci)" title="Supprimer le chapitre">
|
||||
<button type="button" class="btn-remove" (click)="removeChapter(arc, ci)" [title]="'campaignImport.removeChapter' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -118,7 +110,7 @@
|
||||
<div class="node-body">
|
||||
@if (!chapter.existing) {
|
||||
<textarea class="node-desc" [(ngModel)]="chapter.description" [name]="'chap-desc-' + ai + '-' + ci"
|
||||
rows="2" placeholder="Synopsis du chapitre (optionnel)"></textarea>
|
||||
rows="2" [placeholder]="'campaignImport.chapterSynopsisPlaceholder' | translate"></textarea>
|
||||
}
|
||||
<!-- Scènes -->
|
||||
@for (scene of chapter.scenes; track scene; let si = $index) {
|
||||
@@ -129,60 +121,60 @@
|
||||
<div class="scene-fields">
|
||||
<input type="text" class="node-name" [(ngModel)]="scene.name"
|
||||
[name]="'scene-' + ai + '-' + ci + '-' + si" [readonly]="scene.existing"
|
||||
placeholder="Nom de la scène" />
|
||||
[placeholder]="'campaignImport.sceneNamePlaceholder' | translate" />
|
||||
@if (!scene.existing) {
|
||||
<input type="text" class="node-desc-inline" [(ngModel)]="scene.description"
|
||||
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" placeholder="Synopsis (optionnel)" />
|
||||
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" [placeholder]="'campaignImport.synopsisOptionalPlaceholder' | translate" />
|
||||
}
|
||||
</div>
|
||||
@if (scene.existing) {
|
||||
<span class="badge-existing">déjà présent</span>
|
||||
<span class="badge-existing">{{ 'campaignImport.alreadyPresent' | translate }}</span>
|
||||
}
|
||||
@if (!scene.existing) {
|
||||
<button type="button" class="btn-details" (click)="toggleDetails(scene)"
|
||||
title="Narration joueurs, notes MJ, pièces">
|
||||
[title]="'campaignImport.detailsTitle' | translate">
|
||||
<lucide-icon [img]="scene.detailsOpen ? ChevronDown : ChevronRight" [size]="12"></lucide-icon>
|
||||
Détails@if (scene.rooms.length) {
|
||||
<span> · {{ scene.rooms.length }} pièce(s)</span>
|
||||
{{ 'campaignImport.details' | translate }}@if (scene.rooms.length) {
|
||||
<span> {{ 'campaignImport.roomsCount' | translate:{ n: scene.rooms.length } }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@if (!scene.existing) {
|
||||
<button type="button" class="btn-remove" (click)="removeScene(chapter, si)" title="Supprimer la scène">
|
||||
<button type="button" class="btn-remove" (click)="removeScene(chapter, si)" [title]="'campaignImport.removeScene' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (scene.detailsOpen && !scene.existing) {
|
||||
<div class="scene-details">
|
||||
<label class="field-label">À lire aux joueurs</label>
|
||||
<label class="field-label">{{ 'campaignImport.playerNarrationLabel' | translate }}</label>
|
||||
<textarea class="node-desc" [(ngModel)]="scene.playerNarration"
|
||||
[name]="'scene-pn-' + ai + '-' + ci + '-' + si" rows="3"
|
||||
placeholder="Texte d'encadré lu aux joueurs (optionnel)"></textarea>
|
||||
<label class="field-label">Notes MJ</label>
|
||||
[placeholder]="'campaignImport.playerNarrationPlaceholder' | translate"></textarea>
|
||||
<label class="field-label">{{ 'campaignImport.gmNotesLabel' | translate }}</label>
|
||||
<textarea class="node-desc" [(ngModel)]="scene.gmNotes"
|
||||
[name]="'scene-gm-' + ai + '-' + ci + '-' + si" rows="3"
|
||||
placeholder="Secrets, développement, conséquences (optionnel)"></textarea>
|
||||
[placeholder]="'campaignImport.gmNotesPlaceholder' | translate"></textarea>
|
||||
<!-- Pièces (donjon) : vide = scène narrative classique -->
|
||||
<span class="field-label">Pièces (lieu explorable)</span>
|
||||
<span class="field-label">{{ 'campaignImport.roomsLabel' | translate }}</span>
|
||||
<div class="rooms">
|
||||
@for (room of scene.rooms; track room; let ri = $index) {
|
||||
<div class="room-row">
|
||||
<input type="text" class="room-name" [(ngModel)]="room.name"
|
||||
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Pièce" />
|
||||
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.roomPlaceholder' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.description"
|
||||
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Description" />
|
||||
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'common.description' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.enemies"
|
||||
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Ennemis" />
|
||||
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.enemiesPlaceholder' | translate" />
|
||||
<input type="text" class="room-field" [(ngModel)]="room.loot"
|
||||
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Trésor" />
|
||||
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" title="Supprimer la pièce">
|
||||
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" [placeholder]="'campaignImport.lootPlaceholder' | translate" />
|
||||
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" [title]="'campaignImport.removeRoom' | translate">
|
||||
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addRoom(scene)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Pièce
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.room' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,32 +182,29 @@
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addScene(chapter)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Scène
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.scene' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add-inline" (click)="addChapter(arc)">
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Chapitre
|
||||
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> {{ 'campaignImport.chapter' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn-add" (click)="addArc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> Ajouter un arc
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> {{ 'campaignImport.addArc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PNJ détectés dans le PDF : revue par cases à cocher -->
|
||||
@if (npcs.length > 0) {
|
||||
<div class="npc-review">
|
||||
<h3>PNJ et créatures détectés ({{ npcs.length }})</h3>
|
||||
<p class="hint">
|
||||
Cochez ceux à créer dans la campagne (description issue du livre).
|
||||
Ceux déjà présents dans la campagne sont grisés et ne seront pas recréés.
|
||||
</p>
|
||||
<h3>{{ 'campaignImport.npcReviewTitle' | translate:{ n: npcs.length } }}</h3>
|
||||
<p class="hint">{{ 'campaignImport.npcReviewHint' | translate }}</p>
|
||||
<ul class="npc-list">
|
||||
@for (npc of npcs; track npc.name) {
|
||||
<li class="npc-row" [class.npc-existing]="npc.existing">
|
||||
@@ -223,7 +212,7 @@
|
||||
<input type="checkbox" [(ngModel)]="npc.selected" [disabled]="npc.existing">
|
||||
<strong>{{ npc.name }}</strong>
|
||||
@if (npc.existing) {
|
||||
<em class="npc-tag">déjà présent</em>
|
||||
<em class="npc-tag">{{ 'campaignImport.alreadyPresent' | translate }}</em>
|
||||
}
|
||||
</label>
|
||||
@if (npc.description) {
|
||||
@@ -241,9 +230,9 @@
|
||||
<div class="review-actions">
|
||||
<button type="button" class="btn-primary" [disabled]="applying || !hasNewContent" (click)="apply()">
|
||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||
{{ applying ? 'Création…' : 'Créer dans la campagne' }}
|
||||
{{ (applying ? 'campaignImport.creating' : 'campaignImport.createInCampaign') | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
|
||||
<button type="button" class="btn-secondary" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LucideAngularModule, ArrowLeft, Upload, Plus, Trash2,
|
||||
ChevronDown, ChevronRight, Swords, BookOpen, MapPin, Check
|
||||
} from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { CampaignImportService } from '../../../services/campaign-import.service';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
import { CharacterService } from '../../../services/character.service';
|
||||
@@ -53,7 +54,7 @@ interface NpcNode { name: string; description: string; selected: boolean; existi
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-campaign-import',
|
||||
imports: [FormsModule, LucideAngularModule],
|
||||
imports: [FormsModule, LucideAngularModule, TranslatePipe],
|
||||
templateUrl: './campaign-import.component.html',
|
||||
styleUrls: ['./campaign-import.component.scss']
|
||||
})
|
||||
@@ -109,12 +110,13 @@ export class CampaignImportComponent implements OnInit {
|
||||
private randomTableService: RandomTableService,
|
||||
private enemyService: EnemyService,
|
||||
private campaignSidebar: CampaignSidebarService,
|
||||
private pageTitle: PageTitleService
|
||||
private pageTitle: PageTitleService,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.campaignId = this.route.snapshot.paramMap.get('campaignId')!;
|
||||
this.pageTitle.set('Importer une campagne');
|
||||
this.pageTitle.set(this.translate.instant('campaignImport.pageTitle'));
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
|
||||
// Pré-chargement de l'arborescence existante (pour fusionner à la revue).
|
||||
@@ -138,7 +140,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.reviewing = false;
|
||||
this.importError = null;
|
||||
this.applyError = null;
|
||||
this.importPhase = 'Extraction du texte…';
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseExtracting');
|
||||
this.importProgress = null;
|
||||
this.importStatus = null;
|
||||
this.importCounts = null;
|
||||
@@ -150,10 +152,10 @@ export class CampaignImportComponent implements OnInit {
|
||||
// Un morceau vient d'aboutir : le message d'attente est obsolète.
|
||||
this.importStatus = null;
|
||||
if (ev.total === 0) {
|
||||
this.importPhase = 'Extraction du texte…';
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseExtracting');
|
||||
this.importProgress = null;
|
||||
} else {
|
||||
this.importPhase = `Analyse de la campagne… (${ev.current}/${ev.total})`;
|
||||
this.importPhase = this.translate.instant('campaignImport.phaseAnalyzing', { current: ev.current, total: ev.total });
|
||||
this.importProgress = { current: ev.current, total: ev.total };
|
||||
this.importCounts = {
|
||||
arcs: ev.arcCount, chapters: ev.chapterCount,
|
||||
@@ -168,7 +170,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.importProgress = null;
|
||||
this.importStatus = null;
|
||||
if ((ev.arcs ?? []).length === 0 && (ev.npcs ?? []).length === 0) {
|
||||
this.importError = "Aucune structure narrative détectée dans ce PDF.";
|
||||
this.importError = this.translate.instant('campaignImport.errorNoStructure');
|
||||
this.reviewing = false;
|
||||
} else {
|
||||
this.tree = this.buildMergedTree(ev.arcs ?? []);
|
||||
@@ -181,7 +183,9 @@ export class CampaignImportComponent implements OnInit {
|
||||
this.importing = false;
|
||||
this.importPhase = '';
|
||||
this.importProgress = null;
|
||||
this.importError = err?.message ? `Échec de l'import : ${err.message}` : "Échec de l'import du PDF.";
|
||||
this.importError = err?.message
|
||||
? this.translate.instant('campaignImport.errorImportDetail', { message: err.message })
|
||||
: this.translate.instant('campaignImport.errorImport');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -413,7 +417,7 @@ export class CampaignImportComponent implements OnInit {
|
||||
next: () => this.router.navigate(['/campaigns', this.campaignId]),
|
||||
error: () => {
|
||||
this.applying = false;
|
||||
this.applyError = "Échec de la création. La campagne existe-t-elle toujours ?";
|
||||
this.applyError = this.translate.instant('campaignImport.errorApply');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user