feat(hoy): vista previa de mañana en el kiosko del niño
Botón "Mañana ›" en la cabecera: muestra el día siguiente en solo lectura para preparar la mochila la noche antes. La vista se calcula al vuelo desde las plantillas SIN materializar DailyTask (los cambios del panel de esa noche siguen entrando al generarse el día de verdad), con ids sintéticos negativos no accionables, sin temporizador ni progreso. Banner "Así será mañana" y estados vacíos propios. Test de integración: la vista previa no persiste tareas del día siguiente.
This commit is contained in:
@@ -5,8 +5,10 @@ import es.asepeyo.recordalexia.domain.DailyTask;
|
|||||||
import es.asepeyo.recordalexia.domain.Slot;
|
import es.asepeyo.recordalexia.domain.Slot;
|
||||||
import es.asepeyo.recordalexia.domain.SpecialEvent;
|
import es.asepeyo.recordalexia.domain.SpecialEvent;
|
||||||
import es.asepeyo.recordalexia.exception.NotFoundException;
|
import es.asepeyo.recordalexia.exception.NotFoundException;
|
||||||
|
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
||||||
import es.asepeyo.recordalexia.repository.ChildRepository;
|
import es.asepeyo.recordalexia.repository.ChildRepository;
|
||||||
import es.asepeyo.recordalexia.repository.SpecialEventRepository;
|
import es.asepeyo.recordalexia.repository.SpecialEventRepository;
|
||||||
|
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
||||||
import es.asepeyo.recordalexia.security.FamilyContext;
|
import es.asepeyo.recordalexia.security.FamilyContext;
|
||||||
import es.asepeyo.recordalexia.web.dto.TodayResponse;
|
import es.asepeyo.recordalexia.web.dto.TodayResponse;
|
||||||
import es.asepeyo.recordalexia.web.dto.TodayResponse.ChildInfo;
|
import es.asepeyo.recordalexia.web.dto.TodayResponse.ChildInfo;
|
||||||
@@ -15,11 +17,13 @@ import es.asepeyo.recordalexia.web.dto.TodayResponse.TaskView;
|
|||||||
import es.asepeyo.recordalexia.web.dto.TodayResponse.TimerInfo;
|
import es.asepeyo.recordalexia.web.dto.TodayResponse.TimerInfo;
|
||||||
import es.asepeyo.recordalexia.web.dto.TodayResponse.WalletInfo;
|
import es.asepeyo.recordalexia.web.dto.TodayResponse.WalletInfo;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.time.DayOfWeek;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -32,17 +36,23 @@ public class TodayService {
|
|||||||
private final DayGenerationService dayGenerationService;
|
private final DayGenerationService dayGenerationService;
|
||||||
private final ChildRepository childRepository;
|
private final ChildRepository childRepository;
|
||||||
private final SpecialEventRepository specialEventRepository;
|
private final SpecialEventRepository specialEventRepository;
|
||||||
|
private final WeeklyTemplateEntryRepository templateRepository;
|
||||||
|
private final AfternoonRoutineRepository routineRepository;
|
||||||
private final FamilyContext familyContext;
|
private final FamilyContext familyContext;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
public TodayService(DayGenerationService dayGenerationService,
|
public TodayService(DayGenerationService dayGenerationService,
|
||||||
ChildRepository childRepository,
|
ChildRepository childRepository,
|
||||||
SpecialEventRepository specialEventRepository,
|
SpecialEventRepository specialEventRepository,
|
||||||
|
WeeklyTemplateEntryRepository templateRepository,
|
||||||
|
AfternoonRoutineRepository routineRepository,
|
||||||
FamilyContext familyContext,
|
FamilyContext familyContext,
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
this.dayGenerationService = dayGenerationService;
|
this.dayGenerationService = dayGenerationService;
|
||||||
this.childRepository = childRepository;
|
this.childRepository = childRepository;
|
||||||
this.specialEventRepository = specialEventRepository;
|
this.specialEventRepository = specialEventRepository;
|
||||||
|
this.templateRepository = templateRepository;
|
||||||
|
this.routineRepository = routineRepository;
|
||||||
this.familyContext = familyContext;
|
this.familyContext = familyContext;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
@@ -79,6 +89,64 @@ public class TodayService {
|
|||||||
buildTimer(child.getDepartureTime(), LocalTime.now(clock), morning.isEmpty()));
|
buildTimer(child.getDepartureTime(), LocalTime.now(clock), morning.isEmpty()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vista previa de MAÑANA, en solo lectura. Se calcula al vuelo desde las
|
||||||
|
* plantillas (horario semanal y rutinas) SIN materializar DailyTask: así los
|
||||||
|
* cambios que los padres hagan esta noche en el panel siguen entrando cuando
|
||||||
|
* el día se genere de verdad. Los ids son sintéticos y negativos para que
|
||||||
|
* nunca sean accionables (el toggle con id negativo no existe). Sin
|
||||||
|
* temporizador y con progreso a cero: nada que marcar todavía.
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public TodayResponse getTomorrow(Long childId) {
|
||||||
|
Child child = childRepository.findByIdAndFamilyId(childId, familyContext.currentFamilyId())
|
||||||
|
.orElseThrow(() -> new NotFoundException("No existe el niño con id " + childId));
|
||||||
|
|
||||||
|
LocalDate tomorrow = LocalDate.now(clock).plusDays(1);
|
||||||
|
DayOfWeek dayOfWeek = tomorrow.getDayOfWeek();
|
||||||
|
AtomicLong syntheticId = new AtomicLong(-1);
|
||||||
|
|
||||||
|
List<TaskView> morning = templateRepository
|
||||||
|
.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek).stream()
|
||||||
|
.map(entry -> {
|
||||||
|
var actividad = entry.getActivity();
|
||||||
|
int monedas = entry.getCoinsReward() != null ? entry.getCoinsReward()
|
||||||
|
: child.getCoinsPerTask();
|
||||||
|
return new TaskView(syntheticId.getAndDecrement(), actividad.getLabelEs(),
|
||||||
|
actividad.getLabelCa(), actividad.getIcon(), actividad.getColor(),
|
||||||
|
false, monedas, entry.getOrderIndex());
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<TaskView> afternoon = routineRepository
|
||||||
|
.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek).stream()
|
||||||
|
.map(rutina -> {
|
||||||
|
var tarea = rutina.getRoutineTask();
|
||||||
|
int monedas = rutina.getCoinsReward() != null ? rutina.getCoinsReward()
|
||||||
|
: tarea.getCoinsReward() != null ? tarea.getCoinsReward()
|
||||||
|
: child.getCoinsPerTask();
|
||||||
|
return new TaskView(syntheticId.getAndDecrement(), tarea.getLabelEs(),
|
||||||
|
tarea.getLabelCa(), tarea.getIcon(), tarea.getColor(),
|
||||||
|
false, monedas, rutina.getOrderIndex());
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<EventView> events = specialEventRepository
|
||||||
|
.findByChildIdAndEventDate(childId, tomorrow).stream()
|
||||||
|
.map(this::toEventView)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int total = morning.size() + afternoon.size();
|
||||||
|
return new TodayResponse(
|
||||||
|
toChildInfo(child),
|
||||||
|
morning,
|
||||||
|
afternoon,
|
||||||
|
events,
|
||||||
|
new TodayResponse.ProgressView(0, morning.size(), 0, afternoon.size(), 0, total),
|
||||||
|
new WalletInfo(child.getCoins()),
|
||||||
|
new TimerInfo(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
private TaskView toTaskView(DailyTask t) {
|
private TaskView toTaskView(DailyTask t) {
|
||||||
return new TaskView(t.getId(), t.getLabelEs(), t.getLabelCa(), t.getIcon(), t.getColor(),
|
return new TaskView(t.getId(), t.getLabelEs(), t.getLabelCa(), t.getIcon(), t.getColor(),
|
||||||
t.isDone(), t.getCoinsReward(), t.getOrderIndex());
|
t.isDone(), t.getCoinsReward(), t.getOrderIndex());
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ public class ChildController {
|
|||||||
return todayService.getToday(id);
|
return todayService.getToday(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Vista previa de mañana (solo lectura): para preparar la mochila la noche antes. */
|
||||||
|
@GetMapping("/{id}/tomorrow")
|
||||||
|
public TodayResponse tomorrow(@PathVariable Long id) {
|
||||||
|
return todayService.getTomorrow(id);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/wallet")
|
@GetMapping("/{id}/wallet")
|
||||||
public WalletResponse wallet(@PathVariable Long id) {
|
public WalletResponse wallet(@PathVariable Long id) {
|
||||||
return walletService.getWallet(id);
|
return walletService.getWallet(id);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
|||||||
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
||||||
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
||||||
import es.asepeyo.recordalexia.repository.ChildRepository;
|
import es.asepeyo.recordalexia.repository.ChildRepository;
|
||||||
|
import es.asepeyo.recordalexia.repository.DailyTaskRepository;
|
||||||
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||||
import es.asepeyo.recordalexia.repository.FamilySessionRepository;
|
import es.asepeyo.recordalexia.repository.FamilySessionRepository;
|
||||||
import es.asepeyo.recordalexia.repository.RewardRepository;
|
import es.asepeyo.recordalexia.repository.RewardRepository;
|
||||||
@@ -52,6 +53,7 @@ class TodayFlowIT {
|
|||||||
@Autowired private RewardRepository rewardRepository;
|
@Autowired private RewardRepository rewardRepository;
|
||||||
@Autowired private FamilyRepository familyRepository;
|
@Autowired private FamilyRepository familyRepository;
|
||||||
@Autowired private FamilySessionRepository sessionRepository;
|
@Autowired private FamilySessionRepository sessionRepository;
|
||||||
|
@Autowired private DailyTaskRepository dailyTaskRepository;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void flujoVerDiaMarcarTareaYcanjear() throws Exception {
|
void flujoVerDiaMarcarTareaYcanjear() throws Exception {
|
||||||
@@ -99,6 +101,39 @@ class TodayFlowIT {
|
|||||||
assertThat(childRepository.findById(nino.getId()).orElseThrow().getCoins()).isEqualTo(60);
|
assertThat(childRepository.findById(nino.getId()).orElseThrow().getCoins()).isEqualTo(60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void laVistaDeMananaEsSoloLecturaYnoMaterializaTareas() throws Exception {
|
||||||
|
Family family = familyRepository.save(new Family("tmrw@x.com", "h", "Tmrw", "p"));
|
||||||
|
String session = "tomorrow-session";
|
||||||
|
sessionRepository.save(new FamilySession(session, family, Instant.now().plus(1, ChronoUnit.DAYS)));
|
||||||
|
|
||||||
|
LocalDate manana = LocalDate.now().plusDays(1);
|
||||||
|
Child nino = childRepository.save(nuevoNino(family));
|
||||||
|
Activity lengua = new Activity("Lengua", "Llengua", "📖", "#F2A65A");
|
||||||
|
lengua.setFamily(family);
|
||||||
|
lengua = activityRepository.save(lengua);
|
||||||
|
templateRepository.save(new WeeklyTemplateEntry(nino, manana.getDayOfWeek(), lengua, 0));
|
||||||
|
RoutineTask merendar = new RoutineTask("Merendar", "Berenar", "🥪", "#F4C95D");
|
||||||
|
merendar.setFamily(family);
|
||||||
|
merendar = routineTaskRepository.save(merendar);
|
||||||
|
routineRepository.save(new AfternoonRoutine(nino, manana.getDayOfWeek(), merendar, 0));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/children/{id}/tomorrow", nino.getId())
|
||||||
|
.header(SessionAuthFilter.HEADER, session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.morning.length()").value(1))
|
||||||
|
.andExpect(jsonPath("$.morning[0].labelEs").value("Lengua"))
|
||||||
|
.andExpect(jsonPath("$.morning[0].done").value(false))
|
||||||
|
// Ids sintéticos negativos: la vista previa no es accionable.
|
||||||
|
.andExpect(jsonPath("$.morning[0].id").value(-1))
|
||||||
|
.andExpect(jsonPath("$.afternoon.length()").value(1))
|
||||||
|
.andExpect(jsonPath("$.timer.minutesUntilDeparture").doesNotExist());
|
||||||
|
|
||||||
|
// Lo esencial: mirar mañana NO genera las tareas de mañana (el padre aún
|
||||||
|
// puede cambiar el plan esta noche y entrará al generarse de verdad).
|
||||||
|
assertThat(dailyTaskRepository.existsByChildIdAndTaskDate(nino.getId(), manana)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
private Child nuevoNino(Family family) {
|
private Child nuevoNino(Family family) {
|
||||||
Child c = new Child();
|
Child c = new Child();
|
||||||
c.setFamily(family);
|
c.setFamily(family);
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export class ApiService {
|
|||||||
return this.http.get<TodayResponse>(`${this.base}/children/${childId}/today`);
|
return this.http.get<TodayResponse>(`${this.base}/children/${childId}/today`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Vista previa de MAÑANA (solo lectura): preparar la mochila la noche antes. */
|
||||||
|
getTomorrow(childId: number): Observable<TodayResponse> {
|
||||||
|
return this.http.get<TodayResponse>(`${this.base}/children/${childId}/tomorrow`);
|
||||||
|
}
|
||||||
|
|
||||||
/** Marca/desmarca una tarea y devuelve el saldo y progreso actualizados. */
|
/** Marca/desmarca una tarea y devuelve el saldo y progreso actualizados. */
|
||||||
toggleTask(taskId: number): Observable<ToggleResult> {
|
toggleTask(taskId: number): Observable<ToggleResult> {
|
||||||
return this.http.post<ToggleResult>(`${this.base}/tasks/${taskId}/toggle`, {});
|
return this.http.post<ToggleResult>(`${this.base}/tasks/${taskId}/toggle`, {});
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export class I18nService {
|
|||||||
noSchool: { ES: 'HOY NO HAY COLE', CA: 'AVUI NO HI HA COLE' },
|
noSchool: { ES: 'HOY NO HAY COLE', CA: 'AVUI NO HI HA COLE' },
|
||||||
freeAfternoon: { ES: 'TARDE LIBRE', CA: 'TARDA LLIURE' },
|
freeAfternoon: { ES: 'TARDE LIBRE', CA: 'TARDA LLIURE' },
|
||||||
emptyDayTitle: { ES: '¡Hoy no hay tareas!', CA: 'Avui no hi ha tasques!' },
|
emptyDayTitle: { ES: '¡Hoy no hay tareas!', CA: 'Avui no hi ha tasques!' },
|
||||||
|
emptyTomorrowTitle: { ES: '¡Mañana no hay tareas!', CA: 'Demà no hi ha tasques!' },
|
||||||
emptyDaySub: { ES: 'Disfruta del día', CA: 'Gaudeix del dia' },
|
emptyDaySub: { ES: 'Disfruta del día', CA: 'Gaudeix del dia' },
|
||||||
|
today: { ES: 'Hoy', CA: 'Avui' },
|
||||||
|
tomorrow: { ES: 'Mañana', CA: 'Demà' },
|
||||||
|
tomorrowBanner: { ES: 'Así será mañana', CA: 'Així serà demà' },
|
||||||
allDone: { ES: '¡TODO LISTO!', CA: 'TOT FET!' },
|
allDone: { ES: '¡TODO LISTO!', CA: 'TOT FET!' },
|
||||||
great: { ES: '¡GENIAL!', CA: 'GENIAL!' },
|
great: { ES: '¡GENIAL!', CA: 'GENIAL!' },
|
||||||
done: { ES: '¡HECHO!', CA: 'FET!' },
|
done: { ES: '¡HECHO!', CA: 'FET!' },
|
||||||
|
|||||||
@@ -11,31 +11,47 @@
|
|||||||
|
|
||||||
<app-morning-timer [minutes]="t.timer.minutesUntilDeparture" />
|
<app-morning-timer [minutes]="t.timer.minutesUntilDeparture" />
|
||||||
|
|
||||||
|
<!-- Ver mañana / volver a hoy (la vista de mañana es solo lectura) -->
|
||||||
|
<button type="button" class="daybtn" (click)="toggleDay()">
|
||||||
|
@if (viewingTomorrow()) {
|
||||||
|
‹ {{ i18n.t('today') }}
|
||||||
|
} @else {
|
||||||
|
{{ i18n.t('tomorrow') }} ›
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="home__wallet">
|
<div class="home__wallet">
|
||||||
<button type="button" class="iconbtn" (click)="goStore()" aria-label="Tienda">🎁</button>
|
<button type="button" class="iconbtn" (click)="goStore()" aria-label="Tienda">🎁</button>
|
||||||
<app-wallet #wallet [coins]="t.wallet.coins" />
|
<app-wallet #wallet [coins]="t.wallet.coins" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Aviso de vista previa: que el niño sepa que está mirando mañana -->
|
||||||
|
@if (viewingTomorrow()) {
|
||||||
|
<div class="tomorrow">👀 {{ i18n.t('tomorrowBanner') }}</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Eventos del día -->
|
<!-- Eventos del día -->
|
||||||
<app-event-banner [events]="t.specialEvents" [ttsEnabled]="t.child.ttsEnabled" />
|
<app-event-banner [events]="t.specialEvents" [ttsEnabled]="t.child.ttsEnabled" />
|
||||||
|
|
||||||
@if (t.progress.total > 0) {
|
@if (t.progress.total > 0) {
|
||||||
<!-- Conmutador de modo -->
|
@if (!viewingTomorrow()) {
|
||||||
<div class="modes">
|
<!-- Conmutador de modo -->
|
||||||
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'BOARD'" (click)="setMode('BOARD')">
|
<div class="modes">
|
||||||
🗂️ {{ i18n.t('board') }}
|
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'BOARD'" (click)="setMode('BOARD')">
|
||||||
</button>
|
🗂️ {{ i18n.t('board') }}
|
||||||
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'FOCUS'" (click)="setMode('FOCUS')">
|
</button>
|
||||||
🎯 {{ i18n.t('focus') }}
|
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'FOCUS'" (click)="setMode('FOCUS')">
|
||||||
</button>
|
🎯 {{ i18n.t('focus') }}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progreso global -->
|
<!-- Progreso global -->
|
||||||
<app-progress-bar [done]="t.progress.totalDone" [total]="t.progress.total" />
|
<app-progress-bar [done]="t.progress.totalDone" [total]="t.progress.total" />
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Tareas: tablero o foco -->
|
<!-- Tareas: tablero o foco (la vista de mañana siempre en tablero, solo lectura) -->
|
||||||
@if (mode() === 'BOARD') {
|
@if (mode() === 'BOARD' || viewingTomorrow()) {
|
||||||
<app-board-view
|
<app-board-view
|
||||||
[morning]="t.morning"
|
[morning]="t.morning"
|
||||||
[afternoon]="t.afternoon"
|
[afternoon]="t.afternoon"
|
||||||
@@ -49,7 +65,9 @@
|
|||||||
<!-- Día sin tareas: mensaje amable, nunca una pantalla en blanco. -->
|
<!-- Día sin tareas: mensaje amable, nunca una pantalla en blanco. -->
|
||||||
<div class="freeday">
|
<div class="freeday">
|
||||||
<span class="freeday__mascot">{{ t.child.mascot }}</span>
|
<span class="freeday__mascot">{{ t.child.mascot }}</span>
|
||||||
<p class="freeday__title">{{ i18n.t('emptyDayTitle') }}</p>
|
<p class="freeday__title">
|
||||||
|
{{ i18n.t(viewingTomorrow() ? 'emptyTomorrowTitle' : 'emptyDayTitle') }}
|
||||||
|
</p>
|
||||||
<p class="freeday__sub">{{ i18n.t('emptyDaySub') }} 🎉</p>
|
<p class="freeday__sub">{{ i18n.t('emptyDaySub') }} 🎉</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,36 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Botón "Mañana ›" / "‹ Hoy" de la cabecera.
|
||||||
|
.daybtn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: var(--touch-min);
|
||||||
|
padding: 0 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: var(--shadow-btn);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-1);
|
||||||
|
transition: transform var(--dur-press) var(--ease-out);
|
||||||
|
}
|
||||||
|
.daybtn:active { transform: scale(0.95); }
|
||||||
|
|
||||||
|
// Aviso de vista previa de mañana (solo lectura).
|
||||||
|
.tomorrow {
|
||||||
|
align-self: center;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: color-mix(in srgb, var(--accent-purple) 16%, #fff);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-strong);
|
||||||
|
animation: slideUp 0.3s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
// Conmutador Tablero / Foco.
|
// Conmutador Tablero / Foco.
|
||||||
.modes {
|
.modes {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export class HomeComponent implements OnInit {
|
|||||||
protected readonly coins = signal<FlyingCoin[]>([]);
|
protected readonly coins = signal<FlyingCoin[]>([]);
|
||||||
protected readonly celebrating = signal(false);
|
protected readonly celebrating = signal(false);
|
||||||
protected readonly lastEarned = signal(0);
|
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 childId!: number;
|
||||||
private coinSeq = 0;
|
private coinSeq = 0;
|
||||||
@@ -69,14 +71,18 @@ export class HomeComponent implements OnInit {
|
|||||||
return t ? [...t.morning, ...t.afternoon] : [];
|
return t ? [...t.morning, ...t.afternoon] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Fecha de hoy formateada en el idioma activo. */
|
/** Fecha mostrada (hoy o mañana, según la vista) formateada en el idioma activo. */
|
||||||
protected readonly dateLabel = computed(() => {
|
protected readonly dateLabel = computed(() => {
|
||||||
const locale = this.i18n.lang() === 'CA' ? 'ca-ES' : 'es-ES';
|
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, {
|
return new Intl.DateTimeFormat(locale, {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
}).format(new Date());
|
}).format(date);
|
||||||
});
|
});
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -107,8 +113,25 @@ export class HomeComponent implements OnInit {
|
|||||||
this.api.updateSettings(this.childId, { viewMode: mode }).subscribe();
|
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). */
|
/** Marca/desmarca una tarea y aplica el feedback (monedas, sonido, celebración). */
|
||||||
onToggle(ev: ToggleEvent): void {
|
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) => {
|
this.api.toggleTask(ev.taskId).subscribe((result) => {
|
||||||
const current = this.today();
|
const current = this.today();
|
||||||
if (!current) {
|
if (!current) {
|
||||||
|
|||||||
Reference in New Issue
Block a user