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' },
|
wrongPin: { ES: 'PIN incorrecto', CA: 'PIN incorrecte' },
|
||||||
// Panel de padres
|
// Panel de padres
|
||||||
logout: { ES: 'Salir', CA: 'Sortir' },
|
logout: { ES: 'Salir', CA: 'Sortir' },
|
||||||
|
tabChildren: { ES: 'Niños', CA: 'Nens' },
|
||||||
tabSchedule: { ES: 'Horario', CA: 'Horari' },
|
tabSchedule: { ES: 'Horario', CA: 'Horari' },
|
||||||
tabMaterials: { ES: 'Materiales', CA: 'Materials' },
|
tabMaterials: { ES: 'Materiales', CA: 'Materials' },
|
||||||
tabEvents: { ES: 'Eventos', CA: 'Esdeveniments' },
|
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 { AuthService } from '../../core/auth.service';
|
||||||
import { I18nService } from '../../core/i18n.service';
|
import { I18nService } from '../../core/i18n.service';
|
||||||
import { ChildSummary } from '../../core/models';
|
import { ChildSummary } from '../../core/models';
|
||||||
|
import { ChildrenTabComponent } from './children-tab.component';
|
||||||
import { ScheduleTabComponent } from './schedule-tab.component';
|
import { ScheduleTabComponent } from './schedule-tab.component';
|
||||||
import { MaterialsTabComponent } from './materials-tab.component';
|
import { MaterialsTabComponent } from './materials-tab.component';
|
||||||
import { EventsTabComponent } from './events-tab.component';
|
import { EventsTabComponent } from './events-tab.component';
|
||||||
import { RoutinesTabComponent } from './routines-tab.component';
|
import { RoutinesTabComponent } from './routines-tab.component';
|
||||||
import { RewardsTabComponent } from './rewards-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. */
|
/** Panel de padres: barra de pestañas, selector de niño y salida. */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-parents',
|
selector: 'app-parents',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
ChildrenTabComponent,
|
||||||
ScheduleTabComponent,
|
ScheduleTabComponent,
|
||||||
MaterialsTabComponent,
|
MaterialsTabComponent,
|
||||||
EventsTabComponent,
|
EventsTabComponent,
|
||||||
@@ -27,6 +29,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
|
|||||||
template: `
|
template: `
|
||||||
<header class="ptop">
|
<header class="ptop">
|
||||||
<div class="ptabs">
|
<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() === '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() === '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>
|
<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>
|
</header>
|
||||||
|
|
||||||
<main class="pbody">
|
<main class="pbody">
|
||||||
@if (tab() !== 'materials') {
|
@if (tab() !== 'materials' && tab() !== 'children') {
|
||||||
<div class="pchild">
|
<div class="pchild">
|
||||||
<label class="adm-label">{{ i18n.t('child') }}:</label>
|
<label class="adm-label">{{ i18n.t('child') }}:</label>
|
||||||
<select class="adm-input" [(ngModel)]="selectedId">
|
<select class="adm-input" [(ngModel)]="selectedId">
|
||||||
@@ -49,6 +52,7 @@ type Tab = 'schedule' | 'materials' | 'events' | 'routines' | 'rewards';
|
|||||||
}
|
}
|
||||||
|
|
||||||
@switch (tab()) {
|
@switch (tab()) {
|
||||||
|
@case ('children') { <app-children-tab (changed)="reloadChildren()" /> }
|
||||||
@case ('schedule') { <app-schedule-tab [childId]="selectedId" /> }
|
@case ('schedule') { <app-schedule-tab [childId]="selectedId" /> }
|
||||||
@case ('materials') { <app-materials-tab /> }
|
@case ('materials') { <app-materials-tab /> }
|
||||||
@case ('events') { <app-events-tab [childId]="selectedId" /> }
|
@case ('events') { <app-events-tab [childId]="selectedId" /> }
|
||||||
@@ -95,14 +99,20 @@ export class ParentsComponent {
|
|||||||
protected readonly i18n = inject(I18nService);
|
protected readonly i18n = inject(I18nService);
|
||||||
|
|
||||||
protected readonly children = signal<ChildSummary[]>([]);
|
protected readonly children = signal<ChildSummary[]>([]);
|
||||||
protected readonly tab = signal<Tab>('schedule');
|
protected readonly tab = signal<Tab>('children');
|
||||||
protected selectedId = 0;
|
protected selectedId = 0;
|
||||||
|
|
||||||
constructor() {
|
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.parentApi.listChildren().subscribe((list) => {
|
||||||
this.children.set(list);
|
this.children.set(list);
|
||||||
if (list.length && !this.selectedId) {
|
const stillExists = list.some((c) => c.id === this.selectedId);
|
||||||
this.selectedId = list[0].id;
|
if (!stillExists) {
|
||||||
|
this.selectedId = list.length ? list[0].id : 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user