feat(rutinas): catálogo de rutinas reutilizable por familia
Normaliza las rutinas de tarde replicando el patrón de las mañanas: RoutineTask es el catálogo (gemela de Activity) y AfternoonRoutine pasa a ser pura asignación niño+día. Incluye migración Liquibase 003 con consolidación de duplicados y rollback, y la cascada de monedas (override del día -> catálogo -> coinsPerTask) en DayGenerationService.
This commit is contained in:
@@ -7,6 +7,7 @@ import es.asepeyo.recordalexia.domain.EventType;
|
|||||||
import es.asepeyo.recordalexia.domain.Family;
|
import es.asepeyo.recordalexia.domain.Family;
|
||||||
import es.asepeyo.recordalexia.domain.MaterialItem;
|
import es.asepeyo.recordalexia.domain.MaterialItem;
|
||||||
import es.asepeyo.recordalexia.domain.Reward;
|
import es.asepeyo.recordalexia.domain.Reward;
|
||||||
|
import es.asepeyo.recordalexia.domain.RoutineTask;
|
||||||
import es.asepeyo.recordalexia.domain.SpecialEvent;
|
import es.asepeyo.recordalexia.domain.SpecialEvent;
|
||||||
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
||||||
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
||||||
@@ -15,6 +16,7 @@ import es.asepeyo.recordalexia.repository.ChildRepository;
|
|||||||
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||||
import es.asepeyo.recordalexia.repository.MaterialItemRepository;
|
import es.asepeyo.recordalexia.repository.MaterialItemRepository;
|
||||||
import es.asepeyo.recordalexia.repository.RewardRepository;
|
import es.asepeyo.recordalexia.repository.RewardRepository;
|
||||||
|
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
|
||||||
import es.asepeyo.recordalexia.repository.SpecialEventRepository;
|
import es.asepeyo.recordalexia.repository.SpecialEventRepository;
|
||||||
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
@@ -51,6 +53,7 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
private final ActivityRepository activityRepository;
|
private final ActivityRepository activityRepository;
|
||||||
private final WeeklyTemplateEntryRepository templateRepository;
|
private final WeeklyTemplateEntryRepository templateRepository;
|
||||||
private final AfternoonRoutineRepository routineRepository;
|
private final AfternoonRoutineRepository routineRepository;
|
||||||
|
private final RoutineTaskRepository routineTaskRepository;
|
||||||
private final SpecialEventRepository eventRepository;
|
private final SpecialEventRepository eventRepository;
|
||||||
private final RewardRepository rewardRepository;
|
private final RewardRepository rewardRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
@@ -60,6 +63,7 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
MaterialItemRepository materialRepository, ActivityRepository activityRepository,
|
MaterialItemRepository materialRepository, ActivityRepository activityRepository,
|
||||||
WeeklyTemplateEntryRepository templateRepository,
|
WeeklyTemplateEntryRepository templateRepository,
|
||||||
AfternoonRoutineRepository routineRepository,
|
AfternoonRoutineRepository routineRepository,
|
||||||
|
RoutineTaskRepository routineTaskRepository,
|
||||||
SpecialEventRepository eventRepository, RewardRepository rewardRepository,
|
SpecialEventRepository eventRepository, RewardRepository rewardRepository,
|
||||||
PasswordEncoder passwordEncoder, Clock clock) {
|
PasswordEncoder passwordEncoder, Clock clock) {
|
||||||
this.familyRepository = familyRepository;
|
this.familyRepository = familyRepository;
|
||||||
@@ -68,6 +72,7 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
this.activityRepository = activityRepository;
|
this.activityRepository = activityRepository;
|
||||||
this.templateRepository = templateRepository;
|
this.templateRepository = templateRepository;
|
||||||
this.routineRepository = routineRepository;
|
this.routineRepository = routineRepository;
|
||||||
|
this.routineTaskRepository = routineTaskRepository;
|
||||||
this.eventRepository = eventRepository;
|
this.eventRepository = eventRepository;
|
||||||
this.rewardRepository = rewardRepository;
|
this.rewardRepository = rewardRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
@@ -86,6 +91,7 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
|
|
||||||
var materials = seedMaterials(family);
|
var materials = seedMaterials(family);
|
||||||
var activities = seedActivities(family, materials);
|
var activities = seedActivities(family, materials);
|
||||||
|
var routines = seedRoutineCatalog(family);
|
||||||
seedRewards(family);
|
seedRewards(family);
|
||||||
|
|
||||||
Child nora = childRepository.save(child(family, "Nora", "🦊", "#F2A65A", 7, 42, LocalTime.of(8, 30)));
|
Child nora = childRepository.save(child(family, "Nora", "🦊", "#F2A65A", 7, 42, LocalTime.of(8, 30)));
|
||||||
@@ -94,7 +100,7 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
|
|
||||||
for (Child c : List.of(nora, leo, mia)) {
|
for (Child c : List.of(nora, leo, mia)) {
|
||||||
seedWeeklyMornings(c, activities);
|
seedWeeklyMornings(c, activities);
|
||||||
seedAfternoonRoutines(c);
|
seedAfternoonRoutines(c, routines);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDate today = LocalDate.now(clock);
|
LocalDate today = LocalDate.now(clock);
|
||||||
@@ -185,19 +191,31 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
templateRepository.save(new WeeklyTemplateEntry(child, day, activity, 0));
|
templateRepository.save(new WeeklyTemplateEntry(child, day, activity, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seedAfternoonRoutines(Child child) {
|
/** 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,
|
for (DayOfWeek day : List.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY,
|
||||||
DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)) {
|
DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)) {
|
||||||
routineRepository.save(new AfternoonRoutine(child, day, "Deshacer la mochila",
|
routineRepository.save(new AfternoonRoutine(child, day, r.mochila, 0));
|
||||||
"Buidar la motxilla", "🎒", "#F2A65A", 0));
|
routineRepository.save(new AfternoonRoutine(child, day, r.merienda, 1));
|
||||||
routineRepository.save(new AfternoonRoutine(child, day, "Merendar",
|
routineRepository.save(new AfternoonRoutine(child, day, r.deberes, 2));
|
||||||
"Berenar", "🥪", "#F4C95D", 1));
|
routineRepository.save(new AfternoonRoutine(child, day, r.piano, 3));
|
||||||
routineRepository.save(new AfternoonRoutine(child, day, "Hacer los deberes",
|
routineRepository.save(new AfternoonRoutine(child, day, r.mesa, 4));
|
||||||
"Fer els deures", "📝", "#5B8DEF", 2));
|
|
||||||
routineRepository.save(new AfternoonRoutine(child, day, "Practicar piano",
|
|
||||||
"Practicar piano", "🎹", "#A78BD0", 3));
|
|
||||||
routineRepository.save(new AfternoonRoutine(child, day, "Recoger la mesa",
|
|
||||||
"Parar taula", "🍽️", "#7FBF6B", 4));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,4 +228,8 @@ public class DataSeeder implements ApplicationRunner {
|
|||||||
private record ActivitiesCatalog(Activity gimnasia, Activity musica, Activity matematicas,
|
private record ActivitiesCatalog(Activity gimnasia, Activity musica, Activity matematicas,
|
||||||
Activity lengua) {
|
Activity lengua) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RoutinesCatalog(RoutineTask mochila, RoutineTask merienda, RoutineTask deberes,
|
||||||
|
RoutineTask piano, RoutineTask mesa) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import jakarta.persistence.ManyToOne;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
|
|
||||||
/** Rutina de tarde recurrente de un niño para un día de la semana (reordenable). */
|
/**
|
||||||
|
* Asignación de una rutina del catálogo ({@link RoutineTask}) a un niño para un día de
|
||||||
|
* la semana (reordenable). Es la gemela de tarde de {@link WeeklyTemplateEntry}: aquí
|
||||||
|
* NO vive la definición (texto, emoji, color), que es reutilizable y se lee del
|
||||||
|
* {@link RoutineTask} referenciado. Así una misma rutina sirve para varios días y niños.
|
||||||
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "afternoon_routine")
|
@Table(name = "afternoon_routine")
|
||||||
public class AfternoonRoutine {
|
public class AfternoonRoutine {
|
||||||
@@ -30,33 +35,24 @@ public class AfternoonRoutine {
|
|||||||
@Column(name = "day_of_week")
|
@Column(name = "day_of_week")
|
||||||
private DayOfWeek dayOfWeek;
|
private DayOfWeek dayOfWeek;
|
||||||
|
|
||||||
@Column(name = "label_es")
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
private String labelEs;
|
@JoinColumn(name = "routine_task_id")
|
||||||
|
private RoutineTask routineTask;
|
||||||
@Column(name = "label_ca")
|
|
||||||
private String labelCa;
|
|
||||||
|
|
||||||
private String icon;
|
|
||||||
private String color;
|
|
||||||
|
|
||||||
@Column(name = "order_index")
|
@Column(name = "order_index")
|
||||||
private int orderIndex;
|
private int orderIndex;
|
||||||
|
|
||||||
/** Monedas de la rutina; si es null se usa coinsPerTask del niño. */
|
/** Monedas para este día concreto; si es null se usa la cascada catálogo→niño. */
|
||||||
@Column(name = "coins_reward")
|
@Column(name = "coins_reward")
|
||||||
private Integer coinsReward;
|
private Integer coinsReward;
|
||||||
|
|
||||||
protected AfternoonRoutine() {
|
protected AfternoonRoutine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AfternoonRoutine(Child child, DayOfWeek dayOfWeek, String labelEs, String labelCa,
|
public AfternoonRoutine(Child child, DayOfWeek dayOfWeek, RoutineTask routineTask, int orderIndex) {
|
||||||
String icon, String color, int orderIndex) {
|
|
||||||
this.child = child;
|
this.child = child;
|
||||||
this.dayOfWeek = dayOfWeek;
|
this.dayOfWeek = dayOfWeek;
|
||||||
this.labelEs = labelEs;
|
this.routineTask = routineTask;
|
||||||
this.labelCa = labelCa;
|
|
||||||
this.icon = icon;
|
|
||||||
this.color = color;
|
|
||||||
this.orderIndex = orderIndex;
|
this.orderIndex = orderIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,36 +76,12 @@ public class AfternoonRoutine {
|
|||||||
this.dayOfWeek = dayOfWeek;
|
this.dayOfWeek = dayOfWeek;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLabelEs() {
|
public RoutineTask getRoutineTask() {
|
||||||
return labelEs;
|
return routineTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLabelEs(String labelEs) {
|
public void setRoutineTask(RoutineTask routineTask) {
|
||||||
this.labelEs = labelEs;
|
this.routineTask = routineTask;
|
||||||
}
|
|
||||||
|
|
||||||
public String getLabelCa() {
|
|
||||||
return labelCa;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLabelCa(String labelCa) {
|
|
||||||
this.labelCa = labelCa;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIcon() {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIcon(String icon) {
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getColor() {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(String color) {
|
|
||||||
this.color = color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getOrderIndex() {
|
public int getOrderIndex() {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package es.asepeyo.recordalexia.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rutina reutilizable del catálogo de la familia (ej. "Merendar", "Hacer los deberes").
|
||||||
|
* Es la DEFINICIÓN: texto bilingüe + emoji + color + monedas por defecto. La asignación
|
||||||
|
* a un niño y a un día concreto se modela aparte en {@link AfternoonRoutine}, igual que
|
||||||
|
* {@link Activity} (catálogo de mañana) se asigna mediante {@link WeeklyTemplateEntry}.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "routine_task")
|
||||||
|
public class RoutineTask {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "family_id")
|
||||||
|
private Family family;
|
||||||
|
|
||||||
|
@Column(name = "label_es")
|
||||||
|
private String labelEs;
|
||||||
|
|
||||||
|
@Column(name = "label_ca")
|
||||||
|
private String labelCa;
|
||||||
|
|
||||||
|
private String icon;
|
||||||
|
private String color;
|
||||||
|
|
||||||
|
/** Monedas por defecto de la rutina; si es null, la asignación o el niño deciden. */
|
||||||
|
@Column(name = "coins_reward")
|
||||||
|
private Integer coinsReward;
|
||||||
|
|
||||||
|
protected RoutineTask() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutineTask(String labelEs, String labelCa, String icon, String color) {
|
||||||
|
this.labelEs = labelEs;
|
||||||
|
this.labelCa = labelCa;
|
||||||
|
this.icon = icon;
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Family getFamily() {
|
||||||
|
return family;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFamily(Family family) {
|
||||||
|
this.family = family;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabelEs() {
|
||||||
|
return labelEs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabelEs(String labelEs) {
|
||||||
|
this.labelEs = labelEs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabelCa() {
|
||||||
|
return labelCa;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabelCa(String labelCa) {
|
||||||
|
this.labelCa = labelCa;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(String color) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCoinsReward() {
|
||||||
|
return coinsReward;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoinsReward(Integer coinsReward) {
|
||||||
|
this.coinsReward = coinsReward;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package es.asepeyo.recordalexia.repository;
|
||||||
|
|
||||||
|
import es.asepeyo.recordalexia.domain.RoutineTask;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
/** Catálogo de rutinas de tarde, SIEMPRE scopeado a la familia (tenant). */
|
||||||
|
public interface RoutineTaskRepository extends JpaRepository<RoutineTask, Long> {
|
||||||
|
|
||||||
|
List<RoutineTask> findByFamilyId(Long familyId);
|
||||||
|
|
||||||
|
Optional<RoutineTask> findByIdAndFamilyId(Long id, Long familyId);
|
||||||
|
}
|
||||||
@@ -73,16 +73,36 @@ public class DayGenerationService {
|
|||||||
monedas, TaskOrigin.TEMPLATE, entrada.getOrderIndex()));
|
monedas, TaskOrigin.TEMPLATE, entrada.getOrderIndex()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tarde: una tarea por cada rutina del día ---
|
// --- Tarde: una tarea por cada rutina asignada al día ---
|
||||||
|
// La definición (texto, emoji, color) se lee del catálogo (RoutineTask), igual
|
||||||
|
// que la mañana la lee de Activity. Monedas con cascada: override del día →
|
||||||
|
// valor por defecto del catálogo → coinsPerTask del niño.
|
||||||
List<AfternoonRoutine> rutinas =
|
List<AfternoonRoutine> rutinas =
|
||||||
routineRepository.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek);
|
routineRepository.findByChildIdAndDayOfWeekOrderByOrderIndexAsc(childId, dayOfWeek);
|
||||||
for (AfternoonRoutine rutina : rutinas) {
|
for (AfternoonRoutine rutina : rutinas) {
|
||||||
int monedas = rutina.getCoinsReward() != null ? rutina.getCoinsReward() : child.getCoinsPerTask();
|
var tarea = rutina.getRoutineTask();
|
||||||
|
int monedas = resolverMonedas(rutina.getCoinsReward(), tarea.getCoinsReward(),
|
||||||
|
child.getCoinsPerTask());
|
||||||
nuevas.add(new DailyTask(child, date, Slot.AFTERNOON,
|
nuevas.add(new DailyTask(child, date, Slot.AFTERNOON,
|
||||||
rutina.getLabelEs(), rutina.getLabelCa(), rutina.getIcon(), rutina.getColor(),
|
tarea.getLabelEs(), tarea.getLabelCa(), tarea.getIcon(), tarea.getColor(),
|
||||||
monedas, TaskOrigin.ROUTINE, rutina.getOrderIndex()));
|
monedas, TaskOrigin.ROUTINE, rutina.getOrderIndex()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return dailyTaskRepository.saveAll(nuevas);
|
return dailyTaskRepository.saveAll(nuevas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resuelve las monedas de una rutina de tarde por precedencia: primero el override
|
||||||
|
* del día (asignación), si no el valor por defecto del catálogo, y como último
|
||||||
|
* recurso las monedas por tarea del niño.
|
||||||
|
*/
|
||||||
|
private int resolverMonedas(Integer overrideDia, Integer porDefectoCatalogo, int porTareaNino) {
|
||||||
|
if (overrideDia != null) {
|
||||||
|
return overrideDia;
|
||||||
|
}
|
||||||
|
if (porDefectoCatalogo != null) {
|
||||||
|
return porDefectoCatalogo;
|
||||||
|
}
|
||||||
|
return porTareaNino;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Catálogo reutilizable de rutinas de tarde.
|
||||||
|
# Normaliza afternoon_routine: la definición (texto/emoji/color) pasa a routine_task
|
||||||
|
# (catálogo por familia) y afternoon_routine queda como pura asignación niño+día.
|
||||||
|
# Migra los datos existentes deduplicando las rutinas repetidas por día y familia.
|
||||||
|
# Cada changeset lleva rollback explícito (Liquibase los revierte en orden inverso).
|
||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 300-create-routine-task
|
||||||
|
author: recordalexia
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: routine_task
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGINT, autoIncrement: true, constraints: { primaryKey: true, nullable: false } }
|
||||||
|
- column: { name: family_id, type: BIGINT, constraints: { nullable: false } }
|
||||||
|
- column: { name: label_es, type: VARCHAR(120), constraints: { nullable: false } }
|
||||||
|
- column: { name: label_ca, type: VARCHAR(120), constraints: { nullable: false } }
|
||||||
|
- column: { name: icon, type: VARCHAR(16), constraints: { nullable: false } }
|
||||||
|
- column: { name: color, type: VARCHAR(9), constraints: { nullable: false } }
|
||||||
|
- column: { name: coins_reward, type: INT }
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: routine_task
|
||||||
|
baseColumnNames: family_id
|
||||||
|
referencedTableName: family
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_routine_task_family
|
||||||
|
onDelete: CASCADE
|
||||||
|
rollback:
|
||||||
|
- dropTable:
|
||||||
|
tableName: routine_task
|
||||||
|
|
||||||
|
# Poblar el catálogo deduplicando las rutinas existentes por (familia + definición).
|
||||||
|
# La familia se obtiene a través del niño dueño de la rutina.
|
||||||
|
- changeSet:
|
||||||
|
id: 301-seed-routine-task-from-existing
|
||||||
|
author: recordalexia
|
||||||
|
changes:
|
||||||
|
- sql:
|
||||||
|
sql: >-
|
||||||
|
INSERT INTO routine_task (family_id, label_es, label_ca, icon, color, coins_reward)
|
||||||
|
SELECT DISTINCT c.family_id, ar.label_es, ar.label_ca, ar.icon, ar.color, NULL
|
||||||
|
FROM afternoon_routine ar
|
||||||
|
JOIN child c ON c.id = ar.child_id;
|
||||||
|
rollback:
|
||||||
|
- sql:
|
||||||
|
sql: DELETE FROM routine_task;
|
||||||
|
|
||||||
|
- changeSet:
|
||||||
|
id: 302-add-routine-task-id
|
||||||
|
author: recordalexia
|
||||||
|
changes:
|
||||||
|
- addColumn:
|
||||||
|
tableName: afternoon_routine
|
||||||
|
columns:
|
||||||
|
- column: { name: routine_task_id, type: BIGINT }
|
||||||
|
rollback:
|
||||||
|
- dropColumn:
|
||||||
|
tableName: afternoon_routine
|
||||||
|
columnName: routine_task_id
|
||||||
|
|
||||||
|
# Enlazar cada asignación con su rutina de catálogo por (familia + definición exacta).
|
||||||
|
- changeSet:
|
||||||
|
id: 303-link-routine-task
|
||||||
|
author: recordalexia
|
||||||
|
changes:
|
||||||
|
# Subconsulta correlacionada (SQL estándar, portable a H2 y PostgreSQL).
|
||||||
|
- sql:
|
||||||
|
sql: >-
|
||||||
|
UPDATE afternoon_routine ar
|
||||||
|
SET routine_task_id = (
|
||||||
|
SELECT rt.id FROM routine_task rt
|
||||||
|
WHERE rt.family_id = (SELECT c.family_id FROM child c WHERE c.id = ar.child_id)
|
||||||
|
AND rt.label_es = ar.label_es
|
||||||
|
AND rt.label_ca = ar.label_ca
|
||||||
|
AND rt.icon = ar.icon
|
||||||
|
AND rt.color = ar.color
|
||||||
|
);
|
||||||
|
rollback: [] # no-op: el enlace se deshace al recrear las columnas en 304
|
||||||
|
|
||||||
|
# Cerrar: NOT NULL + FK, y eliminar las columnas de definición ya migradas.
|
||||||
|
- changeSet:
|
||||||
|
id: 304-finalize-afternoon-routine
|
||||||
|
author: recordalexia
|
||||||
|
changes:
|
||||||
|
- addNotNullConstraint:
|
||||||
|
tableName: afternoon_routine
|
||||||
|
columnName: routine_task_id
|
||||||
|
columnDataType: BIGINT
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: afternoon_routine
|
||||||
|
baseColumnNames: routine_task_id
|
||||||
|
referencedTableName: routine_task
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_routine_assignment_task
|
||||||
|
- dropColumn: { tableName: afternoon_routine, columnName: label_es }
|
||||||
|
- dropColumn: { tableName: afternoon_routine, columnName: label_ca }
|
||||||
|
- dropColumn: { tableName: afternoon_routine, columnName: icon }
|
||||||
|
- dropColumn: { tableName: afternoon_routine, columnName: color }
|
||||||
|
rollback:
|
||||||
|
# Recrear las columnas de definición y repoblarlas desde el catálogo.
|
||||||
|
- addColumn:
|
||||||
|
tableName: afternoon_routine
|
||||||
|
columns:
|
||||||
|
- column: { name: label_es, type: VARCHAR(120) }
|
||||||
|
- column: { name: label_ca, type: VARCHAR(120) }
|
||||||
|
- column: { name: icon, type: VARCHAR(16) }
|
||||||
|
- column: { name: color, type: VARCHAR(9) }
|
||||||
|
- sql:
|
||||||
|
sql: >-
|
||||||
|
UPDATE afternoon_routine ar
|
||||||
|
SET label_es = (SELECT rt.label_es FROM routine_task rt WHERE rt.id = ar.routine_task_id),
|
||||||
|
label_ca = (SELECT rt.label_ca FROM routine_task rt WHERE rt.id = ar.routine_task_id),
|
||||||
|
icon = (SELECT rt.icon FROM routine_task rt WHERE rt.id = ar.routine_task_id),
|
||||||
|
color = (SELECT rt.color FROM routine_task rt WHERE rt.id = ar.routine_task_id);
|
||||||
|
- dropForeignKeyConstraint:
|
||||||
|
baseTableName: afternoon_routine
|
||||||
|
constraintName: fk_routine_assignment_task
|
||||||
@@ -7,12 +7,14 @@ import es.asepeyo.recordalexia.domain.AfternoonRoutine;
|
|||||||
import es.asepeyo.recordalexia.domain.Child;
|
import es.asepeyo.recordalexia.domain.Child;
|
||||||
import es.asepeyo.recordalexia.domain.DailyTask;
|
import es.asepeyo.recordalexia.domain.DailyTask;
|
||||||
import es.asepeyo.recordalexia.domain.Family;
|
import es.asepeyo.recordalexia.domain.Family;
|
||||||
|
import es.asepeyo.recordalexia.domain.RoutineTask;
|
||||||
import es.asepeyo.recordalexia.domain.Slot;
|
import es.asepeyo.recordalexia.domain.Slot;
|
||||||
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
||||||
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.FamilyRepository;
|
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||||
|
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
|
||||||
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -32,6 +34,7 @@ class DayGenerationServiceTest {
|
|||||||
@Autowired private ActivityRepository activityRepository;
|
@Autowired private ActivityRepository activityRepository;
|
||||||
@Autowired private WeeklyTemplateEntryRepository templateRepository;
|
@Autowired private WeeklyTemplateEntryRepository templateRepository;
|
||||||
@Autowired private AfternoonRoutineRepository routineRepository;
|
@Autowired private AfternoonRoutineRepository routineRepository;
|
||||||
|
@Autowired private RoutineTaskRepository routineTaskRepository;
|
||||||
@Autowired private FamilyRepository familyRepository;
|
@Autowired private FamilyRepository familyRepository;
|
||||||
|
|
||||||
private Family family;
|
private Family family;
|
||||||
@@ -48,7 +51,8 @@ class DayGenerationServiceTest {
|
|||||||
actividad.setFamily(family);
|
actividad.setFamily(family);
|
||||||
Activity mates = activityRepository.save(actividad);
|
Activity mates = activityRepository.save(actividad);
|
||||||
templateRepository.save(new WeeklyTemplateEntry(nino, dia, mates, 0));
|
templateRepository.save(new WeeklyTemplateEntry(nino, dia, mates, 0));
|
||||||
routineRepository.save(new AfternoonRoutine(nino, dia, "Deberes", "Deures", "📝", "#F2A65A", 0));
|
RoutineTask deberes = routineTaskRepository.save(routineTask("Deberes", "Deures", "📝", "#F2A65A"));
|
||||||
|
routineRepository.save(new AfternoonRoutine(nino, dia, deberes, 0));
|
||||||
|
|
||||||
List<DailyTask> primera = dayGenerationService.generateIfAbsent(nino.getId(), fecha);
|
List<DailyTask> primera = dayGenerationService.generateIfAbsent(nino.getId(), fecha);
|
||||||
|
|
||||||
@@ -63,6 +67,45 @@ class DayGenerationServiceTest {
|
|||||||
assertThat(segunda).hasSize(2);
|
assertThat(segunda).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aplicaLaCascadaDeMonedasDeLaRutina() {
|
||||||
|
family = familyRepository.save(new Family("coins@x.com", "h", "C", "p"));
|
||||||
|
LocalDate fecha = LocalDate.of(2026, 6, 22);
|
||||||
|
DayOfWeek dia = fecha.getDayOfWeek();
|
||||||
|
|
||||||
|
// El niño rinde 10 por tarea por defecto.
|
||||||
|
Child nino = childRepository.save(nuevoNino());
|
||||||
|
nino.setCoinsPerTask(10);
|
||||||
|
|
||||||
|
// (a) Override por día (8) gana al catálogo (5) y al niño (10).
|
||||||
|
RoutineTask conDefecto = routineTaskRepository.save(routineTask("Merienda", "Berenar", "🥪", "#F4C95D"));
|
||||||
|
conDefecto.setCoinsReward(5);
|
||||||
|
AfternoonRoutine conOverride = new AfternoonRoutine(nino, dia, conDefecto, 0);
|
||||||
|
conOverride.setCoinsReward(8);
|
||||||
|
routineRepository.save(conOverride);
|
||||||
|
|
||||||
|
// (b) Sin override → vale el del catálogo (5).
|
||||||
|
RoutineTask soloCatalogo = routineTaskRepository.save(routineTask("Leer", "Llegir", "📖", "#F2A65A"));
|
||||||
|
soloCatalogo.setCoinsReward(5);
|
||||||
|
routineRepository.save(new AfternoonRoutine(nino, dia, soloCatalogo, 1));
|
||||||
|
|
||||||
|
// (c) Sin override ni catálogo → cae a coinsPerTask del niño (10).
|
||||||
|
RoutineTask sinNada = routineTaskRepository.save(routineTask("Recoger", "Recollir", "🧹", "#7FBF6B"));
|
||||||
|
routineRepository.save(new AfternoonRoutine(nino, dia, sinNada, 2));
|
||||||
|
|
||||||
|
List<DailyTask> tareas = dayGenerationService.generateIfAbsent(nino.getId(), fecha);
|
||||||
|
|
||||||
|
assertThat(tareas).filteredOn(t -> t.getSlot() == Slot.AFTERNOON)
|
||||||
|
.extracting(DailyTask::getCoinsReward)
|
||||||
|
.containsExactly(8, 5, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoutineTask routineTask(String es, String ca, String icon, String color) {
|
||||||
|
RoutineTask rt = new RoutineTask(es, ca, icon, color);
|
||||||
|
rt.setFamily(family);
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
private Child nuevoNino() {
|
private Child nuevoNino() {
|
||||||
Child c = new Child();
|
Child c = new Child();
|
||||||
c.setFamily(family);
|
c.setFamily(family);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import es.asepeyo.recordalexia.domain.Child;
|
|||||||
import es.asepeyo.recordalexia.domain.Family;
|
import es.asepeyo.recordalexia.domain.Family;
|
||||||
import es.asepeyo.recordalexia.domain.FamilySession;
|
import es.asepeyo.recordalexia.domain.FamilySession;
|
||||||
import es.asepeyo.recordalexia.domain.Reward;
|
import es.asepeyo.recordalexia.domain.Reward;
|
||||||
|
import es.asepeyo.recordalexia.domain.RoutineTask;
|
||||||
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
import es.asepeyo.recordalexia.domain.WeeklyTemplateEntry;
|
||||||
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
import es.asepeyo.recordalexia.repository.ActivityRepository;
|
||||||
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
import es.asepeyo.recordalexia.repository.AfternoonRoutineRepository;
|
||||||
@@ -21,6 +22,7 @@ import es.asepeyo.recordalexia.repository.ChildRepository;
|
|||||||
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
import es.asepeyo.recordalexia.repository.FamilyRepository;
|
||||||
import es.asepeyo.recordalexia.repository.FamilySessionRepository;
|
import es.asepeyo.recordalexia.repository.FamilySessionRepository;
|
||||||
import es.asepeyo.recordalexia.repository.RewardRepository;
|
import es.asepeyo.recordalexia.repository.RewardRepository;
|
||||||
|
import es.asepeyo.recordalexia.repository.RoutineTaskRepository;
|
||||||
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
import es.asepeyo.recordalexia.repository.WeeklyTemplateEntryRepository;
|
||||||
import es.asepeyo.recordalexia.security.SessionAuthFilter;
|
import es.asepeyo.recordalexia.security.SessionAuthFilter;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
@@ -46,6 +48,7 @@ class TodayFlowIT {
|
|||||||
@Autowired private ActivityRepository activityRepository;
|
@Autowired private ActivityRepository activityRepository;
|
||||||
@Autowired private WeeklyTemplateEntryRepository templateRepository;
|
@Autowired private WeeklyTemplateEntryRepository templateRepository;
|
||||||
@Autowired private AfternoonRoutineRepository routineRepository;
|
@Autowired private AfternoonRoutineRepository routineRepository;
|
||||||
|
@Autowired private RoutineTaskRepository routineTaskRepository;
|
||||||
@Autowired private RewardRepository rewardRepository;
|
@Autowired private RewardRepository rewardRepository;
|
||||||
@Autowired private FamilyRepository familyRepository;
|
@Autowired private FamilyRepository familyRepository;
|
||||||
@Autowired private FamilySessionRepository sessionRepository;
|
@Autowired private FamilySessionRepository sessionRepository;
|
||||||
@@ -62,7 +65,10 @@ class TodayFlowIT {
|
|||||||
mates.setFamily(family);
|
mates.setFamily(family);
|
||||||
mates = activityRepository.save(mates);
|
mates = activityRepository.save(mates);
|
||||||
templateRepository.save(new WeeklyTemplateEntry(nino, hoy, mates, 0));
|
templateRepository.save(new WeeklyTemplateEntry(nino, hoy, mates, 0));
|
||||||
routineRepository.save(new AfternoonRoutine(nino, hoy, "Deberes", "Deures", "📝", "#F2A65A", 0));
|
RoutineTask deberes = new RoutineTask("Deberes", "Deures", "📝", "#F2A65A");
|
||||||
|
deberes.setFamily(family);
|
||||||
|
deberes = routineTaskRepository.save(deberes);
|
||||||
|
routineRepository.save(new AfternoonRoutine(nino, hoy, deberes, 0));
|
||||||
Reward premio = new Reward("Tablet", "Tauleta", "🎮", "#5B8DEF", 5);
|
Reward premio = new Reward("Tablet", "Tauleta", "🎮", "#5B8DEF", 5);
|
||||||
premio.setFamily(family);
|
premio.setFamily(family);
|
||||||
premio = rewardRepository.save(premio);
|
premio = rewardRepository.save(premio);
|
||||||
|
|||||||
Reference in New Issue
Block a user