Add scouthub-attivita-fe
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
.wizard-page {
|
||||
max-width: 760px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.titolo {
|
||||
font-size: 26px;
|
||||
margin: 0 0 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin: 18px 0 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step-tab {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
padding: 10px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.step-tab--attivo {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.step-content--gap {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.field {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field--sm {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.text-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.text-input--sm {
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stato-choices {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.stato-choice {
|
||||
cursor: pointer;
|
||||
padding: 9px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.paragrafo-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.paragrafo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.paragrafo-tipo {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-primary-text-strong);
|
||||
}
|
||||
|
||||
.paragrafo-azioni {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.azione {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.azione--danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.formattazione {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fmt-btn {
|
||||
cursor: pointer;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.fmt-btn--italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.paragrafo-textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.aggiungi-paragrafo {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--color-border-strong);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.riepilogo {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.riepilogo-nome {
|
||||
font-weight: 700;
|
||||
font-size: 19px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.riepilogo-meta {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.riepilogo-linea {
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.wizard-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 28px;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<div class="page wizard-page">
|
||||
<h1 class="titolo">{{ wizardTitle() }}</h1>
|
||||
|
||||
<div class="steps">
|
||||
@for (label of stepLabels; track $index) {
|
||||
<div
|
||||
class="step-tab"
|
||||
[class.step-tab--attivo]="step() === $index"
|
||||
(click)="setStep($index)"
|
||||
>
|
||||
{{ $index + 1 }}. {{ label }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (step() === 0) {
|
||||
<div class="step-content">
|
||||
<label class="field">
|
||||
Nome attività
|
||||
<input
|
||||
class="text-input"
|
||||
type="text"
|
||||
[value]="nome()"
|
||||
(input)="nome.set($any($event.target).value)"
|
||||
placeholder="Es. Caccia al tesoro nel bosco"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
Stato iniziale
|
||||
<div class="stato-choices">
|
||||
@for (s of stati; track s.id) {
|
||||
<div
|
||||
class="stato-choice"
|
||||
[class.stato-choice--attivo]="stato().id === s.id"
|
||||
[style.background]="stato().id === s.id ? statoStile(s.id).bg : 'transparent'"
|
||||
[style.color]="stato().id === s.id ? statoStile(s.id).color : 'var(--color-text-muted)'"
|
||||
[style.border-color]="stato().id === s.id ? statoStile(s.id).border : 'var(--color-border)'"
|
||||
(click)="selectStato(s)"
|
||||
>
|
||||
{{ s.nome }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (step() === 1) {
|
||||
<div class="step-content step-content--gap">
|
||||
<app-chip-field
|
||||
label="Branca"
|
||||
[chips]="brancaList()"
|
||||
[(query)]="brancaQuery"
|
||||
[suggestions]="brancaSuggestions()"
|
||||
(select)="addBranca($event)"
|
||||
(remove)="removeBranca($event)"
|
||||
/>
|
||||
<app-chip-field
|
||||
label="Categoria"
|
||||
[chips]="categoriaList()"
|
||||
[(query)]="categoriaQuery"
|
||||
[suggestions]="categoriaSuggestions()"
|
||||
(select)="addCategoria($event)"
|
||||
(remove)="removeCategoria($event)"
|
||||
/>
|
||||
<app-chip-field
|
||||
label="Periodo dell'anno"
|
||||
[chips]="periodoAnnoList()"
|
||||
[(query)]="periodoQuery"
|
||||
[suggestions]="periodoSuggestions()"
|
||||
(select)="addPeriodo($event)"
|
||||
(remove)="removePeriodo($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (step() === 2) {
|
||||
<div class="step-content">
|
||||
<app-chip-field
|
||||
label="Materiale"
|
||||
[chips]="materialeList()"
|
||||
[(query)]="materialeQuery"
|
||||
[suggestions]="materialeSuggestions()"
|
||||
(select)="addMateriale($event)"
|
||||
(remove)="removeMateriale($event)"
|
||||
/>
|
||||
@for (materiale of materialeList(); track materiale.id) {
|
||||
<label class="field field--sm">
|
||||
Note/proprietà per "{{ materiale.nome }}"
|
||||
<input
|
||||
class="text-input text-input--sm"
|
||||
type="text"
|
||||
[value]="materiale.proprieta || ''"
|
||||
(input)="setMaterialeProprieta(materiale.id, $any($event.target).value)"
|
||||
placeholder="es. quantità, note d'uso"
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (step() === 3) {
|
||||
<div class="step-content">
|
||||
@for (paragrafo of paragrafoList(); track $index; let i = $index) {
|
||||
<div class="paragrafo-card">
|
||||
<div class="paragrafo-header">
|
||||
<span class="paragrafo-tipo">{{ paragrafo.tipo.id === 'TITOLO' ? 'Titolo' : 'Paragrafo ' + i }}</span>
|
||||
@if (paragrafo.tipo.id !== 'TITOLO') {
|
||||
<div class="paragrafo-azioni">
|
||||
<span class="azione" (click)="moveParagrafo(i, -1)">↑</span>
|
||||
<span class="azione" (click)="moveParagrafo(i, 1)">↓</span>
|
||||
<span class="azione azione--danger" (click)="removeParagrafo(i)">✕</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (paragrafo.tipo.id !== 'TITOLO') {
|
||||
<div class="formattazione">
|
||||
<span class="fmt-btn" (click)="wrapSelection(ta, i, '**')">B</span>
|
||||
<span class="fmt-btn fmt-btn--italic" (click)="wrapSelection(ta, i, '*')">I</span>
|
||||
</div>
|
||||
}
|
||||
<textarea
|
||||
#ta
|
||||
class="paragrafo-textarea"
|
||||
[value]="paragrafo.corpo"
|
||||
(input)="updateParagrafoCorpo(i, $any($event.target).value)"
|
||||
[rows]="paragrafo.tipo.id === 'TITOLO' ? 1 : 3"
|
||||
[placeholder]="paragrafo.tipo.id === 'TITOLO' ? 'Titolo dell\'attività' : 'Scrivi il contenuto di questo paragrafo...'"
|
||||
></textarea>
|
||||
</div>
|
||||
}
|
||||
<div class="aggiungi-paragrafo" (click)="addParagrafo()">+ Aggiungi paragrafo</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (step() === 4) {
|
||||
<div class="riepilogo">
|
||||
<div class="riepilogo-nome">{{ nome() }}</div>
|
||||
<div class="riepilogo-meta">Di {{ autore() }} · Stato: {{ stato().nome }}</div>
|
||||
<div class="riepilogo-linea">{{ summaryLine() }}</div>
|
||||
<div class="riepilogo-linea">
|
||||
{{ nonTitoloCount() }} paragraf{{ nonTitoloCount() === 1 ? 'o' : 'i' }} oltre al titolo
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="wizard-nav">
|
||||
<div class="btn-secondary" [style.visibility]="step() === 0 ? 'hidden' : 'visible'" (click)="prevStep()">
|
||||
Indietro
|
||||
</div>
|
||||
@if (isLastStep()) {
|
||||
<div class="btn-primary" (click)="salva()">Salva attività</div>
|
||||
} @else {
|
||||
<div class="btn-primary" (click)="nextStep()">Avanti</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,259 @@
|
||||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
import {
|
||||
Attivita,
|
||||
Branca,
|
||||
Categoria,
|
||||
Materiale,
|
||||
Paragrafo,
|
||||
PeriodoAnno,
|
||||
STATI,
|
||||
STATO_BOZZA,
|
||||
Stato,
|
||||
TIPO_PARAGRAFO,
|
||||
TIPO_TITOLO,
|
||||
} from '../../core/models/attivita.model';
|
||||
import { AttivitaService } from '../../core/services/attivita';
|
||||
import { AutocompleteService } from '../../core/services/autocomplete';
|
||||
import { statoStyle } from '../../core/utils/stato-style';
|
||||
import { ChipField, ChipOption } from '../../shared/chip-field/chip-field';
|
||||
|
||||
const STEP_LABELS = ['Info base', 'Classificazione', 'Materiale', 'Paragrafi', 'Riepilogo'];
|
||||
|
||||
@Component({
|
||||
selector: 'app-activity-form',
|
||||
imports: [ChipField],
|
||||
templateUrl: './activity-form.html',
|
||||
styleUrl: './activity-form.css',
|
||||
})
|
||||
export class ActivityForm {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly attivitaService = inject(AttivitaService);
|
||||
private readonly autocompleteService = inject(AutocompleteService);
|
||||
private readonly keycloak = inject(Keycloak);
|
||||
|
||||
readonly editingId = signal<number | null>(null);
|
||||
readonly step = signal(0);
|
||||
readonly stepLabels = STEP_LABELS;
|
||||
readonly stati = STATI;
|
||||
|
||||
readonly nome = signal('');
|
||||
readonly autore = signal(this.nomeUtenteAutenticato());
|
||||
readonly stato = signal<Stato>(STATO_BOZZA);
|
||||
readonly brancaList = signal<Branca[]>([]);
|
||||
readonly categoriaList = signal<Categoria[]>([]);
|
||||
readonly periodoAnnoList = signal<PeriodoAnno[]>([]);
|
||||
readonly materialeList = signal<Materiale[]>([]);
|
||||
readonly paragrafoList = signal<Paragrafo[]>([{ corpo: '', tipo: TIPO_TITOLO, ordine: 0 }]);
|
||||
|
||||
readonly brancaQuery = signal('');
|
||||
readonly categoriaQuery = signal('');
|
||||
readonly periodoQuery = signal('');
|
||||
readonly materialeQuery = signal('');
|
||||
|
||||
readonly brancaSuggestions = signal<ChipOption[]>([]);
|
||||
readonly categoriaSuggestions = signal<ChipOption[]>([]);
|
||||
readonly periodoSuggestions = signal<ChipOption[]>([]);
|
||||
readonly materialeSuggestions = signal<ChipOption[]>([]);
|
||||
|
||||
readonly wizardTitle = computed(() => (this.editingId() ? 'Modifica attività' : 'Nuova attività'));
|
||||
readonly isLastStep = computed(() => this.step() === 4);
|
||||
readonly nonTitoloCount = computed(() => this.paragrafoList().filter((p) => p.tipo.id !== 'TITOLO').length);
|
||||
readonly summaryLine = computed(() => {
|
||||
const nomi = [...this.brancaList().map((b) => b.nome), ...this.categoriaList().map((c) => c.nome)];
|
||||
return nomi.length > 0 ? nomi.join(' · ') : 'Nessuna classificazione selezionata';
|
||||
});
|
||||
|
||||
constructor() {
|
||||
const idParam = this.route.snapshot.paramMap.get('idAttivita');
|
||||
if (idParam) {
|
||||
const id = Number(idParam);
|
||||
this.editingId.set(id);
|
||||
this.attivitaService.getOne(id).subscribe((attivita) => this.popolaForm(attivita));
|
||||
}
|
||||
|
||||
effect(() => {
|
||||
const query = this.brancaQuery();
|
||||
const selezionati = new Set(this.brancaList().map((b) => b.id));
|
||||
if (!query.trim()) {
|
||||
this.brancaSuggestions.set([]);
|
||||
return;
|
||||
}
|
||||
this.autocompleteService.branca(query).subscribe({
|
||||
next: (r) => this.brancaSuggestions.set(this.toChipOptions(r, selezionati)),
|
||||
error: () => this.brancaSuggestions.set([]),
|
||||
});
|
||||
});
|
||||
effect(() => {
|
||||
const query = this.categoriaQuery();
|
||||
const selezionati = new Set(this.categoriaList().map((c) => c.id));
|
||||
if (!query.trim()) {
|
||||
this.categoriaSuggestions.set([]);
|
||||
return;
|
||||
}
|
||||
this.autocompleteService.categoria(query).subscribe({
|
||||
next: (r) => this.categoriaSuggestions.set(this.toChipOptions(r, selezionati)),
|
||||
error: () => this.categoriaSuggestions.set([]),
|
||||
});
|
||||
});
|
||||
effect(() => {
|
||||
const query = this.periodoQuery();
|
||||
const selezionati = new Set(this.periodoAnnoList().map((p) => p.id));
|
||||
if (!query.trim()) {
|
||||
this.periodoSuggestions.set([]);
|
||||
return;
|
||||
}
|
||||
this.autocompleteService.periodoAnno(query).subscribe({
|
||||
next: (r) => this.periodoSuggestions.set(this.toChipOptions(r, selezionati)),
|
||||
error: () => this.periodoSuggestions.set([]),
|
||||
});
|
||||
});
|
||||
effect(() => {
|
||||
const query = this.materialeQuery();
|
||||
const selezionati = new Set(this.materialeList().map((m) => m.id));
|
||||
if (!query.trim()) {
|
||||
this.materialeSuggestions.set([]);
|
||||
return;
|
||||
}
|
||||
this.autocompleteService.materiale(query).subscribe({
|
||||
next: (r) => this.materialeSuggestions.set(this.toChipOptions(r, selezionati)),
|
||||
error: () => this.materialeSuggestions.set([]),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private toChipOptions(
|
||||
risultati: { id: number | null; nome: string | null }[],
|
||||
esclusi: Set<number>
|
||||
): ChipOption[] {
|
||||
return risultati
|
||||
.filter((r): r is { id: number; nome: string } => r.id !== null && r.nome !== null)
|
||||
.filter((r) => !esclusi.has(r.id));
|
||||
}
|
||||
|
||||
private nomeUtenteAutenticato(): string {
|
||||
const token = this.keycloak.tokenParsed;
|
||||
return (token?.['name'] as string) ?? (token?.['preferred_username'] as string) ?? (token?.['email'] as string) ?? '';
|
||||
}
|
||||
|
||||
private popolaForm(attivita: Attivita): void {
|
||||
this.nome.set(attivita.nome);
|
||||
this.autore.set(attivita.autore);
|
||||
this.stato.set(attivita.stato);
|
||||
this.brancaList.set([...attivita.brancaList]);
|
||||
this.categoriaList.set([...attivita.categoriaList]);
|
||||
this.periodoAnnoList.set([...attivita.periodoAnnoList]);
|
||||
this.materialeList.set(attivita.materialeList.map((m) => ({ ...m })));
|
||||
this.paragrafoList.set(attivita.paragrafoList.map((p) => ({ ...p })));
|
||||
}
|
||||
|
||||
selectStato(stato: Stato): void {
|
||||
this.stato.set(stato);
|
||||
}
|
||||
|
||||
statoStile(idStato: string) {
|
||||
return statoStyle(idStato);
|
||||
}
|
||||
|
||||
addBranca(chip: ChipOption): void {
|
||||
this.brancaList.update((lista) => [...lista, { id: chip.id, nome: chip.nome }]);
|
||||
this.brancaQuery.set('');
|
||||
}
|
||||
removeBranca(chip: ChipOption): void {
|
||||
this.brancaList.update((lista) => lista.filter((b) => b.id !== chip.id));
|
||||
}
|
||||
|
||||
addCategoria(chip: ChipOption): void {
|
||||
this.categoriaList.update((lista) => [...lista, { id: chip.id, nome: chip.nome }]);
|
||||
this.categoriaQuery.set('');
|
||||
}
|
||||
removeCategoria(chip: ChipOption): void {
|
||||
this.categoriaList.update((lista) => lista.filter((c) => c.id !== chip.id));
|
||||
}
|
||||
|
||||
addPeriodo(chip: ChipOption): void {
|
||||
this.periodoAnnoList.update((lista) => [...lista, { id: chip.id, nome: chip.nome }]);
|
||||
this.periodoQuery.set('');
|
||||
}
|
||||
removePeriodo(chip: ChipOption): void {
|
||||
this.periodoAnnoList.update((lista) => lista.filter((p) => p.id !== chip.id));
|
||||
}
|
||||
|
||||
addMateriale(chip: ChipOption): void {
|
||||
this.materialeList.update((lista) => [...lista, { id: chip.id, nome: chip.nome, proprieta: '' }]);
|
||||
this.materialeQuery.set('');
|
||||
}
|
||||
removeMateriale(chip: ChipOption): void {
|
||||
this.materialeList.update((lista) => lista.filter((m) => m.id !== chip.id));
|
||||
}
|
||||
setMaterialeProprieta(id: number, valore: string): void {
|
||||
this.materialeList.update((lista) => lista.map((m) => (m.id === id ? { ...m, proprieta: valore } : m)));
|
||||
}
|
||||
|
||||
addParagrafo(): void {
|
||||
this.paragrafoList.update((lista) => [
|
||||
...lista,
|
||||
{ corpo: '', tipo: TIPO_PARAGRAFO, ordine: lista.length },
|
||||
]);
|
||||
}
|
||||
removeParagrafo(idx: number): void {
|
||||
this.paragrafoList.update((lista) =>
|
||||
lista.filter((_, i) => i !== idx).map((p, i) => ({ ...p, ordine: i }))
|
||||
);
|
||||
}
|
||||
moveParagrafo(idx: number, direzione: -1 | 1): void {
|
||||
this.paragrafoList.update((lista) => {
|
||||
const j = idx + direzione;
|
||||
if (j < 1 || j >= lista.length) {
|
||||
return lista;
|
||||
}
|
||||
const copia = [...lista];
|
||||
[copia[idx], copia[j]] = [copia[j], copia[idx]];
|
||||
return copia.map((p, i) => ({ ...p, ordine: i }));
|
||||
});
|
||||
}
|
||||
updateParagrafoCorpo(idx: number, valore: string): void {
|
||||
this.paragrafoList.update((lista) => lista.map((p, i) => (i === idx ? { ...p, corpo: valore } : p)));
|
||||
}
|
||||
wrapSelection(textarea: HTMLTextAreaElement, idx: number, marker: string): void {
|
||||
const valore = this.paragrafoList()[idx].corpo || '';
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selezione = valore.slice(start, end) || 'testo';
|
||||
const nuovoValore = valore.slice(0, start) + marker + selezione + marker + valore.slice(end);
|
||||
this.updateParagrafoCorpo(idx, nuovoValore);
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.selectionStart = start + marker.length;
|
||||
textarea.selectionEnd = start + marker.length + selezione.length;
|
||||
});
|
||||
}
|
||||
|
||||
nextStep(): void {
|
||||
this.step.update((s) => Math.min(4, s + 1));
|
||||
}
|
||||
prevStep(): void {
|
||||
this.step.update((s) => Math.max(0, s - 1));
|
||||
}
|
||||
setStep(n: number): void {
|
||||
this.step.set(n);
|
||||
}
|
||||
|
||||
salva(): void {
|
||||
const attivita: Attivita = {
|
||||
id: this.editingId() ?? undefined,
|
||||
nome: this.nome(),
|
||||
autore: this.autore(),
|
||||
stato: this.stato(),
|
||||
brancaList: this.brancaList(),
|
||||
categoriaList: this.categoriaList(),
|
||||
periodoAnnoList: this.periodoAnnoList(),
|
||||
materialeList: this.materialeList(),
|
||||
paragrafoList: this.paragrafoList(),
|
||||
};
|
||||
this.attivitaService.save(attivita).subscribe(() => this.router.navigate(['/mie-attivita']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
.page {
|
||||
max-width: 760px;
|
||||
padding-top: 36px;
|
||||
}
|
||||
|
||||
.branca-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.branca-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.branca-nomi {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.titolo {
|
||||
font-size: 32px;
|
||||
margin: 6px 0 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 15px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stato {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chips-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chip--categoria {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.chip--periodo {
|
||||
background: var(--color-periodo-bg);
|
||||
border: 1px solid var(--color-periodo-border);
|
||||
color: var(--color-periodo-text);
|
||||
}
|
||||
|
||||
.materiale-box {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.materiale-title {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.materiale-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.materiale-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.materiale-icon {
|
||||
color: var(--color-primary-text-strong);
|
||||
}
|
||||
|
||||
.materiale-nome {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.materiale-proprieta {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.paragrafi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.paragrafo-titolo {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin: 8px 0 0;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.paragrafo-corpo {
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
@if (attivita(); as a) {
|
||||
<div class="page">
|
||||
<div class="branca-row">
|
||||
@for (branca of a.brancaList; track branca.id) {
|
||||
<span class="branca-dot" [style.background]="branca.colore" [title]="branca.nome"></span>
|
||||
}
|
||||
<span class="branca-nomi">{{ brancaNomi() }}</span>
|
||||
</div>
|
||||
<h1 class="titolo">{{ a.nome }}</h1>
|
||||
<div class="meta">
|
||||
Di {{ a.autore }} · Aggiornata il {{ dataModificaFmt() }} ·
|
||||
<span class="stato" [style.color]="statoTextColor()">{{ a.stato.nome }}</span>
|
||||
</div>
|
||||
<div class="chips-row">
|
||||
@for (categoria of a.categoriaList; track categoria.id) {
|
||||
<span class="chip chip--categoria">{{ categoria.nome }}</span>
|
||||
}
|
||||
@for (periodo of a.periodoAnnoList; track periodo.id) {
|
||||
<span class="chip chip--periodo">{{ periodo.nome }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (a.materialeList.length > 0) {
|
||||
<div class="materiale-box">
|
||||
<div class="materiale-title">Materiale occorrente</div>
|
||||
<div class="materiale-list">
|
||||
@for (materiale of a.materialeList; track materiale.id) {
|
||||
<div class="materiale-item">
|
||||
<span class="materiale-icon">🧰</span>
|
||||
<span class="materiale-nome">{{ materiale.nome }}</span>
|
||||
@if (materiale.proprieta) {
|
||||
<span class="materiale-proprieta">— {{ materiale.proprieta }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="paragrafi">
|
||||
@for (paragrafo of paragrafiOrdinati(); track paragrafo.id) {
|
||||
@if (isTitolo(paragrafo)) {
|
||||
<h2 class="paragrafo-titolo">{{ paragrafo.corpo }}</h2>
|
||||
} @else {
|
||||
<p class="paragrafo-corpo">
|
||||
@for (token of tokens(paragrafo); track $index) {
|
||||
<span [style.font-weight]="token.weight" [style.font-style]="token.style">{{ token.text }}</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!attivita() && !caricamento()) {
|
||||
<div class="page empty-state">Attività non trovata.</div>
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Attivita, Paragrafo } from '../../core/models/attivita.model';
|
||||
import { AttivitaService } from '../../core/services/attivita';
|
||||
import { formatData, parseMarkdown, statoStyle } from '../../core/utils/stato-style';
|
||||
|
||||
@Component({
|
||||
selector: 'app-detail',
|
||||
imports: [],
|
||||
templateUrl: './detail.html',
|
||||
styleUrl: './detail.css',
|
||||
})
|
||||
export class Detail {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly attivitaService = inject(AttivitaService);
|
||||
|
||||
readonly attivita = signal<Attivita | null>(null);
|
||||
readonly caricamento = signal(true);
|
||||
|
||||
readonly dataModificaFmt = computed(() => formatData(this.attivita()?.dataModifica));
|
||||
readonly statoTextColor = computed(() => statoStyle(this.attivita()?.stato.id ?? 'BO').color);
|
||||
readonly brancaNomi = computed(() => (this.attivita()?.brancaList ?? []).map((b) => b.nome).join(', '));
|
||||
readonly paragrafiOrdinati = computed(() =>
|
||||
[...(this.attivita()?.paragrafoList ?? [])].sort((a, b) => a.ordine - b.ordine)
|
||||
);
|
||||
|
||||
constructor() {
|
||||
const idAttivita = Number(this.route.snapshot.paramMap.get('idAttivita'));
|
||||
this.attivitaService.getOne(idAttivita).subscribe({
|
||||
next: (attivita) => {
|
||||
this.attivita.set(attivita);
|
||||
this.caricamento.set(false);
|
||||
},
|
||||
error: () => this.caricamento.set(false),
|
||||
});
|
||||
}
|
||||
|
||||
isTitolo(paragrafo: Paragrafo): boolean {
|
||||
return paragrafo.tipo.id === 'TITOLO';
|
||||
}
|
||||
|
||||
tokens(paragrafo: Paragrafo) {
|
||||
return parseMarkdown(paragrafo.corpo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Attività pubblicate</h1>
|
||||
<a class="search-bar" routerLink="/ricerca">
|
||||
<span>🔍</span><span>Cerca un'attività...</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (!caricamento() && attivitaList().length > 0) {
|
||||
<div class="grid">
|
||||
@for (attivita of attivitaList(); track attivita.id) {
|
||||
<app-activity-card [attivita]="attivita" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!caricamento() && attivitaList().length === 0) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🏕️</div>
|
||||
<div class="empty-title">Nessuna attività pubblicata</div>
|
||||
<div class="empty-subtitle">Le attività pubblicate dai capi compariranno qui.</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
import { Attivita } from '../../core/models/attivita.model';
|
||||
import { AttivitaService } from '../../core/services/attivita';
|
||||
import { ActivityCard } from '../../shared/activity-card/activity-card';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [RouterLink, ActivityCard],
|
||||
templateUrl: './home.html',
|
||||
styleUrl: './home.css',
|
||||
})
|
||||
export class Home {
|
||||
private readonly attivitaService = inject(AttivitaService);
|
||||
|
||||
readonly attivitaList = signal<Attivita[]>([]);
|
||||
readonly caricamento = signal(true);
|
||||
|
||||
constructor() {
|
||||
this.attivitaService.getListHome().subscribe({
|
||||
next: (lista) => {
|
||||
this.attivitaList.set(lista);
|
||||
this.caricamento.set(false);
|
||||
},
|
||||
error: () => this.caricamento.set(false),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.lista {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.riga {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.riga-info {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.riga-titolo {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.riga-data {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stato-options {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stato-option {
|
||||
cursor: pointer;
|
||||
padding: 7px 13px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.riga-modifica {
|
||||
cursor: pointer;
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title page-title--sm">Le mie attività</h1>
|
||||
<a class="btn-primary" routerLink="/nuova-attivita">+ Nuova attività</a>
|
||||
</div>
|
||||
|
||||
@if (attivitaList().length > 0) {
|
||||
<div class="lista">
|
||||
@for (attivita of attivitaList(); track attivita.id) {
|
||||
<div class="riga">
|
||||
<div class="riga-info">
|
||||
<a class="riga-titolo" [routerLink]="['/attivita/dettaglio', attivita.id]">{{ attivita.nome }}</a>
|
||||
<div class="riga-data">Aggiornata il {{ dataModificaFmt(attivita) }}</div>
|
||||
</div>
|
||||
<div class="stato-options">
|
||||
@for (stato of stati; track stato.id) {
|
||||
<div
|
||||
class="stato-option"
|
||||
[class.stato-option--attivo]="isStatoAttivo(attivita, stato)"
|
||||
[style.background]="isStatoAttivo(attivita, stato) ? stileStato(stato.id).bg : 'transparent'"
|
||||
[style.color]="isStatoAttivo(attivita, stato) ? stileStato(stato.id).color : 'var(--color-text-muted)'"
|
||||
[style.border-color]="isStatoAttivo(attivita, stato) ? stileStato(stato.id).border : 'var(--color-border)'"
|
||||
(click)="cambiaStato(attivita, stato)"
|
||||
>
|
||||
{{ stato.nome }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="riga-modifica" (click)="modifica(attivita)">Modifica</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (!caricamento()) {
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div class="empty-title">Non hai ancora creato attività</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
|
||||
import { Attivita, STATI, Stato } from '../../core/models/attivita.model';
|
||||
import { AttivitaService } from '../../core/services/attivita';
|
||||
import { formatData, statoStyle } from '../../core/utils/stato-style';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-activity',
|
||||
imports: [RouterLink],
|
||||
templateUrl: './my-activity.html',
|
||||
styleUrl: './my-activity.css',
|
||||
})
|
||||
export class MyActivity {
|
||||
private readonly attivitaService = inject(AttivitaService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly attivitaList = signal<Attivita[]>([]);
|
||||
readonly caricamento = signal(true);
|
||||
readonly stati = STATI;
|
||||
|
||||
constructor() {
|
||||
this.caricaLista();
|
||||
}
|
||||
|
||||
private caricaLista(): void {
|
||||
this.attivitaService.getListMy().subscribe({
|
||||
next: (lista) => {
|
||||
this.attivitaList.set(lista);
|
||||
this.caricamento.set(false);
|
||||
},
|
||||
error: () => this.caricamento.set(false),
|
||||
});
|
||||
}
|
||||
|
||||
dataModificaFmt(attivita: Attivita): string {
|
||||
return formatData(attivita.dataModifica);
|
||||
}
|
||||
|
||||
stileStato(idStato: string) {
|
||||
return statoStyle(idStato);
|
||||
}
|
||||
|
||||
isStatoAttivo(attivita: Attivita, stato: Stato): boolean {
|
||||
return attivita.stato.id === stato.id;
|
||||
}
|
||||
|
||||
cambiaStato(attivita: Attivita, stato: Stato): void {
|
||||
if (!attivita.id || attivita.stato.id === stato.id) {
|
||||
return;
|
||||
}
|
||||
this.attivitaService.changeStato(attivita.id, stato.id).subscribe(() => this.caricaLista());
|
||||
}
|
||||
|
||||
modifica(attivita: Attivita): void {
|
||||
this.router.navigate(['/modifica-attivita', attivita.id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.profile-page {
|
||||
max-width: 520px;
|
||||
padding-top: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
.titolo {
|
||||
font-size: 24px;
|
||||
margin: 0 0 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sottotitolo {
|
||||
color: var(--color-text-muted);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="page profile-page">
|
||||
<div class="avatar">👤</div>
|
||||
<h1 class="titolo">Il tuo profilo</h1>
|
||||
<p class="sottotitolo">
|
||||
La gestione account arriverà con l'autenticazione. Per ora, accedi alle tue attività qui sotto.
|
||||
</p>
|
||||
<a class="btn-primary" routerLink="/mie-attivita">Vai a Le mie attività</a>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
imports: [RouterLink],
|
||||
templateUrl: './profile.html',
|
||||
styleUrl: './profile.css',
|
||||
})
|
||||
export class Profile {}
|
||||
@@ -0,0 +1,85 @@
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
margin: 0 0 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-menu);
|
||||
z-index: 5;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.suggestion-group-label {
|
||||
padding: 10px 16px 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 28px;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary-soft);
|
||||
border: 1px solid var(--color-primary-soft-border);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.filter-remove {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.results-label {
|
||||
font-weight: 700;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="page">
|
||||
<h1 class="page-title">Cerca attività</h1>
|
||||
|
||||
<div class="search-input-wrap">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
[value]="searchQuery()"
|
||||
(input)="onQueryChange($event)"
|
||||
(keydown)="onQueryKeyDown($event)"
|
||||
placeholder="Cerca per nome, branca, categoria, materiale, periodo..."
|
||||
/>
|
||||
@if (showSuggestions()) {
|
||||
<div class="suggestions">
|
||||
@for (group of suggestionGroups(); track group.label) {
|
||||
<div class="suggestion-group-label">{{ group.label }}</div>
|
||||
@for (suggestion of group.objectsList; track suggestion.id) {
|
||||
<div class="suggestion-item" (click)="selectSuggestion(suggestion)">
|
||||
{{ suggestion.nome }}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="active-filters">
|
||||
@for (filtro of activeFilters(); track $index) {
|
||||
<div class="filter-chip">
|
||||
<span>{{ filtroLabel(filtro.gruppo) }}: {{ filtro.nome }}</span>
|
||||
<span class="filter-remove" (click)="removeFiltro($index)">×</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="results-label">{{ resultsLabel() }}</div>
|
||||
|
||||
@if (results().length > 0) {
|
||||
<div class="grid">
|
||||
@for (attivita of results(); track attivita.id) {
|
||||
<app-activity-card [attivita]="attivita" />
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty-state">Nessun risultato con questi filtri.</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
|
||||
import { Attivita } from '../../core/models/attivita.model';
|
||||
import { AutocompleteGroup, GruppoFiltro, SearchObjectDto } from '../../core/models/search.model';
|
||||
import { AttivitaService } from '../../core/services/attivita';
|
||||
import { AutocompleteService } from '../../core/services/autocomplete';
|
||||
import { NavigationService } from '../../core/services/navigation';
|
||||
import { ActivityCard } from '../../shared/activity-card/activity-card';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
imports: [ActivityCard],
|
||||
templateUrl: './search.html',
|
||||
styleUrl: './search.css',
|
||||
})
|
||||
export class Search {
|
||||
private readonly attivitaService = inject(AttivitaService);
|
||||
private readonly autocompleteService = inject(AutocompleteService);
|
||||
readonly navigationService = inject(NavigationService);
|
||||
|
||||
readonly searchQuery = this.navigationService.searchQuery;
|
||||
readonly activeFilters = this.navigationService.filtri;
|
||||
|
||||
readonly suggestionGroups = signal<AutocompleteGroup[]>([]);
|
||||
readonly showSuggestions = computed(() => this.suggestionGroups().length > 0);
|
||||
|
||||
readonly results = signal<Attivita[]>([]);
|
||||
readonly resultsLabel = computed(() => {
|
||||
const n = this.results().length;
|
||||
return `${n} risultat${n === 1 ? 'o' : 'i'}`;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const filtri: SearchObjectDto[] = this.activeFilters();
|
||||
this.attivitaService.search(filtri).subscribe({
|
||||
next: (lista) => this.results.set(lista),
|
||||
error: () => this.results.set([]),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onQueryChange(event: Event): void {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
this.searchQuery.set(value);
|
||||
if (!value.trim()) {
|
||||
this.suggestionGroups.set([]);
|
||||
return;
|
||||
}
|
||||
this.autocompleteService.search(value).subscribe({
|
||||
next: (groups) => this.suggestionGroups.set(groups),
|
||||
error: () => this.suggestionGroups.set([]),
|
||||
});
|
||||
}
|
||||
|
||||
onQueryKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter' && this.searchQuery().trim()) {
|
||||
this.navigationService.addFiltro({ id: null, nome: this.searchQuery().trim(), gruppo: 'testo' });
|
||||
this.searchQuery.set('');
|
||||
this.suggestionGroups.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
selectSuggestion(suggestion: SearchObjectDto): void {
|
||||
const nome = suggestion.nome ?? this.searchQuery().trim();
|
||||
if (!nome) {
|
||||
return;
|
||||
}
|
||||
this.navigationService.addFiltro({
|
||||
id: suggestion.id,
|
||||
nome,
|
||||
gruppo: suggestion.gruppo as GruppoFiltro,
|
||||
});
|
||||
this.searchQuery.set('');
|
||||
this.suggestionGroups.set([]);
|
||||
}
|
||||
|
||||
removeFiltro(index: number): void {
|
||||
this.navigationService.removeFiltro(index);
|
||||
}
|
||||
|
||||
filtroLabel(gruppo: string): string {
|
||||
const etichette: Record<string, string> = {
|
||||
testo: 'Testo',
|
||||
branca: 'Branca',
|
||||
categoria: 'Categoria',
|
||||
materiale: 'Materiale',
|
||||
periodoAnno: 'Periodo',
|
||||
};
|
||||
return etichette[gruppo] ?? gruppo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user