Add scouthub-home-fe
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
.crea-gruppo {
|
||||
max-width: 480px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.crea-gruppo__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.crea-gruppo__field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.crea-gruppo__error {
|
||||
color: var(--mat-sys-error, #b3261e);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<section class="crea-gruppo">
|
||||
<h1>Crea un nuovo gruppo scout</h1>
|
||||
<p>Il tuo account non è ancora collegato a nessun gruppo scout: creane uno per continuare.</p>
|
||||
|
||||
<form class="crea-gruppo__form" (submit)="$event.preventDefault(); submit()" novalidate>
|
||||
<mat-form-field appearance="outline" class="crea-gruppo__field">
|
||||
<mat-label>Nome del gruppo scout</mat-label>
|
||||
<input matInput [formControl]="nome" placeholder="Es. Agesci Milano 1" />
|
||||
@if (nome.hasError('required')) {
|
||||
<mat-error>Il nome del gruppo è obbligatorio.</mat-error>
|
||||
} @else if (nome.hasError('minlength')) {
|
||||
<mat-error>Il nome deve avere almeno 3 caratteri.</mat-error>
|
||||
} @else if (nome.hasError('maxlength')) {
|
||||
<mat-error>Il nome non può superare i 100 caratteri.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (errorMessage(); as message) {
|
||||
<p class="crea-gruppo__error" role="alert">{{ message }}</p>
|
||||
}
|
||||
|
||||
<button mat-flat-button color="primary" type="submit" [disabled]="submitting()">
|
||||
{{ submitting() ? 'Creazione in corso…' : 'Crea gruppo' }}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { requireAuthGuard } from '../core/require-auth.guard';
|
||||
|
||||
export const CREA_GRUPPO_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./crea-gruppo').then((m) => m.CreaGruppo),
|
||||
canActivate: [requireAuthGuard]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,129 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
import { CreaGruppo } from './crea-gruppo';
|
||||
import { GruppiApiService } from './gruppi-api.service';
|
||||
|
||||
describe('CreaGruppo', () => {
|
||||
let component: CreaGruppo;
|
||||
let fixture: ComponentFixture<CreaGruppo>;
|
||||
let gruppiApi: { creaGruppo: ReturnType<typeof vi.fn> };
|
||||
let keycloak: { updateToken: ReturnType<typeof vi.fn> };
|
||||
let router: { navigateByUrl: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(async () => {
|
||||
gruppiApi = { creaGruppo: vi.fn() };
|
||||
keycloak = { updateToken: vi.fn().mockResolvedValue(true) };
|
||||
router = { navigateByUrl: vi.fn().mockResolvedValue(true) };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CreaGruppo],
|
||||
providers: [
|
||||
{ provide: GruppiApiService, useValue: gruppiApi },
|
||||
{ provide: Keycloak, useValue: keycloak },
|
||||
{ provide: Router, useValue: router }
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreaGruppo);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('si crea correttamente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('validazione del campo nome', () => {
|
||||
it('è invalido se vuoto (required)', () => {
|
||||
component.nome.setValue('');
|
||||
expect(component.nome.hasError('required')).toBe(true);
|
||||
});
|
||||
|
||||
it('è invalido con meno di 3 caratteri', () => {
|
||||
component.nome.setValue('Ab');
|
||||
expect(component.nome.hasError('minlength')).toBe(true);
|
||||
});
|
||||
|
||||
it('è valido con esattamente 3 caratteri', () => {
|
||||
component.nome.setValue('Ab1');
|
||||
expect(component.nome.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('è invalido oltre i 100 caratteri', () => {
|
||||
component.nome.setValue('a'.repeat(101));
|
||||
expect(component.nome.hasError('maxlength')).toBe(true);
|
||||
});
|
||||
|
||||
it('è valido con esattamente 100 caratteri', () => {
|
||||
component.nome.setValue('a'.repeat(100));
|
||||
expect(component.nome.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('è valido con un nome tipico', () => {
|
||||
component.nome.setValue('Agesci Milano 1');
|
||||
expect(component.nome.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('non chiama l\'API e marca il controllo come touched se il nome non è valido', async () => {
|
||||
component.nome.setValue('');
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(gruppiApi.creaGruppo).not.toHaveBeenCalled();
|
||||
expect(component.nome.touched).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gestione errore 409 (nome già esistente)', () => {
|
||||
it('mostra il messaggio del backend e permette di correggere il nome senza ricaricare la pagina', async () => {
|
||||
component.nome.setValue('Agesci Milano 1');
|
||||
gruppiApi.creaGruppo.mockReturnValue(
|
||||
throwError(
|
||||
() =>
|
||||
new HttpErrorResponse({
|
||||
status: 409,
|
||||
error: { message: 'Esiste già un gruppo scout con nome "Agesci Milano 1"' }
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(component.errorMessage()).toBe('Esiste già un gruppo scout con nome "Agesci Milano 1"');
|
||||
expect(component.submitting()).toBe(false);
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
|
||||
// il form resta utilizzabile: si può correggere il nome e ritentare senza reload
|
||||
component.nome.setValue('Agesci Milano 2');
|
||||
expect(component.nome.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('mostra un messaggio generico per errori diversi da 409', async () => {
|
||||
component.nome.setValue('Agesci Milano 1');
|
||||
gruppiApi.creaGruppo.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 500 })));
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(component.errorMessage()).toBe('Si è verificato un errore imprevisto. Riprova.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirect dopo il successo', () => {
|
||||
it('aggiorna forzatamente il token e reindirizza alla home dopo la creazione del gruppo', async () => {
|
||||
component.nome.setValue('Agesci Milano 1');
|
||||
gruppiApi.creaGruppo.mockReturnValue(of({ orgId: 'org-1', gruppiCreati: ['Capi'] }));
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(gruppiApi.creaGruppo).toHaveBeenCalledWith('Agesci Milano 1');
|
||||
expect(keycloak.updateToken).toHaveBeenCalledWith(-1);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
expect(component.submitting()).toBe(false);
|
||||
expect(component.errorMessage()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Router } from '@angular/router';
|
||||
import Keycloak from 'keycloak-js';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { GruppiApiService } from './gruppi-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-crea-gruppo',
|
||||
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatProgressSpinnerModule],
|
||||
templateUrl: './crea-gruppo.html',
|
||||
styleUrl: './crea-gruppo.css'
|
||||
})
|
||||
export class CreaGruppo {
|
||||
private readonly gruppiApi = inject(GruppiApiService);
|
||||
private readonly keycloak = inject(Keycloak);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly nome = new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(3), Validators.maxLength(100)]
|
||||
});
|
||||
|
||||
readonly submitting = signal(false);
|
||||
readonly errorMessage = signal<string | null>(null);
|
||||
|
||||
async submit(): Promise<void> {
|
||||
if (this.submitting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nome.invalid) {
|
||||
this.nome.markAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorMessage.set(null);
|
||||
this.submitting.set(true);
|
||||
|
||||
try {
|
||||
await firstValueFrom(this.gruppiApi.creaGruppo(this.nome.value.trim()));
|
||||
// Il gruppo è appena stato creato su Keycloak: il token corrente non contiene ancora
|
||||
// il claim "organization" aggiornato, va quindi forzato un refresh prima di navigare.
|
||||
await this.keycloak.updateToken(-1).catch(() => undefined);
|
||||
this.submitting.set(false);
|
||||
await this.router.navigateByUrl('/');
|
||||
} catch (error) {
|
||||
this.submitting.set(false);
|
||||
this.handleError(error as HttpErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: HttpErrorResponse): void {
|
||||
if (error.status === 409) {
|
||||
this.errorMessage.set(error.error?.message ?? 'Esiste già un gruppo scout con questo nome.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorMessage.set('Si è verificato un errore imprevisto. Riprova.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
export interface CreaGruppoResponse {
|
||||
orgId: string;
|
||||
gruppiCreati: string[];
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GruppiApiService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
creaGruppo(nome: string): Observable<CreaGruppoResponse> {
|
||||
return this.http.post<CreaGruppoResponse>(`${environment.orgServiceApiBaseUrl}/gruppi`, { nome });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user