/**
* TaskMate Chore Auction Card (#982)
*
* A reverse auction on one occurrence of an unpopular chore: the child this
* card belongs to bids the fewest points they would do it for, lowest bid
* wins, and the winner gets that occurrence at their price. Bids are sealed —
* the card shows how many bids are in, the child's own bid, and after closing
* the winner and the winning price. Never anyone else's amount.
*
* The auctions are fetched over the `taskmate/auctions/list` WebSocket
* command, which applies the linked-child rule and only ever returns this
* child's own bid. `sensor.taskmate_auctions` carries a public digest (bid
* counts, results); the card re-fetches whenever that digest moves.
*
* Single layout, so it consumes the design tokens directly (like the bounty
* 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
* an ac- prefix: the shared design kit defines its own .btn / .bar / .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 = "#5e35b1";
const AUCTIONS_ENTITY = "sensor.taskmate_auctions";
// 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 TaskMateAuctionCard extends LitElement {
static get properties() {
return {
hass: { type: Object },
config: { type: Object },
_auctions: { type: Array },
_error: { type: String },
_sheet: { type: Object },
_busy: { type: Boolean },
};
}
constructor() {
super();
this._auctions = null;
this._error = "";
this._sheet = null;
this._busy = false;
this._tick = null;
this._fetchedKey = 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;
this._fetchedKey = null;
}
static getStubConfig() {
return { entity: "sensor.taskmate_overview", child_id: "" };
}
static getConfigElement() {
return document.createElement("taskmate-auction-card-editor");
}
getCardSize() {
return 6;
}
connectedCallback() {
super.connectedCallback();
// Countdowns tick client-side from the server's closing times; re-render
// once a second only while an auction is actually open.
this._tick = setInterval(() => {
if ((this._auctions || []).some(a => a.status === "open")) this.requestUpdate();
}, 1000);
this._fetchedKey = null;
}
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;
}
updated() {
// Re-fetch whenever the public digest moves (a bid came in, one closed).
const key = this._digestKey();
if (this.hass && key !== this._fetchedKey) {
this._fetchedKey = key;
this._fetch();
}
}
_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 || {};
}
_digestKey() {
const digest = this._attrs().auctions || this.hass?.states?.[AUCTIONS_ENTITY]?.attributes?.auctions || [];
return `${this.config?.child_id}|${JSON.stringify(digest)}`;
}
_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 : "?";
}
_lang() {
return this.hass?.locale?.language || this.hass?.language || undefined;
}
/** "Saturday 4 October" (long) or "Sat 4 Oct" for an ISO date. */
_date(iso, long = true) {
const d = new Date(`${iso}T00:00:00`);
if (Number.isNaN(d.getTime())) return iso || "";
return d.toLocaleDateString(this._lang(), long
? { weekday: "long", day: "numeric", month: "long" }
: { weekday: "short", day: "numeric", month: "short" });
}
_when(isoInstant) {
const d = new Date(isoInstant || "");
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleString(this._lang(), { weekday: "short", hour: "2-digit", minute: "2-digit" });
}
_fmtDur(ms) {
const sec = Math.max(0, Math.round(ms / 1000));
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (d) return this._t("bounty.dur_days", { d, h });
if (h) return this._t("bounty.dur_hours", { h, m: String(m).padStart(2, "0") });
return this._t("bounty.dur_minutes", { m, s: String(s).padStart(2, "0") });
}
_left(a) {
const t = Date.parse(a.closes_at || "");
return Number.isFinite(t) ? t - Date.now() : 0;
}
_avatar(childId, size = 30) {
const children = this._children();
const idx = Math.max(0, children.findIndex(c => String(c.id) === String(childId)));
const name = this._childName(childId);
const colour = `var(--tmd-c${(idx % 6) + 1}, ${CHILD_COLOURS[idx % CHILD_COLOURS.length]})`;
return html`${name.charAt(0).toUpperCase()}`;
}
/**
* 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