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/);
+ });
+}