feat: app completa recordaLexia (fases 1-5)
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.
This commit is contained in:
151
frontend/src/app/features/parents/materials-tab.component.ts
Normal file
151
frontend/src/app/features/parents/materials-tab.component.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ParentApiService } from '../../core/parent-api.service';
|
||||
import { I18nService } from '../../core/i18n.service';
|
||||
import { ActivityView, MaterialView } from '../../core/models';
|
||||
|
||||
/** Pestaña Materiales: catálogo de materiales y actividades (con su material). */
|
||||
@Component({
|
||||
selector: 'app-materials-tab',
|
||||
imports: [FormsModule],
|
||||
template: `
|
||||
<!-- Alta de material -->
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Nuevo material</p>
|
||||
<div class="adm-row">
|
||||
<input class="adm-input adm-input--sm" [(ngModel)]="mIcon" placeholder="🎒" maxlength="4" />
|
||||
<input class="adm-input" [(ngModel)]="mEs" placeholder="Nombre (ES)" />
|
||||
<input class="adm-input" [(ngModel)]="mCa" placeholder="Nom (CA)" />
|
||||
<input class="adm-input adm-input--sm" type="color" [(ngModel)]="mColor" />
|
||||
<button class="adm-btn" [disabled]="!mEs || !mCa || !mIcon" (click)="addMaterial()">+ {{ i18n.t('add') }}</button>
|
||||
</div>
|
||||
<div class="adm-list" style="margin-top:12px">
|
||||
@for (m of materials(); track m.id) {
|
||||
<div class="adm-item">
|
||||
<span>{{ m.icon }}</span>
|
||||
<span class="adm-item__grow">{{ i18n.label(m.labelEs, m.labelCa) }}</span>
|
||||
<button class="adm-del" (click)="delMaterial(m)">✕</button>
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="adm-empty">{{ i18n.t('none') }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alta de actividad con su material -->
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Nueva actividad</p>
|
||||
<div class="adm-row">
|
||||
<input class="adm-input adm-input--sm" [(ngModel)]="aIcon" placeholder="🤸" maxlength="4" />
|
||||
<input class="adm-input" [(ngModel)]="aEs" placeholder="Actividad (ES)" />
|
||||
<input class="adm-input" [(ngModel)]="aCa" placeholder="Activitat (CA)" />
|
||||
<input class="adm-input adm-input--sm" type="color" [(ngModel)]="aColor" />
|
||||
</div>
|
||||
<p class="adm-label" style="margin-top:12px">Material que conlleva:</p>
|
||||
<div class="adm-row">
|
||||
@for (m of materials(); track m.id) {
|
||||
<label class="adm-chip">
|
||||
<input type="checkbox" [checked]="selectedMaterials.has(m.id)" (change)="toggleMaterial(m.id)" />
|
||||
{{ m.icon }} {{ i18n.label(m.labelEs, m.labelCa) }}
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
<div class="adm-row" style="margin-top:12px">
|
||||
<button class="adm-btn" [disabled]="!aEs || !aCa || !aIcon" (click)="addActivity()">
|
||||
+ {{ i18n.t('add') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actividades existentes -->
|
||||
<div class="adm-card">
|
||||
<p class="adm-label">Actividades</p>
|
||||
<div class="adm-list">
|
||||
@for (a of activities(); track a.id) {
|
||||
<div class="adm-item">
|
||||
<span>{{ a.icon }}</span>
|
||||
<span class="adm-item__grow">
|
||||
<strong>{{ i18n.label(a.labelEs, a.labelCa) }}</strong>
|
||||
— {{ materialNames(a) }}
|
||||
</span>
|
||||
<button class="adm-del" (click)="delActivity(a)">✕</button>
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="adm-empty">{{ i18n.t('none') }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class MaterialsTabComponent {
|
||||
private readonly api = inject(ParentApiService);
|
||||
protected readonly i18n = inject(I18nService);
|
||||
|
||||
protected readonly materials = signal<MaterialView[]>([]);
|
||||
protected readonly activities = signal<ActivityView[]>([]);
|
||||
|
||||
protected mIcon = '';
|
||||
protected mEs = '';
|
||||
protected mCa = '';
|
||||
protected mColor = '#5b8def';
|
||||
protected aIcon = '';
|
||||
protected aEs = '';
|
||||
protected aCa = '';
|
||||
protected aColor = '#7fbf6b';
|
||||
protected selectedMaterials = new Set<number>();
|
||||
|
||||
constructor() {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
private reload(): void {
|
||||
this.api.listMaterials().subscribe((l) => this.materials.set(l));
|
||||
this.api.listActivities().subscribe((l) => this.activities.set(l));
|
||||
}
|
||||
|
||||
materialNames(a: ActivityView): string {
|
||||
const byId = new Map(this.materials().map((m) => [m.id, this.i18n.label(m.labelEs, m.labelCa)]));
|
||||
return a.materialIds.map((id) => byId.get(id) ?? '·').join(', ') || '—';
|
||||
}
|
||||
|
||||
toggleMaterial(id: number): void {
|
||||
if (this.selectedMaterials.has(id)) {
|
||||
this.selectedMaterials.delete(id);
|
||||
} else {
|
||||
this.selectedMaterials.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
addMaterial(): void {
|
||||
this.api
|
||||
.createMaterial({ labelEs: this.mEs, labelCa: this.mCa, icon: this.mIcon, color: this.mColor })
|
||||
.subscribe(() => {
|
||||
this.mEs = this.mCa = this.mIcon = '';
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
delMaterial(m: MaterialView): void {
|
||||
this.api.deleteMaterial(m.id).subscribe(() => this.reload());
|
||||
}
|
||||
|
||||
addActivity(): void {
|
||||
this.api
|
||||
.createActivity({
|
||||
labelEs: this.aEs,
|
||||
labelCa: this.aCa,
|
||||
icon: this.aIcon,
|
||||
color: this.aColor,
|
||||
materialIds: [...this.selectedMaterials],
|
||||
})
|
||||
.subscribe(() => {
|
||||
this.aEs = this.aCa = this.aIcon = '';
|
||||
this.selectedMaterials.clear();
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
delActivity(a: ActivityView): void {
|
||||
this.api.deleteActivity(a.id).subscribe(() => this.reload());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user