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.SpecialEvent;
|
||||
import es.asepeyo.recordalexia.exception.NotFoundException;
|
||||
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
||||
import es.asepeyo.recordalexia.repository.ChildRepository;
|
||||
import es.asepeyo.recordalexia.repository.SpecialEventRepository;
|
||||
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
||||
import es.asepeyo.recordalexia.security.FamilyContext;
|
||||
import es.asepeyo.recordalexia.web.dto.TodayResponse;
|
||||
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.WalletInfo;
|
||||
import java.time.Clock;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -32,17 +36,23 @@ public class TodayService {
|
||||
private final DayGenerationService dayGenerationService;
|
||||
private final ChildRepository childRepository;
|
||||
private final SpecialEventRepository specialEventRepository;
|
||||
private final WeeklyTemplateEntryRepository templateRepository;
|
||||
private final AfternoonRoutineRepository routineRepository;
|
||||
private final FamilyContext familyContext;
|
||||
private final Clock clock;
|
||||
|
||||
public TodayService(DayGenerationService dayGenerationService,
|
||||
ChildRepository childRepository,
|
||||
SpecialEventRepository specialEventRepository,
|
||||
WeeklyTemplateEntryRepository templateRepository,
|
||||
AfternoonRoutineRepository routineRepository,
|
||||
FamilyContext familyContext,
|
||||
Clock clock) {
|
||||
this.dayGenerationService = dayGenerationService;
|
||||
this.childRepository = childRepository;
|
||||
this.specialEventRepository = specialEventRepository;
|
||||
this.templateRepository = templateRepository;
|
||||
this.routineRepository = routineRepository;
|
||||
this.familyContext = familyContext;
|
||||
this.clock = clock;
|
||||
}
|
||||
@@ -79,6 +89,64 @@ public class TodayService {
|
||||
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) {
|
||||
return new TaskView(t.getId(), t.getLabelEs(), t.getLabelCa(), t.getIcon(), t.getColor(),
|
||||
t.isDone(), t.getCoinsReward(), t.getOrderIndex());
|
||||
|
||||
@@ -46,6 +46,12 @@ public class ChildController {
|
||||
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")
|
||||
public WalletResponse wallet(@PathVariable Long 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.AfternoonRoutineRepository;
|
||||
import es.asepeyo.recordalexia.repository.ChildRepository;
|
||||
import es.asepeyo.recordalexia.repository.DailyTaskRepository;
|
||||
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||
import es.asepeyo.recordalexia.repository.FamilySessionRepository;
|
||||
import es.asepeyo.recordalexia.repository.RewardRepository;
|
||||
@@ -52,6 +53,7 @@ class TodayFlowIT {
|
||||
@Autowired private RewardRepository rewardRepository;
|
||||
@Autowired private FamilyRepository familyRepository;
|
||||
@Autowired private FamilySessionRepository sessionRepository;
|
||||
@Autowired private DailyTaskRepository dailyTaskRepository;
|
||||
|
||||
@Test
|
||||
void flujoVerDiaMarcarTareaYcanjear() throws Exception {
|
||||
@@ -99,6 +101,39 @@ class TodayFlowIT {
|
||||
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) {
|
||||
Child c = new Child();
|
||||
c.setFamily(family);
|
||||
|
||||
Reference in New Issue
Block a user