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.AfternoonRoutine;
import es.asepeyo.recordalexia.domain.Child;
import es.asepeyo.recordalexia.domain.RoutineTask;
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
import es.asepeyo.recordalexia.exception.NotFoundException;
import es.asepeyo.recordalexia.repository.ActivityRepository;
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
import es.asepeyo.recordalexia.repository.ChildRepository;
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
import es.asepeyo.recordalexia.security.FamilyContext;
import es.asepeyo.recordalexia.web.dto.ParentDtos.AfternoonRoutineRequest;
@@ -36,17 +38,20 @@ public class ParentScheduleController {
private final WeeklyTemplateEntryRepository templateRepository;
private final AfternoonRoutineRepository routineRepository;
private final RoutineTaskRepository routineTaskRepository;
private final ChildRepository childRepository;
private final ActivityRepository activityRepository;
private final FamilyContext familyContext;
public ParentScheduleController(WeeklyTemplateEntryRepository templateRepository,
AfternoonRoutineRepository routineRepository,
RoutineTaskRepository routineTaskRepository,
ChildRepository childRepository,
ActivityRepository activityRepository,
FamilyContext familyContext) {
this.templateRepository = templateRepository;
this.routineRepository = routineRepository;
this.routineTaskRepository = routineTaskRepository;
this.childRepository = childRepository;
this.activityRepository = activityRepository;
this.familyContext = familyContext;
@@ -97,14 +102,20 @@ public class ParentScheduleController {
.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")
public ResponseEntity<RoutineView> createRoutine(@RequestBody AfternoonRoutineRequest req) {
@Transactional
public ResponseEntity<List<RoutineView>> createRoutine(@RequestBody AfternoonRoutineRequest req) {
Child child = requireChild(req.childId());
AfternoonRoutine routine = new AfternoonRoutine(child, DayOfWeek.valueOf(req.dayOfWeek()),
req.labelEs(), req.labelCa(), req.icon(), req.color(),
req.orderIndex() != null ? req.orderIndex() : 0);
RoutineTask task = routineTaskRepository.findByIdAndFamilyId(req.routineTaskId(), fid())
.orElseThrow(() -> new NotFoundException("No existe la rutina con id " + req.routineTaskId()));
int orderIndex = req.orderIndex() != null ? req.orderIndex() : 0;
List<RoutineView> created = req.daysOfWeek().stream().map(day -> {
AfternoonRoutine routine = new AfternoonRoutine(child, DayOfWeek.valueOf(day), task, orderIndex);
routine.setCoinsReward(req.coinsReward());
return ResponseEntity.ok(toRoutineView(routineRepository.save(routine)));
return toRoutineView(routineRepository.save(routine));
}).toList();
return ResponseEntity.ok(created);
}
/** 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) {
RoutineTask t = r.getRoutineTask();
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());
}
}

View File

@@ -32,14 +32,23 @@ public final class ParentDtos {
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 ---
public record WeeklyEntryRequest(
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(
Long childId, String dayOfWeek, String labelEs, String labelCa, String icon, String color,
Integer orderIndex, Integer coinsReward) {
Long childId, Long routineTaskId, List<String> daysOfWeek, Integer orderIndex,
Integer coinsReward) {
}
/** 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) {
}
/** 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(
Long id, Long childId, String dayOfWeek, Long activityId, String activityLabelEs,
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(
Long id, Long childId, String dayOfWeek, String labelEs, String labelCa, String icon,
String color, int orderIndex, Integer coinsReward) {
Long id, Long childId, String dayOfWeek, Long routineTaskId, String labelEs, String labelCa,
String icon, String color, int orderIndex, Integer coinsReward) {
}
public record EventAdminView(