251 files
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user