feat(panel): pestaña de alta y baja de niños

Añade la gestión de perfiles de hijo en el panel de padres (crear con
mascota/nombre/edad/color/hora de salida y borrar), que faltaba: una familia
recién registrada empieza sin niños y necesita poder añadirlos. El backend
(CRUD de Child) ya existía; faltaba la UI.
This commit is contained in:
Jaume Garriga Maestre
2026-06-21 13:19:54 +02:00
parent 24a0c8a0dd
commit aa42f0cb0b
3 changed files with 117 additions and 5 deletions

View File

@@ -40,6 +40,7 @@ export class I18nService {
wrongPin: { ES: 'PIN incorrecto', CA: 'PIN incorrecte' },
// Panel de padres
logout: { ES: 'Salir', CA: 'Sortir' },
tabChildren: { ES: 'Niños', CA: 'Nens' },
tabSchedule: { ES: 'Horario', CA: 'Horari' },
tabMaterials: { ES: 'Materiales', CA: 'Materials' },
tabEvents: { ES: 'Eventos', CA: 'Esdeveniments' },

View File

@@ -0,0 +1,101 @@
import { Component, EventEmitter, Output, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ParentApiService } from '../../core/parent-api.service';
import { I18nService } from '../../core/i18n.service';
import { ChildSummary } from '../../core/models';
/** Pestaña Niños: dar de alta y de baja perfiles de hijo en la familia. */
@Component({
selector: 'app-children-tab',
imports: [FormsModule],
template: `
<!-- Alta -->
<div class="adm-card">
<p class="adm-label">Nuevo niño/a</p>
<div class="adm-row">
<input class="adm-input adm-input--sm" [(ngModel)]="mascot" placeholder="🦊" maxlength="4"
aria-label="Mascota (emoji)" />
<input class="adm-input" [(ngModel)]="name" placeholder="Nombre" />
<input class="adm-input adm-input--sm" type="number" min="1" max="18" [(ngModel)]="age"
placeholder="Edad" aria-label="Edad" />
<input class="adm-input adm-input--sm" type="time" [(ngModel)]="departureTime"
aria-label="Hora de salida" />
<input class="adm-input adm-input--sm" type="color" [(ngModel)]="accentColor"
aria-label="Color" />
<button class="adm-btn" [disabled]="!name || !mascot || !age" (click)="add()">
+ {{ i18n.t('add') }}
</button>
</div>
<p class="adm-empty" style="text-align:left">
La mascota es un emoji y el color identifica al niño en sus tarjetas.
</p>
</div>
<!-- Lista -->
<div class="adm-card">
<p class="adm-label">Perfiles</p>
<div class="adm-list">
@for (c of children(); track c.id) {
<div class="adm-item" [style.borderLeft]="'5px solid ' + c.accentColor">
<span style="font-size:28px">{{ c.mascot }}</span>
<span class="adm-item__grow"><strong>{{ c.name }}</strong> · {{ c.age }} años · 🪙 {{ c.coins }}</span>
<button class="adm-del" (click)="remove(c)" aria-label="Borrar">✕</button>
</div>
} @empty {
<p class="adm-empty">Aún no hay niños. Añade el primero arriba ✨</p>
}
</div>
</div>
`,
})
export class ChildrenTabComponent {
/** Avisa al panel de que la lista de niños cambió (para refrescar el selector). */
@Output() changed = new EventEmitter<void>();
private readonly api = inject(ParentApiService);
protected readonly i18n = inject(I18nService);
protected readonly children = signal<ChildSummary[]>([]);
protected mascot = '';
protected name = '';
protected age: number | null = null;
protected departureTime = '';
protected accentColor = '#F2A65A';
constructor() {
this.reload();
}
private reload(): void {
this.api.listChildren().subscribe((list) => this.children.set(list));
}
add(): void {
if (!this.name || !this.mascot || !this.age) {
return;
}
this.api
.createChild({
name: this.name,
mascot: this.mascot,
accentColor: this.accentColor,
age: this.age,
departureTime: this.departureTime || undefined,
})
.subscribe(() => {
this.name = this.mascot = this.departureTime = '';
this.age = null;
this.reload();
this.changed.emit();
});
}
remove(c: ChildSummary): void {
// Borra el niño y todo lo suyo (horario, rutinas, tareas, monedero...).
this.api.deleteChild(c.id).subscribe(() => {
this.reload();
this.changed.emit();
});
}
}

View File

@@ -5,19 +5,21 @@ import { ParentApiService } from '../../core/parent-api.service';
import { AuthService } from '../../core/auth.service';
import { I18nService } from '../../core/i18n.service';
import { ChildSummary } from '../../core/models';
import { ChildrenTabComponent } from './children-tab.component';
import { ScheduleTabComponent } from './schedule-tab.component';
import { MaterialsTabComponent } from './materials-tab.component';
import { EventsTabComponent } from './events-tab.component';
import { RoutinesTabComponent } from './routines-tab.component';
import { RewardsTabComponent } from './rewards-tab.component';
type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
type Tab = 'children' | 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
/** Panel de padres: barra de pestañas, selector de niño y salida. */
@Component({
selector: 'app-parents',
imports: [
FormsModule,
ChildrenTabComponent,
ScheduleTabComponent,
MaterialsTabComponent,
EventsTabComponent,
@@ -27,6 +29,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
template: `
<header class="ptop">
<div class="ptabs">
<button class="ptab" [class.ptab--on]="tab() === 'children'" (click)="tab.set('children')">👦 {{ i18n.t('tabChildren') }}</button>
<button class="ptab" [class.ptab--on]="tab() === 'schedule'" (click)="tab.set('schedule')">📅 {{ i18n.t('tabSchedule') }}</button>
<button class="ptab" [class.ptab--on]="tab() === 'materials'" (click)="tab.set('materials')">🎒 {{ i18n.t('tabMaterials') }}</button>
<button class="ptab" [class.ptab--on]="tab() === 'events'" (click)="tab.set('events')">📋 {{ i18n.t('tabEvents') }}</button>
@@ -37,7 +40,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
</header>
<main class="pbody">
@if (tab() !== 'materials') {
@if (tab() !== 'materials' && tab() !== 'children') {
<div class="pchild">
<label class="adm-label">{{ i18n.t('child') }}:</label>
<select class="adm-input" [(ngModel)]="selectedId">
@@ -49,6 +52,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
}
@switch (tab()) {
@case ('children') { <app-children-tab (changed)="reloadChildren()" /> }
@case ('schedule') { <app-schedule-tab [childId]="selectedId" /> }
@case ('materials') { <app-materials-tab /> }
@case ('events') { <app-events-tab [childId]="selectedId" /> }
@@ -95,14 +99,20 @@ export class ParentsComponent {
protected readonly i18n = inject(I18nService);
protected readonly children = signal<ChildSummary[]>([]);
protected readonly tab = signal<Tab>('schedule');
protected readonly tab = signal<Tab>('children');
protected selectedId = 0;
constructor() {
this.reloadChildren();
}
/** Recarga la lista de niños y mantiene válido el niño seleccionado. */
reloadChildren(): void {
this.parentApi.listChildren().subscribe((list) => {
this.children.set(list);
if (list.length && !this.selectedId) {
this.selectedId = list[0].id;
const stillExists = list.some((c) => c.id === this.selectedId);
if (!stillExists) {
this.selectedId = list.length ? list[0].id : 0;
}
});
}