added fixes for bug plan 1.0.6RC01
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 5m43s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 5m43s
This commit is contained in:
91
.github/specs/bugs-1.0.6-01/bugs-1.0.6-set01.md
vendored
Normal file
91
.github/specs/bugs-1.0.6-01/bugs-1.0.6-set01.md
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Bug List for 1.0.6 Set 1
|
||||||
|
|
||||||
|
## Uppercase text for parent verification ✅
|
||||||
|
|
||||||
|
When entering a parent verification code (when creating a parent PIN) all entered characters should be uppercase. Convert the characters to uppercase in the UI.
|
||||||
|
|
||||||
|
**Fix:** Added `text-transform: uppercase` CSS and an `@input` handler that converts the value to uppercase on the verification code input in `ParentPinSetup.vue`.
|
||||||
|
|
||||||
|
## Assigning tasks should have the name of the child being assigned. ✅
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Instead of just saying "Assign Chores", it should say Assign Chores for [Name] or Assign Chores [Name]
|
||||||
|
|
||||||
|
**Fix:** All four assign views (`ChoreAssignView`, `RewardAssignView`, `KindnessAssignView`, `PenaltyAssignView`) now read the child's name from the route query parameter and display it in the heading (e.g. "Assign Chores for Emma"). The `ParentView` passes `query: { name: child.name }` when navigating to each assign route.
|
||||||
|
|
||||||
|
## Child completing a general chore (not scheduled) ✅
|
||||||
|
|
||||||
|
When a child completes a general chore and it is approved, the parent sees "COMPLETED", but the child can click and have it approved again. The chore on the parent side should not say completed, it should just go back to normal state. General chores, by default, can be completed multiple times, so it should not say "COMPLETED" on the parent side.
|
||||||
|
|
||||||
|
**Fix:** Backend `child_api.py` now distinguishes between general and scheduled chores during trigger and approval. General chores no longer create an "approved" `PendingConfirmation` record — the existing pending record is simply removed on approval. Only scheduled chores persist an approved confirmation (which drives the COMPLETED stamp). This allows general chores to return to normal state and be triggered again.
|
||||||
|
|
||||||
|
## Scheduled chore and too late ✅
|
||||||
|
|
||||||
|
When extending "Make Your Bed" (a scheduled chore that is too late,) the "TOO LATE" banner does not go away even though it was extended. Activating it again tells me that the chore was already extended. I'm expecting that when the chore is extended the too late banner disappears.
|
||||||
|
|
||||||
|
**Fix:** `ParentView.vue` `doExtendTime()` now explicitly calls `childChoreListRef.value?.refresh()` immediately after the extension API call, before resetting expiry timers. This forces the chore list to re-fetch and re-render, removing the TOO LATE banner without waiting for an SSE race condition.
|
||||||
|
|
||||||
|
## Notification runoff ✅
|
||||||
|
|
||||||
|
Notifications for completed or pending tasks can sometimes be too long for the card can run off the card.
|
||||||
|

