Correction de plusieurs anomalies : problème de switch entre 2 templates (par exemple si on était sur un template 1 et qu'on voulait passer directement au 2, ce dernier ne chargeait pas) ; correction du soucis d'apparition de la sidebar à gauche qui disparaissait sans explication ; problème de redirection : lorsqu'on terminait de créer un PJ / PNJ ; on arrivait sur l'accueil de la campagne au lieu de voir le résultat de la création. Problème de redirection également lors du clique sur un PNJ / PJ sur le coté : on arrivait sur l'édition au lieu de la présentation. Correction de la première lettre stylisée : tout est au même style comme ça plus de probleme de lecture. Nouveautées : stylisation des modales (notamment suppression, warning.....) avec en prime l'ajout d'un warning lors du changement de système pour avertir que les fiches persos ne sont pas conservées. Ajout d'une option pour créer un game system directement à la création d'une campagne afin de faciliter la mise en place de cette dernière. Ajout d'un bouton pour créer un nouveau template directement lorsqu'on créer une page : ça permet de créer un template et de revenir sur la page qu'on était en train de créer sans perdre le titre. Passage en bêta 0.8.4
200 lines
8.1 KiB
TypeScript
200 lines
8.1 KiB
TypeScript
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { forkJoin, of } from 'rxjs';
|
|
import { switchMap } from 'rxjs/operators';
|
|
import { LucideAngularModule, Trash2, Sparkles } from 'lucide-angular';
|
|
import { CampaignService } from '../../../services/campaign.service';
|
|
import { CharacterService } from '../../../services/character.service';
|
|
import { NpcService } from '../../../services/npc.service';
|
|
import { PageService } from '../../../services/page.service';
|
|
import { LayoutService } from '../../../services/layout.service';
|
|
import { PageTitleService } from '../../../services/page-title.service';
|
|
import { Arc } from '../../../services/campaign.model';
|
|
import { Page } from '../../../services/page.model';
|
|
import { loadCampaignTreeData, buildCampaignSidebarConfig } from '../../campaign-tree.helper';
|
|
import { LoreLinkPickerComponent } from '../../../shared/lore-link-picker/lore-link-picker.component';
|
|
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
|
|
import { ImageGalleryComponent } from '../../../shared/image-gallery/image-gallery.component';
|
|
import { IconPickerComponent } from '../../../shared/icon-picker/icon-picker.component';
|
|
import { CAMPAIGN_ICON_OPTIONS } from '../../campaign-icons';
|
|
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
|
|
|
|
/**
|
|
* Écran de détail/modification d'un Arc.
|
|
* Route : /campaigns/:campaignId/arcs/:arcId
|
|
*
|
|
* Intègre le picker de pages Lore (phase B2 cross-context) :
|
|
* si la campagne parente est associée à un Lore (`campaign.loreId`), les pages
|
|
* de ce Lore sont proposées dans un autocomplete pour lier cet arc à des
|
|
* personnages / lieux / objets du Lore.
|
|
*/
|
|
@Component({
|
|
selector: 'app-arc-edit',
|
|
standalone: true,
|
|
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent],
|
|
templateUrl: './arc-edit.component.html',
|
|
styleUrls: ['./arc-edit.component.scss']
|
|
})
|
|
export class ArcEditComponent implements OnInit, OnDestroy {
|
|
readonly Trash2 = Trash2;
|
|
readonly Sparkles = Sparkles;
|
|
readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS;
|
|
selectedIcon: string | null = null;
|
|
|
|
/** État drawer chat IA (b5.7 — intégration Campagne). */
|
|
chatOpen = false;
|
|
readonly chatQuickSuggestions = [
|
|
'Propose 3 thèmes majeurs pour cet arc',
|
|
'Imagine des enjeux qui mettent la pression sur les joueurs',
|
|
'Suggère un dénouement en deux actes'
|
|
];
|
|
|
|
toggleChat(): void { this.chatOpen = !this.chatOpen; }
|
|
|
|
form: FormGroup;
|
|
campaignId = '';
|
|
arcId = '';
|
|
arc: Arc | null = null;
|
|
|
|
/** Pages disponibles pour le picker (vide si la campagne n'a pas de loreId). */
|
|
availablePages: Page[] = [];
|
|
/** ID du Lore associé à la campagne (null si campagne sans univers). */
|
|
loreId: string | null = null;
|
|
/** IDs des pages liées à cet arc (bind sur app-lore-link-picker). */
|
|
relatedPageIds: string[] = [];
|
|
|
|
/** IDs des images illustrant cet arc (bind sur app-image-gallery editable). */
|
|
illustrationImageIds: string[] = [];
|
|
/** IDs des images utilisees comme cartes / plans (outil de table). */
|
|
mapImageIds: string[] = [];
|
|
|
|
constructor(
|
|
private fb: FormBuilder,
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
private campaignService: CampaignService,
|
|
private characterService: CharacterService,
|
|
private npcService: NpcService,
|
|
private pageService: PageService,
|
|
private layoutService: LayoutService,
|
|
private pageTitleService: PageTitleService,
|
|
private confirmDialog: ConfirmDialogService
|
|
) {
|
|
this.form = this.fb.group({
|
|
name: ['', Validators.required],
|
|
description: [''],
|
|
themes: [''],
|
|
stakes: [''],
|
|
gmNotes: [''],
|
|
rewards: [''],
|
|
resolution: ['']
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
// On s'abonne à paramMap plutôt que de lire snapshot une fois : Angular
|
|
// réutilise le composant quand on navigue entre arcs frères via l'arbre
|
|
// (même route pattern), et ngOnInit ne se relance pas.
|
|
this.route.paramMap.subscribe(pm => {
|
|
const newCampaignId = pm.get('campaignId')!;
|
|
const newArcId = pm.get('arcId')!;
|
|
if (newArcId !== this.arcId || newCampaignId !== this.campaignId) {
|
|
this.campaignId = newCampaignId;
|
|
this.arcId = newArcId;
|
|
this.loadAll();
|
|
}
|
|
});
|
|
}
|
|
|
|
private loadAll(): void {
|
|
// On déclenche d'abord les 4 appels indépendants, puis on charge les pages
|
|
// du Lore associé UNIQUEMENT si la campagne en a un (switchMap conditionnel).
|
|
forkJoin({
|
|
campaign: this.campaignService.getCampaignById(this.campaignId),
|
|
allCampaigns: this.campaignService.getAllCampaigns(),
|
|
arc: this.campaignService.getArcById(this.arcId),
|
|
treeData: loadCampaignTreeData(this.campaignService, this.campaignId, this.characterService, this.npcService)
|
|
}).pipe(
|
|
switchMap(data => {
|
|
const lid = data.campaign.loreId ?? null;
|
|
// Pas de loreId → pas de picker, on retourne une liste vide.
|
|
const pages$ = lid ? this.pageService.getByLoreId(lid) : of([] as Page[]);
|
|
return pages$.pipe(
|
|
switchMap(pages => of({ ...data, pages, loreId: lid }))
|
|
);
|
|
})
|
|
).subscribe(({ campaign, allCampaigns, arc, treeData, pages, loreId }) => {
|
|
this.arc = arc;
|
|
this.loreId = loreId;
|
|
this.availablePages = pages;
|
|
this.relatedPageIds = [...(arc.relatedPageIds ?? [])];
|
|
this.selectedIcon = arc.icon ?? null;
|
|
this.illustrationImageIds = [...(arc.illustrationImageIds ?? [])];
|
|
this.mapImageIds = [...(arc.mapImageIds ?? [])];
|
|
this.pageTitleService.set(arc.name);
|
|
this.form.patchValue({
|
|
name: arc.name,
|
|
description: arc.description ?? '',
|
|
themes: arc.themes ?? '',
|
|
stakes: arc.stakes ?? '',
|
|
gmNotes: arc.gmNotes ?? '',
|
|
rewards: arc.rewards ?? '',
|
|
resolution: arc.resolution ?? ''
|
|
});
|
|
|
|
this.layoutService.show(buildCampaignSidebarConfig(campaign, allCampaigns, treeData, this.campaignId));
|
|
});
|
|
}
|
|
|
|
submit(): void {
|
|
if (this.form.invalid || !this.arc) return;
|
|
this.campaignService.updateArc(this.arcId, {
|
|
name: this.form.value.name,
|
|
description: this.form.value.description,
|
|
campaignId: this.campaignId,
|
|
order: this.arc.order ?? 1,
|
|
themes: this.form.value.themes,
|
|
stakes: this.form.value.stakes,
|
|
gmNotes: this.form.value.gmNotes,
|
|
rewards: this.form.value.rewards,
|
|
resolution: this.form.value.resolution,
|
|
relatedPageIds: this.relatedPageIds,
|
|
illustrationImageIds: this.illustrationImageIds,
|
|
mapImageIds: this.mapImageIds,
|
|
icon: this.selectedIcon
|
|
}).subscribe({
|
|
next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]),
|
|
error: () => console.error('Erreur lors de la sauvegarde')
|
|
});
|
|
}
|
|
|
|
delete(): void {
|
|
this.confirmDialog.confirm({
|
|
title: 'Supprimer l\'arc',
|
|
message: `Supprimer l'arc "${this.arc?.name}" ?`,
|
|
details: ['Cette action est irréversible.'],
|
|
confirmLabel: 'Supprimer',
|
|
variant: 'danger'
|
|
}).then(ok => {
|
|
if (!ok) return;
|
|
this.campaignService.deleteArc(this.arcId).subscribe({
|
|
next: () => this.router.navigate(['/campaigns', this.campaignId]),
|
|
error: () => console.error('Erreur lors de la suppression')
|
|
});
|
|
});
|
|
}
|
|
|
|
cancel(): void {
|
|
this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]);
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
// Volontairement vide : la sidebar reste prise en charge par le composant
|
|
// suivant (autre sous-route ou le composant detail parent) qui appellera
|
|
// show(). Eviter d'appeler hide() ici previent le clignotement / la
|
|
// disparition de la sidebar lors des navigations internes a la section.
|
|
}
|
|
}
|