feat: add chore, kindness, and penalty management components
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s

- Implemented ChoreAssignView for assigning chores to children.
- Created ChoreConfirmDialog for confirming chore completion.
- Developed KindnessAssignView for assigning kindness acts.
- Added PenaltyAssignView for assigning penalties.
- Introduced ChoreEditView and ChoreView for editing and viewing chores.
- Created KindnessEditView and KindnessView for managing kindness acts.
- Developed PenaltyEditView and PenaltyView for managing penalties.
- Added TaskSubNav for navigation between chores, kindness acts, and penalties.
This commit is contained in:
2026-02-28 11:25:56 -05:00
parent 65e987ceb6
commit d7316bb00a
61 changed files with 7364 additions and 647 deletions

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
Migration script: Convert legacy is_good field to type field across all data.
Steps:
1. tasks.json: is_good=True → type='chore', is_good=False → type='penalty'. Remove is_good field.
2. pending_rewards.json → pending_confirmations.json: Convert PendingReward records to
PendingConfirmation format with entity_type='reward'.
3. tracking_events.json: Update entity_type='task''chore' or 'penalty' based on the
referenced task's old is_good value.
4. child_overrides.json: Update entity_type='task''chore' or 'penalty' based on the
referenced task's old is_good value.
Usage:
cd backend
python -m scripts.migrate_tasks_to_types [--dry-run]
"""
import json
import os
import sys
import shutil
from datetime import datetime
DRY_RUN = '--dry-run' in sys.argv
DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'db')
def load_json(filename: str) -> dict:
path = os.path.join(DATA_DIR, filename)
if not os.path.exists(path):
return {"_default": {}}
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(filename: str, data: dict) -> None:
path = os.path.join(DATA_DIR, filename)
if DRY_RUN:
print(f" [DRY RUN] Would write {path}")
return
# Backup original
backup_path = path + f'.bak.{datetime.now().strftime("%Y%m%d_%H%M%S")}'
if os.path.exists(path):
shutil.copy2(path, backup_path)
print(f" Backed up {path}{backup_path}")
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f" Wrote {path}")
def migrate_tasks() -> dict[str, str]:
"""Migrate tasks.json: is_good → type. Returns task_id → type mapping."""
print("\n=== Step 1: Migrate tasks.json ===")
data = load_json('tasks.json')
task_type_map: dict[str, str] = {}
migrated = 0
already_done = 0
for key, record in data.get("_default", {}).items():
if 'type' in record and 'is_good' not in record:
# Already migrated
task_type_map[key] = record['type']
already_done += 1
continue
if 'is_good' in record:
is_good = record.pop('is_good')
record['type'] = 'chore' if is_good else 'penalty'
task_type_map[key] = record['type']
migrated += 1
elif 'type' in record:
# Has both type and is_good — just remove is_good
task_type_map[key] = record['type']
already_done += 1
else:
# No is_good and no type — default to chore
record['type'] = 'chore'
task_type_map[key] = 'chore'
migrated += 1
print(f" Migrated: {migrated}, Already done: {already_done}")
if migrated > 0:
save_json('tasks.json', data)
else:
print(" No changes needed.")
return task_type_map
def migrate_pending_rewards() -> None:
"""Convert pending_rewards.json → pending_confirmations.json."""
print("\n=== Step 2: Migrate pending_rewards.json → pending_confirmations.json ===")
pr_data = load_json('pending_rewards.json')
pc_path = os.path.join(DATA_DIR, 'pending_confirmations.json')
if not os.path.exists(os.path.join(DATA_DIR, 'pending_rewards.json')):
print(" pending_rewards.json not found — skipping.")
return
records = pr_data.get("_default", {})
if not records:
print(" No pending reward records to migrate.")
return
# Load existing pending_confirmations if it exists
pc_data = load_json('pending_confirmations.json')
pc_records = pc_data.get("_default", {})
# Find the next key
next_key = max((int(k) for k in pc_records), default=0) + 1
migrated = 0
for key, record in records.items():
# Convert PendingReward → PendingConfirmation
new_record = {
'child_id': record.get('child_id', ''),
'entity_id': record.get('reward_id', ''),
'entity_type': 'reward',
'user_id': record.get('user_id', ''),
'status': record.get('status', 'pending'),
'approved_at': None,
'created_at': record.get('created_at', 0),
'updated_at': record.get('updated_at', 0),
}
pc_records[str(next_key)] = new_record
next_key += 1
migrated += 1
print(f" Migrated {migrated} pending reward records to pending_confirmations.")
pc_data["_default"] = pc_records
save_json('pending_confirmations.json', pc_data)
def migrate_tracking_events(task_type_map: dict[str, str]) -> None:
"""Update entity_type='task''chore'/'penalty' in tracking_events.json."""
print("\n=== Step 3: Migrate tracking_events.json ===")
data = load_json('tracking_events.json')
records = data.get("_default", {})
migrated = 0
for key, record in records.items():
if record.get('entity_type') == 'task':
entity_id = record.get('entity_id', '')
# Look up the task's type
new_type = task_type_map.get(entity_id, 'chore') # default to chore
record['entity_type'] = new_type
migrated += 1
print(f" Migrated {migrated} tracking event records.")
if migrated > 0:
save_json('tracking_events.json', data)
else:
print(" No changes needed.")
def migrate_child_overrides(task_type_map: dict[str, str]) -> None:
"""Update entity_type='task''chore'/'penalty' in child_overrides.json."""
print("\n=== Step 4: Migrate child_overrides.json ===")
data = load_json('child_overrides.json')
records = data.get("_default", {})
migrated = 0
for key, record in records.items():
if record.get('entity_type') == 'task':
entity_id = record.get('entity_id', '')
new_type = task_type_map.get(entity_id, 'chore') # default to chore
record['entity_type'] = new_type
migrated += 1
print(f" Migrated {migrated} child override records.")
if migrated > 0:
save_json('child_overrides.json', data)
else:
print(" No changes needed.")
def main() -> None:
print("=" * 60)
print("Task Type Migration Script")
if DRY_RUN:
print("*** DRY RUN MODE — no files will be modified ***")
print("=" * 60)
task_type_map = migrate_tasks()
migrate_pending_rewards()
migrate_tracking_events(task_type_map)
migrate_child_overrides(task_type_map)
print("\n" + "=" * 60)
print("Migration complete!" + (" (DRY RUN)" if DRY_RUN else ""))
print("=" * 60)
if __name__ == '__main__':
main()