Ajout d'un mode "jeu" (possibilité de lancer des sessions dans une campagne). Cela permet de faire de prendre des notes en live au cours d'une partie et d'avoir plusieurs outils sous la main pour aider le mj :
- Possibilité de parler à une IA pour règle de jeu ou élément de lore / campagne au cours d'une partie comme aide mémoire - Onglet dédié aux personnages de la campagne - Onglet dédié aux scènes - Onglet avec dès pour ceux qui souhaitent ; Possibilité de rajouté une note en tant qu'évènement, jet de dès ou encore action du joueur par exemple. D'autres ajouts seront fait dans le futur (notamment des tables aléatoires pour PNJ en live).
This commit is contained in:
276
web/src/app/sessions/session-detail/session-detail.component.ts
Normal file
276
web/src/app/sessions/session-detail/session-detail.component.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
LucideAngularModule, LucideIconData,
|
||||
Dices, ArrowLeft, Square, Trash2, Pencil, Check,
|
||||
StickyNote, Sparkles, UserCheck, Plus, X
|
||||
} from 'lucide-angular';
|
||||
import { catchError, switchMap, filter, map } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
import { SessionService } from '../../services/session.service';
|
||||
import { Session } from '../../services/session.model';
|
||||
import {
|
||||
SessionEntry, SessionEntryInput, EntryType, ENTRY_TYPE_META
|
||||
} from '../../services/session-entry.model';
|
||||
import { SessionEntryService } from '../../services/session-entry.service';
|
||||
import { LayoutService } from '../../services/layout.service';
|
||||
import { PageTitleService } from '../../services/page-title.service';
|
||||
import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog.service';
|
||||
import { SessionReferencePanelComponent } from '../session-reference-panel/session-reference-panel.component';
|
||||
import { DiceRollResult } from '../session-dice-panel/session-dice-panel.component';
|
||||
|
||||
/**
|
||||
* Vue détail d'une Session avec journal horodaté.
|
||||
* Form de saisie en haut, timeline en dessous (plus récent en premier).
|
||||
* Le layout dédié "mode jeu" sera ajouté en Phase 4.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-session-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule, RouterLink, SessionReferencePanelComponent],
|
||||
templateUrl: './session-detail.component.html',
|
||||
styleUrls: ['./session-detail.component.scss']
|
||||
})
|
||||
export class SessionDetailComponent implements OnInit, OnDestroy {
|
||||
readonly Dices = Dices;
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Square = Square;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Pencil = Pencil;
|
||||
readonly Check = Check;
|
||||
readonly Plus = Plus;
|
||||
readonly X = X;
|
||||
|
||||
/** Mapping enum → composant Lucide pour le rendu des icônes par type. */
|
||||
readonly typeIcons: Record<EntryType, LucideIconData> = {
|
||||
NOTE: StickyNote,
|
||||
EVENT: Sparkles,
|
||||
DICE_ROLL: Dices,
|
||||
PLAYER_ACTION: UserCheck,
|
||||
};
|
||||
readonly entryTypes: EntryType[] = ['NOTE', 'EVENT', 'DICE_ROLL', 'PLAYER_ACTION'];
|
||||
readonly entryTypeMeta = ENTRY_TYPE_META;
|
||||
|
||||
session: Session | null = null;
|
||||
/** Timeline triée du plus récent au plus ancien (DESC) pour l'UX en partie. */
|
||||
entries: SessionEntry[] = [];
|
||||
|
||||
editingName = false;
|
||||
editName = '';
|
||||
|
||||
/** State de la zone "Ajouter une entrée". */
|
||||
newEntryType: EntryType = 'NOTE';
|
||||
newEntryContent = '';
|
||||
submittingEntry = false;
|
||||
|
||||
/** Id de l'entrée en cours d'édition (null si aucune). */
|
||||
editingEntryId: string | null = null;
|
||||
editEntryType: EntryType = 'NOTE';
|
||||
editEntryContent = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private sessionService: SessionService,
|
||||
private entryService: SessionEntryService,
|
||||
private layoutService: LayoutService,
|
||||
private pageTitleService: PageTitleService,
|
||||
private confirmDialog: ConfirmDialogService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.layoutService.hide();
|
||||
this.route.paramMap.pipe(
|
||||
map(pm => pm.get('id')),
|
||||
filter((id): id is string => !!id),
|
||||
switchMap(id => this.sessionService.getSessionById(id).pipe(
|
||||
catchError(() => of(null))
|
||||
))
|
||||
).subscribe(session => {
|
||||
this.session = session;
|
||||
if (session) {
|
||||
this.pageTitleService.set(session.name);
|
||||
this.loadEntries(session.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadEntries(sessionId: string): void {
|
||||
this.entryService.getEntries(sessionId).pipe(
|
||||
catchError(() => of([] as SessionEntry[]))
|
||||
).subscribe(list => {
|
||||
this.entries = list.slice().sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────── Renommage de la Session ───────────────
|
||||
|
||||
startRename(): void {
|
||||
if (!this.session) return;
|
||||
this.editName = this.session.name;
|
||||
this.editingName = true;
|
||||
}
|
||||
|
||||
cancelRename(): void {
|
||||
this.editingName = false;
|
||||
this.editName = '';
|
||||
}
|
||||
|
||||
saveRename(): void {
|
||||
if (!this.session || !this.editName.trim()) return;
|
||||
this.sessionService.renameSession(this.session.id, this.editName.trim()).subscribe({
|
||||
next: updated => {
|
||||
this.session = updated;
|
||||
this.editingName = false;
|
||||
this.pageTitleService.set(updated.name);
|
||||
},
|
||||
error: () => console.error('Erreur lors du renommage de la session')
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────── Fin / suppression de Session ───────────────
|
||||
|
||||
endSession(): void {
|
||||
if (!this.session || !this.session.active) return;
|
||||
const session = this.session;
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Terminer la session ?',
|
||||
message: `Marquer la session "${session.name}" comme terminée ?`,
|
||||
details: ['Tu pourras toujours consulter son contenu après.'],
|
||||
confirmLabel: 'Terminer',
|
||||
variant: 'warning'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
this.sessionService.endSession(session.id).subscribe({
|
||||
next: updated => this.session = updated,
|
||||
error: () => console.error('Erreur lors de la fin de session')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteSession(): void {
|
||||
if (!this.session) return;
|
||||
const session = this.session;
|
||||
const entryCount = this.entries.length;
|
||||
const details = [
|
||||
entryCount > 0
|
||||
? `${entryCount} entrée${entryCount > 1 ? 's' : ''} de journal sera également supprimée.`
|
||||
: 'Aucune entrée de journal pour cette session.',
|
||||
'Cette action est irréversible.'
|
||||
];
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer la session ?',
|
||||
message: `Supprimer définitivement la session "${session.name}" ?`,
|
||||
details,
|
||||
confirmLabel: 'Supprimer',
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
const campaignId = session.campaignId;
|
||||
this.sessionService.deleteSession(session.id).subscribe({
|
||||
next: () => this.router.navigate(['/campaigns', campaignId]),
|
||||
error: () => console.error('Erreur lors de la suppression de la session')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────── Ajout d'entrée ───────────────
|
||||
|
||||
submitNewEntry(): void {
|
||||
if (!this.session || this.submittingEntry) return;
|
||||
const content = this.newEntryContent.trim();
|
||||
if (!content) return;
|
||||
this.submittingEntry = true;
|
||||
const input: SessionEntryInput = { type: this.newEntryType, content };
|
||||
this.entryService.createEntry(this.session.id, input).subscribe({
|
||||
next: created => {
|
||||
this.submittingEntry = false;
|
||||
this.entries = [created, ...this.entries];
|
||||
this.newEntryContent = '';
|
||||
},
|
||||
error: () => {
|
||||
this.submittingEntry = false;
|
||||
console.error('Erreur lors de l\'ajout de l\'entrée');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────── Édition d'entrée ───────────────
|
||||
|
||||
startEditEntry(entry: SessionEntry): void {
|
||||
this.editingEntryId = entry.id;
|
||||
this.editEntryType = entry.type;
|
||||
this.editEntryContent = entry.content;
|
||||
}
|
||||
|
||||
cancelEditEntry(): void {
|
||||
this.editingEntryId = null;
|
||||
this.editEntryContent = '';
|
||||
}
|
||||
|
||||
saveEditEntry(entry: SessionEntry): void {
|
||||
if (!this.session) return;
|
||||
const content = this.editEntryContent.trim();
|
||||
if (!content) return;
|
||||
const input: SessionEntryInput = { type: this.editEntryType, content };
|
||||
this.entryService.updateEntry(this.session.id, entry.id, input).subscribe({
|
||||
next: updated => {
|
||||
this.entries = this.entries.map(e => e.id === updated.id ? updated : e);
|
||||
this.editingEntryId = null;
|
||||
},
|
||||
error: () => console.error('Erreur lors de la mise à jour de l\'entrée')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Réception d'un jet de dés depuis le panneau latéral.
|
||||
* On crée une entrée DICE_ROLL dans le journal avec le résumé formaté.
|
||||
*/
|
||||
onDiceRolled(result: DiceRollResult): void {
|
||||
if (!this.session || !this.session.active) return;
|
||||
const input: SessionEntryInput = { type: 'DICE_ROLL', content: result.summary };
|
||||
this.entryService.createEntry(this.session.id, input).subscribe({
|
||||
next: created => this.entries = [created, ...this.entries],
|
||||
error: () => console.error('Erreur lors de l\'ajout du jet au journal')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Réception d'une réponse IA à sauvegarder dans le journal.
|
||||
* Type NOTE par défaut car c'est le MJ qui choisit de capter une suggestion
|
||||
* comme repère — pas un évènement de partie en lui-même.
|
||||
*/
|
||||
onAiReplyToJournal(content: string): void {
|
||||
if (!this.session || !this.session.active) return;
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) return;
|
||||
const input: SessionEntryInput = { type: 'NOTE', content: '💡 ' + trimmed };
|
||||
this.entryService.createEntry(this.session.id, input).subscribe({
|
||||
next: created => this.entries = [created, ...this.entries],
|
||||
error: () => console.error('Erreur lors de l\'ajout de la suggestion IA au journal')
|
||||
});
|
||||
}
|
||||
|
||||
deleteEntry(entry: SessionEntry): void {
|
||||
if (!this.session) return;
|
||||
const session = this.session;
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer cette entrée ?',
|
||||
message: 'Cette entrée du journal sera définitivement supprimée.',
|
||||
confirmLabel: 'Supprimer',
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
this.entryService.deleteEntry(session.id, entry.id).subscribe({
|
||||
next: () => this.entries = this.entries.filter(e => e.id !== entry.id),
|
||||
error: () => console.error('Erreur lors de la suppression de l\'entrée')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.layoutService.hide();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user