From 1f968e82cdc50d386a931c0990c36a6e1ef2fce5 Mon Sep 17 00:00:00 2001 From: Jaume Garriga Maestre Date: Mon, 22 Jun 2026 23:14:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(alta):=20dataset=20inicial=20al=20crear=20?= =?UTF-8?q?familia=20y=20ni=C3=B1o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al registrar una familia se siembra el catálogo de materiales escolares típicos y el catálogo de rutinas. Al dar de alta un niño se le asignan las rutinas de tarde de L-V escaladas por su edad (tramos 8-9 / 10-11 / 12-14), reutilizando el catálogo de la familia. --- .../recordalexia/security/AuthService.java | 7 +- .../recordalexia/service/ChildService.java | 9 +- .../service/InitialDatasetService.java | 125 ++++++++++++++++++ .../service/InitialDatasetServiceTest.java | 66 +++++++++ 4 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/es/asepeyo/recordalexia/service/InitialDatasetService.java create mode 100644 backend/src/test/java/es/asepeyo/recordalexia/service/InitialDatasetServiceTest.java diff --git a/backend/src/main/java/es/asepeyo/recordalexia/security/AuthService.java b/backend/src/main/java/es/asepeyo/recordalexia/security/AuthService.java index 4159caa..3f4e2e2 100644 --- a/backend/src/main/java/es/asepeyo/recordalexia/security/AuthService.java +++ b/backend/src/main/java/es/asepeyo/recordalexia/security/AuthService.java @@ -3,6 +3,7 @@ package es.asepeyo.recordalexia.security; import es.asepeyo.recordalexia.domain.Family; import es.asepeyo.recordalexia.exception.ConflictException; import es.asepeyo.recordalexia.repository.FamilyRepository; +import es.asepeyo.recordalexia.service.InitialDatasetService; import java.util.Optional; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -19,12 +20,14 @@ public class AuthService { private final FamilyRepository familyRepository; private final SessionAuthService sessions; private final PasswordEncoder encoder; + private final InitialDatasetService initialDataset; public AuthService(FamilyRepository familyRepository, SessionAuthService sessions, - PasswordEncoder encoder) { + PasswordEncoder encoder, InitialDatasetService initialDataset) { this.familyRepository = familyRepository; this.sessions = sessions; this.encoder = encoder; + this.initialDataset = initialDataset; } /** Registra una familia nueva y abre sesión (auto-login). Devuelve el handle. */ @@ -36,6 +39,8 @@ public class AuthService { } Family family = familyRepository.save( new Family(normalized, encoder.encode(rawPass), name, encoder.encode(rawPin))); + // Sembrar el catálogo inicial (materiales + rutinas) para no empezar de cero. + initialDataset.provisionFamily(family); return sessions.openSession(family); } diff --git a/backend/src/main/java/es/asepeyo/recordalexia/service/ChildService.java b/backend/src/main/java/es/asepeyo/recordalexia/service/ChildService.java index a93864e..345a005 100644 --- a/backend/src/main/java/es/asepeyo/recordalexia/service/ChildService.java +++ b/backend/src/main/java/es/asepeyo/recordalexia/service/ChildService.java @@ -25,12 +25,14 @@ public class ChildService { private final ChildRepository childRepository; private final FamilyRepository familyRepository; private final FamilyContext familyContext; + private final InitialDatasetService initialDataset; public ChildService(ChildRepository childRepository, FamilyRepository familyRepository, - FamilyContext familyContext) { + FamilyContext familyContext, InitialDatasetService initialDataset) { this.childRepository = childRepository; this.familyRepository = familyRepository; this.familyContext = familyContext; + this.initialDataset = initialDataset; } @Transactional(readOnly = true) @@ -89,7 +91,10 @@ public class ChildService { // El niño nace en la familia de la sesión. child.setFamily(familyRepository.getReferenceById(familyContext.currentFamilyId())); applyRequest(child, req); - return childRepository.save(child); + Child saved = childRepository.save(child); + // Sembrar las rutinas de tarde acordes a su edad (van tomando responsabilidades). + initialDataset.provisionChildRoutines(saved); + return saved; } @Transactional diff --git a/backend/src/main/java/es/asepeyo/recordalexia/service/InitialDatasetService.java b/backend/src/main/java/es/asepeyo/recordalexia/service/InitialDatasetService.java new file mode 100644 index 0000000..f2a5864 --- /dev/null +++ b/backend/src/main/java/es/asepeyo/recordalexia/service/InitialDatasetService.java @@ -0,0 +1,125 @@ +package es.asepeyo.recordalexia.service; + +import es.asepeyo.recordalexia.domain.AfternoonRoutine; +import es.asepeyo.recordalexia.domain.Child; +import es.asepeyo.recordalexia.domain.Family; +import es.asepeyo.recordalexia.domain.MaterialItem; +import es.asepeyo.recordalexia.domain.RoutineTask; +import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository; +import es.asepeyo.recordalexia.repository.MaterialItemRepository; +import es.asepeyo.recordalexia.repository.RoutineTaskRepository; +import java.time.DayOfWeek; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Provisión inicial de datos al dar de alta una familia o un niño, para que no haya + * que crearlo todo a mano: + * - Familia nueva: catálogo de materiales escolares típicos + catálogo de rutinas. + * - Niño nuevo: rutinas de tarde de lunes a viernes, escalonadas por su edad (van + * tomando responsabilidades según el tramo 8-9 / 10-11 / 12-14), referenciando las + * rutinas del catálogo de su familia. + */ +@Service +public class InitialDatasetService { + + private static final List LABORABLES = List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); + + /** Material escolar típico que se siembra en toda familia nueva. */ + private static final List MATERIALES = List.of( + new MaterialSeed("Estuche", "Estoig", "✏️", "#F4C95D", "general"), + new MaterialSeed("Agenda", "Agenda", "📔", "#5B8DEF", "general"), + new MaterialSeed("Libreta", "Llibreta", "📓", "#A78BD0", "general"), + new MaterialSeed("Libro de texto", "Llibre de text", "📘", "#5B8DEF", "general"), + new MaterialSeed("Lectura", "Lectura", "📖", "#F2A65A", "lengua"), + new MaterialSeed("Almuerzo", "Esmorzar", "🍎", "#EC8FA4", "general"), + new MaterialSeed("Botella de agua", "Ampolla d'aigua", "💧", "#5BC0BE", "general"), + new MaterialSeed("Ropa de gimnasia", "Roba d'EF", "👕", "#7FBF6B", "gimnasia"), + new MaterialSeed("Zapatillas", "Sabatilles", "👟", "#7FBF6B", "gimnasia")); + + /** + * Catálogo de rutinas con la edad mínima a partir de la cual se asigna al niño. + * minAge 0 = base (todos), 10 = a partir de 10-11, 12 = a partir de 12-14. + */ + private static final List RUTINAS = List.of( + // Base: autonomía básica (8-9 y también válidas para más pequeños). + new RoutineSeed("Deshacer la mochila", "Buidar la motxilla", "🎒", "#F2A65A", 0), + new RoutineSeed("Merendar", "Berenar", "🥪", "#F4C95D", 0), + new RoutineSeed("Hacer los deberes", "Fer els deures", "📝", "#5B8DEF", 0), + new RoutineSeed("Preparar la mochila", "Preparar la motxilla", "🎒", "#A78BD0", 0), + new RoutineSeed("Lavarse los dientes", "Rentar-se les dents", "🪥", "#5BC0BE", 0), + // 10-11: organización y primeras tareas de casa. + new RoutineSeed("Poner y recoger la mesa", "Parar i recollir taula", "🍽️", "#7FBF6B", 10), + new RoutineSeed("Hacer la cama", "Fer el llit", "🛏️", "#EC8FA4", 10), + new RoutineSeed("Revisar la agenda", "Revisar l'agenda", "📔", "#5B8DEF", 10), + new RoutineSeed("Leer 20 minutos", "Llegir 20 minuts", "📖", "#F2A65A", 10), + // 12-14: corresponsabilidad y gestión propia. + new RoutineSeed("Planificar la semana", "Planificar la setmana", "🗓️", "#A78BD0", 12), + new RoutineSeed("Ayudar a preparar la cena", "Ajudar a fer el sopar", "🍳", "#F2A65A", 12), + new RoutineSeed("Ocuparme de mi ropa", "Ocupar-me de la meva roba", "🧺", "#5BC0BE", 12), + new RoutineSeed("Estudio autónomo", "Estudi autònom", "📚", "#5B8DEF", 12), + new RoutineSeed("Tarea de casa de la semana", "Tasca de casa de la setmana", "🧹", "#7FBF6B", 12)); + + private final MaterialItemRepository materialRepository; + private final RoutineTaskRepository routineTaskRepository; + private final AfternoonRoutineRepository routineRepository; + + public InitialDatasetService(MaterialItemRepository materialRepository, + RoutineTaskRepository routineTaskRepository, + AfternoonRoutineRepository routineRepository) { + this.materialRepository = materialRepository; + this.routineTaskRepository = routineTaskRepository; + this.routineRepository = routineRepository; + } + + /** Siembra el catálogo (materiales + rutinas) de una familia recién creada. */ + @Transactional + public void provisionFamily(Family family) { + for (MaterialSeed m : MATERIALES) { + MaterialItem item = new MaterialItem(m.es(), m.ca(), m.icon(), m.color(), m.category()); + item.setFamily(family); + materialRepository.save(item); + } + for (RoutineSeed r : RUTINAS) { + RoutineTask task = new RoutineTask(r.es(), r.ca(), r.icon(), r.color()); + task.setFamily(family); + routineTaskRepository.save(task); + } + } + + /** + * Asigna a un niño recién creado las rutinas de tarde acordes a su edad, de lunes a + * viernes. Reutiliza las rutinas del catálogo de su familia (las que sembró + * {@link #provisionFamily}); si la familia no tiene catálogo, no hace nada. + */ + @Transactional + public void provisionChildRoutines(Child child) { + List catalogo = routineTaskRepository.findByFamilyId(child.getFamily().getId()); + // Rutinas que corresponden a la edad del niño, en el orden definido en RUTINAS. + List aplicables = RUTINAS.stream() + .filter(r -> child.getAge() >= r.minAge()) + .map(r -> buscarEnCatalogo(catalogo, r.es())) + .filter(java.util.Objects::nonNull) + .toList(); + + for (DayOfWeek dia : LABORABLES) { + int orden = 0; + for (RoutineTask tarea : aplicables) { + routineRepository.save(new AfternoonRoutine(child, dia, tarea, orden++)); + } + } + } + + /** Localiza en el catálogo la rutina por su etiqueta ES (clave estable del seed). */ + private RoutineTask buscarEnCatalogo(List catalogo, String labelEs) { + return catalogo.stream().filter(t -> t.getLabelEs().equals(labelEs)).findFirst().orElse(null); + } + + private record MaterialSeed(String es, String ca, String icon, String color, String category) { + } + + private record RoutineSeed(String es, String ca, String icon, String color, int minAge) { + } +} diff --git a/backend/src/test/java/es/asepeyo/recordalexia/service/InitialDatasetServiceTest.java b/backend/src/test/java/es/asepeyo/recordalexia/service/InitialDatasetServiceTest.java new file mode 100644 index 0000000..1dd3ad4 --- /dev/null +++ b/backend/src/test/java/es/asepeyo/recordalexia/service/InitialDatasetServiceTest.java @@ -0,0 +1,66 @@ +package es.asepeyo.recordalexia.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import es.asepeyo.recordalexia.domain.Child; +import es.asepeyo.recordalexia.domain.Family; +import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository; +import es.asepeyo.recordalexia.repository.ChildRepository; +import es.asepeyo.recordalexia.repository.FamilyRepository; +import es.asepeyo.recordalexia.repository.MaterialItemRepository; +import es.asepeyo.recordalexia.repository.RoutineTaskRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +/** Provisión inicial: catálogo al alta de familia y rutinas escalonadas al alta de niño. */ +@SpringBootTest +@Transactional +class InitialDatasetServiceTest { + + @Autowired private InitialDatasetService initialDataset; + @Autowired private FamilyRepository familyRepository; + @Autowired private ChildRepository childRepository; + @Autowired private MaterialItemRepository materialRepository; + @Autowired private RoutineTaskRepository routineTaskRepository; + @Autowired private AfternoonRoutineRepository routineRepository; + + @Test + void elAltaDeFamiliaSiembraElCatalogo() { + Family family = familyRepository.save(new Family("cat@x.com", "h", "Cat", "p")); + + initialDataset.provisionFamily(family); + + assertThat(materialRepository.findByFamilyId(family.getId())).hasSize(9); + assertThat(routineTaskRepository.findByFamilyId(family.getId())).hasSize(14); + } + + @Test + void lasRutinasDelNinoSeEscalanPorEdad() { + Family family = familyRepository.save(new Family("edad@x.com", "h", "Edad", "p")); + initialDataset.provisionFamily(family); + + // 8 años → solo el set base (5 rutinas) x 5 días laborables = 25. + Child pequeno = childRepository.save(nino(family, 8)); + initialDataset.provisionChildRoutines(pequeno); + assertThat(routineRepository.findByChildIdOrderByDayOfWeekAscOrderIndexAsc(pequeno.getId())) + .hasSize(5 * 5); + + // 13 años → todas las rutinas (14) x 5 días = 70. + Child mayor = childRepository.save(nino(family, 13)); + initialDataset.provisionChildRoutines(mayor); + assertThat(routineRepository.findByChildIdOrderByDayOfWeekAscOrderIndexAsc(mayor.getId())) + .hasSize(14 * 5); + } + + private Child nino(Family family, int edad) { + Child c = new Child(); + c.setFamily(family); + c.setName("Test"); + c.setMascot("🦊"); + c.setAccentColor("#F2A65A"); + c.setAge(edad); + return c; + } +}