diff --git a/backend/build.gradle b/backend/build.gradle index af88be4..e04e89f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,11 +25,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.liquibase:liquibase-core' runtimeOnly 'org.postgresql:postgresql' - // H2 solo en test: permite que el contexto cargue sin un Postgres real en la - // Fase 1 (esqueleto). La infraestructura de test realista (Testcontainers) - // se introducirá en la Fase 2, junto con el dominio. + // H2 en test para los tests rápidos de servicio/web. La verificación de las + // migraciones Liquibase y del mapeo JPA (ddl-auto=validate) se hace contra un + // Postgres REAL vía Testcontainers (MigrationIT): H2 no detecta + // incompatibilidades de tipos/SQL propias de Postgres (lección aprendida con + // el CAST(NULL AS INTEGER) del changeset 003). testRuntimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:postgresql' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/backend/src/main/java/es/asepeyo/recordalexia/bootstrap/DataSeeder.java b/backend/src/main/java/es/asepeyo/recordalexia/bootstrap/DataSeeder.java index 90fb12e..b93f89c 100644 --- a/backend/src/main/java/es/asepeyo/recordalexia/bootstrap/DataSeeder.java +++ b/backend/src/main/java/es/asepeyo/recordalexia/bootstrap/DataSeeder.java @@ -7,18 +7,16 @@ import es.asepeyo.recordalexia.domain.EventType; import es.asepeyo.recordalexia.domain.Family; import es.asepeyo.recordalexia.domain.MaterialItem; import es.asepeyo.recordalexia.domain.Reward; -import es.asepeyo.recordalexia.domain.RoutineTask; import es.asepeyo.recordalexia.domain.SpecialEvent; 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.FamilyRepository; import es.asepeyo.recordalexia.repository.MaterialItemRepository; import es.asepeyo.recordalexia.repository.RewardRepository; -import es.asepeyo.recordalexia.repository.RoutineTaskRepository; import es.asepeyo.recordalexia.repository.SpecialEventRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; +import es.asepeyo.recordalexia.service.InitialDatasetService; import java.time.Clock; import java.time.DayOfWeek; import java.time.LocalDate; @@ -52,29 +50,26 @@ public class DataSeeder implements ApplicationRunner { private final MaterialItemRepository materialRepository; private final ActivityRepository activityRepository; private final WeeklyTemplateEntryRepository templateRepository; - private final AfternoonRoutineRepository routineRepository; - private final RoutineTaskRepository routineTaskRepository; private final SpecialEventRepository eventRepository; private final RewardRepository rewardRepository; + private final InitialDatasetService initialDataset; private final PasswordEncoder passwordEncoder; private final Clock clock; public DataSeeder(FamilyRepository familyRepository, ChildRepository childRepository, MaterialItemRepository materialRepository, ActivityRepository activityRepository, WeeklyTemplateEntryRepository templateRepository, - AfternoonRoutineRepository routineRepository, - RoutineTaskRepository routineTaskRepository, SpecialEventRepository eventRepository, RewardRepository rewardRepository, + InitialDatasetService initialDataset, PasswordEncoder passwordEncoder, Clock clock) { this.familyRepository = familyRepository; this.childRepository = childRepository; this.materialRepository = materialRepository; this.activityRepository = activityRepository; this.templateRepository = templateRepository; - this.routineRepository = routineRepository; - this.routineTaskRepository = routineTaskRepository; this.eventRepository = eventRepository; this.rewardRepository = rewardRepository; + this.initialDataset = initialDataset; this.passwordEncoder = passwordEncoder; this.clock = clock; } @@ -89,9 +84,10 @@ public class DataSeeder implements ApplicationRunner { Family family = familyRepository.save(new Family(DEMO_EMAIL, passwordEncoder.encode(DEMO_PASSWORD), "Familia demo", passwordEncoder.encode(DEMO_PIN))); - var materials = seedMaterials(family); - var activities = seedActivities(family, materials); - var routines = seedRoutineCatalog(family); + // Catálogo base (materiales + rutinas): la MISMA provisión que recibe cualquier + // familia real al registrarse. La demo solo añade sus extras encima. + initialDataset.provisionFamily(family); + var activities = seedActivities(family); seedRewards(family); Child nora = childRepository.save(child(family, "Nora", "🦊", "#F2A65A", 7, 42, LocalTime.of(8, 30))); @@ -100,7 +96,8 @@ public class DataSeeder implements ApplicationRunner { for (Child c : List.of(nora, leo, mia)) { seedWeeklyMornings(c, activities); - seedAfternoonRoutines(c, routines); + // Rutinas de tarde escalonadas por edad, desde el catálogo de la familia. + initialDataset.provisionChildRoutines(c); } LocalDate today = LocalDate.now(clock); @@ -110,38 +107,40 @@ public class DataSeeder implements ApplicationRunner { "Ficha de mates", "Fitxa de mates", "📎", "#5B8DEF")); } - private MaterialsCatalog seedMaterials(Family family) { - return new MaterialsCatalog( - material(family, "Estuche", "Estoig", "✏️", "#F4C95D", "general"), - material(family, "Libro de mates", "Llibre de mates", "📘", "#5B8DEF", "matematicas"), - material(family, "Regla", "Regle", "📏", "#5B8DEF", "matematicas"), - material(family, "Flauta", "Flauta", "🎵", "#A78BD0", "musica"), - material(family, "Libreta", "Llibreta", "📓", "#A78BD0", "musica"), - material(family, "Ropa de gimnasia", "Roba d'EF", "👕", "#7FBF6B", "gimnasia"), - material(family, "Zapatillas", "Sabatilles", "👟", "#7FBF6B", "gimnasia"), - material(family, "Toalla", "Tovallola", "🧖", "#5BC0BE", "gimnasia"), - material(family, "Agua", "Aigua", "💧", "#5BC0BE", "gimnasia"), - material(family, "Lectura", "Lectura", "📖", "#F2A65A", "lengua"), - material(family, "Cuaderno", "Quadern", "📒", "#F2A65A", "lengua"), - material(family, "Almuerzo", "Esmorzar", "🍎", "#EC8FA4", "general")); - } - private MaterialItem material(Family family, String es, String ca, String icon, String color, String category) { MaterialItem m = new MaterialItem(es, ca, icon, color, category); m.setFamily(family); return materialRepository.save(m); } - private ActivitiesCatalog seedActivities(Family family, MaterialsCatalog m) { + /** + * Actividades de la demo. El material sale del catálogo base ya provisionado + * (por su etiqueta ES) más tres extras que solo la demo necesita. + */ + private ActivitiesCatalog seedActivities(Family family) { + List base = materialRepository.findByFamilyId(family.getId()); + MaterialItem flauta = material(family, "Flauta", "Flauta", "🎵", "#A78BD0", "musica"); + MaterialItem regla = material(family, "Regla", "Regle", "📏", "#5B8DEF", "matematicas"); + MaterialItem toalla = material(family, "Toalla", "Tovallola", "🧖", "#5BC0BE", "gimnasia"); + Activity gimnasia = activity(family, "Gimnasia", "Gimnàstica", "🤸", "#7FBF6B", - m.ropaGimnasia, m.zapatillas, m.toalla, m.agua); - Activity musica = activity(family, "Música", "Música", "🎵", "#A78BD0", m.flauta, m.libreta); + fromBase(base, "Ropa de gimnasia"), fromBase(base, "Zapatillas"), toalla, + fromBase(base, "Botella de agua")); + Activity musica = activity(family, "Música", "Música", "🎵", "#A78BD0", + flauta, fromBase(base, "Libreta")); Activity mates = activity(family, "Matemáticas", "Matemàtiques", "📘", "#5B8DEF", - m.libroMates, m.regla, m.estuche); - Activity lengua = activity(family, "Lengua", "Llengua", "📖", "#F2A65A", m.lectura, m.cuaderno); + fromBase(base, "Libro de texto"), regla, fromBase(base, "Estuche")); + Activity lengua = activity(family, "Lengua", "Llengua", "📖", "#F2A65A", + fromBase(base, "Lectura"), fromBase(base, "Libreta")); return new ActivitiesCatalog(gimnasia, musica, mates, lengua); } + /** Localiza un material del catálogo base por su etiqueta ES (clave estable del seed). */ + private MaterialItem fromBase(List base, String labelEs) { + return base.stream().filter(m -> m.getLabelEs().equals(labelEs)).findFirst() + .orElseThrow(() -> new IllegalStateException("Falta el material base: " + labelEs)); + } + private Activity activity(Family family, String es, String ca, String icon, String color, MaterialItem... mats) { Activity activity = new Activity(es, ca, icon, color); activity.setFamily(family); @@ -191,45 +190,7 @@ public class DataSeeder implements ApplicationRunner { templateRepository.save(new WeeklyTemplateEntry(child, day, activity, 0)); } - /** Catálogo de rutinas reutilizables de la familia (se definen una sola vez). */ - private RoutinesCatalog seedRoutineCatalog(Family family) { - return new RoutinesCatalog( - routineTask(family, "Deshacer la mochila", "Buidar la motxilla", "🎒", "#F2A65A"), - routineTask(family, "Merendar", "Berenar", "🥪", "#F4C95D"), - routineTask(family, "Hacer los deberes", "Fer els deures", "📝", "#5B8DEF"), - routineTask(family, "Practicar piano", "Practicar piano", "🎹", "#A78BD0"), - routineTask(family, "Recoger la mesa", "Parar taula", "🍽️", "#7FBF6B")); - } - - private RoutineTask routineTask(Family family, String es, String ca, String icon, String color) { - RoutineTask rt = new RoutineTask(es, ca, icon, color); - rt.setFamily(family); - return routineTaskRepository.save(rt); - } - - /** Asigna las rutinas del catálogo a cada día laborable, reutilizando las mismas. */ - private void seedAfternoonRoutines(Child child, RoutinesCatalog r) { - for (DayOfWeek day : List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)) { - routineRepository.save(new AfternoonRoutine(child, day, r.mochila, 0)); - routineRepository.save(new AfternoonRoutine(child, day, r.merienda, 1)); - routineRepository.save(new AfternoonRoutine(child, day, r.deberes, 2)); - routineRepository.save(new AfternoonRoutine(child, day, r.piano, 3)); - routineRepository.save(new AfternoonRoutine(child, day, r.mesa, 4)); - } - } - - private record MaterialsCatalog( - MaterialItem estuche, MaterialItem libroMates, MaterialItem regla, MaterialItem flauta, - MaterialItem libreta, MaterialItem ropaGimnasia, MaterialItem zapatillas, MaterialItem toalla, - MaterialItem agua, MaterialItem lectura, MaterialItem cuaderno, MaterialItem almuerzo) { - } - private record ActivitiesCatalog(Activity gimnasia, Activity musica, Activity matematicas, Activity lengua) { } - - private record RoutinesCatalog(RoutineTask mochila, RoutineTask merienda, RoutineTask deberes, - RoutineTask piano, RoutineTask mesa) { - } } diff --git a/backend/src/main/java/es/asepeyo/recordalexia/repository/AfternoonRoutineRepository.java b/backend/src/main/java/es/asepeyo/recordalexia/repository/AfternoonRoutineRepository.java index 3e4d889..7873d6b 100644 --- a/backend/src/main/java/es/asepeyo/recordalexia/repository/AfternoonRoutineRepository.java +++ b/backend/src/main/java/es/asepeyo/recordalexia/repository/AfternoonRoutineRepository.java @@ -11,4 +11,7 @@ public interface AfternoonRoutineRepository extends JpaRepository findByChildIdAndDayOfWeekOrderByOrderIndexAsc(Long childId, DayOfWeek dayOfWeek); List findByChildIdOrderByDayOfWeekAscOrderIndexAsc(Long childId); + + /** Nº de asignaciones de un niño en un día; da el siguiente orderIndex al asignar. */ + int countByChildIdAndDayOfWeek(Long childId, DayOfWeek dayOfWeek); } diff --git a/backend/src/main/java/es/asepeyo/recordalexia/web/ParentScheduleController.java b/backend/src/main/java/es/asepeyo/recordalexia/web/ParentScheduleController.java index f435194..1919b62 100644 --- a/backend/src/main/java/es/asepeyo/recordalexia/web/ParentScheduleController.java +++ b/backend/src/main/java/es/asepeyo/recordalexia/web/ParentScheduleController.java @@ -109,9 +109,12 @@ public class ParentScheduleController { Child child = requireChild(req.childId()); 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 created = req.daysOfWeek().stream().map(day -> { - AfternoonRoutine routine = new AfternoonRoutine(child, DayOfWeek.valueOf(day), task, orderIndex); + DayOfWeek dayOfWeek = DayOfWeek.valueOf(day); + // Sin orden explícito, la rutina se añade al final DE CADA día destino. + int orderIndex = req.orderIndex() != null ? req.orderIndex() + : routineRepository.countByChildIdAndDayOfWeek(child.getId(), dayOfWeek); + AfternoonRoutine routine = new AfternoonRoutine(child, dayOfWeek, task, orderIndex); routine.setCoinsReward(req.coinsReward()); return toRoutineView(routineRepository.save(routine)); }).toList(); diff --git a/backend/src/main/resources/db/changelog/changes/.gitkeep b/backend/src/main/resources/db/changelog/changes/.gitkeep deleted file mode 100644 index 76bcd9a..0000000 --- a/backend/src/main/resources/db/changelog/changes/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Los changesets de Liquibase de la Fase 2 (dominio) vivirán aquí. diff --git a/backend/src/main/resources/db/changelog/changes/004-routine-task-cascade.yaml b/backend/src/main/resources/db/changelog/changes/004-routine-task-cascade.yaml new file mode 100644 index 0000000..abe68e2 --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/004-routine-task-cascade.yaml @@ -0,0 +1,30 @@ +# Borrado en cascada del catálogo de rutinas. +# Al eliminar una rutina del catálogo (routine_task) deben desaparecer sus +# asignaciones a días (afternoon_routine): es el modelo mental del padre +# ("quito Merendar" = desaparece de todos los días). Sin esto, el DELETE +# fallaba con violación de FK y el panel devolvía un error 500. +databaseChangeLog: + - changeSet: + id: 400-cascade-routine-assignment + author: recordalexia + changes: + - dropForeignKeyConstraint: + baseTableName: afternoon_routine + constraintName: fk_routine_assignment_task + - addForeignKeyConstraint: + baseTableName: afternoon_routine + baseColumnNames: routine_task_id + referencedTableName: routine_task + referencedColumnNames: id + constraintName: fk_routine_assignment_task + onDelete: CASCADE + rollback: + - dropForeignKeyConstraint: + baseTableName: afternoon_routine + constraintName: fk_routine_assignment_task + - addForeignKeyConstraint: + baseTableName: afternoon_routine + baseColumnNames: routine_task_id + referencedTableName: routine_task + referencedColumnNames: id + constraintName: fk_routine_assignment_task diff --git a/backend/src/test/java/es/asepeyo/recordalexia/MigrationIT.java b/backend/src/test/java/es/asepeyo/recordalexia/MigrationIT.java new file mode 100644 index 0000000..3c1ff5d --- /dev/null +++ b/backend/src/test/java/es/asepeyo/recordalexia/MigrationIT.java @@ -0,0 +1,45 @@ +package es.asepeyo.recordalexia; + +import static org.assertj.core.api.Assertions.assertThat; + +import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository; +import es.asepeyo.recordalexia.repository.FamilyRepository; +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.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Verifica el changelog completo de Liquibase contra un Postgres REAL (misma + * imagen que producción). Cubre lo que H2 no puede: tipado estricto de Postgres + * en la migración SQL, y que el esquema resultante casa con el mapeo JPA + * (ddl-auto=validate). Con el seeder activado comprueba además el estado final. + * + * Se desactiva sola si Docker no está disponible en la máquina. + */ +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +@TestPropertySource(properties = "recordalexia.seed.enabled=true") +class MigrationIT { + + @Container + @ServiceConnection + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); + + @Autowired private FamilyRepository familyRepository; + @Autowired private RoutineTaskRepository routineTaskRepository; + @Autowired private AfternoonRoutineRepository routineRepository; + + @Test + void lasMigracionesAplicanYelSeederPueblaEnPostgresReal() { + // Si llegamos aquí, Liquibase aplicó 001-004 y Hibernate validó el esquema. + assertThat(familyRepository.findByEmailIgnoreCase("demo@recordalexia.local")).isPresent(); + assertThat(routineTaskRepository.count()).isEqualTo(14); + assertThat(routineRepository.count()).isEqualTo(75); // 3 niños x 5 rutinas x 5 días + } +} diff --git a/backend/src/test/java/es/asepeyo/recordalexia/bootstrap/DataSeederIT.java b/backend/src/test/java/es/asepeyo/recordalexia/bootstrap/DataSeederIT.java index cae4cbc..a232eba 100644 --- a/backend/src/test/java/es/asepeyo/recordalexia/bootstrap/DataSeederIT.java +++ b/backend/src/test/java/es/asepeyo/recordalexia/bootstrap/DataSeederIT.java @@ -3,9 +3,12 @@ package es.asepeyo.recordalexia.bootstrap; import static org.assertj.core.api.Assertions.assertThat; import es.asepeyo.recordalexia.repository.ActivityRepository; +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.RewardRepository; +import es.asepeyo.recordalexia.repository.RoutineTaskRepository; import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -14,7 +17,8 @@ import org.springframework.test.context.TestPropertySource; /** * Verifica que el sembrado del prototipo (familia demo + sus datos) se ejecuta - * correctamente. Usa una BD H2 propia y el seeder activado. + * correctamente. La demo se apoya en la MISMA provisión inicial que una familia + * real (InitialDatasetService) y añade encima sus extras. Usa una BD H2 propia. */ @SpringBootTest @TestPropertySource(properties = { @@ -28,6 +32,9 @@ class DataSeederIT { @Autowired private RewardRepository rewardRepository; @Autowired private ActivityRepository activityRepository; @Autowired private WeeklyTemplateEntryRepository templateRepository; + @Autowired private MaterialItemRepository materialRepository; + @Autowired private RoutineTaskRepository routineTaskRepository; + @Autowired private AfternoonRoutineRepository routineRepository; @Test void siembraLaFamiliaDemoConSusDatos() { @@ -37,4 +44,12 @@ class DataSeederIT { assertThat(activityRepository.count()).isEqualTo(4); // 4 actividades assertThat(templateRepository.count()).isEqualTo(15); // 3 niños x 5 días } + + @Test + void laDemoUsaLaProvisionInicialComun() { + assertThat(materialRepository.count()).isEqualTo(12); // 9 base + 3 extras demo + assertThat(routineTaskRepository.count()).isEqualTo(14); // catálogo común completo + // 3 niños (6, 7 y 9 años) -> tramo base: 5 rutinas x 5 días laborables cada uno. + assertThat(routineRepository.count()).isEqualTo(75); + } } diff --git a/backend/src/test/java/es/asepeyo/recordalexia/repository/RoutineTaskCascadeTest.java b/backend/src/test/java/es/asepeyo/recordalexia/repository/RoutineTaskCascadeTest.java new file mode 100644 index 0000000..85a6653 --- /dev/null +++ b/backend/src/test/java/es/asepeyo/recordalexia/repository/RoutineTaskCascadeTest.java @@ -0,0 +1,61 @@ +package es.asepeyo.recordalexia.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import es.asepeyo.recordalexia.domain.AfternoonRoutine; +import es.asepeyo.recordalexia.domain.Child; +import es.asepeyo.recordalexia.domain.Family; +import es.asepeyo.recordalexia.domain.RoutineTask; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.DayOfWeek; +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; + +/** + * Borrar una rutina del catálogo arrastra sus asignaciones a días (ON DELETE + * CASCADE del changeset 004). Es el modelo mental del padre: "quito Merendar" + * significa que desaparece de todos los días, no un error 500. + */ +@SpringBootTest +@Transactional +class RoutineTaskCascadeTest { + + @Autowired private FamilyRepository familyRepository; + @Autowired private ChildRepository childRepository; + @Autowired private RoutineTaskRepository routineTaskRepository; + @Autowired private AfternoonRoutineRepository routineRepository; + @PersistenceContext private EntityManager entityManager; + + @Test + void borrarRutinaDelCatalogoArrastraSusAsignaciones() { + Family family = familyRepository.save(new Family("cascade@x.com", "h", "C", "p")); + Child child = new Child(); + child.setFamily(family); + child.setName("Test"); + child.setMascot("🦊"); + child.setAccentColor("#F2A65A"); + child.setAge(8); + child = childRepository.save(child); + + RoutineTask merendar = new RoutineTask("Merendar", "Berenar", "🥪", "#F4C95D"); + merendar.setFamily(family); + merendar = routineTaskRepository.save(merendar); + routineRepository.save(new AfternoonRoutine(child, DayOfWeek.MONDAY, merendar, 0)); + routineRepository.save(new AfternoonRoutine(child, DayOfWeek.TUESDAY, merendar, 0)); + routineRepository.flush(); + // Contexto limpio, como en una petición real: el cascade lo resuelve la BD, + // no la sesión de Hibernate (que si tuviera las asignaciones cargadas se + // quejaría de referencias a una entidad borrada). + entityManager.clear(); + + routineTaskRepository.deleteById(merendar.getId()); + routineTaskRepository.flush(); + + assertThat(routineRepository.findByChildIdOrderByDayOfWeekAscOrderIndexAsc(child.getId())) + .isEmpty(); + assertThat(routineTaskRepository.findById(merendar.getId())).isEmpty(); + } +} diff --git a/frontend/src/app/features/parents/routines-tab.component.ts b/frontend/src/app/features/parents/routines-tab.component.ts index 6290a73..31196ab 100644 --- a/frontend/src/app/features/parents/routines-tab.component.ts +++ b/frontend/src/app/features/parents/routines-tab.component.ts @@ -210,13 +210,12 @@ export class RoutinesTabComponent { if (this.selectedTaskId === null || this.selectedDays().length === 0) { return; } - const order = this.routinesForDay().length; + // El backend calcula el orden al final de CADA día destino. this.api .createRoutine({ childId: this._childId, routineTaskId: this.selectedTaskId, daysOfWeek: this.selectedDays(), - orderIndex: order, }) .subscribe(() => { this.selectedTaskId = null;