import { Component, OnInit, ViewChild, computed, inject, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ApiService } from '../../core/api.service'; import { FontPreferenceService } from '../../core/font-preference.service'; import { I18nService } from '../../core/i18n.service'; import { SoundService } from '../../core/sound.service'; import { TodayResponse, ViewMode } from '../../core/models'; import { BoardViewComponent } from './board-view.component'; import { FocusViewComponent } from './focus-view.component'; import { WalletComponent } from './wallet.component'; import { MorningTimerComponent } from './morning-timer.component'; import { ProgressBarComponent } from './progress-bar.component'; import { EventBannerComponent } from './event-banner.component'; import { CelebrationOverlayComponent } from './celebration-overlay.component'; import { ToggleEvent } from './task-card.component'; /** Moneda voladora: parte del check de la tarea y vuela al monedero. */ interface FlyingCoin { id: number; x: number; y: number; dx: number; dy: number; flying: boolean; } /** * Pantalla "HOY". Orquesta la carga del día, el conmutado Tablero/Foco (persistido), * el marcado de tareas con su feedback (monedas voladoras, rebote del monedero, * sonido) y la celebración al completar el día. */ @Component({ selector: 'app-home', imports: [ BoardViewComponent, FocusViewComponent, WalletComponent, MorningTimerComponent, ProgressBarComponent, EventBannerComponent, CelebrationOverlayComponent, ], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) export class HomeComponent implements OnInit { private readonly api = inject(ApiService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly sound = inject(SoundService); private readonly fontPreference = inject(FontPreferenceService); protected readonly i18n = inject(I18nService); @ViewChild('wallet') private wallet?: WalletComponent; protected readonly today = signal(null); protected readonly loading = signal(true); protected readonly mode = signal('BOARD'); protected readonly coins = signal([]); protected readonly celebrating = signal(false); protected readonly lastEarned = signal(0); /** true mientras se muestra la vista previa (solo lectura) de mañana. */ protected readonly viewingTomorrow = signal(false); private childId!: number; private coinSeq = 0; /** Todas las tareas en orden (mañana y luego tarde) para el modo Foco. */ protected readonly allTasks = computed(() => { const t = this.today(); return t ? [...t.morning, ...t.afternoon] : []; }); /** Fecha mostrada (hoy o mañana, según la vista) formateada en el idioma activo. */ protected readonly dateLabel = computed(() => { const locale = this.i18n.lang() === 'CA' ? 'ca-ES' : 'es-ES'; const date = new Date(); if (this.viewingTomorrow()) { date.setDate(date.getDate() + 1); } return new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', }).format(date); }); ngOnInit(): void { this.childId = Number(this.route.snapshot.paramMap.get('childId')); this.load(); } private load(): void { this.api.getToday(this.childId).subscribe({ next: (data) => { this.today.set(data); this.mode.set(data.child.viewMode); this.i18n.setLang(data.child.language); // Aplica la preferencia de tipografía de ESTE niño. this.fontPreference.apply(data.child.dyslexiaFont); this.loading.set(false); }, error: () => this.loading.set(false), }); } /** Cambia entre Tablero y Foco y persiste la preferencia del niño. */ setMode(mode: ViewMode): void { if (mode === this.mode()) { return; } this.mode.set(mode); this.api.updateSettings(this.childId, { viewMode: mode }).subscribe(); } /** Alterna entre hoy y la vista previa (solo lectura) de mañana. */ toggleDay(): void { if (this.viewingTomorrow()) { this.viewingTomorrow.set(false); this.load(); return; } this.api.getTomorrow(this.childId).subscribe((data) => { this.today.set(data); this.viewingTomorrow.set(true); }); } /** Marca/desmarca una tarea y aplica el feedback (monedas, sonido, celebración). */ onToggle(ev: ToggleEvent): void { // La vista de mañana es solo lectura: mirar, no marcar. if (this.viewingTomorrow()) { return; } this.api.toggleTask(ev.taskId).subscribe((result) => { const current = this.today(); if (!current) { return; } // Actualiza estado del día de forma inmutable. const apply = (list: typeof current.morning) => list.map((task) => (task.id === ev.taskId ? { ...task, done: result.done } : task)); const updated: TodayResponse = { ...current, morning: apply(current.morning), afternoon: apply(current.afternoon), progress: result.progress, wallet: { coins: result.newBalance }, }; this.today.set(updated); // Feedback positivo solo al ganar monedas (marcar, no desmarcar). Sonido y // vibración se disparan juntos sobre el mismo evento (armonía multimodal). if (result.coinsEarned > 0) { this.flyCoins(ev.x, ev.y, result.coinsEarned); this.wallet?.bump(); if (current.child.soundEnabled) { this.sound.playReward(); this.sound.buzz(); } } // Celebración al completar TODO el día. if (result.progress.total > 0 && result.progress.totalDone === result.progress.total) { this.lastEarned.set(result.coinsEarned); this.celebrating.set(true); } }); } /** Lanza monedas desde (x,y) hacia el monedero. */ private flyCoins(x: number, y: number, earned: number): void { const rect = this.wallet?.getRect(); if (!rect) { return; } const targetX = rect.left + rect.width / 2; const targetY = rect.top + rect.height / 2; const n = Math.min(Math.max(1, Math.round(earned / 5)), 6); const nuevas: FlyingCoin[] = []; for (let k = 0; k < n; k++) { const jitterX = (k - n / 2) * 14; const startX = x + jitterX; const startY = y; nuevas.push({ id: this.coinSeq++, x: startX, y: startY, dx: targetX - startX, dy: targetY - startY, flying: false, }); } this.coins.update((c) => [...c, ...nuevas]); // En el siguiente frame, activa la transición (vuelo). requestAnimationFrame(() => requestAnimationFrame(() => this.coins.update((c) => c.map((coin) => (nuevas.some((nc) => nc.id === coin.id) ? { ...coin, flying: true } : coin)), ), ), ); // Retira las monedas tras la animación. const ids = new Set(nuevas.map((c) => c.id)); setTimeout(() => this.coins.update((c) => c.filter((coin) => !ids.has(coin.id))), 800); } goProfiles(): void { this.router.navigate(['/']); } goStore(): void { this.router.navigate(['/store', this.childId]); } }