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:
@@ -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' },
|
||||
|
||||
101
frontend/src/app/features/parents/children-tab.component.ts
Normal file
101
frontend/src/app/features/parents/children-tab.component.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user