Files
LoreMind/web/src/app/campaigns/character/character-edit/character-edit.component.ts
IETM_FIXE\ietm6 23878f1c63 Migration Angular 20 -> 21 : fin de la migration securite (0 vulnerabilite npm)
- ng update @angular/core@21 @angular/cli@21 (corrige GHSA-jrmj-c5cx-3cw6, XSS SVG)
- Migration automatique des templates vers le control flow natif (@if/@for, 138 fichiers)
- Correction des 5 `track` invalides generes par la migration (trackBy a 1 argument
  -> track $index) + suppression des fonctions trackBy mortes
- TypeScript 5.9, zone.js 0.15 ; npm audit : 0 vulnerabilite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:55:36 +02:00

171 lines
6.2 KiB
TypeScript

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Save, ArrowLeft, User, Trash2, Sparkles } from 'lucide-angular';
import { CharacterService } from '../../../services/character.service';
import { CampaignService } from '../../../services/campaign.service';
import { GameSystemService } from '../../../services/game-system.service';
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
import { TemplateField } from '../../../services/template.model';
import { AiChatDrawerComponent } from '../../../shared/ai-chat-drawer/ai-chat-drawer.component';
import { DynamicFieldsFormComponent } from '../../../shared/dynamic-fields-form/dynamic-fields-form.component';
import { SingleImagePickerComponent } from '../../../shared/single-image-picker/single-image-picker.component';
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
/**
* Editeur plein ecran d'une fiche de personnage (PJ).
* Refonte 2026-04-30 : remplace le markdown libre par un formulaire dynamique
* pilote par le characterTemplate du GameSystem associe a la campagne.
*
* Comportements :
* - Si la campagne n'a pas de GameSystem ou si son template est vide, affiche
* uniquement les champs universels (nom, portrait, header).
* - Le picker d'images dedie portrait/header est hors scope MVP — pour l'instant
* saisie manuelle d'IDs d'images.
*/
@Component({
selector: 'app-character-edit',
imports: [FormsModule, LucideAngularModule, AiChatDrawerComponent, DynamicFieldsFormComponent, SingleImagePickerComponent],
templateUrl: './character-edit.component.html',
styleUrls: ['./character-edit.component.scss']
})
export class CharacterEditComponent implements OnInit {
readonly Save = Save;
readonly ArrowLeft = ArrowLeft;
readonly User = User;
readonly Trash2 = Trash2;
readonly Sparkles = Sparkles;
chatOpen = false;
readonly chatQuickSuggestions = [
'Propose une backstory coherente avec l\'univers',
'Suggere 3 objectifs personnels pour ce personnage',
'Aide-moi a equilibrer les stats de combat'
];
toggleChat(): void { this.chatOpen = !this.chatOpen; }
campaignId: string | null = null;
/** Partie propriétaire du PJ — lue depuis la route (les PJ sont scoping Playthrough). */
playthroughId: string | null = null;
characterId: string | null = null;
name = '';
portraitImageId: string | null = null;
headerImageId: string | null = null;
values: Record<string, string> = {};
imageValues: Record<string, string[]> = {};
keyValueValues: Record<string, Record<string, string>> = {};
templateFields: TemplateField[] = [];
private order = 0;
constructor(
private route: ActivatedRoute,
private router: Router,
private service: CharacterService,
private campaignService: CampaignService,
private gameSystemService: GameSystemService,
private campaignSidebar: CampaignSidebarService,
private confirmDialog: ConfirmDialogService
) {}
ngOnInit(): void {
const params = this.route.snapshot.paramMap;
this.campaignId = params.get('campaignId');
this.playthroughId = params.get('playthroughId');
this.characterId = params.get('characterId');
if (this.campaignId) {
this.loadTemplateForCampaign(this.campaignId);
this.campaignSidebar.show(this.campaignId);
}
if (this.characterId) {
this.service.getById(this.characterId).subscribe({
next: (c) => {
this.name = c.name;
this.portraitImageId = c.portraitImageId ?? null;
this.headerImageId = c.headerImageId ?? null;
this.values = c.values ?? {};
this.imageValues = c.imageValues ?? {};
this.keyValueValues = c.keyValueValues ?? {};
this.order = c.order ?? 0;
},
error: () => this.back()
});
}
}
private loadTemplateForCampaign(campaignId: string): void {
this.campaignService.getCampaignById(campaignId).subscribe({
next: (campaign) => {
if (!campaign.gameSystemId) {
this.templateFields = [];
return;
}
this.gameSystemService.getById(campaign.gameSystemId).subscribe({
next: (gs) => { this.templateFields = gs.characterTemplate ?? []; },
error: () => { this.templateFields = []; }
});
},
error: () => { this.templateFields = []; }
});
}
submit(): void {
if (!this.name.trim() || !this.campaignId || !this.playthroughId) return;
const payload = {
name: this.name.trim(),
portraitImageId: this.portraitImageId,
headerImageId: this.headerImageId,
values: this.values,
imageValues: this.imageValues,
keyValueValues: this.keyValueValues,
playthroughId: this.playthroughId
};
const isCreation = !this.characterId;
const req = this.characterId
? this.service.update(this.characterId, { ...payload, id: this.characterId, order: this.order })
: this.service.create(payload);
req.subscribe({
next: (saved) => {
if (isCreation && saved.id) {
this.router.navigate(['/campaigns', this.campaignId, 'playthroughs', this.playthroughId, 'characters', saved.id]);
} else {
this.back();
}
},
error: () => console.error('Erreur sauvegarde Character')
});
}
deleteCharacter(): void {
if (!this.characterId) return;
this.confirmDialog.confirm({
title: 'Supprimer la fiche ?',
message: `Supprimer la fiche de "${this.name}" ?`,
details: ['Cette action est irreversible.'],
confirmLabel: 'Supprimer',
variant: 'danger'
}).then(ok => {
if (!ok || !this.characterId) return;
this.service.delete(this.characterId).subscribe({
next: () => this.back(),
error: () => console.error('Erreur suppression Character')
});
});
}
back(): void {
if (this.campaignId && this.playthroughId) {
this.router.navigate(['/campaigns', this.campaignId, 'playthroughs', this.playthroughId]);
} else if (this.campaignId) {
this.router.navigate(['/campaigns', this.campaignId]);
} else {
this.router.navigate(['/campaigns']);
}
}
}