/** * TaskMate Bounty Board Card (#931) * * One-off jobs a parent posts for a fixed number of points. The child this * card belongs to sees the bounties they may claim, claims one (it's theirs * for the claim lock), marks it done — with a photo when the bounty asks for * one — and it goes through the normal approval queue. A sibling's claim shows * dimmed with the time until it returns to the board; bounties the child can't * claim are hidden. * * Reads the `bounties` attribute of sensor.taskmate_bounties through the * attribute resolver, and `children` from the overview entity. * * Single layout, so it consumes the design tokens directly (like the routine * card) rather than carrying a second _renderDesigned() template. Every * --tmd-* reference has the card's original value as its fallback, so classic * — which defines no tokens — keeps the wireframe look. Layout classes carry a * bb- prefix: the shared design kit defines its own .btn / .bar / .av / .chip. */ const LitElement = customElements.get("hui-masonry-view") ? Object.getPrototypeOf(customElements.get("hui-masonry-view")) : Object.getPrototypeOf(customElements.get("hui-view")); const html = LitElement.prototype.html; const css = LitElement.prototype.css; const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d); const DEFAULT_ACCENT = "#e67e22"; const BOUNTIES_ENTITY = "sensor.taskmate_bounties"; // Child avatar colours by position; a design's own palette wins when it has one. const CHILD_COLOURS = ["#ff6b9d", "#3498db", "#1abc9c", "#e67e22", "#9b59b6", "#2ecc71"]; class TaskMateBountyCard extends LitElement { static get properties() { return { hass: { type: Object }, config: { type: Object }, _dialog: { type: Object }, _busy: { type: Boolean }, }; } constructor() { super(); this._dialog = null; this._busy = false; this._tick = null; } setConfig(config) { if (!config || !config.entity) throw new Error("You need to define an entity"); if (!config.child_id) throw new Error("You need to define a child_id"); this.config = config; } static getStubConfig() { return { entity: "sensor.taskmate_overview", child_id: "" }; } static getConfigElement() { return document.createElement("taskmate-bounty-card-editor"); } getCardSize() { return 6; } connectedCallback() { super.connectedCallback(); // Countdowns tick client-side from the server's timestamps. Re-render once // a second only while something on the board is actually counting down. this._tick = setInterval(() => { if (this._hasCountdown()) this.requestUpdate(); }, 1000); } disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._tick); this._tick = null; } shouldUpdate(changedProps) { if (changedProps.has("hass") && changedProps.size === 1) { return window.__taskmate_hasChanged ? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity) : true; } return true; } _t(key, params) { const fn = window.__taskmate_localize; return fn ? fn(this.hass, key, params) : key; } _attrs() { return (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || this.hass?.states?.[this.config.entity]?.attributes || {}; } _bounties() { const attrs = this._attrs(); const list = attrs.bounties || this.hass?.states?.[BOUNTIES_ENTITY]?.attributes?.bounties; return Array.isArray(list) ? list : []; } _children() { return this._attrs().children || []; } _child() { return this._children().find(c => String(c.id) === String(this.config.child_id)); } _childName(id) { const c = this._children().find(ch => String(ch.id) === String(id)); return c ? c.name : "?"; } _eligible(b, childId) { return !Array.isArray(b.eligible) || b.eligible.length === 0 || b.eligible.map(String).includes(String(childId)); } _hasCountdown() { return this._bounties().some(b => b.status === "claimed" || (b.status === "open" && b.expires_at)); } /** * Resolve the design, stamp data-tm-design on the host (so the :host-scoped * token block applies inside this shadow root) and settle the header colour. * The colour is an inline host property: a