App web familiar de rutinas visuales para niños con TDAH: muestra cada día el material del cole y las rutinas de tarde, con gamificación por monedas y tienda de recompensas. Multi-niño y bilingüe ES/CA. Uso doméstico/homelab. Backend (Spring Boot 3.5 / Java 21 / Gradle): - Dominio por capas, PostgreSQL + Liquibase, datos semilla. - API REST con DTOs: /today, toggle con monedas y bonos de bloque/día, monedero, tienda/canje, ajustes y CRUD del panel de padres. - Seguridad ligera por PIN (BCrypt + sesion en memoria), sin Keycloak. - Tests JUnit: generacion del dia, monedas/bonos con reversion, canje, seguridad. Frontend (Angular 19, standalone + signals): - Perfiles, Home (Tablero y Foco), Tienda y panel de padres (5 pestañas). - Tipografia OpenDyslexic conmutable (accesibilidad), i18n ES/CA, TTS y sonido. - Tokens de diseño fieles al handoff (paleta, animaciones, monedas voladoras). Empaquetado: - Docker multi-stage + docker-compose (PostgreSQL + backend + Nginx). - Decisiones de arquitectura documentadas en docs/adr.
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import { Component, Input, inject, signal } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { ParentApiService } from '../../core/parent-api.service';
|
|
import { I18nService } from '../../core/i18n.service';
|
|
import { EventAdminView } from '../../core/models';
|
|
|
|
/** Pestaña Eventos: exámenes y deberes con fecha, por niño. */
|
|
@Component({
|
|
selector: 'app-events-tab',
|
|
imports: [FormsModule],
|
|
template: `
|
|
<div class="adm-card">
|
|
<p class="adm-label">Nuevo evento</p>
|
|
<div class="adm-row">
|
|
<select class="adm-input" [(ngModel)]="type">
|
|
<option value="EXAM">📋 {{ i18n.t('exam') }}</option>
|
|
<option value="HOMEWORK">📎 {{ i18n.t('homework') }}</option>
|
|
</select>
|
|
<input class="adm-input" type="date" [(ngModel)]="date" />
|
|
<input class="adm-input" [(ngModel)]="titleEs" placeholder="Título (ES)" />
|
|
<input class="adm-input" [(ngModel)]="titleCa" placeholder="Títol (CA)" />
|
|
<button class="adm-btn" [disabled]="!date || !titleEs || !titleCa" (click)="add()">+ {{ i18n.t('add') }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="adm-card">
|
|
<div class="adm-list">
|
|
@for (e of events(); track e.id) {
|
|
<div class="adm-item">
|
|
<span>{{ e.type === 'EXAM' ? '📋' : '📎' }}</span>
|
|
<span class="adm-item__grow">
|
|
<strong>{{ i18n.label(e.titleEs, e.titleCa) }}</strong> · {{ e.date }}
|
|
</span>
|
|
<button class="adm-del" (click)="remove(e)">✕</button>
|
|
</div>
|
|
} @empty {
|
|
<p class="adm-empty">{{ i18n.t('none') }}</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
`,
|
|
})
|
|
export class EventsTabComponent {
|
|
@Input({ required: true }) set childId(value: number) {
|
|
this._childId = value;
|
|
this.reload();
|
|
}
|
|
private _childId!: number;
|
|
|
|
private readonly api = inject(ParentApiService);
|
|
protected readonly i18n = inject(I18nService);
|
|
|
|
protected readonly events = signal<EventAdminView[]>([]);
|
|
protected type: 'EXAM' | 'HOMEWORK' = 'EXAM';
|
|
protected date = '';
|
|
protected titleEs = '';
|
|
protected titleCa = '';
|
|
|
|
private reload(): void {
|
|
this.api.listEvents(this._childId).subscribe((l) => this.events.set(l));
|
|
}
|
|
|
|
add(): void {
|
|
const icon = this.type === 'EXAM' ? '📋' : '📎';
|
|
const color = this.type === 'EXAM' ? '#EC8FA4' : '#5B8DEF';
|
|
this.api
|
|
.createEvent({
|
|
childId: this._childId,
|
|
date: this.date,
|
|
type: this.type,
|
|
titleEs: this.titleEs,
|
|
titleCa: this.titleCa,
|
|
icon,
|
|
color,
|
|
})
|
|
.subscribe(() => {
|
|
this.titleEs = this.titleCa = this.date = '';
|
|
this.reload();
|
|
});
|
|
}
|
|
|
|
remove(e: EventAdminView): void {
|
|
this.api.deleteEvent(e.id).subscribe(() => this.reload());
|
|
}
|
|
}
|