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:
@@ -2,17 +2,17 @@
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "icI1wOmbV5D-snH1lnz75Z-IGK-DuzK-RR_rKzjvvMs",
|
||||
"value": "55lJfL_-PofI1V-NQ2E3Jlo4f_IE1aXqOF-wX54ZVSY",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1781792623.050125,
|
||||
"expires": 1782155883.275106,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI0NmNmMmU0Ni1kOGFjLTRlYTItOTExYS1iZjJlMjVjNGI1ZDAiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQwMTc1MjN9.KNGwsOe7xEIo-tDbuSP38p3zB1vEB0KZk3mGEtgHs5s",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiIzZGQ0YTI1Yy04ZDI1LTQwNGEtODU0NS1mYjM2ZGRkYTU5NmYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQzODA3ODN9.b0ACVC63OobLzraiOcn4QDXipHXUVMW-Wn42XJFDOn0",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
@@ -27,11 +27,11 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1774016622749}"
|
||||
"value": "{\"type\":\"logout\",\"at\":1774379882977}"
|
||||
},
|
||||
{
|
||||
"name": "parentAuth",
|
||||
"value": "{\"expiresAt\":1774189423292}"
|
||||
"value": "{\"expiresAt\":1774552683610}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "9eQaO12-M1GpCKeqJOmFb07Sms32J1j62DcEb5-neqs",
|
||||
"value": "us7D-X0NRlXld6bpCl4epHaruPIWbkI895jFn3o84LY",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1781792623.010225,
|
||||
"expires": 1782155883.187584,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiMTFlYjUwNTgtZmYxYi00YjgzLWExMTEtMTJjZjFkZjI4ZjA5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc0MDE3NTIyfQ.VCdPbgWlX4Mdr4NSzcyIybyNNbTSLvZE0YQGmQ-owKw",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNmU0MmNhNGQtMGNkNC00MmUzLWFkOGMtMWYyOTZmMzg5NTE5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc0MzgwNzgzfQ.AthhvOE2qsaJo6x37QbGXbT9oIP2pnFks3Pj14MjmcI",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
@@ -27,11 +27,11 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1774016622727}"
|
||||
"value": "{\"type\":\"logout\",\"at\":1774379882932}"
|
||||
},
|
||||
{
|
||||
"name": "parentAuth",
|
||||
"value": "{\"expiresAt\":1774189423292}"
|
||||
"value": "{\"expiresAt\":1774552683575}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "YAXw1Kh8dLOuSYc1ZW_sgYHWRzo0XivYXmAHAr2RV-o",
|
||||
"value": "VqeLgoC9z5NgU2AtA-9xjl4DaRt0hYKDkmAJMN3dcnw",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1781792619.603786,
|
||||
"expires": 1782155879.453865,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiIzMzAwYjVjNy0yMzIxLTQxMWMtOWZiZC03NzY4ZjM4NTI1OWYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQwMTc1MTl9.2XDC_S2IuKeF_S9HTeHEOCWlUAtlrGpByLdIwrtoxfw",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI3ZjA2Njc5OS03NjI1LTQ0ZGQtODIwYi0zNWRjZmFlYWVjNTMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzQzODA3Nzl9.o9urIT0cfexJY8OarcJ4gtJKqN-_DZOw50VToPl25LU",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": -1,
|
||||
@@ -27,11 +27,11 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1774016619428}"
|
||||
"value": "{\"type\":\"logout\",\"at\":1774379879245}"
|
||||
},
|
||||
{
|
||||
"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
|
||||
v-model="code"
|
||||
maxlength="6"
|
||||
class="code-input"
|
||||
class="code-input uppercase"
|
||||
placeholder="6-digit code"
|
||||
@input="code = code.toUpperCase()"
|
||||
@keyup.enter="isCodeValid && verifyCode()"
|
||||
/>
|
||||
<div class="button-group">
|
||||
@@ -260,6 +261,9 @@ function goBack() {
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -27,6 +27,8 @@ import type {
|
||||
ChoreScheduleModifiedPayload,
|
||||
ChoreTimeExtendedPayload,
|
||||
ChildChoreConfirmationPayload,
|
||||
ChildOverrideSetPayload,
|
||||
ChildOverrideDeletedPayload,
|
||||
} from '@/common/models'
|
||||
import { confirmChore, cancelConfirmChore } from '@/common/api'
|
||||
import {
|
||||
@@ -380,6 +382,8 @@ function removeInactivityListeners() {
|
||||
}
|
||||
|
||||
const childChoreListRef = ref()
|
||||
const childKindnessListRef = ref()
|
||||
const childPenaltyListRef = ref()
|
||||
const readyItemId = ref<string | null>(null)
|
||||
const expiryTimers = ref<number[]>([])
|
||||
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 {
|
||||
if (!item.schedule) return true
|
||||
const today = new Date()
|
||||
@@ -435,6 +465,35 @@ function choreDueLabel(item: ChildTask): string | null {
|
||||
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() {
|
||||
expiryTimers.value.forEach((t) => clearTimeout(t))
|
||||
expiryTimers.value = []
|
||||
@@ -487,6 +546,8 @@ onMounted(async () => {
|
||||
eventBus.on('chore_schedule_modified', handleChoreScheduleModified)
|
||||
eventBus.on('chore_time_extended', handleChoreTimeExtended)
|
||||
eventBus.on('child_chore_confirmation', handleChoreConfirmation)
|
||||
eventBus.on('child_override_set', handleOverrideSet)
|
||||
eventBus.on('child_override_deleted', handleOverrideDeleted)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
if (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_time_extended', handleChoreTimeExtended)
|
||||
eventBus.off('child_chore_confirmation', handleChoreConfirmation)
|
||||
eventBus.off('child_override_set', handleOverrideSet)
|
||||
eventBus.off('child_override_deleted', handleOverrideDeleted)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
clearExpiryTimers()
|
||||
removeInactivityListeners()
|
||||
@@ -555,6 +618,7 @@ onUnmounted(() => {
|
||||
})
|
||||
"
|
||||
:filter-fn="(item: ChildTask) => item.type === 'chore' && isChoreScheduledToday(item)"
|
||||
:sort-fn="childChoreSort"
|
||||
>
|
||||
<template #item="{ item }: { item: ChildTask }">
|
||||
<span v-if="isChoreExpired(item)" class="chore-stamp">TOO LATE</span>
|
||||
@@ -590,6 +654,7 @@ onUnmounted(() => {
|
||||
:getItemClass="
|
||||
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
|
||||
"
|
||||
:sort-fn="childRewardSort"
|
||||
>
|
||||
<template #item="{ item }: { item: RewardStatus }">
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="assign-view">
|
||||
<h2>Assign Chores</h2>
|
||||
<h2>Assign Chores{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||
<div class="list-container">
|
||||
<MessageBlock v-if="countRef === 0" message="No chores">
|
||||
<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 router = useRouter()
|
||||
const childId = route.params.id
|
||||
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||
const listRef = ref()
|
||||
const countRef = ref(-1)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="assign-view">
|
||||
<h2>Assign Kindness Acts</h2>
|
||||
<h2>Assign Kindness Acts{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||
<div class="list-container">
|
||||
<MessageBlock v-if="countRef === 0" message="No kindness acts">
|
||||
<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 router = useRouter()
|
||||
const childId = route.params.id
|
||||
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||
const listRef = ref()
|
||||
const countRef = ref(-1)
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ const childPenaltyListRef = ref()
|
||||
const childRewardListRef = ref()
|
||||
const childKindnessListRef = ref()
|
||||
const showPendingRewardDialog = ref(false)
|
||||
const lastEditedItem = ref<{ id: string; type: 'task' | 'reward' } | null>(null)
|
||||
|
||||
// Chore approve/reject
|
||||
const showChoreApproveDialog = ref(false)
|
||||
@@ -243,14 +244,20 @@ function handleChildModified(event: Event) {
|
||||
function handleOverrideSet(event: Event) {
|
||||
const payload = event.payload as ChildOverrideSetPayload
|
||||
if (child.value && payload.override.child_id === child.value.id) {
|
||||
// Refresh the appropriate list to show the override badge
|
||||
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()
|
||||
const editedId = lastEditedItem.value?.id ?? null
|
||||
const scrollAfterRefresh = (listRef: typeof childChoreListRef) => {
|
||||
listRef.value?.refresh().then(() => {
|
||||
if (editedId) listRef.value?.scrollToItem(editedId)
|
||||
})
|
||||
}
|
||||
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}`)
|
||||
return
|
||||
}
|
||||
childChoreListRef.value?.refresh()
|
||||
setTimeout(() => resetExpiryTimers(), 300)
|
||||
}
|
||||
|
||||
@@ -459,6 +467,25 @@ function isChoreInactive(item: ChildTask): boolean {
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
function clearExpiryTimers() {
|
||||
@@ -556,6 +583,10 @@ async function saveOverride() {
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
lastEditedItem.value = {
|
||||
id: overrideEditTarget.value.entity.id,
|
||||
type: overrideEditTarget.value.type,
|
||||
}
|
||||
showOverrideModal.value = false
|
||||
} else {
|
||||
const { msg } = parseErrorResponse(res)
|
||||
@@ -600,6 +631,8 @@ onMounted(async () => {
|
||||
|
||||
if (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) {
|
||||
const promise = fetchChildData(idParam)
|
||||
promise.then((data) => {
|
||||
@@ -609,6 +642,15 @@ onMounted(async () => {
|
||||
rewards.value = data.rewards || []
|
||||
}
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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>
|
||||
@@ -816,6 +874,7 @@ function goToAssignRewards() {
|
||||
})
|
||||
"
|
||||
:filter-fn="(item) => item.type === 'chore'"
|
||||
:sort-fn="parentChoreSort"
|
||||
>
|
||||
<template #item="{ item }: { item: ChildTask }">
|
||||
<!-- Kebab menu -->
|
||||
@@ -983,6 +1042,7 @@ function goToAssignRewards() {
|
||||
@edit-item="(item) => handleEditItem(item, 'reward')"
|
||||
@item-ready="handleItemReady"
|
||||
:getItemClass="(item) => ({ reward: true })"
|
||||
:sort-fn="parentRewardSort"
|
||||
>
|
||||
<template #item="{ item }: { item: RewardStatus }">
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="assign-view">
|
||||
<h2>Assign Penalties</h2>
|
||||
<h2>Assign Penalties{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||
<div class="list-container">
|
||||
<MessageBlock v-if="countRef === 0" message="No penalties">
|
||||
<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 router = useRouter()
|
||||
const childId = route.params.id
|
||||
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||
const listRef = ref()
|
||||
const countRef = ref(-1)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="reward-assign-view">
|
||||
<h2>Assign Rewards</h2>
|
||||
<h2>Assign Rewards{{ childName ? ` for ${childName}` : '' }}</h2>
|
||||
<div class="reward-view">
|
||||
<MessageBlock v-if="rewardCountRef === 0" message="No rewards">
|
||||
<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 router = useRouter()
|
||||
const childId = route.params.id
|
||||
const childName = typeof route.query.name === 'string' ? route.query.name : ''
|
||||
|
||||
const rewardListRef = ref()
|
||||
const rewardCountRef = ref(-1)
|
||||
|
||||
@@ -52,7 +52,11 @@ const notificationListCountRef = ref(-1)
|
||||
const refreshKey = ref(0)
|
||||
|
||||
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) {
|
||||
@@ -107,6 +111,10 @@ onUnmounted(() => {
|
||||
justify-content: center;
|
||||
margin-inline: auto;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.child-info {
|
||||
display: flex;
|
||||
@@ -114,6 +122,13 @@ onUnmounted(() => {
|
||||
gap: 0.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--dialog-child-name);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.child-info span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.reward-info {
|
||||
@@ -123,13 +138,26 @@ onUnmounted(() => {
|
||||
margin-bottom: 0rem;
|
||||
font-weight: 600;
|
||||
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 {
|
||||
margin: 0 0.7rem;
|
||||
margin: 0 0.4rem;
|
||||
font-weight: 500;
|
||||
color: var(--dialog-message);
|
||||
font-size: 1.05rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
imageFields?: readonly string[]
|
||||
isParentAuthenticated?: boolean
|
||||
filterFn?: (item: any) => boolean
|
||||
sortFn?: (a: any, b: any) => number
|
||||
getItemClass?: (item: any) => string | string[] | Record<string, boolean>
|
||||
enableEdit?: boolean
|
||||
childId?: string
|
||||
@@ -49,6 +50,7 @@ const fetchItems = async () => {
|
||||
// Try to use 'tasks', 'reward_status', or 'items' as fallback
|
||||
let itemList = data[props.itemKey || 'items'] || []
|
||||
if (props.filterFn) itemList = itemList.filter(props.filterFn)
|
||||
if (props.sortFn) itemList = [...itemList].sort(props.sortFn)
|
||||
items.value = itemList
|
||||
// Fetch images for each item
|
||||
await Promise.all(
|
||||
@@ -90,8 +92,19 @@ async function refresh() {
|
||||
defineExpose({
|
||||
refresh,
|
||||
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) => {
|
||||
await nextTick()
|
||||
const wrapper = scrollWrapper.value
|
||||
|
||||
Reference in New Issue
Block a user