feat(rutinas): API de catálogo y asignación multi-día

CRUD del catálogo en /api/parents/routine-tasks y alta de una rutina
a varios días de una vez en ParentScheduleController. RoutineView
deriva la definición del catálogo e incluye routineTaskId.
This commit is contained in:
Jaume Garriga Maestre
2026-06-22 22:49:51 +02:00
parent a58d7bda78
commit d0f3975c42
4 changed files with 123 additions and 11 deletions

View File

@@ -0,0 +1,85 @@
package es.asepeyo.recordalexia.web;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.exception.NotFoundException;
import es.asepeyo.recordalexia.repository.FamilyRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import es.asepeyo.recordalexia.security.FamilyContext;
import es.asepeyo.recordalexia.web.dto.ParentDtos.RoutineTaskRequest;
import es.asepeyo.recordalexia.web.dto.ParentViews.RoutineTaskView;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* CRUD del catálogo de rutinas de tarde, por familia (panel de padres). Es el gemelo de
* {@link ParentCatalogController} para las actividades de mañana: aquí se define una
* rutina una sola vez y luego se asigna a días desde {@link ParentScheduleController}.
*/
@RestController
@RequestMapping("/api/parents/routine-tasks")
public class ParentRoutineCatalogController {
private final RoutineTaskRepository routineTaskRepository;
private final FamilyRepository familyRepository;
private final FamilyContext familyContext;
public ParentRoutineCatalogController(RoutineTaskRepository routineTaskRepository,
FamilyRepository familyRepository,
FamilyContext familyContext) {
this.routineTaskRepository = routineTaskRepository;
this.familyRepository = familyRepository;
this.familyContext = familyContext;
}
private Long fid() {
return familyContext.currentFamilyId();
}
@GetMapping
public List<RoutineTaskView> list() {
return routineTaskRepository.findByFamilyId(fid()).stream().map(this::toView).toList();
}
@PostMapping
public ResponseEntity<RoutineTaskView> create(@RequestBody RoutineTaskRequest req) {
RoutineTask task = new RoutineTask(req.labelEs(), req.labelCa(), req.icon(), req.color());
task.setFamily(familyRepository.getReferenceById(fid()));
task.setCoinsReward(req.coinsReward());
return ResponseEntity.ok(toView(routineTaskRepository.save(task)));
}
/** Editar la definición se refleja en todos los días que usan esta rutina. */
@PutMapping("/{id}")
public ResponseEntity<RoutineTaskView> update(@PathVariable Long id,
@RequestBody RoutineTaskRequest req) {
RoutineTask task = routineTaskRepository.findByIdAndFamilyId(id, fid())
.orElseThrow(() -> new NotFoundException("No existe la rutina con id " + id));
task.setLabelEs(req.labelEs());
task.setLabelCa(req.labelCa());
task.setIcon(req.icon());
task.setColor(req.color());
task.setCoinsReward(req.coinsReward());
return ResponseEntity.ok(toView(routineTaskRepository.save(task)));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
routineTaskRepository.findByIdAndFamilyId(id, fid())
.orElseThrow(() -> new NotFoundException("No existe la rutina con id " + id));
routineTaskRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
private RoutineTaskView toView(RoutineTask t) {
return new RoutineTaskView(t.getId(), t.getLabelEs(), t.getLabelCa(), t.getIcon(),
t.getColor(), t.getCoinsReward());
}
}

View File

@@ -3,11 +3,13 @@ package es.asepeyo.recordalexia.web;
import es.asepeyo.recordalexia.domain.Activity; import es.asepeyo.recordalexia.domain.Activity;
import es.asepeyo.recordalexia.domain.AfternoonRoutine; import es.asepeyo.recordalexia.domain.AfternoonRoutine;
import es.asepeyo.recordalexia.domain.Child; import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry; import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
import es.asepeyo.recordalexia.exception.NotFoundException; import es.asepeyo.recordalexia.exception.NotFoundException;
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.RoutineTaskRepository;
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
import es.asepeyo.recordalexia.security.FamilyContext; import es.asepeyo.recordalexia.security.FamilyContext;
import es.asepeyo.recordalexia.web.dto.ParentDtos.AfternoonRoutineRequest; import es.asepeyo.recordalexia.web.dto.ParentDtos.AfternoonRoutineRequest;
@@ -36,17 +38,20 @@ public class ParentScheduleController {
private final WeeklyTemplateEntryRepository templateRepository; private final WeeklyTemplateEntryRepository templateRepository;
private final AfternoonRoutineRepository routineRepository; private final AfternoonRoutineRepository routineRepository;
private final RoutineTaskRepository routineTaskRepository;
private final ChildRepository childRepository; private final ChildRepository childRepository;
private final ActivityRepository activityRepository; private final ActivityRepository activityRepository;
private final FamilyContext familyContext; private final FamilyContext familyContext;
public ParentScheduleController(WeeklyTemplateEntryRepository templateRepository, public ParentScheduleController(WeeklyTemplateEntryRepository templateRepository,
AfternoonRoutineRepository routineRepository, AfternoonRoutineRepository routineRepository,
RoutineTaskRepository routineTaskRepository,
ChildRepository childRepository, ChildRepository childRepository,
ActivityRepository activityRepository, ActivityRepository activityRepository,
FamilyContext familyContext) { FamilyContext familyContext) {
this.templateRepository = templateRepository; this.templateRepository = templateRepository;
this.routineRepository = routineRepository; this.routineRepository = routineRepository;
this.routineTaskRepository = routineTaskRepository;
this.childRepository = childRepository; this.childRepository = childRepository;
this.activityRepository = activityRepository; this.activityRepository = activityRepository;
this.familyContext = familyContext; this.familyContext = familyContext;
@@ -97,14 +102,20 @@ public class ParentScheduleController {
.map(this::toRoutineView).toList(); .map(this::toRoutineView).toList();
} }
/** Asigna una rutina del catálogo a uno o varios días de una vez (sin duplicar definición). */
@PostMapping("/routines") @PostMapping("/routines")
public ResponseEntity<RoutineView> createRoutine(@RequestBody AfternoonRoutineRequest req) { @Transactional
public ResponseEntity<List<RoutineView>> createRoutine(@RequestBody AfternoonRoutineRequest req) {
Child child = requireChild(req.childId()); Child child = requireChild(req.childId());
AfternoonRoutine routine = new AfternoonRoutine(child, DayOfWeek.valueOf(req.dayOfWeek()), RoutineTask task = routineTaskRepository.findByIdAndFamilyId(req.routineTaskId(), fid())
req.labelEs(), req.labelCa(), req.icon(), req.color(), .orElseThrow(() -> new NotFoundException("No existe la rutina con id " + req.routineTaskId()));
req.orderIndex() != null ? req.orderIndex() : 0); int orderIndex = req.orderIndex() != null ? req.orderIndex() : 0;
routine.setCoinsReward(req.coinsReward()); List<RoutineView> created = req.daysOfWeek().stream().map(day -> {
return ResponseEntity.ok(toRoutineView(routineRepository.save(routine))); AfternoonRoutine routine = new AfternoonRoutine(child, DayOfWeek.valueOf(day), task, orderIndex);
routine.setCoinsReward(req.coinsReward());
return toRoutineView(routineRepository.save(routine));
}).toList();
return ResponseEntity.ok(created);
} }
/** Reordena rutinas (solo las de la familia): orderIndex según la posición recibida. */ /** Reordena rutinas (solo las de la familia): orderIndex según la posición recibida. */
@@ -143,8 +154,9 @@ public class ParentScheduleController {
} }
private RoutineView toRoutineView(AfternoonRoutine r) { private RoutineView toRoutineView(AfternoonRoutine r) {
RoutineTask t = r.getRoutineTask();
return new RoutineView(r.getId(), r.getChild().getId(), r.getDayOfWeek().name(), return new RoutineView(r.getId(), r.getChild().getId(), r.getDayOfWeek().name(),
r.getLabelEs(), r.getLabelCa(), r.getIcon(), r.getColor(), r.getOrderIndex(), t.getId(), t.getLabelEs(), t.getLabelCa(), t.getIcon(), t.getColor(), r.getOrderIndex(),
r.getCoinsReward()); r.getCoinsReward());
} }
} }

