feat: Implement routines feature with CRUD operations and child assignment
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s

- Add backend routines management with add, get, update, delete, and list functionalities.
- Create models for Routine, RoutineItem, RoutineSchedule, and RoutineExtension.
- Develop event types for routine confirmation and modification.
- Implement frontend components for routine assignment, confirmation dialog, and routine management views.
- Add unit tests for routine API and integration tests for routine CRUD flow.
- Create end-to-end test plan for routines feature covering parent and child interactions.
This commit is contained in:
2026-05-05 09:08:19 -04:00
parent 082097b4f9
commit eb775ba7d8
41 changed files with 2847 additions and 29 deletions

View File

@@ -69,6 +69,10 @@ class LockedTable:
child_path = os.path.join(base_dir, 'children.json')
task_path = os.path.join(base_dir, 'tasks.json')
routine_path = os.path.join(base_dir, 'routines.json')
routine_items_path = os.path.join(base_dir, 'routine_items.json')
routine_schedules_path = os.path.join(base_dir, 'routine_schedules.json')
routine_extensions_path = os.path.join(base_dir, 'routine_extensions.json')
reward_path = os.path.join(base_dir, 'rewards.json')
image_path = os.path.join(base_dir, 'images.json')
pending_reward_path = os.path.join(base_dir, 'pending_rewards.json')
@@ -85,6 +89,10 @@ digest_action_tokens_path = os.path.join(base_dir, 'digest_action_tokens.json')
# Use separate TinyDB instances/files for each collection
_child_db = TinyDB(child_path, indent=2)
_task_db = TinyDB(task_path, indent=2)
_routine_db = TinyDB(routine_path, indent=2)
_routine_items_db = TinyDB(routine_items_path, indent=2)
_routine_schedules_db = TinyDB(routine_schedules_path, indent=2)
_routine_extensions_db = TinyDB(routine_extensions_path, indent=2)
_reward_db = TinyDB(reward_path, indent=2)
_image_db = TinyDB(image_path, indent=2)
_pending_rewards_db = TinyDB(pending_reward_path, indent=2)
@@ -101,6 +109,10 @@ _digest_action_tokens_db = TinyDB(digest_action_tokens_path, indent=2)
# Expose table objects wrapped with locking
child_db = LockedTable(_child_db)
task_db = LockedTable(_task_db)
routine_db = LockedTable(_routine_db)
routine_items_db = LockedTable(_routine_items_db)
routine_schedules_db = LockedTable(_routine_schedules_db)
routine_extensions_db = LockedTable(_routine_extensions_db)
reward_db = LockedTable(_reward_db)
image_db = LockedTable(_image_db)
pending_reward_db = LockedTable(_pending_rewards_db)
@@ -117,6 +129,10 @@ digest_action_tokens_db = LockedTable(_digest_action_tokens_db)
if os.environ.get('DB_ENV', 'prod') == 'test':
child_db.truncate()
task_db.truncate()
routine_db.truncate()
routine_items_db.truncate()
routine_schedules_db.truncate()
routine_extensions_db.truncate()
reward_db.truncate()
image_db.truncate()
pending_reward_db.truncate()

View File

@@ -0,0 +1,41 @@
from tinydb import Query
from db.db import routine_extensions_db
from models.routine_extension import RoutineExtension
def get_extension(child_id: str, routine_id: str, date: str) -> RoutineExtension | None:
q = Query()
result = routine_extensions_db.search(
(q.child_id == child_id) & (q.routine_id == routine_id) & (q.date == date)
)
if not result:
return None
return RoutineExtension.from_dict(result[0])
def add_extension(extension: RoutineExtension) -> None:
routine_extensions_db.insert(extension.to_dict())
def delete_extensions_for_child(child_id: str) -> None:
q = Query()
routine_extensions_db.remove(q.child_id == child_id)
def delete_extensions_for_routine(routine_id: str) -> None:
q = Query()
routine_extensions_db.remove(q.routine_id == routine_id)
def delete_extension_for_child_routine(child_id: str, routine_id: str) -> None:
q = Query()
routine_extensions_db.remove((q.child_id == child_id) & (q.routine_id == routine_id))
def get_extension_for_child_routine(child_id: str, routine_id: str) -> RoutineExtension | None:
q = Query()
results = routine_extensions_db.search((q.child_id == child_id) & (q.routine_id == routine_id))
if not results:
return None
latest = max(results, key=lambda r: r.get('date', ''))
return RoutineExtension.from_dict(latest)

