feat: cuentas de familia (multi-tenant), registro/login y preferencias
Convierte recordaLexia de una sola familia a multi-familia, con cuentas propias y persistencia de preferencias. Backend: - Tenant Family (email único + contraseña BCrypt + PIN + prefs de cuenta); family_id en child/activity/material_item/reward; aislamiento por familia (acceso cruzado responde 404). - Auth propia (sin Keycloak): registro/login email+contraseña, sesiones de familia persistidas en BD (sobreviven a reinicios), panel de padres tras PIN. - Liquibase 002-multitenant; seeder crea una familia demo. - Tests de aislamiento entre familias, registro/login y gate del panel. Frontend: - Login, registro y pantalla de cuenta; guards (sesion + PIN) e interceptor de sesion global; perfiles scopeados a la familia. Preferencias: - OpenDyslexic persistida por nino (child.dyslexiaFont) y default de cuenta. Decisiones en docs/adr/0004.
This commit is contained in:
148
frontend/src/app/features/auth/account.component.ts
Normal file
148
frontend/src/app/features/auth/account.component.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
|
||||
/** Pantalla de cuenta: datos, preferencias, cambio de contraseña/PIN y cerrar sesión. */
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
imports: [FormsModule],
|
||||
template: `
|
||||
<main class="acc">
|
||||
<header class="acc__top">
|
||||
<button type="button" class="acc__back" (click)="back()">‹</button>
|
||||
<h1 class="acc__title">⚙️ Mi cuenta</h1>
|
||||
<button type="button" class="acc__logout" (click)="logout()">Cerrar sesión</button>
|
||||
</header>
|
||||
|
||||
@if (auth.family(); as f) {
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Familia</p>
|
||||
<p>{{ f.name }} · {{ f.email }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Preferencias</p>
|
||||
<div class="adm-row">
|
||||
<label class="adm-label">Idioma del panel</label>
|
||||
<select class="adm-input" [(ngModel)]="uiLanguage">
|
||||
<option value="ES">Español</option>
|
||||
<option value="CA">Català</option>
|
||||
</select>
|
||||
<label class="adm-chip">
|
||||
<input type="checkbox" [(ngModel)]="defaultDyslexiaFont" /> OpenDyslexic por defecto
|
||||
</label>
|
||||
<button class="adm-btn" (click)="savePrefs()">Guardar</button>
|
||||
</div>
|
||||
@if (prefsSaved()) { <p class="acc__ok">Preferencias guardadas ✓</p> }
|
||||
</div>
|
||||
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Cambiar contraseña</p>
|
||||
<div class="adm-row">
|
||||
<input class="adm-input" type="password" placeholder="Actual" [(ngModel)]="curPass" />
|
||||
<input class="adm-input" type="password" placeholder="Nueva (mín. 6)" [(ngModel)]="newPass" />
|
||||
<button class="adm-btn" [disabled]="!curPass || newPass.length < 6" (click)="savePassword()">Cambiar</button>
|
||||
</div>
|
||||
@if (passMsg()) { <p class="acc__ok">{{ passMsg() }}</p> }
|
||||
</div>
|
||||
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Cambiar PIN del panel</p>
|
||||
<div class="adm-row">
|
||||
<input class="adm-input" type="password" placeholder="PIN actual" maxlength="4" [(ngModel)]="curPin" />
|
||||
<input class="adm-input" placeholder="PIN nuevo (4 díg.)" maxlength="4" [(ngModel)]="newPin" />
|
||||
<button class="adm-btn" [disabled]="!curPin || newPin.length !== 4" (click)="savePin()">Cambiar</button>
|
||||
</div>
|
||||
@if (pinMsg()) { <p class="acc__ok">{{ pinMsg() }}</p> }
|
||||
</div>
|
||||
</main>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.acc { max-width: 720px; margin: 0 auto; padding: var(--space-5) var(--space-4); }
|
||||
.acc__top { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-5); }
|
||||
.acc__title { flex: 1; margin: 0; font-size: 1.6rem; }
|
||||
.acc__back {
|
||||
all: unset; cursor: pointer; width: var(--touch-nav); height: var(--touch-nav); border-radius: 50%;
|
||||
background: var(--surface); box-shadow: var(--shadow-btn); display: flex; align-items: center;
|
||||
justify-content: center; font-size: 28px; color: var(--text-2);
|
||||
}
|
||||
.acc__logout {
|
||||
all: unset; cursor: pointer; font-family: var(--font-display); font-weight: 700;
|
||||
color: var(--accent-pink); background: color-mix(in srgb, var(--accent-pink) 14%, #fff);
|
||||
padding: 10px 16px; border-radius: var(--radius-pill);
|
||||
}
|
||||
.acc__ok { margin: 10px 0 0; color: var(--accent-green); font-weight: 700; }
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
protected readonly auth = inject(AuthService);
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected uiLanguage = 'ES';
|
||||
protected defaultDyslexiaFont = true;
|
||||
protected curPass = '';
|
||||
protected newPass = '';
|
||||
protected curPin = '';
|
||||
protected newPin = '';
|
||||
protected readonly prefsSaved = signal(false);
|
||||
protected readonly passMsg = signal<string | null>(null);
|
||||
protected readonly pinMsg = signal<string | null>(null);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.auth.loadMe().subscribe((me) => {
|
||||
this.uiLanguage = me.uiLanguage;
|
||||
this.defaultDyslexiaFont = me.defaultDyslexiaFont;
|
||||
});
|
||||
}
|
||||
|
||||
savePrefs(): void {
|
||||
this.http
|
||||
.put<void>('/api/account/prefs', {
|
||||
uiLanguage: this.uiLanguage,
|
||||
defaultDyslexiaFont: this.defaultDyslexiaFont,
|
||||
})
|
||||
.subscribe(() => {
|
||||
this.prefsSaved.set(true);
|
||||
setTimeout(() => this.prefsSaved.set(false), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
savePassword(): void {
|
||||
this.http
|
||||
.put<void>('/api/account/password', { currentPassword: this.curPass, newPassword: this.newPass })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.passMsg.set('Contraseña actualizada ✓');
|
||||
this.curPass = this.newPass = '';
|
||||
},
|
||||
error: () => this.passMsg.set('La contraseña actual no es correcta'),
|
||||
});
|
||||
}
|
||||
|
||||
savePin(): void {
|
||||
this.http
|
||||
.put<void>('/api/account/pin', { currentPin: this.curPin, newPin: this.newPin })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.pinMsg.set('PIN actualizado ✓');
|
||||
this.curPin = this.newPin = '';
|
||||
},
|
||||
error: () => this.pinMsg.set('El PIN actual no es correcto'),
|
||||
});
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.auth.logout().subscribe();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
84
frontend/src/app/features/auth/auth.scss
Normal file
84
frontend/src/app/features/auth/auth.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.auth {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-1);
|
||||
border-radius: var(--radius-card);
|
||||
padding: var(--space-6);
|
||||
box-shadow: var(--shadow-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
text-align: center;
|
||||
animation: slideUp 0.4s ease both;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 0 0 var(--space-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.auth__input {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
padding: 12px 14px;
|
||||
border: 2px solid var(--border-2);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text-strong);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth__err {
|
||||
margin: 0;
|
||||
color: var(--accent-pink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auth__btn {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
min-height: var(--touch-nav);
|
||||
background: var(--accent-blue);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.auth__btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth__alt {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.auth__alt a {
|
||||
color: var(--accent-blue);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
54
frontend/src/app/features/auth/login.component.ts
Normal file
54
frontend/src/app/features/auth/login.component.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
|
||||
/** Acceso de familia con email + contraseña. */
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [FormsModule, RouterLink],
|
||||
template: `
|
||||
<main class="auth">
|
||||
<section class="auth__card">
|
||||
<h1 class="auth__title">recordaLexia 🦊</h1>
|
||||
<p class="auth__sub">Entra con tu cuenta de familia</p>
|
||||
|
||||
<input class="auth__input" type="email" placeholder="Email" [(ngModel)]="email" autocomplete="username" />
|
||||
<input class="auth__input" type="password" placeholder="Contraseña" [(ngModel)]="password"
|
||||
autocomplete="current-password" (keyup.enter)="submit()" />
|
||||
|
||||
@if (error()) { <p class="auth__err">Email o contraseña incorrectos</p> }
|
||||
|
||||
<button class="auth__btn" [disabled]="loading() || !email || !password" (click)="submit()">
|
||||
{{ loading() ? 'Entrando…' : 'Entrar' }}
|
||||
</button>
|
||||
<p class="auth__alt">¿No tienes cuenta? <a routerLink="/register">Crear una</a></p>
|
||||
</section>
|
||||
</main>
|
||||
`,
|
||||
styleUrl: './auth.scss',
|
||||
})
|
||||
export class LoginComponent {
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected email = '';
|
||||
protected password = '';
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly error = signal(false);
|
||||
|
||||
submit(): void {
|
||||
if (!this.email || !this.password) {
|
||||
return;
|
||||
}
|
||||
this.loading.set(true);
|
||||
this.error.set(false);
|
||||
this.auth.login(this.email.trim(), this.password).subscribe({
|
||||
next: () => this.auth.loadMe().subscribe(() => this.router.navigate(['/'])),
|
||||
error: () => {
|
||||
this.error.set(true);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
64
frontend/src/app/features/auth/register.component.ts
Normal file
64
frontend/src/app/features/auth/register.component.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
|
||||
/** Alta de una familia nueva (email + contraseña + PIN del panel). */
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
imports: [FormsModule, RouterLink],
|
||||
template: `
|
||||
<main class="auth">
|
||||
<section class="auth__card">
|
||||
<h1 class="auth__title">Crear familia 🦊</h1>
|
||||
<p class="auth__sub">Una cuenta para toda la familia</p>
|
||||
|
||||
<input class="auth__input" placeholder="Nombre de la familia" [(ngModel)]="name" />
|
||||
<input class="auth__input" type="email" placeholder="Email" [(ngModel)]="email" autocomplete="username" />
|
||||
<input class="auth__input" type="password" placeholder="Contraseña (mín. 6)" [(ngModel)]="password"
|
||||
autocomplete="new-password" />
|
||||
<input class="auth__input" inputmode="numeric" maxlength="4" placeholder="PIN de padres (4 dígitos)"
|
||||
[(ngModel)]="pin" />
|
||||
|
||||
@if (error()) { <p class="auth__err">{{ error() }}</p> }
|
||||
|
||||
<button class="auth__btn" [disabled]="loading() || !valid()" (click)="submit()">
|
||||
{{ loading() ? 'Creando…' : 'Crear cuenta' }}
|
||||
</button>
|
||||
<p class="auth__alt">¿Ya tienes cuenta? <a routerLink="/login">Entrar</a></p>
|
||||
</section>
|
||||
</main>
|
||||
`,
|
||||
styleUrl: './auth.scss',
|
||||
})
|
||||
export class RegisterComponent {
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
protected name = '';
|
||||
protected email = '';
|
||||
protected password = '';
|
||||
protected pin = '';
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly error = signal<string | null>(null);
|
||||
|
||||
valid(): boolean {
|
||||
return !!this.email && this.password.length >= 6 && /^\d{4}$/.test(this.pin) && !!this.name;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (!this.valid()) {
|
||||
return;
|
||||
}
|
||||
this.loading.set(true);
|
||||
this.error.set(null);
|
||||
this.auth.register(this.email.trim(), this.password, this.name.trim(), this.pin).subscribe({
|
||||
next: () => this.auth.loadMe().subscribe(() => this.router.navigate(['/'])),
|
||||
error: (e: HttpErrorResponse) => {
|
||||
this.error.set(e.status === 409 ? 'Ese email ya está registrado' : 'No se pudo crear la cuenta');
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit, ViewChild, computed, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { FontPreferenceService } from '../../core/font-preference.service';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
import { SoundService } from '../../core/sound.service';
|
||||
import { TodayResponse, ViewMode } from '../../core/models';
|
||||
@@ -47,6 +48,7 @@ export class HomeComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly sound = inject(SoundService);
|
||||
private readonly fontPreference = inject(FontPreferenceService);
|
||||
protected readonly i18n = inject(I18nService);
|
||||
|
||||
@ViewChild('wallet') private wallet?: WalletComponent;
|
||||
@@ -88,6 +90,8 @@ export class HomeComponent implements OnInit {
|
||||
this.today.set(data);
|
||||
this.mode.set(data.child.viewMode);
|
||||
this.i18n.setLang(data.child.language);
|
||||
// Aplica la preferencia de tipografía de ESTE niño.
|
||||
this.fontPreference.apply(data.child.dyslexiaFont);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => this.loading.set(false),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
import { ParentSessionService } from '../../core/parent-session.service';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
|
||||
/** Teclado numérico 3×4 para el PIN de padres (4 dígitos, shake al fallar). */
|
||||
@Component({
|
||||
@@ -87,7 +87,7 @@ import { ParentSessionService } from '../../core/parent-session.service';
|
||||
],
|
||||
})
|
||||
export class KeypadComponent {
|
||||
private readonly session = inject(ParentSessionService);
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
protected readonly i18n = inject(I18nService);
|
||||
|
||||
@@ -122,7 +122,7 @@ export class KeypadComponent {
|
||||
}
|
||||
|
||||
private submit(code: string): void {
|
||||
this.session.login(code).subscribe({
|
||||
this.auth.unlockPanel(code).subscribe({
|
||||
next: () => this.router.navigate(['/parents']),
|
||||
error: () => {
|
||||
this.error.set(true);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ParentApiService } from '../../core/parent-api.service';
|
||||
import { ParentSessionService } from '../../core/parent-session.service';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
import { ChildSummary } from '../../core/models';
|
||||
import { ScheduleTabComponent } from './schedule-tab.component';
|
||||
@@ -90,7 +90,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
|
||||
})
|
||||
export class ParentsComponent {
|
||||
private readonly parentApi = inject(ParentApiService);
|
||||
private readonly session = inject(ParentSessionService);
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
protected readonly i18n = inject(I18nService);
|
||||
|
||||
@@ -107,8 +107,9 @@ export class ParentsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** "Salir" del panel: lo bloquea (vuelve a pedir PIN) y regresa al kiosko. */
|
||||
logout(): void {
|
||||
this.session.logout();
|
||||
this.auth.panelUnlocked.set(false);
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import { RewardAdminView } from '../../core/models';
|
||||
<label class="adm-chip">
|
||||
<input type="checkbox" [(ngModel)]="ttsEnabled" (change)="saveSettings()" /> 🗣️ {{ i18n.t('readAloud') }}
|
||||
</label>
|
||||
<label class="adm-chip">
|
||||
<input type="checkbox" [(ngModel)]="dyslexiaFont" (change)="saveSettings()" /> 🔤 OpenDyslexic
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +81,7 @@ export class RewardsTabComponent {
|
||||
this.api.getToday(value).subscribe((t) => {
|
||||
this.soundEnabled = t.child.soundEnabled;
|
||||
this.ttsEnabled = t.child.ttsEnabled;
|
||||
this.dyslexiaFont = t.child.dyslexiaFont;
|
||||
});
|
||||
}
|
||||
private _childId!: number;
|
||||
@@ -94,6 +98,7 @@ export class RewardsTabComponent {
|
||||
protected perDay = 20;
|
||||
protected soundEnabled = true;
|
||||
protected ttsEnabled = true;
|
||||
protected dyslexiaFont = true;
|
||||
|
||||
protected icon = '';
|
||||
protected labelEs = '';
|
||||
@@ -121,7 +126,11 @@ export class RewardsTabComponent {
|
||||
|
||||
saveSettings(): void {
|
||||
this.parentApi
|
||||
.updateSettings(this._childId, { soundEnabled: this.soundEnabled, ttsEnabled: this.ttsEnabled })
|
||||
.updateSettings(this._childId, {
|
||||
soundEnabled: this.soundEnabled,
|
||||
ttsEnabled: this.ttsEnabled,
|
||||
dyslexiaFont: this.dyslexiaFont,
|
||||
})
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<main class="profiles">
|
||||
<button type="button" class="profiles__account" (click)="openAccount()">
|
||||
👨👩👧 {{ auth.family()?.name || 'Mi cuenta' }}
|
||||
</button>
|
||||
|
||||
<h1 class="profiles__title">{{ i18n.t('whoEntersToday') }}</h1>
|
||||
|
||||
@if (loading()) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
}
|
||||
|
||||
.profiles {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -11,6 +12,21 @@
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-6) var(--space-4);
|
||||
|
||||
&__account {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-card);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 2.4rem;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { AuthService } from '../../core/auth.service';
|
||||
import { FontPreferenceService } from '../../core/font-preference.service';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
import { KioskService } from '../../core/kiosk.service';
|
||||
import { ChildSummary } from '../../core/models';
|
||||
@@ -23,6 +25,8 @@ export class ProfileSelectComponent implements OnInit {
|
||||
private readonly api = inject(ApiService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly kiosk = inject(KioskService);
|
||||
protected readonly auth = inject(AuthService);
|
||||
private readonly fontPreference = inject(FontPreferenceService);
|
||||
protected readonly i18n = inject(I18nService);
|
||||
|
||||
protected readonly children = signal<ChildSummary[]>([]);
|
||||
@@ -30,6 +34,18 @@ export class ProfileSelectComponent implements OnInit {
|
||||
protected readonly error = signal(false);
|
||||
|
||||
ngOnInit(): void {
|
||||
// En la pantalla de perfiles aún no hay niño elegido: aplica el default de la cuenta.
|
||||
const applyDefault = () => {
|
||||
const f = this.auth.family();
|
||||
if (f) {
|
||||
this.fontPreference.apply(f.defaultDyslexiaFont);
|
||||
}
|
||||
};
|
||||
if (!this.auth.family()) {
|
||||
this.auth.loadMe().subscribe(applyDefault);
|
||||
} else {
|
||||
applyDefault();
|
||||
}
|
||||
this.api.getChildren().subscribe({
|
||||
next: (list) => {
|
||||
this.children.set(list);
|
||||
@@ -52,4 +68,8 @@ export class ProfileSelectComponent implements OnInit {
|
||||
openParents(): void {
|
||||
this.router.navigate(['/parents']);
|
||||
}
|
||||
|
||||
openAccount(): void {
|
||||
this.router.navigate(['/account']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user