View File

@@ -32,14 +32,23 @@ public final class ParentDtos {
String labelEs, String labelCa, String icon, String color, String category) { String labelEs, String labelCa, String icon, String color, String category) {
} }
/** Alta/edición de una rutina del catálogo de la familia (definición reutilizable). */
public record RoutineTaskRequest(
String labelEs, String labelCa, String icon, String color, Integer coinsReward) {
}
// --- Horario / rutinas --- // --- Horario / rutinas ---
public record WeeklyEntryRequest( public record WeeklyEntryRequest(
Long childId, String dayOfWeek, Long activityId, Integer orderIndex, Integer coinsReward) { Long childId, String dayOfWeek, Long activityId, Integer orderIndex, Integer coinsReward) {
} }
/**
* Asigna una rutina del catálogo a un niño para varios días de una vez. El orden y
* las monedas son opcionales (override por día).
*/
public record AfternoonRoutineRequest( public record AfternoonRoutineRequest(
Long childId, String dayOfWeek, String labelEs, String labelCa, String icon, String color, Long childId, Long routineTaskId, List<String> daysOfWeek, Integer orderIndex,
Integer orderIndex, Integer coinsReward) { Integer coinsReward) {
} }
/** Reordenación de rutinas: lista de ids en el nuevo orden deseado. */ /** Reordenación de rutinas: lista de ids en el nuevo orden deseado. */

View File

@@ -21,14 +21,20 @@ public final class ParentViews {
Long id, String labelEs, String labelCa, String icon, String color, List<Long> materialIds) { Long id, String labelEs, String labelCa, String icon, String color, List<Long> materialIds) {
} }
/** Rutina del catálogo de la familia (definición reutilizable). */
public record RoutineTaskView(
Long id, String labelEs, String labelCa, String icon, String color, Integer coinsReward) {
}
public record WeeklyEntryView( public record WeeklyEntryView(
Long id, Long childId, String dayOfWeek, Long activityId, String activityLabelEs, Long id, Long childId, String dayOfWeek, Long activityId, String activityLabelEs,
String icon, String color, int orderIndex, Integer coinsReward) { String icon, String color, int orderIndex, Integer coinsReward) {
} }
/** Asignación de una rutina a un día; labels/icon/color derivan del catálogo. */
public record RoutineView( public record RoutineView(
Long id, Long childId, String dayOfWeek, String labelEs, String labelCa, String icon, Long id, Long childId, String dayOfWeek, Long routineTaskId, String labelEs, String labelCa,
String color, int orderIndex, Integer coinsReward) { String icon, String color, int orderIndex, Integer coinsReward) {
} }
public record EventAdminView( public record EventAdminView(