diff --git a/backend/config/version.py b/backend/config/version.py index 4f5ad29..f571e02 100644 --- a/backend/config/version.py +++ b/backend/config/version.py @@ -2,7 +2,7 @@ # file: config/version.py import os -BASE_VERSION = "1.0.12" # update manually when releasing features +BASE_VERSION = "1.0.13" # update manually when releasing features def get_full_version() -> str: """ diff --git a/frontend/e2e/.auth/user-cc.json b/frontend/e2e/.auth/user-cc.json index d6847c5..6df02d3 100644 --- a/frontend/e2e/.auth/user-cc.json +++ b/frontend/e2e/.auth/user-cc.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "FZULMCV2SqB1TurS_72i4vCGjYXmL4rQzTvtKFODS-c", + "value": "mY6Ehfjz0l0gIgI1-zrA_HMfYQiNmgnNy6g7v2GoT3M", "domain": "localhost", "path": "/api/auth", - "expires": 1784993494.828813, + "expires": 1785468152.319767, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJiMTkyZDgzZS1mMTQyLTRhZjQtYjAyNC02YzdiNzFmNzFlYjYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzcyMTgzOTR9.7JsdzTZDZd8qErp-GiAaYfU92jCb1L5ROGZXpEaz-dc", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI0YmI3ZTQ3ZS0wNGYxLTQyMDctYjZjYy0yNDM3NDVlOGQ0ZDMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc3MDI5NTJ9.614NMFQB7JcIJ4k4cqKyxlpcyHmt2Hn4PbjCbvLMJ2A", "domain": "localhost", "path": "/", - "expires": 1777218394.827404, + "expires": 1777702952.31857, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,13 +27,13 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777217494503}" + "value": "{\"type\":\"logout\",\"at\":1777692151952}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777390295394}" + "value": "{\"expiresAt\":1777864952730}" } ] } ] -} +} \ No newline at end of file diff --git a/frontend/e2e/.auth/user-delete.json b/frontend/e2e/.auth/user-delete.json index 7165b20..94842f3 100644 --- a/frontend/e2e/.auth/user-delete.json +++ b/frontend/e2e/.auth/user-delete.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "tTh7qrpLh9RjQNVxmjhfQi0VOQwbLa4PJ8xfjlMC9P8", + "value": "fmbL31vJQXFd7NDUWy7eGlRZZoGh0BZipf8CqFOb8zw", "domain": "localhost", "path": "/api/auth", - "expires": 1784993494.703215, + "expires": 1785468152.214735, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiMGY4YzE2YzUtN2ZlNy00ZGZiLWE4ZmMtMTc3NjQ5MDkxOWI4IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3MjE4Mzk0fQ.zSLSZsWZb9N1En9mWCqXxM9d__U7JldwT8pCKaoxzUc", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNzc2YjBkYjctYzYyNy00ODBiLTkzZDYtMTRlMTE4MDQ3NTE5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3NzAyOTUyfQ.I4sfKTo0nsJgKvwDRSztxtyZpWw-oN1y3L4yCtr_CRo", "domain": "localhost", "path": "/", - "expires": 1777218394.702339, + "expires": 1777702952.21406, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,13 +27,13 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777217494445}" + "value": "{\"type\":\"logout\",\"at\":1777692151844}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777390295182}" + "value": "{\"expiresAt\":1777864952556}" } ] } ] -} +} \ No newline at end of file diff --git a/frontend/e2e/.auth/user.json b/frontend/e2e/.auth/user.json index 2f430d9..25d31ea 100644 --- a/frontend/e2e/.auth/user.json +++ b/frontend/e2e/.auth/user.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "iBgMbKeAn8p8X9iS6eumIZHEqKN0yqMKZ09V6nk7s-U", + "value": "YitpHdwm093Mavj6iTYmJOLGLOIKoozGkmYXgmJMTBM", "domain": "localhost", "path": "/api/auth", - "expires": 1784993490.715252, + "expires": 1785468147.453462, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJhM2ZlZjRmNS01MTc4LTRlYWEtOWU0NC1jZjk2MGJiMGQ3ZTkiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzcyMTgzOTB9.PtdzVvtN6rMWPOTS6OfGc8tVSrihktnT2GhVIwKollc", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4ZGNmOWU5Ni1lM2I3LTRkNzYtOWQ1NS01NmE4MjU5ZWQ5NzUiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc3MDI5NDd9.zSetTnajvus3N5uJDBqxRYXfLksoU9ZmkWzqCS0GUvc", "domain": "localhost", "path": "/", - "expires": 1777218390.714722, + "expires": 1777702947.452943, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,13 +27,13 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1777217490520}" + "value": "{\"type\":\"logout\",\"at\":1777692147220}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1777390290893}" + "value": "{\"expiresAt\":1777864947663}" } ] } ] -} +} \ No newline at end of file diff --git a/frontend/e2e/mode_parent/notifications/reward-notification-deny.spec.ts b/frontend/e2e/mode_parent/notifications/reward-notification-deny.spec.ts index 1e49f9d..9727271 100644 --- a/frontend/e2e/mode_parent/notifications/reward-notification-deny.spec.ts +++ b/frontend/e2e/mode_parent/notifications/reward-notification-deny.spec.ts @@ -108,8 +108,8 @@ test.describe('Reward Notification — In-App Denial', () => { await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() - // Look for a Deny button in the dialog - const denyBtn = page.getByRole('button', { name: /deny/i }) + // Look for the reject action in the dialog + const denyBtn = page.getByRole('button', { name: 'Reject', exact: true }) await expect(denyBtn).toBeVisible({ timeout: 5000 }) await denyBtn.click() await expect(denyBtn).not.toBeVisible({ timeout: 5000 }) @@ -127,7 +127,7 @@ test.describe('Reward Notification — In-App Denial', () => { await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() - const denyBtn = page.getByRole('button', { name: /deny/i }) + const denyBtn = page.getByRole('button', { name: 'Reject', exact: true }) await expect(denyBtn).toBeVisible({ timeout: 5000 }) await denyBtn.click() await expect(denyBtn).not.toBeVisible({ timeout: 5000 }) diff --git a/frontend/e2e/mode_parent/task-activation/pending-reward-warning.spec.ts b/frontend/e2e/mode_parent/task-activation/pending-reward-warning.spec.ts index eacdf26..849fe8f 100644 --- a/frontend/e2e/mode_parent/task-activation/pending-reward-warning.spec.ts +++ b/frontend/e2e/mode_parent/task-activation/pending-reward-warning.spec.ts @@ -59,7 +59,9 @@ async function createReward( for (const r of (await pre.json()).rewards ?? []) { if (r.name === name) await request.delete(`/api/reward/${r.id}`) } - await request.put('/api/reward/add', { data: { name, description: 'E2E pending-warn test', cost } }) + await request.put('/api/reward/add', { + data: { name, description: 'E2E pending-warn test', cost }, + }) const list = await request.get('/api/reward/list') return (await list.json()).rewards?.find((r: any) => r.name === name)?.id ?? '' } @@ -160,9 +162,7 @@ test.describe('Pending reward warning dialog', () => { test('Pending reward card shows PENDING banner', async ({ page }) => { await page.goto(`/parent/${childId}`) - const card = rewardSection(page) - .locator('.item-card') - .filter({ hasText: PENDING_REWARD_NAME }) + const card = rewardSection(page).locator('.item-card').filter({ hasText: PENDING_REWARD_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.locator('.pending')).toHaveText('PENDING') }) @@ -177,12 +177,12 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) - // Pending reward warning dialog appears (has a "No" button) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'No', exact: true }).click() + // Pending reward warning dialog appears + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Reject', exact: true }).click() // Dialog dismissed; task confirmation dialog must NOT have appeared - await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible() // Points unchanged await expect(page.locator('.points .value')).toHaveText(String(pointsBefore)) @@ -204,8 +204,8 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) // Pending reward warning dialog - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'Yes' }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Approve', exact: true }).click() // After confirming, the task confirmation dialog appears (has "Cancel" button) await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 }) @@ -219,7 +219,9 @@ test.describe('Pending reward warning dialog', () => { // ── kindness ────────────────────────────────────────────────────────────── - test('Kindness activation — Cancel on pending dialog leaves state unchanged', async ({ page }) => { + test('Kindness activation — Cancel on pending dialog leaves state unchanged', async ({ + page, + }) => { await page.goto(`/parent/${childId}`) const card = kindnessSection(page).locator('.item-card').filter({ hasText: KIND_NAME }) await card.waitFor({ state: 'visible' }) @@ -227,10 +229,10 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'No', exact: true }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Reject', exact: true }).click() - await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible() await expect(page.locator('.points .value')).toHaveText(String(pointsBefore)) }) @@ -245,8 +247,8 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'Yes' }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Approve', exact: true }).click() await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 }) await page.getByRole('button', { name: 'Yes' }).click() @@ -266,10 +268,10 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'No', exact: true }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Reject', exact: true }).click() - await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible() await expect(page.locator('.points .value')).toHaveText(String(pointsBefore)) }) @@ -284,8 +286,8 @@ test.describe('Pending reward warning dialog', () => { await activateItem(card) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'Yes' }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Approve', exact: true }).click() await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 }) await page.getByRole('button', { name: 'Yes' }).click() @@ -320,10 +322,10 @@ test.describe('Pending reward warning dialog', () => { await activateItem(otherCard) // Pending reward warning dialog appears when another reward has a pending request - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'No', exact: true }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Reject', exact: true }).click() - await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible() await expect(page.locator('.points .value')).toHaveText(String(pointsBefore)) // PENDING banner still visible — reward request was not cancelled @@ -346,11 +348,11 @@ test.describe('Pending reward warning dialog', () => { await activateItem(otherCard) - await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible() - await page.getByRole('button', { name: 'Yes' }).click() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible() + await page.getByRole('button', { name: 'Approve', exact: true }).click() // Pending dialog closes; reward confirm does NOT auto-open (other reward must be re-clicked) - await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible() await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible() // The pending reward request was cancelled — PENDING banner should disappear diff --git a/frontend/e2e/mode_parent/task-activation/reward-redemption.spec.ts b/frontend/e2e/mode_parent/task-activation/reward-redemption.spec.ts index 1bcf1f2..4c6aef2 100644 --- a/frontend/e2e/mode_parent/task-activation/reward-redemption.spec.ts +++ b/frontend/e2e/mode_parent/task-activation/reward-redemption.spec.ts @@ -152,7 +152,7 @@ test.describe('Reward redemption', () => { await activateItem(card) await expect(page.getByRole('button', { name: 'Yes' })).toBeVisible() - await page.getByRole('button', { name: 'Cancel' }).click() + await page.getByRole('button', { name: 'No', exact: true }).click() await expect(page.getByRole('button', { name: 'Yes' })).not.toBeVisible() await expect(page.locator('.points .value')).toHaveText(String(before)) diff --git a/frontend/e2e/mode_parent/task-modification/reward-edit-cost.spec.ts b/frontend/e2e/mode_parent/task-modification/reward-edit-cost.spec.ts index 323b18e..d9062dd 100644 --- a/frontend/e2e/mode_parent/task-modification/reward-edit-cost.spec.ts +++ b/frontend/e2e/mode_parent/task-modification/reward-edit-cost.spec.ts @@ -187,11 +187,11 @@ test.describe('Reward edit cost', () => { await card.getByTitle('Edit custom value').click() // PendingRewardDialog should appear warning about the pending state - await expect(page.locator('.modal-message')).toContainText('pending') + await expect(page.getByText(/currently pending/i)).toBeVisible() - // Clicking "No" closes the dialog without opening the override modal - await page.getByRole('button', { name: 'No', exact: true }).click() - await expect(page.locator('.modal-message')).not.toBeVisible() + // Clicking "Reject" closes the dialog without opening the override modal + await page.getByRole('button', { name: 'Reject', exact: true }).click() + await expect(page.getByText(/currently pending/i)).not.toBeVisible() await expect(page.getByRole('button', { name: 'Save' })).not.toBeVisible() }) @@ -209,10 +209,10 @@ test.describe('Reward edit cost', () => { await card.getByTitle('Edit custom value').click() // PendingRewardDialog visible - await expect(page.locator('.modal-message')).toContainText('pending') + await expect(page.getByText(/currently pending/i)).toBeVisible() // Confirming cancels the pending request and opens the override modal - await page.getByRole('button', { name: 'Yes' }).click() + await page.getByRole('button', { name: 'Approve', exact: true }).click() await expect(page.getByRole('button', { name: 'Save' })).toBeVisible({ timeout: 5000 }) await page.getByRole('button', { name: 'Cancel' }).click() }) diff --git a/frontend/src/components/child/ChildView.vue b/frontend/src/components/child/ChildView.vue index bc0677b..63f7ca5 100644 --- a/frontend/src/components/child/ChildView.vue +++ b/frontend/src/components/child/ChildView.vue @@ -452,6 +452,7 @@ function isChoreScheduledToday(item: ChildTask): boolean { } function isChoreExpired(item: ChildTask): boolean { + if (item.pending_status === 'pending') return false if (!item.schedule) return false const today = new Date() const due = getDueTimeToday(item.schedule, today) diff --git a/frontend/src/components/child/ParentView.vue b/frontend/src/components/child/ParentView.vue index e008546..cc59e5d 100644 --- a/frontend/src/components/child/ParentView.vue +++ b/frontend/src/components/child/ParentView.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/components/child/RewardConfirmDialog.vue b/frontend/src/components/child/RewardConfirmDialog.vue index 530e56c..2b2e7e9 100644 --- a/frontend/src/components/child/RewardConfirmDialog.vue +++ b/frontend/src/components/child/RewardConfirmDialog.vue @@ -1,20 +1,22 @@ @@ -36,14 +38,54 @@ defineEmits<{