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:
@@ -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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user