Fix grafica scouthub-home-fe

This commit is contained in:
Lorenzo Sanesi
2026-07-25 12:02:03 +02:00
parent 7e0544c5ab
commit 08271b8a15
60 changed files with 4305 additions and 149 deletions
@@ -1,7 +1,30 @@
.crea-gruppo {
max-width: 480px;
margin: 2rem auto;
padding: 0 1rem;
display: flex;
justify-content: center;
padding-top: 24px;
}
.crea-gruppo__card {
width: min(440px, 100%);
background: var(--mat-sys-surface-container);
border-radius: 32px;
padding: 32px;
box-shadow: 0 12px 32px rgba(46, 43, 37, 0.16);
display: flex;
flex-direction: column;
gap: 13px;
}
.crea-gruppo__title {
font-family: var(--mat-sys-headline-small-font, Caprasimo, sans-serif);
font-size: 22px;
margin: 0;
}
.crea-gruppo__intro {
margin: 0;
font-size: 14px;
opacity: 0.8;
}
.crea-gruppo__form {
@@ -15,6 +38,19 @@
}
.crea-gruppo__error {
color: var(--mat-sys-error, #b3261e);
color: var(--mat-sys-error);
margin: 0;
font-size: 13px;
}
.crea-gruppo__actions {
display: flex;
justify-content: flex-end;
margin-top: 6px;
}
.crea-gruppo__submit {
border-radius: 999px !important;
background: var(--mat-sys-primary) !important;
color: var(--mat-sys-on-primary) !important;
}
@@ -1,26 +1,35 @@
<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>
<div class="crea-gruppo__card">
@if (isAdmin) {
<h1 class="crea-gruppo__title">Crea nuovo gruppo</h1>
<p class="crea-gruppo__intro">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>
<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>
@if (errorMessage(); as message) {
<p class="crea-gruppo__error" role="alert">{{ message }}</p>
}
<div class="crea-gruppo__actions">
<button class="crea-gruppo__submit" mat-flat-button type="submit" [disabled]="submitting()">
{{ submitting() ? 'Creazione in corso…' : 'Crea gruppo' }}
</button>
</div>
</form>
} @else {
<h1 class="crea-gruppo__title">Accesso non consentito</h1>
<p role="alert">Questa pagina è riservata agli utenti con ruolo admin.</p>
}
<button mat-flat-button color="primary" type="submit" [disabled]="submitting()">
{{ submitting() ? 'Creazione in corso…' : 'Crea gruppo' }}
</button>
</form>
</div>
</section>
@@ -11,13 +11,12 @@ 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 () => {
async function setup(roles: string[] = ['admin']): Promise<void> {
gruppiApi = { creaGruppo: vi.fn() };
keycloak = { updateToken: vi.fn().mockResolvedValue(true) };
router = { navigateByUrl: vi.fn().mockResolvedValue(true) };
const keycloak = { tokenParsed: { realm_access: { roles } } };
await TestBed.configureTestingModule({
imports: [CreaGruppo],
@@ -31,13 +30,18 @@ describe('CreaGruppo', () => {
fixture = TestBed.createComponent(CreaGruppo);
component = fixture.componentInstance;
await fixture.whenStable();
});
}
it('si crea correttamente', () => {
it('si crea correttamente', async () => {
await setup();
expect(component).toBeTruthy();
});
describe('validazione del campo nome', () => {
beforeEach(async () => {
await setup();
});
it('è invalido se vuoto (required)', () => {
component.nome.setValue('');
expect(component.nome.hasError('required')).toBe(true);
@@ -79,6 +83,10 @@ describe('CreaGruppo', () => {
});
describe('gestione errore 409 (nome già esistente)', () => {
beforeEach(async () => {
await setup();
});
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(
@@ -113,17 +121,40 @@ describe('CreaGruppo', () => {
});
describe('redirect dopo il successo', () => {
it('aggiorna forzatamente il token e reindirizza alla home dopo la creazione del gruppo', async () => {
beforeEach(async () => {
await setup();
});
it('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();
});
});
describe('controllo del ruolo admin', () => {
beforeEach(async () => {
await setup(['censito']);
});
it('mostra il messaggio di accesso non consentito invece del form', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('form')).toBeNull();
expect(compiled.textContent).toContain('Accesso non consentito');
});
it('non chiama l\'API se l\'utente non ha il ruolo admin', async () => {
component.nome.setValue('Agesci Milano 1');
await component.submit();
expect(gruppiApi.creaGruppo).not.toHaveBeenCalled();
});
});
});
@@ -30,7 +30,15 @@ export class CreaGruppo {
readonly submitting = signal(false);
readonly errorMessage = signal<string | null>(null);
get isAdmin(): boolean {
return (this.keycloak.tokenParsed?.realm_access?.roles ?? []).includes('admin');
}
async submit(): Promise<void> {
if (!this.isAdmin) {
return;
}
if (this.submitting()) {
return;
}
@@ -45,9 +53,6 @@ export class CreaGruppo {
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) {