diff --git a/custom_components/taskmate/www/taskmate-rewards-card.js b/custom_components/taskmate/www/taskmate-rewards-card.js index e9582fec..49bb26e1 100644 --- a/custom_components/taskmate/www/taskmate-rewards-card.js +++ b/custom_components/taskmate/www/taskmate-rewards-card.js @@ -1152,6 +1152,18 @@ class TaskMateRewardsCard extends LitElement { }; } + // The rewards the card lists: those open to everyone plus those assigned to + // the pinned child, or to the selected tab when nothing is pinned (#1074). + // Without the tab filter a child was offered a sibling's reward to claim. + _rewardsFor(allRewards) { + const childId = this.config.child_id || this._selectedChildId; + if (!childId) return allRewards; + return allRewards.filter((reward) => { + const assignedTo = reward.assigned_to || []; + return assignedTo.length === 0 || assignedTo.includes(childId); + }); + } + _selectChild(id) { this._selectedChildId = id; } @@ -1216,15 +1228,7 @@ class TaskMateRewardsCard extends LitElement { const pointsIcon = attrs.points_icon || "mdi:star"; const pointsName = attrs.points_name || this._t("common.stars"); - // Filter rewards based on child_id if configured - let rewards = allRewards; - if (this.config.child_id) { - rewards = allRewards.filter((reward) => { - const assignedTo = reward.assigned_to || []; - // Show reward if assigned_to is empty (available to all) OR contains the child_id - return assignedTo.length === 0 || assignedTo.includes(this.config.child_id); - }); - } + const rewards = this._rewardsFor(allRewards); // Sort rewards: jackpot first, then by calculated cost ascending within each group const sortedRewards = [...rewards].sort((a, b) => { @@ -2009,13 +2013,7 @@ class TaskMateRewardsCard extends LitElement { const pointsIcon = attrs.points_icon || "mdi:star"; const pointsName = attrs.points_name || this._t("common.stars"); - let rewards = allRewards; - if (this.config.child_id) { - rewards = allRewards.filter((reward) => { - const assignedTo = reward.assigned_to || []; - return assignedTo.length === 0 || assignedTo.includes(this.config.child_id); - }); - } + const rewards = this._rewardsFor(allRewards); const sortedRewards = [...rewards].sort((a, b) => { if (a.is_jackpot && !b.is_jackpot) return -1; @@ -2030,16 +2028,20 @@ class TaskMateRewardsCard extends LitElement { const header = this._designHeader(rewards.length); - const body = sortedRewards.length === 0 - ? html`
${this._t('rewards.empty_title')}
${this._t('rewards.empty_subtitle')}
` + const empty = html`
${this._t('rewards.empty_title')}
${this._t('rewards.empty_subtitle')}
`; + // The tabs stay up when only the selected child has nothing, so the other + // children can still be picked (#1074). + const body = allRewards.length === 0 + ? empty : html` ${this._designChildTabs(this._family(children))} ${(anyPoolReward && activeChild) || activeChild ? this._designChildSelector(activeChild, pointsIcon, pointsName, design) : ''} + ${sortedRewards.length === 0 ? empty : html`
${sortedRewards.map((r) => this._designRewardRow(r, children, pointsIcon, pointsName, design))} -
`; + `}`; return html` diff --git a/tests/frontend/taskmate-rewards-card.test.cjs b/tests/frontend/taskmate-rewards-card.test.cjs index f849bc8f..4f584574 100644 --- a/tests/frontend/taskmate-rewards-card.test.cjs +++ b/tests/frontend/taskmate-rewards-card.test.cjs @@ -204,3 +204,50 @@ test("a card pinned to a child still shows that child's progress", () => { assert.deepEqual(progress(pinned), { classic: [25], designed: [25] }); }); + +// With no child_id pinned, every tab listed every reward, so a child was shown +// a reward assigned only to a sibling, with a claim button the backend then +// refused (#1074). The selected tab now filters the list like a pinned card. + +function fullCard(selectedChildId, design, rewards) { + const card = new RewardsCard(); + card.config = { entity: "sensor.taskmate_overview", ...(design ? { card_design: design } : {}) }; + card.hass = { + states: { + "sensor.taskmate_overview": { + state: "2", + attributes: { + children: [ + { id: "kid1", name: "Kid 1", points: 30 }, + { id: "kid2", name: "Kid 2", points: 10 }, + ], + rewards, + }, + }, + }, + }; + card._selectedChildId = selectedChildId; + return render(card.render()).markup; +} + +const OPEN = { id: "film", name: "Film night", cost: 40, assigned_to: [] }; +const SIBLING = { id: "bike", name: "Bike ride", cost: 20, assigned_to: ["kid2"] }; + +for (const design of [null, "playroom"]) { + const label = design || "classic"; + + test(`${label}: a tab lists only the rewards that child can have`, () => { + const kid1 = fullCard("kid1", design, [OPEN, SIBLING]); + const kid2 = fullCard("kid2", design, [OPEN, SIBLING]); + assert.match(kid1, /Film night/); + assert.doesNotMatch(kid1, /Bike ride/); + assert.match(kid2, /Film night/); + assert.match(kid2, /Bike ride/); + }); + + test(`${label}: the tabs stay when the selected child has no rewards`, () => { + const kid1 = fullCard("kid1", design, [SIBLING]); + assert.doesNotMatch(kid1, /Bike ride/); + assert.match(kid1, /Kid 2/); + }); +}