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, delete_extension_for_child_task 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//task//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//task//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 default_hour = data.get('default_hour', 8) default_minute = data.get('default_minute', 0) schedule = ChoreSchedule( child_id=child_id, task_id=task_id, mode='days', day_configs=day_configs, default_hour=default_hour, default_minute=default_minute, ) 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, ) delete_extension_for_child_task(child_id, task_id) 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//task//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//task//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