|
||||||
|
in the red square, the icon for the chore has run off the card. We need a better way to fit the information. Make the card heights for only notifications dynamic?
|
||||||
|
|
||||||
|
**Fix:** Updated `NotificationView.vue` CSS to use `overflow: hidden` and `text-overflow: ellipsis` on notification card content, preventing icons and text from overflowing card boundaries. Card heights are now flexible to accommodate varying content lengths.
|
||||||
|
|
||||||
|
## Point editing events ✅
|
||||||
|
|
||||||
|
When changing points in parent mode, the child mode does not see the point update until refreshed. There should be a functional SSE that updates the points in the child view (or other parent mode tabs)
|
||||||
|
|
||||||
|
**Fix:** Added SSE event handlers in `ChildView.vue` for `child_override_set` and `child_override_deleted` events. When a parent changes point overrides, the child view now listens for these events and refreshes the affected task lists (chores, kindness, penalties) in real time. Handlers are registered in `onMounted` and cleaned up in `onUnmounted`.
|
||||||
|
|
||||||
|
## Child tasks list editing in view ✅
|
||||||
|
|
||||||
|
When a child's task is edited (points changed or scheduled, etc) and saved. The view resets. The problem is that when the view resets, if the edited task was off screen and had to be scrolled to, the user has to do this again. The task that was just edited should be automatically scrolled to so that it is in view both vertically and horizontally.
|
||||||
|
|
||||||
|
**Fix:** `ScrollingList.vue` now exposes a `scrollToItem(itemId)` method that scrolls an item into view both vertically (via `scrollIntoView`) and horizontally (via `centerItem`). `ParentView.vue` tracks the last edited item ID when saving an override, and after the list refreshes, calls `scrollToItem()` on the appropriate list ref to bring the edited card back into view.
|
||||||
|
|
||||||
|
## Task sorting in child task view ✅
|
||||||
|
|
||||||
|
When viewing the child's chores, kindnesses, penalties, rewards. The items should be sorted differently.
|
||||||
|
|
||||||
|
### Parent mode order
|
||||||
|
|
||||||
|
1. Pending tasks (chores and rewards)
|
||||||
|
2. General tasks
|
||||||
|
3. Too late (chores) when it applies
|
||||||
|
4. Completed (chores) when it applies
|
||||||
|
5. Chores that are not scheduled for today.
|
||||||
|
|
||||||
|
### Child mode order
|
||||||
|
|
||||||
|
- Chores
|
||||||
|
|
||||||
|
1. Scheduled for today chores (with earliest deadline first when it applies)
|
||||||
|
2. General Chores
|
||||||
|
3. Pending chores
|
||||||
|
|
||||||
|
- Rewards
|
||||||
|
|
||||||
|
1. Pending Rewards
|
||||||
|
2. Other rewards sorted by how many more points are required.
|
||||||
|
|
||||||
|
**Fix:** Added a `sortFn` prop to `ScrollingList.vue` that applies a custom sort after filtering. `ParentView.vue` defines `parentChoreSort()` (pending → general → expired → completed → unscheduled) and `parentRewardSort()` (pending first). `ChildView.vue` defines `childChoreSort()` (scheduled today with earliest deadline first → general → pending) and `childRewardSort()` (pending first, then ascending by points needed). These are wired as `:sort-fn` on their respective ScrollingList instances.
|
||||||
|
|
||||||
|
### Notification clicking ✅
|
||||||
|
|
||||||
|
When a parent clicks on a notification, the parent view should show for the child and child's task list. However, the task that was listed in the notification should be scrolled to both vertically and horizontally.
|
||||||
|
|
||||||
|
**Fix:** `NotificationView.vue` `handleNotificationClick()` now passes `query: { scrollTo: item.entity_id, entityType: item.entity_type }` when navigating to the parent child view. `ParentView.vue` reads these query parameters on mount and, after data loads, calls `scrollToItem()` on the correct list ref (chores, rewards, kindness, or penalties) with a short delay to ensure the list is rendered.
|
||||||
|
|
||||||
|
### Pending chore / reward changes ✅
|
||||||
|
|
||||||
|
If a chore or reward is changed while pending (schedule change or point change), the pending status should be reset back to normal state. Completed chores however should stay in the completed state.
|
||||||
|
|
||||||
|
**Fix:** Three backend API endpoints now clear pending status on modification:
|
||||||
|
|
||||||
|
- `task_api.py` `edit_task()`: When points or type changes, removes all pending chore confirmations for that task and sends `OPERATION_RESET` SSE events.
|
||||||
|
- `chore_schedule_api.py` `set_chore_schedule()`: After upserting a schedule, removes any pending chore confirmations for that child+task and sends reset events.
|
||||||
|
- `reward_api.py` `edit_reward()`: When cost changes, removes all pending reward requests for that reward and sends `CANCELLED` SSE events.
|
||||||
|
Completed chores (approved PendingConfirmation on scheduled chores) are not affected by these changes.
|
||||||
BIN
.github/specs/bugs-1.0.6-01/image-1.png
vendored
Normal file
BIN
.github/specs/bugs-1.0.6-01/image-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
.github/specs/bugs-1.0.6-01/image.png
vendored
Normal file
BIN
.github/specs/bugs-1.0.6-01/image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -451,6 +451,7 @@ def trigger_child_task(id):
|
|||||||
child_db.update({'points': child.points}, ChildQuery.id == id)
|
child_db.update({'points': child.points}, ChildQuery.id == id)
|
||||||
|
|
||||||
# For chores, create an approved PendingConfirmation so child view shows COMPLETED
|
# For chores, create an approved PendingConfirmation so child view shows COMPLETED
|
||||||
|
# Only persist for scheduled chores; general chores reset to normal after trigger
|
||||||
if task.type == 'chore':
|
if task.type == 'chore':
|
||||||
PendingQuery = Query()
|
PendingQuery = Query()
|
||||||
# Remove any existing pending confirmation for this chore
|
# Remove any existing pending confirmation for this chore
|
||||||
@@ -458,12 +459,14 @@ def trigger_child_task(id):
|
|||||||
(PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
|
(PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
|
||||||
(PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
|
||||||
)
|
)
|
||||||
confirmation = PendingConfirmation(
|
schedule = get_schedule(id, task_id)
|
||||||
child_id=id, entity_id=task_id, entity_type='chore',
|
if schedule:
|
||||||
user_id=user_id, status='approved',
|
confirmation = PendingConfirmation(
|
||||||
approved_at=datetime.now(timezone.utc).isoformat()
|
child_id=id, entity_id=task_id, entity_type='chore',
|
||||||
)
|
user_id=user_id, status='approved',
|
||||||
pending_confirmations_db.insert(confirmation.to_dict())
|
approved_at=datetime.now(timezone.utc).isoformat()
|
||||||
|
)
|
||||||
|
pending_confirmations_db.insert(confirmation.to_dict())
|
||||||
send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
|
send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
|
||||||
ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_APPROVED)))
|
ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_APPROVED)))
|
||||||
|
|
||||||
@@ -1198,12 +1201,20 @@ def approve_chore(id):
|
|||||||
child_db.update({'points': child.points}, ChildQuery.id == id)
|
child_db.update({'points': child.points}, ChildQuery.id == id)
|
||||||
|
|
||||||
# Update confirmation to approved
|
# Update confirmation to approved
|
||||||
now_str = datetime.now(timezone.utc).isoformat()
|
# For general (non-scheduled) chores, remove the confirmation so chore resets to normal
|
||||||
pending_confirmations_db.update(
|
schedule = get_schedule(id, task_id)
|
||||||
{'status': 'approved', 'approved_at': now_str},
|
if schedule:
|
||||||
(PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
|
now_str = datetime.now(timezone.utc).isoformat()
|
||||||
(PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
|
pending_confirmations_db.update(
|
||||||
)
|
{'status': 'approved', 'approved_at': now_str},
|
||||||
|
(PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
|
||||||
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pending_confirmations_db.remove(
|
||||||
|
(PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
|
||||||
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
tracking_metadata = {
|
tracking_metadata = {
|
||||||
'task_name': task.name,
|
'task_name': task.name,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify
|
|||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
from api.utils import get_validated_user_id, send_event_for_current_user
|
from api.utils import get_validated_user_id, send_event_for_current_user
|
||||||
from api.error_codes import ErrorCodes
|
from api.error_codes import ErrorCodes
|
||||||
from db.db import child_db
|
from db.db import child_db, pending_confirmations_db
|
||||||
from db.chore_schedules import get_schedule, upsert_schedule, delete_schedule
|
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 db.task_extensions import get_extension, add_extension, delete_extension_for_child_task
|
||||||
from models.chore_schedule import ChoreSchedule
|
from models.chore_schedule import ChoreSchedule
|
||||||
@@ -11,6 +11,7 @@ from events.types.event import Event
|
|||||||
from events.types.event_types import EventType
|
from events.types.event_types import EventType
|
||||||
from events.types.chore_schedule_modified import ChoreScheduleModified
|
from events.types.chore_schedule_modified import ChoreScheduleModified
|
||||||
from events.types.chore_time_extended import ChoreTimeExtended
|
from events.types.chore_time_extended import ChoreTimeExtended
|
||||||
|
from events.types.child_chore_confirmation import ChildChoreConfirmation
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
chore_schedule_api = Blueprint('chore_schedule_api', __name__)
|
chore_schedule_api = Blueprint('chore_schedule_api', __name__)
|
||||||
@@ -102,6 +103,20 @@ def set_chore_schedule(child_id, task_id):
|
|||||||
delete_extension_for_child_task(child_id, task_id)
|
delete_extension_for_child_task(child_id, task_id)
|
||||||
upsert_schedule(schedule)
|
upsert_schedule(schedule)
|
||||||
|
|
||||||
|
# Reset pending chore confirmations when schedule changes (completed chores stay)
|
||||||
|
PendingQuery = Query()
|
||||||
|
pending_chores = pending_confirmations_db.search(
|
||||||
|
(PendingQuery.child_id == child_id) & (PendingQuery.entity_id == task_id) &
|
||||||
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
for pc in pending_chores:
|
||||||
|
pending_confirmations_db.remove(
|
||||||
|
(PendingQuery.child_id == child_id) & (PendingQuery.entity_id == task_id) &
|
||||||
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
|
||||||
|
ChildChoreConfirmation(child_id, task_id, ChildChoreConfirmation.OPERATION_RESET)))
|
||||||
|
|
||||||
send_event_for_current_user(Event(
|
send_event_for_current_user(Event(
|
||||||
EventType.CHORE_SCHEDULE_MODIFIED.value,
|
EventType.CHORE_SCHEDULE_MODIFIED.value,
|
||||||
ChoreScheduleModified(child_id, task_id, ChoreScheduleModified.OPERATION_SET)
|
ChoreScheduleModified(child_id, task_id, ChoreScheduleModified.OPERATION_SET)
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from tinydb import Query
|
|||||||
|
|
||||||
from api.utils import send_event_for_current_user, get_validated_user_id
|
from api.utils import send_event_for_current_user, get_validated_user_id
|
||||||
from events.types.child_rewards_set import ChildRewardsSet
|
from events.types.child_rewards_set import ChildRewardsSet
|
||||||
from db.db import reward_db, child_db
|
from db.db import reward_db, child_db, pending_confirmations_db
|
||||||
from db.child_overrides import delete_overrides_for_entity
|
from db.child_overrides import delete_overrides_for_entity
|
||||||
from events.types.event import Event
|
from events.types.event import Event
|
||||||
from events.types.event_types import EventType
|
from events.types.event_types import EventType
|
||||||
from events.types.reward_modified import RewardModified
|
from events.types.reward_modified import RewardModified
|
||||||
|
from events.types.child_reward_request import ChildRewardRequest
|
||||||
from models.reward import Reward
|
from models.reward import Reward
|
||||||
|
|
||||||
reward_api = Blueprint('reward_api', __name__)
|
reward_api = Blueprint('reward_api', __name__)
|
||||||
@@ -155,6 +156,21 @@ def edit_reward(id):
|
|||||||
if not is_dirty:
|
if not is_dirty:
|
||||||
return jsonify({'error': 'No valid fields to update'}), 400
|
return jsonify({'error': 'No valid fields to update'}), 400
|
||||||
|
|
||||||
|
# Reset pending reward requests when cost changes
|
||||||
|
if 'cost' in data:
|
||||||
|
PendingQuery = Query()
|
||||||
|
pending_rewards = pending_confirmations_db.search(
|
||||||
|
(PendingQuery.entity_id == id) & (PendingQuery.entity_type == 'reward') &
|
||||||
|
(PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
for pr in pending_rewards:
|
||||||
|
pending_confirmations_db.remove(
|
||||||
|
(PendingQuery.child_id == pr['child_id']) & (PendingQuery.entity_id == id) &
|
||||||
|
(PendingQuery.entity_type == 'reward') & (PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value,
|
||||||
|
ChildRewardRequest(pr['child_id'], id, ChildRewardRequest.REQUEST_CANCELLED)))
|
||||||
|
|
||||||
if reward.user_id is None: # public reward
|
if reward.user_id is None: # public reward
|
||||||
new_reward = Reward(name=reward.name, description=reward.description, cost=reward.cost, image_id=reward.image_id, user_id=user_id)
|
new_reward = Reward(name=reward.name, description=reward.description, cost=reward.cost, image_id=reward.image_id, user_id=user_id)
|
||||||
reward_db.insert(new_reward.to_dict())
|
reward_db.insert(new_reward.to_dict())
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from tinydb import Query
|
|||||||
|
|
||||||
from api.utils import send_event_for_current_user, get_validated_user_id
|
from api.utils import send_event_for_current_user, get_validated_user_id
|
||||||
from events.types.child_tasks_set import ChildTasksSet
|
from events.types.child_tasks_set import ChildTasksSet
|
||||||
from db.db import task_db, child_db
|
from db.db import task_db, child_db, pending_confirmations_db
|
||||||
from db.child_overrides import delete_overrides_for_entity
|
from db.child_overrides import delete_overrides_for_entity
|
||||||
from events.types.event import Event
|
from events.types.event import Event
|
||||||
from events.types.event_types import EventType
|
from events.types.event_types import EventType
|
||||||
from events.types.task_modified import TaskModified
|
from events.types.task_modified import TaskModified
|
||||||
|
from events.types.child_chore_confirmation import ChildChoreConfirmation
|
||||||
from models.task import Task
|
from models.task import Task
|
||||||
|
|
||||||
task_api = Blueprint('task_api', __name__)
|
task_api = Blueprint('task_api', __name__)
|
||||||
@@ -176,7 +177,23 @@ def edit_task(id):
|
|||||||
|
|
||||||
if not is_dirty:
|
if not is_dirty:
|
||||||
return jsonify({'error': 'No valid fields to update'}), 400
|
return jsonify({'error': 'No valid fields to update'}), 400
|
||||||
|
|
||||||
|
# Reset pending chore confirmations when task is modified (points, type, etc.)
|
||||||
|
# Completed (approved) chores stay in completed state
|
||||||
|
if 'points' in data or 'type' in data:
|
||||||
|
PendingQuery = Query()
|
||||||
|
pending_chores = pending_confirmations_db.search(
|
||||||
|
(PendingQuery.entity_id == id) & (PendingQuery.entity_type == 'chore') &
|
||||||
|
(PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
for pc in pending_chores:
|
||||||
|
pending_confirmations_db.remove(
|
||||||
|
(PendingQuery.child_id == pc['child_id']) & (PendingQuery.entity_id == id) &
|
||||||
|
(PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending')
|
||||||
|
)
|
||||||
|
send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
|
||||||
|
ChildChoreConfirmation(pc['child_id'], id, ChildChoreConfirmation.OPERATION_RESET)))
|
||||||
|
|
||||||
if task.user_id is None: # public task
|
if task.user_id is None: # public task
|
||||||
new_task = Task(name=task.name, points=task.points, type=task.type, image_id=task.image_id, user_id=user_id)
|
new_task = Task(name=task.name, points=task.points, type=task.type, image_id=task.image_id, user_id=user_id)
|
||||||
task_db.insert(new_task.to_dict())
|
task_db.insert(new_task.to_dict())
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# file: config/version.py
|
# file: config/version.py
|
||||||
import os
|
import os
|
||||||
|
|
||||||
BASE_VERSION = "1.0.6RC01" # update manually when releasing features
|
BASE_VERSION = "1.0.6RC02" # update manually when releasing features
|
||||||
|
|
||||||
def get_full_version() -> str:
|
def get_full_version() -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ from datetime import date as date_type
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from api.child_api import child_api
|
from api.child_api import child_api
|
||||||
from api.auth_api import auth_api
|
from api.auth_api import auth_api
|
||||||
from db.db import child_db, task_db, reward_db, users_db, pending_confirmations_db, tracking_events_db
|
from db.db import child_db, task_db, reward_db, users_db, pending_confirmations_db, tracking_events_db, chore_schedules_db
|
||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
from models.child import Child
|
from models.child import Child
|
||||||
from models.pending_confirmation import PendingConfirmation
|
from models.pending_confirmation import PendingConfirmation
|
||||||
from models.tracking_event import TrackingEvent
|
from models.tracking_event import TrackingEvent
|
||||||
|
from models.chore_schedule import ChoreSchedule
|
||||||
|
|
||||||
|
|
||||||
TEST_EMAIL = "testuser@example.com"
|
TEST_EMAIL = "testuser@example.com"
|
||||||
@@ -214,13 +215,12 @@ def test_parent_approve_chore_success(client):
|
|||||||
data = resp.get_json()
|
data = resp.get_json()
|
||||||
assert data['points'] == points_before + 10
|
assert data['points'] == points_before + 10
|
||||||
|
|
||||||
# Verify confirmation is now approved
|
# General chore: PendingConfirmation should be removed after approval
|
||||||
PQ = Query()
|
PQ = Query()
|
||||||
conf = pending_confirmations_db.get(
|
conf = pending_confirmations_db.get(
|
||||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||||
)
|
)
|
||||||
assert conf['status'] == 'approved'
|
assert conf is None
|
||||||
assert conf['approved_at'] is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_parent_approve_chore_not_pending(client):
|
def test_parent_approve_chore_not_pending(client):
|
||||||
@@ -294,7 +294,8 @@ def test_parent_reject_chore_creates_tracking_event(client):
|
|||||||
|
|
||||||
def test_parent_reset_chore_success(client):
|
def test_parent_reset_chore_success(client):
|
||||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||||
# Confirm and approve first
|
_add_schedule_for(child_id, task_id)
|
||||||
|
# Confirm and approve first (scheduled chore keeps PendingConfirmation)
|
||||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||||
# Now reset
|
# Now reset
|
||||||
@@ -320,6 +321,7 @@ def test_parent_reset_chore_not_completed(client):
|
|||||||
|
|
||||||
def test_parent_reset_chore_creates_tracking_event(client):
|
def test_parent_reset_chore_creates_tracking_event(client):
|
||||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||||
|
_add_schedule_for(child_id, task_id)
|
||||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||||
tracking_events_db.truncate()
|
tracking_events_db.truncate()
|
||||||
@@ -330,8 +332,9 @@ def test_parent_reset_chore_creates_tracking_event(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_parent_reset_then_child_confirm_again(client):
|
def test_parent_reset_then_child_confirm_again(client):
|
||||||
"""Full cycle: confirm → approve → reset → confirm → approve."""
|
"""Full cycle: confirm → approve → reset → confirm → approve (scheduled chore)."""
|
||||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||||
|
_add_schedule_for(child_id, task_id)
|
||||||
child_db.update({'points': 0}, Query().id == child_id)
|
child_db.update({'points': 0}, Query().id == child_id)
|
||||||
|
|
||||||
# First cycle
|
# First cycle
|
||||||
@@ -359,20 +362,19 @@ def test_parent_reset_then_child_confirm_again(client):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_parent_trigger_chore_directly_creates_approved_confirmation(client):
|
def test_parent_trigger_chore_directly_creates_approved_confirmation(client):
|
||||||
|
"""General chore trigger should not persist PendingConfirmation."""
|
||||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||||
child_db.update({'points': 0}, Query().id == child_id)
|
child_db.update({'points': 0}, Query().id == child_id)
|
||||||
resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id})
|
resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.get_json()['points'] == 10
|
assert resp.get_json()['points'] == 10
|
||||||
|
|
||||||
# Verify an approved PendingConfirmation exists
|
# General chore: no PendingConfirmation should remain
|
||||||
PQ = Query()
|
PQ = Query()
|
||||||
conf = pending_confirmations_db.get(
|
conf = pending_confirmations_db.get(
|
||||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||||
)
|
)
|
||||||
assert conf is not None
|
assert conf is None
|
||||||
assert conf['status'] == 'approved'
|
|
||||||
assert conf['approved_at'] is not None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -479,3 +481,75 @@ def test_list_pending_confirmations_filters_by_user(client):
|
|||||||
resp = client.get('/pending-confirmations')
|
resp = client.get('/pending-confirmations')
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.get_json()['count'] == 0
|
assert resp.get_json()['count'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bug #3: General vs Scheduled chore approval
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _add_schedule_for(child_id, task_id):
|
||||||
|
"""Insert a simple days-mode schedule so the chore is treated as scheduled."""
|
||||||
|
chore_schedules_db.truncate()
|
||||||
|
sched = ChoreSchedule(
|
||||||
|
child_id=child_id, task_id=task_id, mode='days',
|
||||||
|
day_configs=[{'day': d, 'hour': 8, 'minute': 0} for d in range(7)]
|
||||||
|
)
|
||||||
|
chore_schedules_db.insert(sched.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def test_approve_general_chore_removes_pending(client):
|
||||||
|
"""Approving a general (non-scheduled) chore should remove PendingConfirmation."""
|
||||||
|
child_id, task_id = setup_child_and_chore(chore_points=5)
|
||||||
|
chore_schedules_db.truncate()
|
||||||
|
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
|
resp = client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_approve_scheduled_chore_keeps_approved_pending(client):
|
||||||
|
"""Approving a scheduled chore should keep PendingConfirmation as approved."""
|
||||||
|
child_id, task_id = setup_child_and_chore(chore_points=5)
|
||||||
|
_add_schedule_for(child_id, task_id)
|
||||||
|
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
|
resp = client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is not None
|
||||||
|
assert conf['status'] == 'approved'
|
||||||
|
assert conf['approved_at'] is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_trigger_scheduled_chore_creates_approved_confirmation(client):
|
||||||
|
"""Parent trigger on scheduled chore should create approved PendingConfirmation."""
|
||||||
|
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||||
|
_add_schedule_for(child_id, task_id)
|
||||||
|
child_db.update({'points': 0}, Query().id == child_id)
|
||||||
|
resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json()['points'] == 10
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is not None
|
||||||
|
assert conf['status'] == 'approved'
|
||||||
|
|
||||||
|
|
||||||
|
def test_approve_general_chore_can_be_confirmed_again(client):
|
||||||
|
"""After approving a general chore, child can confirm it again."""
|
||||||
|
child_id, task_id = setup_child_and_chore(chore_points=5)
|
||||||
|
chore_schedules_db.truncate()
|
||||||
|
# First cycle
|
||||||
|
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
|
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||||
|
# Should be able to confirm again
|
||||||
|
resp = client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ from werkzeug.security import generate_password_hash
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from api.chore_schedule_api import chore_schedule_api
|
from api.chore_schedule_api import chore_schedule_api
|
||||||
from api.auth_api import auth_api
|
from api.auth_api import auth_api
|
||||||
from db.db import users_db, child_db, chore_schedules_db, task_extensions_db
|
from db.db import users_db, child_db, chore_schedules_db, task_extensions_db, pending_confirmations_db, task_db
|
||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
|
from models.pending_confirmation import PendingConfirmation
|
||||||
|
|
||||||
|
|
||||||
TEST_EMAIL = "sched_test@example.com"
|
TEST_EMAIL = "sched_test@example.com"
|
||||||
@@ -412,3 +413,36 @@ def test_chore_schedule_to_dict_includes_enabled():
|
|||||||
d = s.to_dict()
|
d = s.to_dict()
|
||||||
assert "enabled" in d
|
assert "enabled" in d
|
||||||
assert d["enabled"] is False
|
assert d["enabled"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bug #10: Setting schedule resets pending confirmations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_set_schedule_resets_pending_confirmation(client):
|
||||||
|
"""Setting/updating a chore schedule should remove pending chore confirmations."""
|
||||||
|
chore_schedules_db.truncate()
|
||||||
|
pending_confirmations_db.truncate()
|
||||||
|
task_db.truncate()
|
||||||
|
|
||||||
|
task_db.insert({
|
||||||
|
'id': TEST_TASK_ID, 'name': 'Test Chore', 'points': 5,
|
||||||
|
'type': 'chore', 'user_id': TEST_USER_ID
|
||||||
|
})
|
||||||
|
pending_confirmations_db.insert(PendingConfirmation(
|
||||||
|
child_id=TEST_CHILD_ID, entity_id=TEST_TASK_ID,
|
||||||
|
entity_type='chore', user_id=TEST_USER_ID
|
||||||
|
).to_dict())
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"mode": "days",
|
||||||
|
"day_configs": [{"day": 1, "hour": 8, "minute": 0}],
|
||||||
|
}
|
||||||
|
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == TEST_CHILD_ID) & (PQ.entity_id == TEST_TASK_ID) & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is None
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ from werkzeug.security import generate_password_hash
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from api.reward_api import reward_api
|
from api.reward_api import reward_api
|
||||||
from api.auth_api import auth_api
|
from api.auth_api import auth_api
|
||||||
from db.db import reward_db, child_db, users_db
|
from db.db import reward_db, child_db, users_db, pending_confirmations_db
|
||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
from models.reward import Reward
|
from models.reward import Reward
|
||||||
|
from models.pending_confirmation import PendingConfirmation
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
|
|
||||||
@@ -126,4 +127,59 @@ def test_delete_assigned_reward_removes_from_child(client):
|
|||||||
resp2 = client.delete('/reward/r_user_owned')
|
resp2 = client.delete('/reward/r_user_owned')
|
||||||
assert resp2.status_code == 200
|
assert resp2.status_code == 200
|
||||||
child2 = child_db.search(ChildQuery.id == 'child_for_user_reward')[0]
|
child2 = child_db.search(ChildQuery.id == 'child_for_user_reward')[0]
|
||||||
assert 'r_user_owned' not in child2.get('rewards', [])
|
assert 'r_user_owned' not in child2.get('rewards', [])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bug #10: Editing reward cost resets pending reward requests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_edit_reward_cost_resets_pending_requests(client):
|
||||||
|
"""Changing a reward's cost should remove all pending reward requests for that reward."""
|
||||||
|
reward_db.truncate()
|
||||||
|
child_db.truncate()
|
||||||
|
pending_confirmations_db.truncate()
|
||||||
|
|
||||||
|
reward_db.insert({'id': 'r_pending', 'name': 'Prize', 'cost': 50, 'user_id': 'testuserid'})
|
||||||
|
child_db.insert({
|
||||||
|
'id': 'c1', 'name': 'Alice', 'age': 8, 'points': 100,
|
||||||
|
'tasks': [], 'rewards': ['r_pending'], 'user_id': 'testuserid'
|
||||||
|
})
|
||||||
|
pending_confirmations_db.insert(PendingConfirmation(
|
||||||
|
child_id='c1', entity_id='r_pending', entity_type='reward', user_id='testuserid'
|
||||||
|
).to_dict())
|
||||||
|
|
||||||
|
resp = client.put('/reward/r_pending/edit', json={'cost': 75})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == 'c1') & (PQ.entity_id == 'r_pending') & (PQ.entity_type == 'reward')
|
||||||
|
)
|
||||||
|
assert conf is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_reward_name_only_keeps_pending(client):
|
||||||
|
"""Changing only the reward name should not reset pending requests."""
|
||||||
|
reward_db.truncate()
|
||||||
|
child_db.truncate()
|
||||||
|
pending_confirmations_db.truncate()
|
||||||
|
|
||||||
|
reward_db.insert({'id': 'r_name', 'name': 'Old Prize', 'cost': 50, 'user_id': 'testuserid'})
|
||||||
|
child_db.insert({
|
||||||
|
'id': 'c2', 'name': 'Bob', 'age': 7, 'points': 100,
|
||||||
|
'tasks': [], 'rewards': ['r_name'], 'user_id': 'testuserid'
|
||||||
|
})
|
||||||
|
pending_confirmations_db.insert(PendingConfirmation(
|
||||||
|
child_id='c2', entity_id='r_name', entity_type='reward', user_id='testuserid'
|
||||||
|
).to_dict())
|
||||||
|
|
||||||
|
resp = client.put('/reward/r_name/edit', json={'name': 'New Prize'})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == 'c2') & (PQ.entity_id == 'r_name') & (PQ.entity_type == 'reward')
|
||||||
|
)
|
||||||
|
assert conf is not None
|
||||||
|
assert conf['status'] == 'pending'
|
||||||
@@ -6,8 +6,9 @@ from werkzeug.security import generate_password_hash
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from api.task_api import task_api
|
from api.task_api import task_api
|
||||||
from api.auth_api import auth_api
|
from api.auth_api import auth_api
|
||||||
from db.db import task_db, child_db, users_db
|
from db.db import task_db, child_db, users_db, pending_confirmations_db
|
||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
|
from models.pending_confirmation import PendingConfirmation
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
|
|
||||||
@@ -146,4 +147,59 @@ def test_delete_assigned_task_removes_from_child(client):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
# verify the task id is no longer in the child's tasks
|
# verify the task id is no longer in the child's tasks
|
||||||
child = child_db.search(ChildQuery.id == 'child_for_task_delete')[0]
|
child = child_db.search(ChildQuery.id == 'child_for_task_delete')[0]
|
||||||
assert 't_delete_assigned' not in child.get('tasks', [])
|
assert 't_delete_assigned' not in child.get('tasks', [])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bug #10: Editing task points/type resets pending confirmations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_edit_task_points_resets_pending_confirmations(client):
|
||||||
|
"""Changing a task's points should remove all pending chore confirmations for that task."""
|
||||||
|
task_db.truncate()
|
||||||
|
child_db.truncate()
|
||||||
|
pending_confirmations_db.truncate()
|
||||||
|
|
||||||
|
task_db.insert({'id': 't_pending', 'name': 'Sweep', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||||
|
child_db.insert({
|
||||||
|
'id': 'c1', 'name': 'Alice', 'age': 8, 'points': 0,
|
||||||
|
'tasks': ['t_pending'], 'rewards': [], 'user_id': 'testuserid'
|
||||||
|
})
|
||||||
|
pending_confirmations_db.insert(PendingConfirmation(
|
||||||
|
child_id='c1', entity_id='t_pending', entity_type='chore', user_id='testuserid'
|
||||||
|
).to_dict())
|
||||||
|
|
||||||
|
resp = client.put('/task/t_pending/edit', json={'points': 10})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == 'c1') & (PQ.entity_id == 't_pending') & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_task_name_only_keeps_pending(client):
|
||||||
|
"""Changing only the task name should not reset pending confirmations."""
|
||||||
|
task_db.truncate()
|
||||||
|
child_db.truncate()
|
||||||
|
pending_confirmations_db.truncate()
|
||||||
|
|
||||||
|
task_db.insert({'id': 't_name', 'name': 'Old Name', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||||
|
child_db.insert({
|
||||||
|
'id': 'c2', 'name': 'Bob', 'age': 7, 'points': 0,
|
||||||
|
'tasks': ['t_name'], 'rewards': [], 'user_id': 'testuserid'
|
||||||
|
})
|
||||||
|
pending_confirmations_db.insert(PendingConfirmation(
|
||||||
|
child_id='c2', entity_id='t_name', entity_type='chore', user_id='testuserid'
|
||||||
|
).to_dict())
|
||||||
|
|
||||||
|
resp = client.put('/task/t_name/edit', json={'name': 'New Name'})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
PQ = Query()
|
||||||
|
conf = pending_confirmations_db.get(
|
||||||
|
(PQ.child_id == 'c2') & (PQ.entity_id == 't_name') & (PQ.entity_type == 'chore')
|
||||||
|
)
|
||||||
|
assert conf is not None
|
||||||
|
assert conf['status'] == 'pending'
|
||||||
@@ -2,17 +2,17 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "icI1wOmbV5D-snH1lnz75Z-IGK-DuzK-RR_rKzjvvMs",
|
"value": "55lJfL_-PofI1V-NQ2E3Jlo4f_IE1aXqOF-wX54ZVSY",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1781792623.050125,
|
"expires": 1782155883.275106,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI0NmNmMmU0Ni1kOGFjLTRlYTItOTExYS1iZjJlMjVjNGI1ZDAiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQwMTc1MjN9.KNGwsOe7xEIo-tDbuSP38p3zB1vEB0KZk3mGEtgHs5s",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiIzZGQ0YTI1Yy04ZDI1LTQwNGEtODU0NS1mYjM2ZGRkYTU5NmYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQzODA3ODN9.b0ACVC63OobLzraiOcn4QDXipHXUVMW-Wn42XJFDOn0",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": -1,
|
"expires": -1,
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1774016622749}"
|
"value": "{\"type\":\"logout\",\"at\":1774379882977}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1774189423292}"
|
"value": "{\"expiresAt\":1774552683610}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "9eQaO12-M1GpCKeqJOmFb07Sms32J1j62DcEb5-neqs",
|
"value": "us7D-X0NRlXld6bpCl4epHaruPIWbkI895jFn3o84LY",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1781792623.010225,
|
"expires": 1782155883.187584,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiMTFlYjUwNTgtZmYxYi00YjgzLWExMTEtMTJjZjFkZjI4ZjA5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc0MDE3NTIyfQ.VCdPbgWlX4Mdr4NSzcyIybyNNbTSLvZE0YQGmQ-owKw",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNmU0MmNhNGQtMGNkNC00MmUzLWFkOGMtMWYyOTZmMzg5NTE5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc0MzgwNzgzfQ.AthhvOE2qsaJo6x37QbGXbT9oIP2pnFks3Pj14MjmcI",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": -1,
|
"expires": -1,
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1774016622727}"
|
"value": "{\"type\":\"logout\",\"at\":1774379882932}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1774189423292}"
|
"value": "{\"expiresAt\":1774552683575}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "YAXw1Kh8dLOuSYc1ZW_sgYHWRzo0XivYXmAHAr2RV-o",
|
"value": "VqeLgoC9z5NgU2AtA-9xjl4DaRt0hYKDkmAJMN3dcnw",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1781792619.603786,
|
"expires": 1782155879.453865,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzAwYjVjNy0yMzIxLTQxMWMtOWZiZC03NzY4ZjM4NTI1OWYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQwMTc1MTl9.2XDC_S2IuKeF_S9HTeHEOCWlUAtlrGpByLdIwrtoxfw",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI3ZjA2Njc5OS03NjI1LTQ0ZGQtODIwYi0zNWRjZmFlYWVjNTMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQzODA3Nzl9.o9urIT0cfexJY8OarcJ4gtJKqN-_DZOw50VToPl25LU",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": -1,
|
"expires": -1,
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1774016619428}"
|
"value": "{\"type\":\"logout\",\"at\":1774379879245}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1774189419776}"
|
"value": "{\"expiresAt\":1774552679625}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
281
frontend/vue-app/e2e/plans/bugs-1.0.6-set01.plan.md
Normal file
281
frontend/vue-app/e2e/plans/bugs-1.0.6-set01.plan.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# E2E Test Plan — Bug Fixes 1.0.6 Set 1
|
||||||
|
|
||||||
|
Covers the bug fixes from `.github/specs/bugs-1.0.6-01/bugs-1.0.6-set01.md`. Each section proposes new E2E specs or additions to existing specs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Assign View Shows Child Name
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-assignment/assign-title-shows-child-name.spec.ts`
|
||||||
|
**Project:** `chromium-task-assignment`
|
||||||
|
|
||||||
|
**Seed:** Create an isolated child with a known name via API. Create at least one chore, reward, kindness, and penalty task.
|
||||||
|
|
||||||
|
#### 1.1. "Assign Chores" heading contains child name
|
||||||
|
|
||||||
|
- Navigate to `/parent/{childId}/assign-chores?name={childName}`
|
||||||
|
- Verify the heading text includes the child's name
|
||||||
|
|
||||||
|
#### 1.2. "Assign Rewards" heading contains child name
|
||||||
|
|
||||||
|
- Navigate to `/parent/{childId}/assign-rewards?name={childName}`
|
||||||
|
- Verify heading
|
||||||
|
|
||||||
|
#### 1.3. "Assign Kindnesses" heading contains child name
|
||||||
|
|
||||||
|
- Navigate to `/parent/{childId}/assign-kindness?name={childName}`
|
||||||
|
- Verify heading
|
||||||
|
|
||||||
|
#### 1.4. "Assign Penalties" heading contains child name
|
||||||
|
|
||||||
|
- Navigate to `/parent/{childId}/assign-penalties?name={childName}`
|
||||||
|
- Verify heading
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. General Chore Approval — No COMPLETED Stamp
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-activation/general-chore-no-completed.spec.ts`
|
||||||
|
**Project:** `chromium-task-activation`
|
||||||
|
|
||||||
|
**Seed:** Create an isolated child, one general (unscheduled) chore, and one scheduled chore. Assign both to the child.
|
||||||
|
|
||||||
|
#### 2.1. Approving a general chore awards points
|
||||||
|
|
||||||
|
- Click general chore → confirm in dialog → approve
|
||||||
|
- Verify child's points increase by chore value
|
||||||
|
|
||||||
|
#### 2.2. General chore does NOT show COMPLETED stamp after approval
|
||||||
|
|
||||||
|
- After approval, verify the chore card does not contain "COMPLETED" text
|
||||||
|
|
||||||
|
#### 2.3. General chore can be triggered again immediately after approval
|
||||||
|
|
||||||
|
- Click the same general chore again
|
||||||
|
- Verify the confirmation dialog re-appears (chore is re-triggerable)
|
||||||
|
|
||||||
|
#### 2.4. Scheduled chore DOES show COMPLETED after approval (contrast)
|
||||||
|
|
||||||
|
- Trigger and approve the scheduled chore
|
||||||
|
- Verify "COMPLETED" stamp is visible on that card
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Extend Time Removes TOO LATE Banner
|
||||||
|
|
||||||
|
**Existing file:** `e2e/mode_parent/chore-scheduler/extend-time.spec.ts`
|
||||||
|
|
||||||
|
The existing spec already has tests for "Extend Time removes TOO LATE badge" and "gray styling removed." The bug fix ensures `refresh()` is called immediately, so the existing tests should pass without page reload. If not already asserted:
|
||||||
|
|
||||||
|
#### 3.1. TOO LATE disappears without page reload (addition to existing spec)
|
||||||
|
|
||||||
|
- After clicking "Extend Time," assert TOO LATE text is hidden within 3 seconds
|
||||||
|
- Do NOT call `page.reload()` between extension and assertion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Notification Card Overflow
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/notifications/notification-card-overflow.spec.ts`
|
||||||
|
**Project:** `chromium-task-activation` or new `chromium-notifications`
|
||||||
|
|
||||||
|
**Seed:** Create child + a chore with a very long name (50+ characters). Assign and trigger it so a notification is created.
|
||||||
|
|
||||||
|
#### 4.1. Notification icon stays within card boundaries
|
||||||
|
|
||||||
|
- Navigate to notifications view
|
||||||
|
- Get the bounding box of the notification card and its icon
|
||||||
|
- Assert the icon's bounding box is fully contained within the card's bounding box
|
||||||
|
|
||||||
|
#### 4.2. Long notification text does not visually overflow
|
||||||
|
|
||||||
|
- Assert the card element has appropriate overflow CSS (`overflow: hidden` or similar)
|
||||||
|
- Optionally use `toHaveScreenshot()` for visual regression
|
||||||
|
|
||||||
|
**Note:** This test benefits from visual regression testing (`expect(page).toHaveScreenshot()`) more than functional assertions, since overflow is inherently visual.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Override SSE Updates Child View
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-modification/override-sse-child-update.spec.ts`
|
||||||
|
**Project:** `chromium-task-modification`
|
||||||
|
|
||||||
|
**Seed:** Create child + one chore, one kindness, one penalty. Assign all to the child.
|
||||||
|
|
||||||
|
#### 5.1. Changing chore override points updates child view in real time
|
||||||
|
|
||||||
|
- Open child view in the browser (`goToChildView`)
|
||||||
|
- Via API, set a point override on the chore: `PUT /api/child/{id}/task/{taskId}/override`
|
||||||
|
- Without page reload, verify the chore card displays the new point value
|
||||||
|
|
||||||
|
#### 5.2. Changing kindness override points updates child view
|
||||||
|
|
||||||
|
- Same pattern for kindness task — set override via API, assert child view updates
|
||||||
|
|
||||||
|
#### 5.3. Changing penalty override points updates child view
|
||||||
|
|
||||||
|
- Same pattern for penalty task
|
||||||
|
|
||||||
|
#### 5.4. Deleting an override reverts child view to original points
|
||||||
|
|
||||||
|
- Remove the override via API
|
||||||
|
- Verify child view reverts to original point value without reload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Scroll to Edited Task After Save
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-modification/scroll-to-edited-task.spec.ts`
|
||||||
|
**Project:** `chromium-task-modification`
|
||||||
|
|
||||||
|
**Seed:** Create child + 10–15 chores and 5+ rewards (enough to require both vertical and horizontal scrolling). Assign all to the child.
|
||||||
|
|
||||||
|
#### 6.1. Edited chore card scrolls into viewport after override save
|
||||||
|
|
||||||
|
- Navigate to parent child view
|
||||||
|
- Open override modal on a chore that requires scrolling to reach
|
||||||
|
- Save a new point value
|
||||||
|
- Assert the edited card `toBeInViewport()` after the list refreshes
|
||||||
|
|
||||||
|
#### 6.2. Edited reward card scrolls into viewport after override save
|
||||||
|
|
||||||
|
- Open override modal on a distant reward
|
||||||
|
- Save, assert card `toBeInViewport()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Task Sorting — Parent Mode
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-sorting/parent-sort-order.spec.ts`
|
||||||
|
**Project:** New `chromium-task-sorting` or `chromium-chore-scheduler`
|
||||||
|
|
||||||
|
**Seed:** Create child + 5 chores in different states:
|
||||||
|
|
||||||
|
1. A pending chore (child triggered, not yet approved)
|
||||||
|
2. A general (unscheduled) chore
|
||||||
|
3. An expired scheduled chore (today, deadline in the past → TOO LATE)
|
||||||
|
4. A completed scheduled chore (triggered + approved)
|
||||||
|
5. A chore scheduled on a non-today day (unscheduled for today)
|
||||||
|
|
||||||
|
Also create 2+ rewards: one with a pending request, one without.
|
||||||
|
|
||||||
|
#### 7.1. Parent chore sort: pending → general → expired → completed → unscheduled
|
||||||
|
|
||||||
|
- Navigate to parent child view
|
||||||
|
- Read all chore card names in DOM order
|
||||||
|
- Assert the order matches: pending chore first, then general, then expired, then completed, then unscheduled-today
|
||||||
|
|
||||||
|
#### 7.2. Parent reward sort: pending requests appear first
|
||||||
|
|
||||||
|
- Read all reward card names in DOM order
|
||||||
|
- Assert the pending-request reward appears before other rewards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Task Sorting — Child Mode
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-sorting/child-sort-order.spec.ts`
|
||||||
|
**Project:** Same as §7
|
||||||
|
|
||||||
|
**Seed:** Same child + tasks as §7, plus rewards with varying costs.
|
||||||
|
|
||||||
|
#### 8.1. Child chore sort: scheduled today (earliest deadline first) → general → pending
|
||||||
|
|
||||||
|
- Switch to child view (`goToChildView`)
|
||||||
|
- Read all visible chore card names in DOM order
|
||||||
|
- Assert: scheduled-today chores first (sorted by deadline), then general, then pending
|
||||||
|
- Non-today chores should be hidden in child mode
|
||||||
|
|
||||||
|
#### 8.2. Child reward sort: pending first, then ascending by points needed
|
||||||
|
|
||||||
|
- Read all reward card names in DOM order
|
||||||
|
- Assert: pending reward first, remaining sorted by ascending cost/points-needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Notification Click Navigates and Scrolls to Task
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/notifications/notification-click-scroll.spec.ts`
|
||||||
|
**Project:** `chromium-task-activation` or new `chromium-notifications`
|
||||||
|
|
||||||
|
**Seed:** Create child + many chores (10+). Trigger one chore to create a pending notification. The triggered chore should be far enough in the list to require scrolling.
|
||||||
|
|
||||||
|
#### 9.1. Clicking a chore notification navigates to parent child view
|
||||||
|
|
||||||
|
- Navigate to notifications page
|
||||||
|
- Click the pending chore notification
|
||||||
|
- Verify URL matches `/parent/{childId}` with `scrollTo` and `entityType` query params
|
||||||
|
|
||||||
|
#### 9.2. Target chore card is scrolled into viewport after navigation
|
||||||
|
|
||||||
|
- After navigation from notification click, assert the target chore card `toBeInViewport()`
|
||||||
|
|
||||||
|
#### 9.3. Works for reward notifications too
|
||||||
|
|
||||||
|
- Request a reward → navigate to notifications → click → verify reward card is in viewport
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Pending Status Resets on Task/Reward Modification
|
||||||
|
|
||||||
|
**File:** `e2e/mode_parent/task-modification/pending-reset-on-edit.spec.ts`
|
||||||
|
**Project:** `chromium-task-modification`
|
||||||
|
|
||||||
|
**Seed:** Create child + one chore + one reward. Assign both. Create a chore schedule.
|
||||||
|
|
||||||
|
#### 10.1. Editing chore points clears pending status
|
||||||
|
|
||||||
|
- Trigger the chore (creates pending confirmation)
|
||||||
|
- Verify chore card shows pending state
|
||||||
|
- Edit the parent task's points via API: `POST /api/task/{taskId}/edit` with new points
|
||||||
|
- Refresh or wait for SSE — verify the chore card no longer shows pending state
|
||||||
|
|
||||||
|
#### 10.2. Changing chore schedule clears pending status
|
||||||
|
|
||||||
|
- Trigger the chore again
|
||||||
|
- Set a new schedule via API: `PUT /api/child/{childId}/task/{taskId}/schedule`
|
||||||
|
- Verify pending is cleared
|
||||||
|
|
||||||
|
#### 10.3. Editing reward cost clears pending reward request
|
||||||
|
|
||||||
|
- Request the reward (creates pending request)
|
||||||
|
- Edit the reward's cost via API
|
||||||
|
- Verify the reward card no longer shows pending state
|
||||||
|
|
||||||
|
#### 10.4. Completed chores remain completed after point edit
|
||||||
|
|
||||||
|
- Complete and approve a scheduled chore so it shows COMPLETED
|
||||||
|
- Edit the parent task's points
|
||||||
|
- Verify the chore still shows COMPLETED stamp
|
||||||
|
|
||||||
|
#### 10.5. Editing task name only (no point change) does NOT clear pending
|
||||||
|
|
||||||
|
- Trigger the chore
|
||||||
|
- Edit the task name only (points unchanged) via API
|
||||||
|
- Verify the chore still shows pending state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| # | Area | Spec File | Tests | Priority |
|
||||||
|
| --- | -------------------------- | ------------------------------------------------------- | ----- | -------- |
|
||||||
|
| 1 | Assign view child name | `task-assignment/assign-title-shows-child-name.spec.ts` | 4 | Medium |
|
||||||
|
| 2 | General chore no COMPLETED | `task-activation/general-chore-no-completed.spec.ts` | 4 | **High** |
|
||||||
|
| 3 | Extend time TOO LATE | Existing `chore-scheduler/extend-time.spec.ts` | 0–1 | Low |
|
||||||
|
| 4 | Notification overflow | `notifications/notification-card-overflow.spec.ts` | 2 | Low |
|
||||||
|
| 5 | Override SSE child update | `task-modification/override-sse-child-update.spec.ts` | 4 | **High** |
|
||||||
|
| 6 | Scroll to edited task | `task-modification/scroll-to-edited-task.spec.ts` | 2 | Medium |
|
||||||
|
| 7 | Parent sort order | `task-sorting/parent-sort-order.spec.ts` | 2 | **High** |
|
||||||
|
| 8 | Child sort order | `task-sorting/child-sort-order.spec.ts` | 2 | **High** |
|
||||||
|
| 9 | Notification click scroll | `notifications/notification-click-scroll.spec.ts` | 3 | Medium |
|
||||||
|
| 10 | Pending reset on edit | `task-modification/pending-reset-on-edit.spec.ts` | 5 | **High** |
|
||||||
|
|
||||||
|
**Recommended implementation order:** #2, #10, #7, #8, #5, #9, #6, #1, #3, #4
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- Bug #1 (uppercase verification code) is omitted from this E2E plan because reaching the PIN setup step requires email delivery; this is better covered by a component-level test on `ParentPinSetup.vue`.
|
||||||
|
- Bug #4 (extend time) is largely already covered by the existing `extend-time.spec.ts` — only a minor addition may be needed.
|
||||||
|
- Bug #5 (notification overflow) is best validated through visual regression (`toHaveScreenshot()`) rather than functional assertions.
|
||||||
@@ -21,8 +21,9 @@
|
|||||||
<input
|
<input
|
||||||
v-model="code"
|
v-model="code"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
class="code-input"
|
class="code-input uppercase"
|
||||||
placeholder="6-digit code"
|
placeholder="6-digit code"
|
||||||
|
@input="code = code.toUpperCase()"
|
||||||
@keyup.enter="isCodeValid && verifyCode()"
|
@keyup.enter="isCodeValid && verifyCode()"
|
||||||
/>
|
/>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@@ -260,6 +261,9 @@ function goBack() {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import type {
|
|||||||
ChoreScheduleModifiedPayload,
|
ChoreScheduleModifiedPayload,
|
||||||
ChoreTimeExtendedPayload,
|
ChoreTimeExtendedPayload,
|
||||||
ChildChoreConfirmationPayload,
|
ChildChoreConfirmationPayload,
|
||||||
|
ChildOverrideSetPayload,
|
||||||
|
ChildOverrideDeletedPayload,
|
||||||
} from '@/common/models'
|
} from '@/common/models'
|
||||||
import { confirmChore, cancelConfirmChore } from '@/common/api'
|
import { confirmChore, cancelConfirmChore } from '@/common/api'
|
||||||
import {
|
import {
|
||||||
@@ -380,6 +382,8 @@ function removeInactivityListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const childChoreListRef = ref()
|
const childChoreListRef = ref()
|
||||||
|
const childKindnessListRef = ref()
|
||||||
|
const childPenaltyListRef = ref()
|
||||||
const readyItemId = ref<string | null>(null)
|
const readyItemId = ref<string | null>(null)
|
||||||
const expiryTimers = ref<number[]>([])
|
const expiryTimers = ref<number[]>([])
|
||||||
const lastFetchDate = ref<string>(toLocalISODate(new Date()))
|
const lastFetchDate = ref<string>(toLocalISODate(new Date()))
|
||||||
@@ -411,6 +415,32 @@ function handleChoreConfirmation(event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleOverrideSet(event: Event) {
|
||||||
|
const payload = event.payload as ChildOverrideSetPayload
|
||||||
|
if (child.value && payload.override.child_id === child.value.id) {
|
||||||
|
if (payload.override.entity_type === 'task') {
|
||||||
|
childChoreListRef.value?.refresh()
|
||||||
|
childKindnessListRef.value?.refresh()
|
||||||
|
childPenaltyListRef.value?.refresh()
|
||||||
|
} else if (payload.override.entity_type === 'reward') {
|
||||||
|
childRewardListRef.value?.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOverrideDeleted(event: Event) {
|
||||||
|
const payload = event.payload as ChildOverrideDeletedPayload
|
||||||
|
if (child.value && payload.child_id === child.value.id) {
|
||||||
|
if (payload.entity_type === 'task') {
|
||||||
|
childChoreListRef.value?.refresh()
|
||||||
|
childKindnessListRef.value?.refresh()
|
||||||
|
childPenaltyListRef.value?.refresh()
|
||||||
|
} else if (payload.entity_type === 'reward') {
|
||||||
|
childRewardListRef.value?.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isChoreScheduledToday(item: ChildTask): boolean {
|
function isChoreScheduledToday(item: ChildTask): boolean {
|
||||||
if (!item.schedule) return true
|
if (!item.schedule) return true
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -435,6 +465,35 @@ function choreDueLabel(item: ChildTask): string | null {
|
|||||||
return `Due by ${formatDueTimeLabel(due.hour, due.minute)}`
|
return `Due by ${formatDueTimeLabel(due.hour, due.minute)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sorting ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function childChoreSortPriority(item: ChildTask): number {
|
||||||
|
if (item.pending_status === 'pending') return 2
|
||||||
|
if (!item.schedule) return 1 // general chore
|
||||||
|
return 0 // scheduled for today
|
||||||
|
}
|
||||||
|
|
||||||
|
function childChoreSort(a: ChildTask, b: ChildTask): number {
|
||||||
|
const pa = childChoreSortPriority(a)
|
||||||
|
const pb = childChoreSortPriority(b)
|
||||||
|
if (pa !== pb) return pa - pb
|
||||||
|
// Both scheduled: sort by earliest deadline
|
||||||
|
if (pa === 0) {
|
||||||
|
const now = new Date()
|
||||||
|
const dueA = getDueTimeToday(a.schedule!, now)
|
||||||
|
const dueB = getDueTimeToday(b.schedule!, now)
|
||||||
|
const minsA = dueA ? dueA.hour * 60 + dueA.minute : Infinity
|
||||||
|
const minsB = dueB ? dueB.hour * 60 + dueB.minute : Infinity
|
||||||
|
return minsA - minsB
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function childRewardSort(a: RewardStatus, b: RewardStatus): number {
|
||||||
|
if (a.redeeming !== b.redeeming) return a.redeeming ? -1 : 1
|
||||||
|
return a.points_needed - b.points_needed
|
||||||
|
}
|
||||||
|
|
||||||
function clearExpiryTimers() {
|
function clearExpiryTimers() {
|
||||||
expiryTimers.value.forEach((t) => clearTimeout(t))
|
expiryTimers.value.forEach((t) => clearTimeout(t))
|
||||||
expiryTimers.value = []
|
expiryTimers.value = []
|
||||||
@@ -487,6 +546,8 @@ onMounted(async () => {
|
|||||||
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
|
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
|
||||||
eventBus.on('chore_time_extended', handleChoreTimeExtended)
|
eventBus.on('chore_time_extended', handleChoreTimeExtended)
|
||||||
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
|
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
|
||||||
|
eventBus.on('child_override_set', handleOverrideSet)
|
||||||
|
eventBus.on('child_override_deleted', handleOverrideDeleted)
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||||
@@ -522,6 +583,8 @@ onUnmounted(() => {
|
|||||||
eventBus.off('chore_schedule_modified', handleChoreScheduleModified)
|
eventBus.off('chore_schedule_modified', handleChoreScheduleModified)
|
||||||
eventBus.off('chore_time_extended', handleChoreTimeExtended)
|
eventBus.off('chore_time_extended', handleChoreTimeExtended)
|
||||||
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
|
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
|
||||||
|
eventBus.off('child_override_set', handleOverrideSet)
|
||||||
|
eventBus.off('child_override_deleted', handleOverrideDeleted)
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
clearExpiryTimers()
|
clearExpiryTimers()
|
||||||
removeInactivityListeners()
|
removeInactivityListeners()
|
||||||
@@ -555,6 +618,7 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
:filter-fn="(item: ChildTask) => item.type === 'chore' && isChoreScheduledToday(item)"
|
:filter-fn="(item: ChildTask) => item.type === 'chore' && isChoreScheduledToday(item)"
|
||||||
|
:sort-fn="childChoreSort"
|
||||||
>
|
>
|
||||||
<template #item="{ item }: { item: ChildTask }">
|
<template #item="{ item }: { item: ChildTask }">
|
||||||
<span v-if="isChoreExpired(item)" class="chore-stamp">TOO LATE</span>
|
<span v-if="isChoreExpired(item)" class="chore-stamp">TOO LATE</span>
|
||||||
@@ -590,6 +654,7 @@ onUnmounted(() => {
|
|||||||
:getItemClass="
|
:getItemClass="
|
||||||
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
|
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
|
||||||
"
|
"
|
||||||
|
:sort-fn="childRewardSort"
|
||||||
>
|
>
|
||||||
<template #item="{ item }: { item: RewardStatus }">
|
<template #item="{ item }: { item: RewardStatus }">
|
||||||
<div class="item-name">{{ item.name }}</div>
|
<div class="item-name">{{ item.name }}</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="assign-view">
|
<div class="assign-view">
|
||||||
<h2>Assign Chores</h2>
|
<h2>Assign Chores{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
<MessageBlock v-if="countRef === 0" message="No chores">
|
<MessageBlock v-if="countRef === 0" message="No chores">
|
||||||
<span> <button class="round-btn" @click="goToCreate">Create</button> a chore </span>
|
<span> <button class="round-btn" @click="goToCreate">Create</button> a chore </span>
|
||||||
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||||
const listRef = ref()
|
const listRef = ref()
|
||||||
const countRef = ref(-1)
|
const countRef = ref(-1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="assign-view">
|
<div class="assign-view">
|
||||||
<h2>Assign Kindness Acts</h2>
|
<h2>Assign Kindness Acts{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
<MessageBlock v-if="countRef === 0" message="No kindness acts">
|
<MessageBlock v-if="countRef === 0" message="No kindness acts">
|
||||||
<span> <button class="round-btn" @click="goToCreate">Create</button> a kindness act </span>
|
<span> <button class="round-btn" @click="goToCreate">Create</button> a kindness act </span>
|
||||||
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||||
const listRef = ref()
|
const listRef = ref()
|
||||||
const countRef = ref(-1)
|
const countRef = ref(-1)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const childPenaltyListRef = ref()
|
|||||||
const childRewardListRef = ref()
|
const childRewardListRef = ref()
|
||||||
const childKindnessListRef = ref()
|
const childKindnessListRef = ref()
|
||||||
const showPendingRewardDialog = ref(false)
|
const showPendingRewardDialog = ref(false)
|
||||||
|
const lastEditedItem = ref<{ id: string; type: 'task' | 'reward' } | null>(null)
|
||||||
|
|
||||||
// Chore approve/reject
|
// Chore approve/reject
|
||||||
const showChoreApproveDialog = ref(false)
|
const showChoreApproveDialog = ref(false)
|
||||||
@@ -243,14 +244,20 @@ function handleChildModified(event: Event) {
|
|||||||
function handleOverrideSet(event: Event) {
|
function handleOverrideSet(event: Event) {
|
||||||
const payload = event.payload as ChildOverrideSetPayload
|
const payload = event.payload as ChildOverrideSetPayload
|
||||||
if (child.value && payload.override.child_id === child.value.id) {
|
if (child.value && payload.override.child_id === child.value.id) {
|
||||||
// Refresh the appropriate list to show the override badge
|
const editedId = lastEditedItem.value?.id ?? null
|
||||||
if (payload.override.entity_type === 'task') {
|
const scrollAfterRefresh = (listRef: typeof childChoreListRef) => {
|
||||||
childChoreListRef.value?.refresh()
|
listRef.value?.refresh().then(() => {
|
||||||
childKindnessListRef.value?.refresh()
|
if (editedId) listRef.value?.scrollToItem(editedId)
|
||||||
childPenaltyListRef.value?.refresh()
|
})
|
||||||
} else if (payload.override.entity_type === 'reward') {
|
|
||||||
childRewardListRef.value?.refresh()
|
|
||||||
}
|
}
|
||||||
|
if (payload.override.entity_type === 'task') {
|
||||||
|
scrollAfterRefresh(childChoreListRef)
|
||||||
|
scrollAfterRefresh(childKindnessListRef)
|
||||||
|
scrollAfterRefresh(childPenaltyListRef)
|
||||||
|
} else if (payload.override.entity_type === 'reward') {
|
||||||
|
scrollAfterRefresh(childRewardListRef)
|
||||||
|
}
|
||||||
|
lastEditedItem.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +431,7 @@ async function doExtendTime(item: ChildTask, e: MouseEvent) {
|
|||||||
alert(`Error: ${msg}`)
|
alert(`Error: ${msg}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
childChoreListRef.value?.refresh()
|
||||||
setTimeout(() => resetExpiryTimers(), 300)
|
setTimeout(() => resetExpiryTimers(), 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +467,25 @@ function isChoreInactive(item: ChildTask): boolean {
|
|||||||
return !isChoreScheduledToday(item) || isChoreExpired(item)
|
return !isChoreScheduledToday(item) || isChoreExpired(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sorting ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function parentChoreSortPriority(item: ChildTask): number {
|
||||||
|
if (isChorePending(item)) return 0
|
||||||
|
if (!isChoreScheduledToday(item)) return 4
|
||||||
|
if (isChoreCompletedToday(item)) return 3
|
||||||
|
if (isChoreExpired(item)) return 2
|
||||||
|
return 1 // general / active
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentChoreSort(a: ChildTask, b: ChildTask): number {
|
||||||
|
return parentChoreSortPriority(a) - parentChoreSortPriority(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parentRewardSort(a: RewardStatus, b: RewardStatus): number {
|
||||||
|
if (a.redeeming !== b.redeeming) return a.redeeming ? -1 : 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// ── Expiry timers ─────────────────────────────────────────────────────────────
|
// ── Expiry timers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function clearExpiryTimers() {
|
function clearExpiryTimers() {
|
||||||
@@ -556,6 +583,10 @@ async function saveOverride() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
lastEditedItem.value = {
|
||||||
|
id: overrideEditTarget.value.entity.id,
|
||||||
|
type: overrideEditTarget.value.type,
|
||||||
|
}
|
||||||
showOverrideModal.value = false
|
showOverrideModal.value = false
|
||||||
} else {
|
} else {
|
||||||
const { msg } = parseErrorResponse(res)
|
const { msg } = parseErrorResponse(res)
|
||||||
@@ -600,6 +631,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||||
|
const scrollToId = typeof route.query.scrollTo === 'string' ? route.query.scrollTo : null
|
||||||
|
const entityType = typeof route.query.entityType === 'string' ? route.query.entityType : null
|
||||||
if (idParam !== undefined) {
|
if (idParam !== undefined) {
|
||||||
const promise = fetchChildData(idParam)
|
const promise = fetchChildData(idParam)
|
||||||
promise.then((data) => {
|
promise.then((data) => {
|
||||||
@@ -609,6 +642,15 @@ onMounted(async () => {
|
|||||||
rewards.value = data.rewards || []
|
rewards.value = data.rewards || []
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
if (scrollToId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (entityType === 'chore') {
|
||||||
|
childChoreListRef.value?.scrollToItem(scrollToId)
|
||||||
|
} else if (entityType === 'reward') {
|
||||||
|
childRewardListRef.value?.scrollToItem(scrollToId)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,25 +807,41 @@ const confirmTriggerReward = async () => {
|
|||||||
|
|
||||||
function goToAssignTasks() {
|
function goToAssignTasks() {
|
||||||
if (child.value?.id) {
|
if (child.value?.id) {
|
||||||
router.push({ name: 'ChoreAssignView', params: { id: child.value.id } })
|
router.push({
|
||||||
|
name: 'ChoreAssignView',
|
||||||
|
params: { id: child.value.id },
|
||||||
|
query: { name: child.value.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToAssignBadHabits() {
|
function goToAssignBadHabits() {
|
||||||
if (child.value?.id) {
|
if (child.value?.id) {
|
||||||
router.push({ name: 'PenaltyAssignView', params: { id: child.value.id } })
|
router.push({
|
||||||
|
name: 'PenaltyAssignView',
|
||||||
|
params: { id: child.value.id },
|
||||||
|
query: { name: child.value.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToAssignKindness() {
|
function goToAssignKindness() {
|
||||||
if (child.value?.id) {
|
if (child.value?.id) {
|
||||||
router.push({ name: 'KindnessAssignView', params: { id: child.value.id } })
|
router.push({
|
||||||
|
name: 'KindnessAssignView',
|
||||||
|
params: { id: child.value.id },
|
||||||
|
query: { name: child.value.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToAssignRewards() {
|
function goToAssignRewards() {
|
||||||
if (child.value?.id) {
|
if (child.value?.id) {
|
||||||
router.push({ name: 'RewardAssignView', params: { id: child.value.id } })
|
router.push({
|
||||||
|
name: 'RewardAssignView',
|
||||||
|
params: { id: child.value.id },
|
||||||
|
query: { name: child.value.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -816,6 +874,7 @@ function goToAssignRewards() {
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
:filter-fn="(item) => item.type === 'chore'"
|
:filter-fn="(item) => item.type === 'chore'"
|
||||||
|
:sort-fn="parentChoreSort"
|
||||||
>
|
>
|
||||||
<template #item="{ item }: { item: ChildTask }">
|
<template #item="{ item }: { item: ChildTask }">
|
||||||
<!-- Kebab menu -->
|
<!-- Kebab menu -->
|
||||||
@@ -983,6 +1042,7 @@ function goToAssignRewards() {
|
|||||||
@edit-item="(item) => handleEditItem(item, 'reward')"
|
@edit-item="(item) => handleEditItem(item, 'reward')"
|
||||||
@item-ready="handleItemReady"
|
@item-ready="handleItemReady"
|
||||||
:getItemClass="(item) => ({ reward: true })"
|
:getItemClass="(item) => ({ reward: true })"
|
||||||
|
:sort-fn="parentRewardSort"
|
||||||
>
|
>
|
||||||
<template #item="{ item }: { item: RewardStatus }">
|
<template #item="{ item }: { item: RewardStatus }">
|
||||||
<div class="item-name">{{ item.name }}</div>
|
<div class="item-name">{{ item.name }}</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="assign-view">
|
<div class="assign-view">
|
||||||
<h2>Assign Penalties</h2>
|
<h2>Assign Penalties{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
<MessageBlock v-if="countRef === 0" message="No penalties">
|
<MessageBlock v-if="countRef === 0" message="No penalties">
|
||||||
<span> <button class="round-btn" @click="goToCreate">Create</button> a penalty </span>
|
<span> <button class="round-btn" @click="goToCreate">Create</button> a penalty </span>
|
||||||
@@ -41,6 +41,7 @@ import { TASK_FIELDS } from '@/common/models'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||||
const listRef = ref()
|
const listRef = ref()
|
||||||
const countRef = ref(-1)
|
const countRef = ref(-1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reward-assign-view">
|
<div class="reward-assign-view">
|
||||||
<h2>Assign Rewards</h2>
|
<h2>Assign Rewards{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||||
<div class="reward-view">
|
<div class="reward-view">
|
||||||
<MessageBlock v-if="rewardCountRef === 0" message="No rewards">
|
<MessageBlock v-if="rewardCountRef === 0" message="No rewards">
|
||||||
<span> <button class="round-btn" @click="goToCreateReward">Create</button> a reward </span>
|
<span> <button class="round-btn" @click="goToCreateReward">Create</button> a reward </span>
|
||||||
@@ -41,6 +41,7 @@ import { REWARD_FIELDS } from '@/common/models'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||||
|
|
||||||
const rewardListRef = ref()
|
const rewardListRef = ref()
|
||||||
const rewardCountRef = ref(-1)
|
const rewardCountRef = ref(-1)
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ const notificationListCountRef = ref(-1)
|
|||||||
const refreshKey = ref(0)
|
const refreshKey = ref(0)
|
||||||
|
|
||||||
function handleNotificationClick(item: PendingConfirmation) {
|
function handleNotificationClick(item: PendingConfirmation) {
|
||||||
router.push({ name: 'ParentView', params: { id: item.child_id } })
|
router.push({
|
||||||
|
name: 'ParentView',
|
||||||
|
params: { id: item.child_id },
|
||||||
|
query: { scrollTo: item.entity_id, entityType: item.entity_type },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRewardRequest(event: Event) {
|
function handleRewardRequest(event: Event) {
|
||||||
@@ -107,6 +111,10 @@ onUnmounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.2rem;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.child-info {
|
.child-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -114,6 +122,13 @@ onUnmounted(() => {
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--dialog-child-name);
|
color: var(--dialog-child-name);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-info span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reward-info {
|
.reward-info {
|
||||||
@@ -123,13 +138,26 @@ onUnmounted(() => {
|
|||||||
margin-bottom: 0rem;
|
margin-bottom: 0rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--notification-reward-name);
|
color: var(--notification-reward-name);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-info span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-info img,
|
||||||
|
.child-info img {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.requested-text {
|
.requested-text {
|
||||||
margin: 0 0.7rem;
|
margin: 0 0.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--dialog-message);
|
color: var(--dialog-message);
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
|||||||
imageFields?: readonly string[]
|
imageFields?: readonly string[]
|
||||||
isParentAuthenticated?: boolean
|
isParentAuthenticated?: boolean
|
||||||
filterFn?: (item: any) => boolean
|
filterFn?: (item: any) => boolean
|
||||||
|
sortFn?: (a: any, b: any) => number
|
||||||
getItemClass?: (item: any) => string | string[] | Record<string, boolean>
|
getItemClass?: (item: any) => string | string[] | Record<string, boolean>
|
||||||
enableEdit?: boolean
|
enableEdit?: boolean
|
||||||
childId?: string
|
childId?: string
|
||||||
@@ -49,6 +50,7 @@ const fetchItems = async () => {
|
|||||||
// Try to use 'tasks', 'reward_status', or 'items' as fallback
|
// Try to use 'tasks', 'reward_status', or 'items' as fallback
|
||||||
let itemList = data[props.itemKey || 'items'] || []
|
let itemList = data[props.itemKey || 'items'] || []
|
||||||
if (props.filterFn) itemList = itemList.filter(props.filterFn)
|
if (props.filterFn) itemList = itemList.filter(props.filterFn)
|
||||||
|
if (props.sortFn) itemList = [...itemList].sort(props.sortFn)
|
||||||
items.value = itemList
|
items.value = itemList
|
||||||
// Fetch images for each item
|
// Fetch images for each item
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -90,8 +92,19 @@ async function refresh() {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
refresh,
|
refresh,
|
||||||
items,
|
items,
|
||||||
|
centerItem,
|
||||||
|
scrollToItem,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const scrollToItem = async (itemId: string) => {
|
||||||
|
await nextTick()
|
||||||
|
const card = itemRefs.value[itemId]
|
||||||
|
if (card) {
|
||||||
|
;(card as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
|
}
|
||||||
|
await centerItem(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
const centerItem = async (itemId: string) => {
|
const centerItem = async (itemId: string) => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const wrapper = scrollWrapper.value
|
const wrapper = scrollWrapper.value
|
||||||
|
|||||||
Reference in New Issue
Block a user