251 files

This commit is contained in:
Home Assistant Version Control
2026-10-16 12:38:25 +00:00
parent 3acf0cdf8d
commit 64eb0c647c
251 changed files with 6380 additions and 14560 deletions
@@ -66,6 +66,14 @@ class TaskMateRewardsCard extends LitElement {
return fn ? fn(this.hass, key, params) : key;
}
/** Avatar: child's photo → MDI avatar → default icon. */
_avatarGlyph(source, fallback = "mdi:account-circle", cls = "tm-face-img") {
const face = window.__taskmate_child_visual(source);
return face.kind === "image"
? html`<img class="${cls}" src="${face.url}" alt="" loading="lazy">`
: html`<ha-icon icon="${face.kind === "icon" ? face.icon : fallback}"></ha-icon>`;
}
static get styles() {
const base = css`
:host {
@@ -1104,14 +1112,58 @@ class TaskMateRewardsCard extends LitElement {
// in-card selection to the first child so the claim button renders (and the
// child tabs have a selection). Without this, childId stays null and no claim
// button is ever shown, making rewards impossible to claim.
if (this.hass && this.config && !this.config.child_id && !this._selectedChildId) {
// Adult profiles (#1071): the default is the first family child, and a
// selection that's no longer listed (show_adults switched off) moves back.
if (this.hass && this.config && !this.config.child_id) {
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
|| this.hass.states?.[this.config.entity]?.attributes || {};
const kids = attrs.children || [];
if (kids.length) this._selectedChildId = kids[0].id;
const kids = this._family(attrs.children || []);
if (kids.length && !kids.some((c) => c.id === this._selectedChildId)) this._selectedChildId = kids[0].id;
}
}
// The children a family-wide view lists (#1061/#1071): adults only with
// show_adults, and everyone when the card is pinned to a child_id.
_family(children) {
return (window.__taskmate_family_children ? window.__taskmate_family_children(children, this.config) : children);
}
/**
* Who a reward row lists (#1071). Non-jackpot rewards and the child badges
* follow the family filter. A jackpot's breakdown does too, except that an
* adult who has put points into it stays listed — dropping them would leave
* the segments and the total not adding up.
*/
_rewardPeople(reward, children) {
const assignedTo = reward.assigned_to || [];
const family = this._family(children);
const assigned = (c) => assignedTo.length === 0 || assignedTo.includes(c.id);
const relevantChildren = family.filter(assigned);
const listed = new Set(family.map((c) => c.id));
const hidden = new Set(children.filter((c) => !listed.has(c.id)).map((c) => c.id));
const allocations = reward.pool_allocations || {};
const contributors = relevantChildren.concat(
children.filter((c) => hidden.has(c.id) && assigned(c) && (allocations[c.id] || 0) !== 0)
);
return {
relevantChildren,
contributors,
badgeIds: assignedTo.filter((cid) => !hidden.has(cid)),
};
}
// 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;
}
@@ -1125,7 +1177,7 @@ class TaskMateRewardsCard extends LitElement {
${children.map((c) => html`
<div class="child-tab ${activeId === c.id ? 'selected' : ''}"
@click=${() => this._selectChild(c.id)}>
<ha-icon icon="${c.avatar || 'mdi:account-circle'}"></ha-icon>
${this._avatarGlyph(c, "mdi:account-circle", "tm-face-chip")}
${c.name}
</div>
`)}
@@ -1176,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) => {
@@ -1226,7 +1270,7 @@ class TaskMateRewardsCard extends LitElement {
: ""}
</div>
${this._renderChildTabs(children)}
${this._renderChildTabs(this._family(children))}
${anyPoolReward && activeChild ? this._renderSpendableBanner(activeChild, pointsIcon, pointsName) : ''}
@@ -1353,10 +1397,8 @@ class TaskMateRewardsCard extends LitElement {
// All costs are static — just use reward.cost
const displayCost = this._getDisplayCost(reward);
// Get relevant children for this reward
const relevantChildren = assignedTo.length === 0
? children
: children.filter(c => assignedTo.includes(c.id));
// Get relevant children for this reward (family-filtered, #1071)
const { relevantChildren, contributors, badgeIds } = this._rewardPeople(reward, children);
// Determine active child context
const childId = this.config?.child_id || this._selectedChildId;
@@ -1373,7 +1415,7 @@ class TaskMateRewardsCard extends LitElement {
let childContributions = [];
if (isJackpot) {
relevantChildren.forEach((child, index) => {
contributors.forEach((child, index) => {
// In pool mode, show allocated points only; in wallet mode show wallet points
const points = enablePoolMode
? (poolAllocations[child.id] || 0)
@@ -1390,11 +1432,9 @@ class TaskMateRewardsCard extends LitElement {
if (enablePoolMode) {
// Pool mode: show this child's allocation for this reward
currentStars = childId ? (poolAllocations[childId] || 0) : 0;
} else if (this.config.child_id) {
const filteredChild = children.find(c => c.id === this.config.child_id);
currentStars = filteredChild ? (filteredChild.points || 0) : 0;
} else if (relevantChildren.length > 0) {
currentStars = relevantChildren[0].points || 0;
} else {
// The pinned or selected child, so switching tabs moves the bar (#1068)
currentStars = relevantChild ? (relevantChild.points || 0) : 0;
}
}
@@ -1478,7 +1518,7 @@ class TaskMateRewardsCard extends LitElement {
<div class="assigned-children">
${assignedTo.length === 0
? html`<span class="child-badge all-children">${this._t('rewards.all_children')}</span>`
: assignedTo.map(
: badgeIds.map(
(cid) =>
html`<span class="child-badge">${childMap[cid] || cid}</span>`
)}
@@ -1837,11 +1877,11 @@ class TaskMateRewardsCard extends LitElement {
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
_av(child, tone, size) {
const a = (child && child.avatar) || "";
const inner = a.startsWith("mdi:")
? html`<ha-icon icon="${a}"></ha-icon>`
: a
? html`<img src="${a}" alt="${child.name}">`
const face = window.__taskmate_child_visual(child);
const inner = face.kind === "icon"
? html`<ha-icon icon="${face.icon}"></ha-icon>`
: face.kind === "image"
? html`<img src="${face.url}" alt="${child.name}">`
: ((child && child.name) || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
}
@@ -1852,7 +1892,6 @@ class TaskMateRewardsCard extends LitElement {
* _getDisplayCost — static only). Returns everything the designed rows need.
*/
_rewardData(reward, children) {
const assignedTo = reward.assigned_to || [];
const isJackpot = reward.is_jackpot || false;
const cardForcesPool = this.config.enable_pool_mode === true;
// Jackpot rewards are always pool-mode (#552): everyone deposits into the
@@ -1861,9 +1900,7 @@ class TaskMateRewardsCard extends LitElement {
const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
const displayCost = this._getDisplayCost(reward);
const relevantChildren = assignedTo.length === 0
? children
: children.filter(c => assignedTo.includes(c.id));
const { relevantChildren, contributors } = this._rewardPeople(reward, children);
const childId = this.config?.child_id || this._selectedChildId;
const relevantChild = childId
@@ -1877,7 +1914,7 @@ class TaskMateRewardsCard extends LitElement {
const childContributions = [];
if (isJackpot) {
relevantChildren.forEach((child, index) => {
contributors.forEach((child, index) => {
const points = enablePoolMode
? (poolAllocations[child.id] || 0)
: (child.points || 0);
@@ -1891,11 +1928,9 @@ class TaskMateRewardsCard extends LitElement {
});
} else if (enablePoolMode) {
currentStars = childId ? (poolAllocations[childId] || 0) : 0;
} else if (this.config.child_id) {
const fc = children.find(c => c.id === this.config.child_id);
currentStars = fc ? (fc.points || 0) : 0;
} else if (relevantChildren.length > 0) {
currentStars = relevantChildren[0].points || 0;
} else {
// The pinned or selected child, so switching tabs moves the bar (#1068)
currentStars = relevantChild ? (relevantChild.points || 0) : 0;
}
const percentage = displayCost > 0 ? Math.min((currentStars / displayCost) * 100, 100) : 0;
@@ -1978,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;
@@ -1999,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(children)}
${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}">
@@ -2060,7 +2093,7 @@ class TaskMateRewardsCard extends LitElement {
const badges = showChildBadges && !d.isJackpot
? (assignedTo.length === 0
? html`<span class="chip soft rw-badge rw-badge-all">${this._t('rewards.all_children')}</span>`
: assignedTo.map((cid) => html`<span class="chip rw-badge">${childMap[cid] || cid}</span>`))
: this._rewardPeople(reward, children).badgeIds.map((cid) => html`<span class="chip rw-badge">${childMap[cid] || cid}</span>`))
: '';
const costBadge = html`<span class="chip rw-cost"><ha-icon style="--mdc-icon-size:1.1em;vertical-align:-0.15em" icon="${pointsIcon}"></ha-icon> ${d.displayCost} ${pointsName}</span>`;
@@ -2349,6 +2382,7 @@ class TaskMateRewardsCardEditor extends LitElement {
show_child_badges: this._t('rewards.editor.show_child_badges'),
enable_pool_mode: this._t('rewards.editor.enable_pool_mode'),
expand_to_fit: this._t('rewards.editor.expand_to_fit'),
show_adults: this._t('common.editor.show_adults'),
};
return labels[schemaEntry.name] ?? schemaEntry.name;
};
@@ -2361,6 +2395,7 @@ class TaskMateRewardsCardEditor extends LitElement {
show_child_badges: this._t('rewards.editor.show_child_badges_helper'),
enable_pool_mode: this._t('rewards.editor.enable_pool_mode_helper'),
expand_to_fit: this._t('rewards.editor.expand_to_fit_helper'),
show_adults: this._t('common.editor.show_adults_helper'),
};
return helpers[schemaEntry.name] ?? '';
};
@@ -2378,13 +2413,14 @@ class TaskMateRewardsCardEditor extends LitElement {
show_child_badges: this.config.show_child_badges !== false,
enable_pool_mode: this.config.enable_pool_mode === true,
expand_to_fit: this.config.expand_to_fit !== false,
show_adults: this.config.show_adults === true,
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._buildSchema()}
.schema=${this._buildSchema().concat(window.__taskmate_adults_field ? window.__taskmate_adults_field(this.hass, this.config) : [])}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._formChanged}
@@ -2423,7 +2459,7 @@ class TaskMateRewardsCardEditor extends LitElement {
} else if (key === 'show_child_badges' && value === true) {
// Default is true — omit to keep config minimal
delete newConfig[key];
} else if (key === 'enable_pool_mode' && value === false) {
} else if ((key === 'enable_pool_mode' || key === 'show_adults') && value === false) {
delete newConfig[key];
} else if (key === 'expand_to_fit' && value === true) {
// Default is true (fluid) — omit to keep config minimal