Amélioration de l'IA pour la partie atelier PDF
Mise en place d'un outil permettant de faire des tableau d'objets pour des boutiques par exemple
This commit is contained in:
@@ -27,6 +27,10 @@ export const routes: Routes = [
|
||||
{ path: 'campaigns/:campaignId/npcs/:npcId', loadComponent: () => import('./campaigns/npc/npc-view/npc-view.component').then(m => m.NpcViewComponent) },
|
||||
{ path: 'campaigns/:campaignId/notebooks', loadComponent: () => import('./campaigns/notebook/notebook-list/notebook-list.component').then(m => m.NotebookListComponent) },
|
||||
{ path: 'campaigns/:campaignId/notebooks/:notebookId', loadComponent: () => import('./campaigns/notebook/notebook-detail/notebook-detail.component').then(m => m.NotebookDetailComponent) },
|
||||
{ path: 'campaigns/:campaignId/item-catalogs', loadComponent: () => import('./campaigns/item-catalog/item-catalog-list/item-catalog-list.component').then(m => m.ItemCatalogListComponent) },
|
||||
{ path: 'campaigns/:campaignId/item-catalogs/create', loadComponent: () => import('./campaigns/item-catalog/item-catalog-edit/item-catalog-edit.component').then(m => m.ItemCatalogEditComponent) },
|
||||
{ path: 'campaigns/:campaignId/item-catalogs/:catalogId/edit', loadComponent: () => import('./campaigns/item-catalog/item-catalog-edit/item-catalog-edit.component').then(m => m.ItemCatalogEditComponent) },
|
||||
{ path: 'campaigns/:campaignId/item-catalogs/:catalogId', loadComponent: () => import('./campaigns/item-catalog/item-catalog-view/item-catalog-view.component').then(m => m.ItemCatalogViewComponent) },
|
||||
{ path: 'campaigns/:campaignId/random-tables/create', loadComponent: () => import('./campaigns/random-table/random-table-edit/random-table-edit.component').then(m => m.RandomTableEditComponent) },
|
||||
{ path: 'campaigns/:campaignId/random-tables/:tableId/edit', loadComponent: () => import('./campaigns/random-table/random-table-edit/random-table-edit.component').then(m => m.RandomTableEditComponent) },
|
||||
{ path: 'campaigns/:campaignId/random-tables/:tableId', loadComponent: () => import('./campaigns/random-table/random-table-view/random-table-view.component').then(m => m.RandomTableViewComponent) },
|
||||
|
||||
@@ -225,6 +225,14 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T
|
||||
route: `/campaigns/${campaignId}/notebooks`
|
||||
};
|
||||
|
||||
// Catalogues d'objets (boutiques, butins…) → page de liste (outil).
|
||||
const catalogsNode: TreeItem = {
|
||||
id: 'item-catalogs-root',
|
||||
label: 'Catalogues d\'objets',
|
||||
iconKey: 'package',
|
||||
route: `/campaigns/${campaignId}/item-catalogs`
|
||||
};
|
||||
|
||||
// Importer un PDF de campagne → arborescence (outil, comme tables & ateliers).
|
||||
const importNode: TreeItem = {
|
||||
id: 'import-pdf-root',
|
||||
@@ -233,7 +241,7 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T
|
||||
route: `/campaigns/${campaignId}/import`
|
||||
};
|
||||
|
||||
return [...arcNodes, npcsNode, tablesNode, notebooksNode, importNode];
|
||||
return [...arcNodes, npcsNode, tablesNode, notebooksNode, catalogsNode, importNode];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="ice-page">
|
||||
<div class="ice-toolbar">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon> Retour
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn-save" (click)="save()" [disabled]="saving">
|
||||
<lucide-icon [img]="Save" [size]="14"></lucide-icon>
|
||||
{{ saving ? 'Enregistrement…' : 'Enregistrer' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1>{{ catalogId ? 'Éditer le catalogue' : 'Nouveau catalogue d\'objets' }}</h1>
|
||||
|
||||
<div class="error" *ngIf="errorMessage">{{ errorMessage }}</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ic-name">Nom *</label>
|
||||
<input id="ic-name" type="text" [(ngModel)]="name" placeholder="Ex: Échoppe de l'armurier nain">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ic-desc">Description</label>
|
||||
<textarea id="ic-desc" rows="2" [(ngModel)]="description" placeholder="À quoi sert ce catalogue / cette boutique ?"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Génération IA -->
|
||||
<div class="ai-box">
|
||||
<div class="ai-title"><lucide-icon [img]="Sparkles" [size]="15"></lucide-icon> Générer avec l'IA</div>
|
||||
<p class="ai-hint">Décris la boutique/le butin ; l'IA propose les objets (revois-les avant d'enregistrer). Contextualisé avec ta campagne.</p>
|
||||
<textarea rows="2" [(ngModel)]="aiPrompt"
|
||||
placeholder="Ex: boutique d'alchimiste dans une cité portuaire, objets exotiques"></textarea>
|
||||
<div class="ai-actions">
|
||||
<button class="btn-ai" (click)="generateWithAI()" [disabled]="generating">
|
||||
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
|
||||
{{ generating ? 'Génération…' : 'Générer' }}
|
||||
</button>
|
||||
<span class="ai-error" *ngIf="aiError">{{ aiError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="items-head">
|
||||
<h2>Objets</h2>
|
||||
<button class="btn-mini" (click)="addItem()">
|
||||
<lucide-icon [img]="Plus" [size]="13"></lucide-icon> Ajouter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="item-row head">
|
||||
<span class="c-name">Nom</span>
|
||||
<span class="c-price">Prix</span>
|
||||
<span class="c-cat">Catégorie</span>
|
||||
<span class="c-desc">Description</span>
|
||||
<span class="c-del"></span>
|
||||
</div>
|
||||
|
||||
<div class="item-row" *ngFor="let it of items; let i = index">
|
||||
<input class="c-name" type="text" [(ngModel)]="it.name" placeholder="Objet">
|
||||
<input class="c-price" type="text" [(ngModel)]="it.price" placeholder="50 po">
|
||||
<input class="c-cat" type="text" [(ngModel)]="it.category" placeholder="Armes">
|
||||
<input class="c-desc" type="text" [(ngModel)]="it.description" placeholder="Effet / détails">
|
||||
<button class="c-del btn-del" (click)="removeItem(i)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="empty-hint" *ngIf="items.length === 0">Aucun objet — clique « Ajouter ».</p>
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
.ice-page { max-width: 980px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
|
||||
|
||||
.ice-toolbar {
|
||||
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;
|
||||
.spacer { flex: 1; }
|
||||
button {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.04);
|
||||
color: inherit; cursor: pointer; font-size: 0.85rem;
|
||||
&:hover { background: rgba(255,255,255,0.09); }
|
||||
&:disabled { opacity: 0.5; cursor: default; }
|
||||
}
|
||||
.btn-save { background: #667eea; border-color: #667eea; color: #fff; }
|
||||
.btn-save:hover:not(:disabled) { background: #5568d3; }
|
||||
}
|
||||
|
||||
h1 { font-size: 1.4rem; margin: 0 0 1rem; }
|
||||
h2 { font-size: 1rem; margin: 0; }
|
||||
|
||||
.error {
|
||||
padding: 0.6rem 0.9rem; border-radius: 6px; margin-bottom: 1rem;
|
||||
background: rgba(224,90,90,0.12); border: 1px solid rgba(224,90,90,0.4); color: #e88;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex; flex-direction: column; gap: 0.3rem; margin-bottom: 1rem;
|
||||
label { font-size: 0.85rem; color: var(--color-text-muted, #9aa0aa); }
|
||||
input, textarea {
|
||||
padding: 0.5rem 0.7rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.04);
|
||||
color: inherit; font: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-box {
|
||||
margin: 1rem 0; padding: 0.85rem 1rem; border-radius: 10px;
|
||||
background: rgba(168,130,255,0.08); border: 1px solid rgba(168,130,255,0.28);
|
||||
.ai-title { display: flex; align-items: center; gap: 0.4rem; font-weight: 600; color: #c4a8ff; }
|
||||
.ai-hint { margin: 0.3rem 0 0.5rem; font-size: 0.8rem; color: var(--color-text-muted, #9aa0aa); }
|
||||
textarea {
|
||||
width: 100%; padding: 0.5rem 0.7rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.04); color: inherit; font: inherit;
|
||||
}
|
||||
.ai-actions { display: flex; align-items: center; gap: 0.7rem; margin-top: 0.5rem; }
|
||||
.btn-ai {
|
||||
display: inline-flex; align-items: center; gap: 0.4rem;
|
||||
padding: 0.45rem 0.9rem; border: none; border-radius: 7px;
|
||||
background: #8a6dff; color: #fff; font-weight: 600; cursor: pointer;
|
||||
&:hover:not(:disabled) { background: #7a5cf0; }
|
||||
&:disabled { opacity: 0.55; cursor: default; }
|
||||
}
|
||||
.ai-error { color: #e88; font-size: 0.82rem; }
|
||||
}
|
||||
|
||||
.items-head {
|
||||
display: flex; align-items: center; justify-content: space-between; margin: 1.25rem 0 0.5rem;
|
||||
.btn-mini {
|
||||
display: inline-flex; align-items: center; gap: 0.3rem;
|
||||
padding: 0.3rem 0.6rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.04);
|
||||
color: inherit; cursor: pointer; font-size: 0.8rem;
|
||||
&:hover { background: rgba(255,255,255,0.09); }
|
||||
}
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: grid; grid-template-columns: 1.4fr 80px 1fr 1.8fr 36px;
|
||||
gap: 0.5rem; align-items: center; margin-bottom: 0.4rem;
|
||||
&.head {
|
||||
font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--color-text-muted, #9aa0aa); margin-bottom: 0.3rem;
|
||||
}
|
||||
input {
|
||||
padding: 0.4rem 0.55rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.04);
|
||||
color: inherit; font: inherit; width: 100%;
|
||||
}
|
||||
.btn-del {
|
||||
display: inline-flex; align-items: center; justify-content: center; padding: 0.35rem;
|
||||
border-radius: 6px; border: 1px solid rgba(224,90,90,0.3);
|
||||
background: rgba(224,90,90,0.08); color: #e88; cursor: pointer;
|
||||
&:hover { background: rgba(224,90,90,0.18); }
|
||||
}
|
||||
}
|
||||
|
||||
.empty-hint { color: var(--color-text-muted, #9aa0aa); font-style: italic; }
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, Save, Plus, Trash2, Sparkles } from 'lucide-angular';
|
||||
import { ItemCatalogService } from '../../../services/item-catalog.service';
|
||||
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
|
||||
import { ItemCatalog, CatalogItem, ItemCatalogCreate } from '../../../services/item-catalog.model';
|
||||
|
||||
/**
|
||||
* Création/édition d'un catalogue d'objets (boutique, butin…).
|
||||
* Routes : /campaigns/:campaignId/item-catalogs/create
|
||||
* /campaigns/:campaignId/item-catalogs/:catalogId/edit
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-item-catalog-edit',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LucideAngularModule],
|
||||
templateUrl: './item-catalog-edit.component.html',
|
||||
styleUrls: ['./item-catalog-edit.component.scss']
|
||||
})
|
||||
export class ItemCatalogEditComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Save = Save;
|
||||
readonly Plus = Plus;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Sparkles = Sparkles;
|
||||
|
||||
campaignId: string | null = null;
|
||||
catalogId: string | null = null;
|
||||
|
||||
name = '';
|
||||
description = '';
|
||||
items: CatalogItem[] = [];
|
||||
|
||||
saving = false;
|
||||
errorMessage = '';
|
||||
|
||||
aiPrompt = '';
|
||||
generating = false;
|
||||
aiError = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private service: ItemCatalogService,
|
||||
private campaignSidebar: CampaignSidebarService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const params = this.route.snapshot.paramMap;
|
||||
this.campaignId = params.get('campaignId');
|
||||
this.catalogId = params.get('catalogId');
|
||||
if (this.campaignId) this.campaignSidebar.show(this.campaignId);
|
||||
|
||||
if (this.catalogId) {
|
||||
this.service.getById(this.catalogId).subscribe({
|
||||
next: (c: ItemCatalog) => {
|
||||
this.name = c.name;
|
||||
this.description = c.description ?? '';
|
||||
this.items = c.items.map(i => ({ ...i }));
|
||||
},
|
||||
error: () => this.back()
|
||||
});
|
||||
} else {
|
||||
this.addItem();
|
||||
}
|
||||
}
|
||||
|
||||
addItem(): void {
|
||||
this.items.push({ name: '', price: '', category: '', description: '' });
|
||||
}
|
||||
|
||||
removeItem(i: number): void {
|
||||
this.items.splice(i, 1);
|
||||
}
|
||||
|
||||
generateWithAI(): void {
|
||||
if (!this.campaignId) return;
|
||||
if (!this.aiPrompt.trim()) { this.aiError = 'Décris le catalogue à générer.'; return; }
|
||||
this.generating = true;
|
||||
this.aiError = '';
|
||||
this.service.generate(this.campaignId, this.aiPrompt.trim()).subscribe({
|
||||
next: (c) => {
|
||||
this.generating = false;
|
||||
if (c.name && !this.name.trim()) this.name = c.name;
|
||||
if (c.description) this.description = c.description;
|
||||
this.items = (c.items ?? []).map(i => ({ ...i }));
|
||||
},
|
||||
error: (err) => {
|
||||
this.generating = false;
|
||||
this.aiError = err?.error?.message || 'Échec de la génération IA. Réessaie ou reformule.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.campaignId) return;
|
||||
if (!this.name.trim()) { this.errorMessage = 'Le nom est requis.'; return; }
|
||||
this.saving = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
const cleanItems = this.items
|
||||
.filter(i => i.name.trim())
|
||||
.map(i => ({
|
||||
name: i.name.trim(),
|
||||
price: i.price?.trim() || undefined,
|
||||
category: i.category?.trim() || undefined,
|
||||
description: i.description?.trim() || undefined
|
||||
}));
|
||||
|
||||
if (this.catalogId) {
|
||||
const payload: ItemCatalog = {
|
||||
id: this.catalogId,
|
||||
name: this.name.trim(),
|
||||
description: this.description.trim() || undefined,
|
||||
campaignId: this.campaignId,
|
||||
items: cleanItems
|
||||
};
|
||||
this.service.update(this.catalogId, payload).subscribe({
|
||||
next: () => this.goToView(this.catalogId!),
|
||||
error: (e) => this.fail(e)
|
||||
});
|
||||
} else {
|
||||
const payload: ItemCatalogCreate = {
|
||||
name: this.name.trim(),
|
||||
description: this.description.trim() || undefined,
|
||||
campaignId: this.campaignId,
|
||||
items: cleanItems
|
||||
};
|
||||
this.service.create(payload).subscribe({
|
||||
next: (c) => this.goToView(c.id!),
|
||||
error: (e) => this.fail(e)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private goToView(id: string): void {
|
||||
this.saving = false;
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'item-catalogs', id]);
|
||||
}
|
||||
|
||||
private fail(err: unknown): void {
|
||||
this.saving = false;
|
||||
this.errorMessage = 'Échec de l\'enregistrement.';
|
||||
console.error('ItemCatalog save failed', err);
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(this.campaignId ? ['/campaigns', this.campaignId] : ['/campaigns']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="icl-page">
|
||||
<div class="icl-toolbar">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon> Retour
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn-create" (click)="create()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> Nouveau catalogue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="icl-header">
|
||||
<h1><lucide-icon [img]="Package" [size]="22"></lucide-icon> Catalogues d'objets</h1>
|
||||
<p class="icl-hint">
|
||||
Boutiques, butins, trésors… à remplir à la main ou via l'IA. Consultables en session
|
||||
quand les joueurs visitent une échoppe.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="icl-list">
|
||||
<p class="empty" *ngIf="catalogs.length === 0">Aucun catalogue pour l'instant.</p>
|
||||
<button class="icl-item" *ngFor="let c of catalogs" (click)="open(c)">
|
||||
<lucide-icon [img]="Package" [size]="16"></lucide-icon>
|
||||
<span class="icl-item-name">{{ c.name }}</span>
|
||||
<span class="icl-item-count">{{ c.items.length }} objet(s)</span>
|
||||
<span class="icl-del" (click)="remove(c, $event)" title="Supprimer">
|
||||
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
.icl-page { max-width: 760px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
|
||||
|
||||
.icl-toolbar {
|
||||
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;
|
||||
.spacer { flex: 1; }
|
||||
button {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.4rem 0.8rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.04);
|
||||
color: inherit; cursor: pointer; font-size: 0.85rem;
|
||||
&:hover { background: rgba(255,255,255,0.09); }
|
||||
}
|
||||
.btn-create { background: #667eea; border-color: #667eea; color: #fff; }
|
||||
.btn-create:hover { background: #5568d3; }
|
||||
}
|
||||
|
||||
.icl-header {
|
||||
margin-bottom: 1.25rem;
|
||||
h1 { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.35rem; font-size: 1.5rem; }
|
||||
.icl-hint { margin: 0; color: var(--color-text-muted, #9aa0aa); font-size: 0.9rem; }
|
||||
}
|
||||
|
||||
.icl-list { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.empty { color: var(--color-text-muted, #9aa0aa); font-style: italic; }
|
||||
|
||||
.icl-item {
|
||||
display: flex; align-items: center; gap: 0.55rem;
|
||||
padding: 0.7rem 0.85rem; border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03);
|
||||
color: inherit; cursor: pointer; text-align: left;
|
||||
&:hover { background: rgba(255,255,255,0.07); }
|
||||
.icl-item-name { flex: 1; font-weight: 500; }
|
||||
.icl-item-count { font-size: 0.78rem; color: var(--color-text-muted, #9aa0aa); }
|
||||
.icl-del { display: inline-flex; padding: 0.25rem; border-radius: 5px; color: #e88;
|
||||
&:hover { background: rgba(224,90,90,0.15); } }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, Plus, Trash2, Package } from 'lucide-angular';
|
||||
import { ItemCatalogService } from '../../../services/item-catalog.service';
|
||||
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
|
||||
import { ItemCatalog } from '../../../services/item-catalog.model';
|
||||
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
|
||||
|
||||
/**
|
||||
* Liste des catalogues d'objets d'une campagne + création.
|
||||
* Route : /campaigns/:campaignId/item-catalogs
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-item-catalog-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './item-catalog-list.component.html',
|
||||
styleUrls: ['./item-catalog-list.component.scss']
|
||||
})
|
||||
export class ItemCatalogListComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Plus = Plus;
|
||||
readonly Trash2 = Trash2;
|
||||
readonly Package = Package;
|
||||
|
||||
campaignId = '';
|
||||
catalogs: ItemCatalog[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private service: ItemCatalogService,
|
||||
private campaignSidebar: CampaignSidebarService,
|
||||
private confirmDialog: ConfirmDialogService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.campaignId = this.route.snapshot.paramMap.get('campaignId') ?? '';
|
||||
if (this.campaignId) {
|
||||
this.campaignSidebar.show(this.campaignId);
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.service.getByCampaign(this.campaignId).subscribe({
|
||||
next: (list) => this.catalogs = list,
|
||||
error: () => this.catalogs = []
|
||||
});
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'item-catalogs', 'create']);
|
||||
}
|
||||
|
||||
open(c: ItemCatalog): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'item-catalogs', c.id]);
|
||||
}
|
||||
|
||||
remove(c: ItemCatalog, ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer le catalogue',
|
||||
message: `Supprimer « ${c.name} » ?`,
|
||||
confirmLabel: 'Supprimer',
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
this.service.delete(c.id!).subscribe(() => this.load());
|
||||
});
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(['/campaigns', this.campaignId]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="ic-page" *ngIf="catalog">
|
||||
<div class="ic-toolbar">
|
||||
<button class="btn-back" (click)="back()">
|
||||
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon> Retour
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn-edit" (click)="edit()">
|
||||
<lucide-icon [img]="Edit3" [size]="14"></lucide-icon> Éditer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="ic-header">
|
||||
<h1><lucide-icon [img]="Package" [size]="22"></lucide-icon> {{ catalog.name }}</h1>
|
||||
<p class="ic-desc" *ngIf="catalog.description">{{ catalog.description }}</p>
|
||||
</header>
|
||||
|
||||
<p class="ic-empty" *ngIf="catalog.items.length === 0">
|
||||
Aucun objet — édite le catalogue pour en ajouter.
|
||||
</p>
|
||||
|
||||
<section class="ic-group" *ngFor="let g of groups">
|
||||
<h2 *ngIf="g.category !== '—'">{{ g.category }}</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr *ngFor="let it of g.items">
|
||||
<td class="col-name">{{ it.name }}</td>
|
||||
<td class="col-price">{{ it.price }}</td>
|
||||
<td class="col-desc">{{ it.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
.ic-page { max-width: 880px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
|
||||
|
||||
.ic-toolbar {
|
||||
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;
|
||||
.spacer { flex: 1; }
|
||||
button {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.4rem 0.7rem; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.04);
|
||||
color: inherit; cursor: pointer; font-size: 0.85rem;
|
||||
&:hover { background: rgba(255,255,255,0.09); }
|
||||
}
|
||||
}
|
||||
|
||||
.ic-header {
|
||||
margin-bottom: 1.25rem;
|
||||
h1 { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.35rem; font-size: 1.5rem; }
|
||||
.ic-desc { margin: 0; color: var(--color-text-muted, #9aa0aa); }
|
||||
}
|
||||
|
||||
.ic-empty { color: var(--color-text-muted, #9aa0aa); font-style: italic; }
|
||||
|
||||
.ic-group {
|
||||
margin-bottom: 1.25rem;
|
||||
h2 {
|
||||
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
color: #c4a8ff; margin: 0 0 0.4rem; padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid rgba(168,130,255,0.2);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
td {
|
||||
padding: 0.45rem 0.6rem; border-bottom: 1px solid rgba(255,255,255,0.06); vertical-align: top;
|
||||
}
|
||||
.col-name { font-weight: 500; white-space: nowrap; }
|
||||
.col-price {
|
||||
white-space: nowrap; color: #e0c074; font-variant-numeric: tabular-nums;
|
||||
text-align: right; width: 90px;
|
||||
}
|
||||
.col-desc { color: var(--color-text-muted, #cfd3da); font-size: 0.9rem; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, Edit3, Package } from 'lucide-angular';
|
||||
import { ItemCatalogService } from '../../../services/item-catalog.service';
|
||||
import { CampaignSidebarService } from '../../../services/campaign-sidebar.service';
|
||||
import { ItemCatalog, CatalogItem } from '../../../services/item-catalog.model';
|
||||
|
||||
interface ItemGroup { category: string; items: CatalogItem[]; }
|
||||
|
||||
/**
|
||||
* Vue d'un catalogue d'objets : liste (groupée par catégorie).
|
||||
* Route : /campaigns/:campaignId/item-catalogs/:catalogId
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-item-catalog-view',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './item-catalog-view.component.html',
|
||||
styleUrls: ['./item-catalog-view.component.scss']
|
||||
})
|
||||
export class ItemCatalogViewComponent implements OnInit {
|
||||
readonly ArrowLeft = ArrowLeft;
|
||||
readonly Edit3 = Edit3;
|
||||
readonly Package = Package;
|
||||
|
||||
campaignId: string | null = null;
|
||||
catalogId: string | null = null;
|
||||
catalog: ItemCatalog | null = null;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private service: ItemCatalogService,
|
||||
private campaignSidebar: CampaignSidebarService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const params = this.route.snapshot.paramMap;
|
||||
this.campaignId = params.get('campaignId');
|
||||
this.catalogId = params.get('catalogId');
|
||||
if (this.catalogId) {
|
||||
this.service.getById(this.catalogId).subscribe({
|
||||
next: c => this.catalog = c,
|
||||
error: () => this.back()
|
||||
});
|
||||
}
|
||||
if (this.campaignId) this.campaignSidebar.show(this.campaignId);
|
||||
}
|
||||
|
||||
/** Objets groupés par catégorie (non catégorisés en dernier). */
|
||||
get groups(): ItemGroup[] {
|
||||
const map = new Map<string, CatalogItem[]>();
|
||||
for (const it of this.catalog?.items ?? []) {
|
||||
const cat = (it.category ?? '').trim() || '—';
|
||||
if (!map.has(cat)) map.set(cat, []);
|
||||
map.get(cat)!.push(it);
|
||||
}
|
||||
return [...map.entries()]
|
||||
.sort(([a], [b]) => (a === '—' ? 1 : b === '—' ? -1 : a.localeCompare(b, 'fr')))
|
||||
.map(([category, items]) => ({ category, items }));
|
||||
}
|
||||
|
||||
edit(): void {
|
||||
if (this.campaignId && this.catalogId) {
|
||||
this.router.navigate(['/campaigns', this.campaignId, 'item-catalogs', this.catalogId, 'edit']);
|
||||
}
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(this.campaignId ? ['/campaigns', this.campaignId] : ['/campaigns']);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Folder,
|
||||
Users, Swords, MapPin, Shield, Crown, Skull, Gem,
|
||||
BookOpen, Scroll, Wand2, Sparkles, TreePine, Mountain,
|
||||
Ship, Flame, Star, Moon, Key, Globe, Compass, Dices, FileUp, LucideIconData
|
||||
Ship, Flame, Star, Moon, Key, Globe, Compass, Dices, FileUp, Package, LucideIconData
|
||||
} from 'lucide-angular';
|
||||
import { CAMPAIGN_ICON_OPTIONS } from '../campaigns/campaign-icons';
|
||||
|
||||
@@ -44,6 +44,7 @@ export const LORE_ICON_OPTIONS: IconOption[] = [
|
||||
{ key: 'compass', icon: Compass },
|
||||
{ key: 'dice', icon: Dices },
|
||||
{ key: 'file-up', icon: FileUp },
|
||||
{ key: 'package', icon: Package },
|
||||
];
|
||||
|
||||
/** Icône par défaut pour un dossier sans icône. */
|
||||
|
||||
26
web/src/app/services/item-catalog.model.ts
Normal file
26
web/src/app/services/item-catalog.model.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/** Reflet de l'ItemCatalogDTO côté Core. Un catalogue d'objets (boutique, butin…). */
|
||||
|
||||
export interface CatalogItem {
|
||||
name: string;
|
||||
price?: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ItemCatalog {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
campaignId: string;
|
||||
order?: number;
|
||||
items: CatalogItem[];
|
||||
}
|
||||
|
||||
export interface ItemCatalogCreate {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
campaignId: string;
|
||||
items: CatalogItem[];
|
||||
}
|
||||
39
web/src/app/services/item-catalog.service.ts
Normal file
39
web/src/app/services/item-catalog.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ItemCatalog, ItemCatalogCreate } from './item-catalog.model';
|
||||
|
||||
/**
|
||||
* CRUD des catalogues d'objets (campagne) + génération IA. Mirroir de RandomTableService.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ItemCatalogService {
|
||||
private apiUrl = '/api/item-catalogs';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getByCampaign(campaignId: string): Observable<ItemCatalog[]> {
|
||||
return this.http.get<ItemCatalog[]>(`${this.apiUrl}/campaign/${campaignId}`);
|
||||
}
|
||||
|
||||
getById(id: string): Observable<ItemCatalog> {
|
||||
return this.http.get<ItemCatalog>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
create(payload: ItemCatalogCreate): Observable<ItemCatalog> {
|
||||
return this.http.post<ItemCatalog>(this.apiUrl, payload);
|
||||
}
|
||||
|
||||
update(id: string, payload: ItemCatalog): Observable<ItemCatalog> {
|
||||
return this.http.put<ItemCatalog>(`${this.apiUrl}/${id}`, payload);
|
||||
}
|
||||
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
/** Génère une PROPOSITION de catalogue via l'IA (non persistée) à préremplir. */
|
||||
generate(campaignId: string, description: string): Observable<ItemCatalog> {
|
||||
return this.http.post<ItemCatalog>(`${this.apiUrl}/generate`, { campaignId, description });
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,8 @@
|
||||
[sessionId]="session.id"
|
||||
[canAddToJournal]="session.active"
|
||||
(rolled)="onDiceRolled($event)"
|
||||
(aiReplyToJournal)="onAiReplyToJournal($event)">
|
||||
(aiReplyToJournal)="onAiReplyToJournal($event)"
|
||||
(noteToJournal)="onItemNoteToJournal($event)">
|
||||
</app-session-reference-panel>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -265,6 +265,22 @@ export class SessionDetailComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Réception d'un objet de catalogue à consigner dans le journal.
|
||||
* Le panneau fournit déjà une chaîne formatée (🛒 …) : on la sauvegarde
|
||||
* telle quelle comme entrée NOTE.
|
||||
*/
|
||||
onItemNoteToJournal(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 l\'objet au journal')
|
||||
});
|
||||
}
|
||||
|
||||
deleteEntry(entry: SessionEntry): void {
|
||||
if (!this.session) return;
|
||||
const session = this.session;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class="sic-panel">
|
||||
<p class="loading-hint" *ngIf="loading">Chargement…</p>
|
||||
|
||||
<!-- Liste des catalogues -->
|
||||
<div *ngIf="!loading && !selected" class="sic-list">
|
||||
<p class="empty-hint" *ngIf="catalogs.length === 0">
|
||||
Aucun catalogue d'objets dans cette campagne.
|
||||
</p>
|
||||
<button *ngFor="let c of catalogs" type="button" class="sic-item" (click)="select(c)">
|
||||
<lucide-icon [img]="Package" [size]="13"></lucide-icon>
|
||||
<span class="sic-item-name">{{ c.name }}</span>
|
||||
<span class="sic-item-count">{{ c.items.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Catalogue sélectionné -->
|
||||
<div *ngIf="selected" class="sic-detail">
|
||||
<button type="button" class="sic-back" (click)="backToList()">
|
||||
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Catalogues
|
||||
</button>
|
||||
<h4>{{ selected.name }}</h4>
|
||||
|
||||
<div class="sic-group" *ngFor="let g of groups">
|
||||
<div class="sic-group-title" *ngIf="g.category !== '—'">{{ g.category }}</div>
|
||||
<div class="sic-obj" *ngFor="let it of g.items">
|
||||
<div class="sic-obj-main">
|
||||
<span class="sic-obj-name">{{ it.name }}</span>
|
||||
<span class="sic-obj-price" *ngIf="it.price">{{ it.price }}</span>
|
||||
</div>
|
||||
<div class="sic-obj-desc" *ngIf="it.description">{{ it.description }}</div>
|
||||
<button type="button" class="sic-note" (click)="note(it)" [disabled]="!canAddToJournal"
|
||||
title="Consigner au journal (ex. achat)">
|
||||
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon> Journal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
.sic-panel { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.25rem; }
|
||||
|
||||
.loading-hint, .empty-hint {
|
||||
color: var(--color-text-muted, #9aa0aa); font-size: 0.85rem; font-style: italic; padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.sic-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.sic-item {
|
||||
display: flex; align-items: center; gap: 0.45rem; width: 100%;
|
||||
padding: 0.45rem 0.6rem; border-radius: 7px;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.03);
|
||||
color: inherit; cursor: pointer; text-align: left;
|
||||
&:hover { background: rgba(255,255,255,0.08); }
|
||||
.sic-item-name { flex: 1; }
|
||||
.sic-item-count {
|
||||
font-size: 0.72rem; color: var(--color-text-muted, #9aa0aa);
|
||||
background: rgba(255,255,255,0.06); padding: 0.05rem 0.35rem; border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.sic-detail { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.sic-back {
|
||||
display: inline-flex; align-items: center; gap: 0.25rem; align-self: flex-start;
|
||||
padding: 0.2rem 0.4rem; border: none; background: none;
|
||||
color: var(--color-text-muted, #9aa0aa); cursor: pointer; font-size: 0.8rem;
|
||||
&:hover { color: inherit; }
|
||||
}
|
||||
h4 { margin: 0; font-size: 0.95rem; }
|
||||
|
||||
.sic-group-title {
|
||||
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: #c4a8ff; margin: 0.5rem 0 0.2rem;
|
||||
}
|
||||
|
||||
.sic-obj {
|
||||
padding: 0.4rem 0.5rem; border-radius: 7px; background: rgba(255,255,255,0.03); margin-bottom: 0.3rem;
|
||||
.sic-obj-main { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
|
||||
.sic-obj-name { font-weight: 500; }
|
||||
.sic-obj-price { color: #e0c074; font-size: 0.82rem; white-space: nowrap; }
|
||||
.sic-obj-desc { font-size: 0.82rem; color: var(--color-text-muted, #cfd3da); margin-top: 0.15rem; }
|
||||
.sic-note {
|
||||
display: inline-flex; align-items: center; gap: 0.25rem; margin-top: 0.35rem;
|
||||
padding: 0.2rem 0.45rem; border-radius: 5px; font-size: 0.74rem;
|
||||
border: 1px solid rgba(255,255,255,0.14); background: rgba(255,255,255,0.05); color: inherit; cursor: pointer;
|
||||
&:hover:not(:disabled) { background: rgba(255,255,255,0.1); }
|
||||
&:disabled { opacity: 0.5; cursor: default; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, Package, BookmarkPlus, ChevronLeft } from 'lucide-angular';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { ItemCatalogService } from '../../services/item-catalog.service';
|
||||
import { ItemCatalog, CatalogItem } from '../../services/item-catalog.model';
|
||||
|
||||
interface ItemGroup { category: string; items: CatalogItem[]; }
|
||||
|
||||
/**
|
||||
* Panneau « Objets » du mode jeu : consulter les catalogues (boutiques) de la
|
||||
* campagne et consigner un objet au journal (ex. « le joueur achète X »).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-session-item-catalogs-panel',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './session-item-catalogs-panel.component.html',
|
||||
styleUrls: ['./session-item-catalogs-panel.component.scss']
|
||||
})
|
||||
export class SessionItemCatalogsPanelComponent implements OnInit {
|
||||
readonly Package = Package;
|
||||
readonly BookmarkPlus = BookmarkPlus;
|
||||
readonly ChevronLeft = ChevronLeft;
|
||||
|
||||
@Input() campaignId!: string;
|
||||
@Input() canAddToJournal = true;
|
||||
/** Émis pour consigner un objet au journal (entrée NOTE). */
|
||||
@Output() noteToJournal = new EventEmitter<string>();
|
||||
|
||||
catalogs: ItemCatalog[] = [];
|
||||
loading = false;
|
||||
selected: ItemCatalog | null = null;
|
||||
|
||||
constructor(private service: ItemCatalogService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.campaignId) return;
|
||||
this.loading = true;
|
||||
this.service.getByCampaign(this.campaignId).pipe(catchError(() => of([] as ItemCatalog[])))
|
||||
.subscribe(list => { this.catalogs = list; this.loading = false; });
|
||||
}
|
||||
|
||||
select(c: ItemCatalog): void { this.selected = c; }
|
||||
backToList(): void { this.selected = null; }
|
||||
|
||||
get groups(): ItemGroup[] {
|
||||
const map = new Map<string, CatalogItem[]>();
|
||||
for (const it of this.selected?.items ?? []) {
|
||||
const cat = (it.category ?? '').trim() || '—';
|
||||
if (!map.has(cat)) map.set(cat, []);
|
||||
map.get(cat)!.push(it);
|
||||
}
|
||||
return [...map.entries()]
|
||||
.sort(([a], [b]) => (a === '—' ? 1 : b === '—' ? -1 : a.localeCompare(b, 'fr')))
|
||||
.map(([category, items]) => ({ category, items }));
|
||||
}
|
||||
|
||||
note(it: CatalogItem): void {
|
||||
if (!this.canAddToJournal || !this.selected) return;
|
||||
const price = it.price ? ` (${it.price})` : '';
|
||||
this.noteToJournal.emit(`🛒 ${this.selected.name} — ${it.name}${price}`);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@
|
||||
<lucide-icon [img]="Table2" [size]="14"></lucide-icon>
|
||||
Tables
|
||||
</button>
|
||||
<button type="button"
|
||||
class="ref-tab"
|
||||
[class.ref-tab--active]="activeTab === 'objects'"
|
||||
(click)="selectTab('objects')">
|
||||
<lucide-icon [img]="Package" [size]="14"></lucide-icon>
|
||||
Objets
|
||||
</button>
|
||||
<button type="button"
|
||||
class="ref-tab"
|
||||
[class.ref-tab--active]="activeTab === 'characters'"
|
||||
@@ -64,6 +71,14 @@
|
||||
(aiReplyToJournal)="onAiSaveToJournal($event)">
|
||||
</app-session-random-tables-panel>
|
||||
|
||||
<!-- ====== Catalogues d'objets ====== -->
|
||||
<app-session-item-catalogs-panel
|
||||
*ngIf="activeTab === 'objects'"
|
||||
[campaignId]="campaignId"
|
||||
[canAddToJournal]="canAddToJournal"
|
||||
(noteToJournal)="onItemNote($event)">
|
||||
</app-session-item-catalogs-panel>
|
||||
|
||||
<!-- ====== Personnages (PJ + PNJ) ====== -->
|
||||
<div *ngIf="activeTab === 'characters'" class="ref-list">
|
||||
<p class="loading-hint" *ngIf="loadingChars">Chargement…</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2 } from 'lucide-angular';
|
||||
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2, Package } from 'lucide-angular';
|
||||
import { catchError, of } from 'rxjs';
|
||||
import { CampaignService } from '../../services/campaign.service';
|
||||
import { CharacterService } from '../../services/character.service';
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
} from '../session-dice-panel/session-dice-panel.component';
|
||||
import { SessionAiChatPanelComponent } from '../session-ai-chat-panel/session-ai-chat-panel.component';
|
||||
import { SessionRandomTablesPanelComponent } from '../session-random-tables-panel/session-random-tables-panel.component';
|
||||
import { SessionItemCatalogsPanelComponent } from '../session-item-catalogs-panel/session-item-catalogs-panel.component';
|
||||
|
||||
type TabId = 'dice' | 'tables' | 'characters' | 'scenes' | 'ai';
|
||||
type TabId = 'dice' | 'tables' | 'objects' | 'characters' | 'scenes' | 'ai';
|
||||
|
||||
/**
|
||||
* Panneau latéral du mode jeu : référence rapide en lecture seule.
|
||||
@@ -30,7 +31,7 @@ type TabId = 'dice' | 'tables' | 'characters' | 'scenes' | 'ai';
|
||||
@Component({
|
||||
selector: 'app-session-reference-panel',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent],
|
||||
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent, SessionItemCatalogsPanelComponent],
|
||||
templateUrl: './session-reference-panel.component.html',
|
||||
styleUrls: ['./session-reference-panel.component.scss']
|
||||
})
|
||||
@@ -42,6 +43,7 @@ export class SessionReferencePanelComponent implements OnChanges {
|
||||
readonly ExternalLink = ExternalLink;
|
||||
readonly Sparkles = Sparkles;
|
||||
readonly Table2 = Table2;
|
||||
readonly Package = Package;
|
||||
|
||||
@Input() campaignId!: string;
|
||||
/** Partie active — nécessaire pour charger les PJ (refonte Playthrough). */
|
||||
@@ -51,6 +53,8 @@ export class SessionReferencePanelComponent implements OnChanges {
|
||||
@Output() rolled = new EventEmitter<DiceRollResult>();
|
||||
/** Émis quand l'IA répond et que le MJ veut sauvegarder la réponse comme entrée. */
|
||||
@Output() aiReplyToJournal = new EventEmitter<string>();
|
||||
/** Émis pour consigner un objet de catalogue au journal (entrée NOTE). */
|
||||
@Output() noteToJournal = new EventEmitter<string>();
|
||||
|
||||
activeTab: TabId = 'dice';
|
||||
|
||||
@@ -143,4 +147,8 @@ export class SessionReferencePanelComponent implements OnChanges {
|
||||
onAiSaveToJournal(content: string): void {
|
||||
this.aiReplyToJournal.emit(content);
|
||||
}
|
||||
|
||||
onItemNote(content: string): void {
|
||||
this.noteToJournal.emit(content);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user