View File

@@ -0,0 +1,45 @@
from tinydb import Query
from db.db import routine_items_db
from models.routine_item import RoutineItem
def add_item(item: RoutineItem) -> None:
routine_items_db.insert(item.to_dict())
def get_item(item_id: str) -> RoutineItem | None:
q = Query()
result = routine_items_db.search(q.id == item_id)
if not result:
return None
return RoutineItem.from_dict(result[0])
def get_items_for_routine(routine_id: str) -> list[RoutineItem]:
q = Query()
results = routine_items_db.search(q.routine_id == routine_id)
items = [RoutineItem.from_dict(r) for r in results]
return sorted(items, key=lambda i: (i.order, i.created_at))
def update_item(item: RoutineItem) -> bool:
q = Query()
existing = routine_items_db.get(q.id == item.id)
if not existing:
return False
routine_items_db.update(item.to_dict(), q.id == item.id)
return True
def delete_item(item_id: str) -> bool:
q = Query()
existing = routine_items_db.get(q.id == item_id)
if not existing:
return False
routine_items_db.remove(q.id == item_id)
return True
def delete_for_routine(routine_id: str) -> None:
q = Query()
routine_items_db.remove(q.routine_id == routine_id)

View File

@@ -0,0 +1,42 @@
from tinydb import Query
from db.db import routine_schedules_db
from models.routine_schedule import RoutineSchedule
def get_schedule(child_id: str, routine_id: str) -> RoutineSchedule | None:
q = Query()
result = routine_schedules_db.search((q.child_id == child_id) & (q.routine_id == routine_id))
if not result:
return None
return RoutineSchedule.from_dict(result[0])
def upsert_schedule(schedule: RoutineSchedule) -> None:
q = Query()
existing = routine_schedules_db.get((q.child_id == schedule.child_id) & (q.routine_id == schedule.routine_id))
if existing:
routine_schedules_db.update(
schedule.to_dict(),
(q.child_id == schedule.child_id) & (q.routine_id == schedule.routine_id)
)
else:
routine_schedules_db.insert(schedule.to_dict())
def delete_schedule(child_id: str, routine_id: str) -> bool:
q = Query()
existing = routine_schedules_db.get((q.child_id == child_id) & (q.routine_id == routine_id))
if not existing:
return False
routine_schedules_db.remove((q.child_id == child_id) & (q.routine_id == routine_id))
return True
def delete_schedules_for_child(child_id: str) -> None:
q = Query()
routine_schedules_db.remove(q.child_id == child_id)
def delete_schedules_for_routine(routine_id: str) -> None:
q = Query()
routine_schedules_db.remove(q.routine_id == routine_id)

39
backend/db/routines.py Normal file
View File

@@ -0,0 +1,39 @@
from tinydb import Query
from db.db import routine_db
from models.routine import Routine
def add_routine(routine: Routine) -> None:
routine_db.insert(routine.to_dict())
def get_routine(routine_id: str) -> Routine | None:
q = Query()
result = routine_db.search(q.id == routine_id)
if not result:
return None
return Routine.from_dict(result[0])
def update_routine(routine: Routine) -> bool:
q = Query()
existing = routine_db.get(q.id == routine.id)
if not existing:
return False
routine_db.update(routine.to_dict(), q.id == routine.id)
return True
def delete_routine(routine_id: str) -> bool:
q = Query()
existing = routine_db.get(q.id == routine_id)
if not existing:
return False
routine_db.remove(q.id == routine_id)
return True
def list_routines_for_user(user_id: str) -> list[Routine]:
q = Query()
results = routine_db.search((q.user_id == user_id) | (q.user_id == None))
return [Routine.from_dict(r) for r in results]