Add TimeSelector and ScheduleModal components with tests
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m45s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m45s
- Implemented TimeSelector component for selecting time with AM/PM toggle and minute/hour increment/decrement functionality. - Created ScheduleModal component for scheduling chores with options for specific days or intervals. - Added utility functions for scheduling logic in scheduleUtils.ts. - Developed comprehensive tests for TimeSelector and scheduleUtils functions to ensure correct behavior.
This commit is contained in:
@@ -28,6 +28,9 @@ from models.tracking_event import TrackingEvent
|
||||
from api.utils import get_validated_user_id
|
||||
from utils.tracking_logger import log_tracking_event
|
||||
from collections import defaultdict
|
||||
from db.chore_schedules import get_schedule
|
||||
from db.task_extensions import get_extension
|
||||
from datetime import date as date_type
|
||||
import logging
|
||||
|
||||
child_api = Blueprint('child_api', __name__)
|
||||
@@ -273,6 +276,18 @@ def list_child_tasks(id):
|
||||
ct_dict = ct.to_dict()
|
||||
if custom_value is not None:
|
||||
ct_dict['custom_value'] = custom_value
|
||||
|
||||
# Attach schedule and most recent extension_date (client does all time math)
|
||||
if task.get('is_good'):
|
||||
schedule = get_schedule(id, tid)
|
||||
ct_dict['schedule'] = schedule.to_dict() if schedule else None
|
||||
today_str = date_type.today().isoformat()
|
||||
ext = get_extension(id, tid, today_str)
|
||||
ct_dict['extension_date'] = ext.date if ext else None
|
||||
else:
|
||||
ct_dict['schedule'] = None
|
||||
ct_dict['extension_date'] = None
|
||||
|
||||
child_tasks.append(ct_dict)
|
||||
|
||||
return jsonify({'tasks': child_tasks}), 200
|
||||
|
||||
144
backend/api/chore_schedule_api.py
Normal file
144
backend/api/chore_schedule_api.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from tinydb import Query
|
||||
from api.utils import get_validated_user_id, send_event_for_current_user
|
||||
from api.error_codes import ErrorCodes
|
||||
from db.db import child_db
|
||||
from db.chore_schedules import get_schedule, upsert_schedule, delete_schedule
|
||||
from db.task_extensions import get_extension, add_extension
|
||||
from models.chore_schedule import ChoreSchedule
|
||||
from models.task_extension import TaskExtension
|
||||
from events.types.event import Event
|
||||
from events.types.event_types import EventType
|
||||
from events.types.chore_schedule_modified import ChoreScheduleModified
|
||||
from events.types.chore_time_extended import ChoreTimeExtended
|
||||
import logging
|
||||
|
||||
chore_schedule_api = Blueprint('chore_schedule_api', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _validate_child(child_id: str, user_id: str):
|
||||
"""Return child dict if found and owned by user, else None."""
|
||||
ChildQuery = Query()
|
||||
result = child_db.search((ChildQuery.id == child_id) & (ChildQuery.user_id == user_id))
|
||||
return result[0] if result else None
|
||||
|
||||
|
||||
@chore_schedule_api.route('/child/<child_id>/task/<task_id>/schedule', methods=['GET'])
|
||||
def get_chore_schedule(child_id, task_id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
||||
|
||||
if not _validate_child(child_id, user_id):
|
||||
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
||||
|
||||
schedule = get_schedule(child_id, task_id)
|
||||
if not schedule:
|
||||
return jsonify({'error': 'Schedule not found'}), 404
|
||||
|
||||
return jsonify(schedule.to_dict()), 200
|
||||
|
||||
|
||||
@chore_schedule_api.route('/child/<child_id>/task/<task_id>/schedule', methods=['PUT'])
|
||||
def set_chore_schedule(child_id, task_id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
||||
|
||||
if not _validate_child(child_id, user_id):
|
||||
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
mode = data.get('mode')
|
||||
if mode not in ('days', 'interval'):
|
||||
return jsonify({'error': 'mode must be "days" or "interval"', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
|
||||
if mode == 'days':
|
||||
day_configs = data.get('day_configs', [])
|
||||
if not isinstance(day_configs, list):
|
||||
return jsonify({'error': 'day_configs must be a list', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
schedule = ChoreSchedule(
|
||||
child_id=child_id,
|
||||
task_id=task_id,
|
||||
mode='days',
|
||||
day_configs=day_configs,
|
||||
)
|
||||
else:
|
||||
interval_days = data.get('interval_days', 2)
|
||||
anchor_weekday = data.get('anchor_weekday', 0)
|
||||
interval_hour = data.get('interval_hour', 0)
|
||||
interval_minute = data.get('interval_minute', 0)
|
||||
if not isinstance(interval_days, int) or not (2 <= interval_days <= 7):
|
||||
return jsonify({'error': 'interval_days must be an integer between 2 and 7', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
if not isinstance(anchor_weekday, int) or not (0 <= anchor_weekday <= 6):
|
||||
return jsonify({'error': 'anchor_weekday must be an integer between 0 and 6', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
schedule = ChoreSchedule(
|
||||
child_id=child_id,
|
||||
task_id=task_id,
|
||||
mode='interval',
|
||||
interval_days=interval_days,
|
||||
anchor_weekday=anchor_weekday,
|
||||
interval_hour=interval_hour,
|
||||
interval_minute=interval_minute,
|
||||
)
|
||||
|
||||
upsert_schedule(schedule)
|
||||
|
||||
send_event_for_current_user(Event(
|
||||
EventType.CHORE_SCHEDULE_MODIFIED.value,
|
||||
ChoreScheduleModified(child_id, task_id, ChoreScheduleModified.OPERATION_SET)
|
||||
))
|
||||
|
||||
return jsonify(schedule.to_dict()), 200
|
||||
|
||||
|
||||
@chore_schedule_api.route('/child/<child_id>/task/<task_id>/schedule', methods=['DELETE'])
|
||||
def delete_chore_schedule(child_id, task_id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
||||
|
||||
if not _validate_child(child_id, user_id):
|
||||
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
||||
|
||||
removed = delete_schedule(child_id, task_id)
|
||||
if not removed:
|
||||
return jsonify({'error': 'Schedule not found'}), 404
|
||||
|
||||
send_event_for_current_user(Event(
|
||||
EventType.CHORE_SCHEDULE_MODIFIED.value,
|
||||
ChoreScheduleModified(child_id, task_id, ChoreScheduleModified.OPERATION_DELETED)
|
||||
))
|
||||
|
||||
return jsonify({'message': 'Schedule deleted'}), 200
|
||||
|
||||
|
||||
@chore_schedule_api.route('/child/<child_id>/task/<task_id>/extend', methods=['POST'])
|
||||
def extend_chore_time(child_id, task_id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
||||
|
||||
if not _validate_child(child_id, user_id):
|
||||
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
date = data.get('date')
|
||||
if not date or not isinstance(date, str):
|
||||
return jsonify({'error': 'date is required (ISO date string)', 'code': ErrorCodes.MISSING_FIELD}), 400
|
||||
|
||||
# 409 if already extended for this date
|
||||
existing = get_extension(child_id, task_id, date)
|
||||
if existing:
|
||||
return jsonify({'error': 'Chore already extended for this date', 'code': 'ALREADY_EXTENDED'}), 409
|
||||
|
||||
extension = TaskExtension(child_id=child_id, task_id=task_id, date=date)
|
||||
add_extension(extension)
|
||||
|
||||
send_event_for_current_user(Event(
|
||||
EventType.CHORE_TIME_EXTENDED.value,
|
||||
ChoreTimeExtended(child_id, task_id)
|
||||
))
|
||||
|
||||
return jsonify(extension.to_dict()), 200
|
||||
Reference in New Issue
Block a user