From f82ba251608ffa2124d722f2d748f74c06196fbe Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Tue, 25 Nov 2025 21:22:30 -0500 Subject: [PATCH] round 2 --- api/child_api.py | 46 ++++ api/child_rewards.py | 15 ++ api/reward_api.py | 44 +++- models/base.py | 32 +++ models/child.py | 20 +- models/image.py | 24 +- models/reward.py | 20 +- models/task.py | 20 +- .../src/components/ChildrenListView.vue | 16 +- .../src/components/reward/RewardEditView.vue | 244 ++++++++++++++++++ .../src/components/reward/RewardList.vue | 212 +++++++++++++++ .../src/components/reward/RewardView.vue | 143 +++++++++- .../src/components/task/TaskEditView.vue | 1 - web/vue-app/src/layout/ChildLayout.vue | 105 +++++--- web/vue-app/src/layout/ParentLayout.vue | 6 +- web/vue-app/src/router/index.ts | 12 + 16 files changed, 860 insertions(+), 100 deletions(-) create mode 100644 api/child_rewards.py create mode 100644 models/base.py create mode 100644 web/vue-app/src/components/reward/RewardEditView.vue create mode 100644 web/vue-app/src/components/reward/RewardList.vue diff --git a/api/child_api.py b/api/child_api.py index a81e8f9..f36a2ba 100644 --- a/api/child_api.py +++ b/api/child_api.py @@ -3,6 +3,7 @@ from tinydb import Query from db.db import child_db, task_db, reward_db from api.reward_status import RewardStatus from api.child_tasks import ChildTask +from api.child_rewards import ChildReward from models.child import Child from models.task import Task @@ -227,6 +228,51 @@ def remove_reward_from_child(id): return jsonify({'message': f'Reward {reward_id} removed from {child["name"]}.'}), 200 return jsonify({'error': 'Reward not assigned to child'}), 400 +@child_api.route('/child//list-rewards', methods=['GET']) +def list_child_rewards(id): + ChildQuery = Query() + result = child_db.search(ChildQuery.id == id) + if not result: + return jsonify({'error': 'Child not found'}), 404 + + child = result[0] + reward_ids = child.get('rewards', []) + + RewardQuery = Query() + child_rewards = [] + for rid in reward_ids: + reward = reward_db.get(RewardQuery.id == rid) + if not reward: + continue + cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id')) + child_rewards.append(cr.to_dict()) + + return jsonify({'rewards': child_rewards}), 200 + +@child_api.route('/child//list-assignable-rewards', methods=['GET']) +def list_assignable_rewards(id): + ChildQuery = Query() + result = child_db.search(ChildQuery.id == id) + if not result: + return jsonify({'error': 'Child not found'}), 404 + + child = result[0] + assigned_ids = set(child.get('rewards', [])) + + all_reward_ids = [r.get('id') for r in reward_db.all() if r and r.get('id')] + assignable_ids = [rid for rid in all_reward_ids if rid not in assigned_ids] + + RewardQuery = Query() + assignable_rewards = [] + for rid in assignable_ids: + reward = reward_db.get(RewardQuery.id == rid) + if not reward: + continue + cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id')) + assignable_rewards.append(cr.to_dict()) + + return jsonify({'rewards': assignable_rewards, 'count': len(assignable_rewards)}), 200 + @child_api.route('/child//trigger-reward', methods=['POST']) def trigger_child_reward(id): data = request.get_json() diff --git a/api/child_rewards.py b/api/child_rewards.py new file mode 100644 index 0000000..97c8b09 --- /dev/null +++ b/api/child_rewards.py @@ -0,0 +1,15 @@ +# api/child_rewards.py +class ChildReward: + def __init__(self, name: str, cost: int, image_id: str, reward_id: str): + self.name = name + self.cost = cost + self.image_id = image_id + self.id = reward_id + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'cost': self.cost, + 'image_id': self.image_id + } diff --git a/api/reward_api.py b/api/reward_api.py index 212f2f9..8c372a4 100644 --- a/api/reward_api.py +++ b/api/reward_api.py @@ -19,6 +19,8 @@ def add_reward(): reward_db.insert(reward.to_dict()) return jsonify({'message': f'Reward {name} added.'}), 201 + + @reward_api.route('/reward/', methods=['GET']) def get_reward(id): RewardQuery = Query() @@ -45,4 +47,44 @@ def delete_reward(id): rewards.remove(id) child_db.update({'rewards': rewards}, ChildQuery.id == child.get('id')) return jsonify({'message': f'Reward {id} deleted.'}), 200 - return jsonify({'error': 'Reward not found'}), 404 \ No newline at end of file + return jsonify({'error': 'Reward not found'}), 404 + +@reward_api.route('/reward//edit', methods=['PUT']) +def edit_reward(id): + RewardQuery = Query() + existing = reward_db.get(RewardQuery.id == id) + if not existing: + return jsonify({'error': 'Reward not found'}), 404 + + data = request.get_json(force=True) or {} + updates = {} + + if 'name' in data: + name = (data.get('name') or '').strip() + if not name: + return jsonify({'error': 'Name cannot be empty'}), 400 + updates['name'] = name + + if 'description' in data: + desc = (data.get('description') or '').strip() + if not desc: + return jsonify({'error': 'Description cannot be empty'}), 400 + updates['description'] = desc + + if 'cost' in data: + cost = data.get('cost') + if not isinstance(cost, int): + return jsonify({'error': 'Cost must be an integer'}), 400 + if cost <= 0: + return jsonify({'error': 'Cost must be a positive integer'}), 400 + updates['cost'] = cost + + if 'image_id' in data: + updates['image_id'] = data.get('image_id', '') + + if not updates: + return jsonify({'error': 'No valid fields to update'}), 400 + + reward_db.update(updates, RewardQuery.id == id) + updated = reward_db.get(RewardQuery.id == id) + return jsonify(updated), 200 diff --git a/models/base.py b/models/base.py new file mode 100644 index 0000000..79b9878 --- /dev/null +++ b/models/base.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass, field +import uuid +import time + +@dataclass +class BaseModel: + id: str = field(init=False) + created_at: float = field(init=False) + updated_at: float = field(init=False) + + def __post_init__(self): + self.id = str(uuid.uuid4()) + now = time.time() + self.created_at = now + self.updated_at = now + + def touch(self): + self.updated_at = time.time() + + def to_dict(self): + return { + 'id': self.id, + 'created_at': self.created_at, + 'updated_at': self.updated_at + } + + @classmethod + def _apply_base_fields(cls, obj, d: dict): + obj.id = d.get('id', obj.id) + obj.created_at = d.get('created_at', obj.created_at) + obj.updated_at = d.get('updated_at', obj.updated_at) + return obj diff --git a/models/child.py b/models/child.py index bae131f..33331f2 100644 --- a/models/child.py +++ b/models/child.py @@ -1,24 +1,18 @@ from dataclasses import dataclass, field -import uuid +from models.base import BaseModel @dataclass -class Child: +class Child(BaseModel): name: str age: int | None = None tasks: list[str] = field(default_factory=list) rewards: list[str] = field(default_factory=list) points: int = 0 image_id: str | None = None - id: str | None = None - - def __post_init__(self): - if self.id is None: - self.id = str(uuid.uuid4()) @classmethod def from_dict(cls, d: dict): - return cls( - id=d.get('id'), + obj = cls( name=d.get('name'), age=d.get('age'), tasks=d.get('tasks', []), @@ -26,14 +20,16 @@ class Child: points=d.get('points', 0), image_id=d.get('image_id') ) + return cls._apply_base_fields(obj, d) def to_dict(self): - return { - 'id': self.id, + base = super().to_dict() + base.update({ 'name': self.name, 'age': self.age, 'tasks': self.tasks, 'rewards': self.rewards, 'points': self.points, 'image_id': self.image_id - } + }) + return base diff --git a/models/image.py b/models/image.py index a7816d5..046cf67 100644 --- a/models/image.py +++ b/models/image.py @@ -1,30 +1,26 @@ from dataclasses import dataclass -import uuid +from models.base import BaseModel @dataclass -class Image: +class Image(BaseModel): type: int extension: str permanent: bool = False - id: str | None = None - - def __post_init__(self): - if self.id is None: - self.id = str(uuid.uuid4()) @classmethod def from_dict(cls, d: dict): - return cls( - id=d.get('id'), + obj = cls( type=d.get('type'), - permanent=d.get('permanent', False), - extension=d.get('extension') + extension=d.get('extension'), + permanent=d.get('permanent', False) ) + return cls._apply_base_fields(obj, d) def to_dict(self): - return { - 'id': self.id, + base = super().to_dict() + base.update({ 'type': self.type, 'permanent': self.permanent, 'extension': self.extension - } + }) + return base diff --git a/models/reward.py b/models/reward.py index 6c8f697..c8ae1ac 100644 --- a/models/reward.py +++ b/models/reward.py @@ -1,33 +1,29 @@ from dataclasses import dataclass -import uuid +from models.base import BaseModel @dataclass -class Reward: +class Reward(BaseModel): name: str description: str cost: int image_id: str | None = None - id: str | None = None - - def __post_init__(self): - if self.id is None: - self.id = str(uuid.uuid4()) @classmethod def from_dict(cls, d: dict): - return cls( - id=d.get('id'), + obj = cls( name=d.get('name'), description=d.get('description'), cost=d.get('cost', 0), image_id=d.get('image_id') ) + return cls._apply_base_fields(obj, d) def to_dict(self): - return { - 'id': self.id, + base = super().to_dict() + base.update({ 'name': self.name, 'description': self.description, 'cost': self.cost, 'image_id': self.image_id - } + }) + return base diff --git a/models/task.py b/models/task.py index fae0ea9..0a22bc1 100644 --- a/models/task.py +++ b/models/task.py @@ -1,33 +1,29 @@ from dataclasses import dataclass -import uuid +from models.base import BaseModel @dataclass -class Task: +class Task(BaseModel): name: str points: int is_good: bool image_id: str | None = None - id: str | None = None - - def __post_init__(self): - if self.id is None: - self.id = str(uuid.uuid4()) @classmethod def from_dict(cls, d: dict): - return cls( - id=d.get('id'), + obj = cls( name=d.get('name'), points=d.get('points', 0), is_good=d.get('is_good', True), image_id=d.get('image_id') ) + return cls._apply_base_fields(obj, d) def to_dict(self): - return { - 'id': self.id, + base = super().to_dict() + base.update({ 'name': self.name, 'points': self.points, 'is_good': self.is_good, 'image_id': self.image_id - } + }) + return base diff --git a/web/vue-app/src/components/ChildrenListView.vue b/web/vue-app/src/components/ChildrenListView.vue index c316e6a..1be1194 100644 --- a/web/vue-app/src/components/ChildrenListView.vue +++ b/web/vue-app/src/components/ChildrenListView.vue @@ -1,5 +1,5 @@ + + diff --git a/web/vue-app/src/components/reward/RewardList.vue b/web/vue-app/src/components/reward/RewardList.vue new file mode 100644 index 0000000..90052d3 --- /dev/null +++ b/web/vue-app/src/components/reward/RewardList.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/web/vue-app/src/components/reward/RewardView.vue b/web/vue-app/src/components/reward/RewardView.vue index c67e168..6b5dd88 100644 --- a/web/vue-app/src/components/reward/RewardView.vue +++ b/web/vue-app/src/components/reward/RewardView.vue @@ -1,14 +1,145 @@ + + diff --git a/web/vue-app/src/components/task/TaskEditView.vue b/web/vue-app/src/components/task/TaskEditView.vue index 514a062..2c1d977 100644 --- a/web/vue-app/src/components/task/TaskEditView.vue +++ b/web/vue-app/src/components/task/TaskEditView.vue @@ -195,7 +195,6 @@ function onAddImage({ id, file }: { id: string; file: File }) { margin: 0 auto; display: flex; flex-direction: column; - height: 100vh; overflow-y: auto; box-sizing: border-box; } diff --git a/web/vue-app/src/layout/ChildLayout.vue b/web/vue-app/src/layout/ChildLayout.vue index 0b38ebe..33ab538 100644 --- a/web/vue-app/src/layout/ChildLayout.vue +++ b/web/vue-app/src/layout/ChildLayout.vue @@ -20,9 +20,11 @@ const showBack = computed(() => route.path !== '/child')