Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions custom_components/taskmate/www/taskmate-rewards-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
Expand All @@ -2030,16 +2028,20 @@ class TaskMateRewardsCard extends LitElement {

const header = this._designHeader(rewards.length);

const body = sortedRewards.length === 0
? html`<div class="tmd-empty">${this._t('rewards.empty_title')}<br>${this._t('rewards.empty_subtitle')}</div>`
const empty = html`<div class="tmd-empty">${this._t('rewards.empty_title')}<br>${this._t('rewards.empty_subtitle')}</div>`;
// 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`
<div class="rw-list ${this.config.expand_to_fit === false ? 'rw-scroll' : ''}">
${sortedRewards.map((r) => this._designRewardRow(r, children, pointsIcon, pointsName, design))}
</div>`;
</div>`}`;

return html`
<ha-card class="tmd" style="--hd:${hd}">
Expand Down
47 changes: 47 additions & 0 deletions tests/frontend/taskmate-rewards-card.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
}
Loading