251 files
This commit is contained in:
@@ -292,6 +292,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
// to lean on) and our shell was stripped out in the meantime.
|
||||
if (!this._rendered || !this.querySelector(".tm-shell")) this._render();
|
||||
this._bindHaPickers(false);
|
||||
if (this._backdateCard) this._backdateCard.hass = value;
|
||||
}
|
||||
get hass() { return this._hass; }
|
||||
set narrow(_v) {}
|
||||
@@ -646,6 +647,9 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (act === "save-gift") { this._doGiftPoints(); return; }
|
||||
if (act === "adjust-points") { this._doAdjustPoints(t.dataset.id, Number(t.dataset.delta)); return; }
|
||||
if (act === "adjust-points-custom") { this._openAdjustDialog(t.dataset.id); return; }
|
||||
if (act === "backdate-open") { this._openBackdateDialog(t.dataset.id); return; }
|
||||
if (act === "save-backdate") { this._saveBackdateDialog(); return; }
|
||||
if (act === "close-backdate") { this._closeDialog(true); this._fetchState(); return; }
|
||||
if (act === "sound-preview-field") {
|
||||
const sel = t.closest(".tm-sound-row")?.querySelector('select[data-field="completion_sound"]');
|
||||
if (sel) this._previewSound(sel.value);
|
||||
@@ -1973,8 +1977,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
unavailability_entity: c.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!c.pause_streak_when_unavailable,
|
||||
linked_user_id: c.linked_user_id || "",
|
||||
picture_entity: c.picture_entity || "",
|
||||
birthday: c.birthday || "",
|
||||
age_group: c.age_group || "",
|
||||
is_adult: !!c.is_adult,
|
||||
presence_entity: c.presence_entity || "",
|
||||
// Kiosk PIN (#930): write-only. The panel only knows whether one is set.
|
||||
kiosk_pin: "", kiosk_pin_clear: false,
|
||||
@@ -1985,8 +1991,8 @@ class TaskMatePanel extends HTMLElement {
|
||||
name: "", avatar: "mdi:account-circle", availability_entity: "",
|
||||
availability_inverted: false, unavailability_entity: "",
|
||||
pause_streak_when_unavailable: false,
|
||||
linked_user_id: "", presence_entity: "",
|
||||
birthday: "", age_group: "",
|
||||
linked_user_id: "", picture_entity: "", presence_entity: "",
|
||||
birthday: "", age_group: "", is_adult: false,
|
||||
kiosk_pin: "", kiosk_pin_clear: false, has_kiosk_pin: false,
|
||||
} });
|
||||
}
|
||||
@@ -2035,14 +2041,16 @@ class TaskMatePanel extends HTMLElement {
|
||||
availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
|
||||
unavailability_entity: d.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "",
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "",
|
||||
presence_entity: d.presence_entity || "" }
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "", is_adult: !!d.is_adult,
|
||||
presence_entity: d.presence_entity || "",
|
||||
picture_entity: d.picture_entity || "" }
|
||||
: { type: "taskmate/update_child", child_id: d.id, name: d.name.trim(), avatar: d.avatar || "mdi:account-circle",
|
||||
availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
|
||||
unavailability_entity: d.unavailability_entity || "",
|
||||
pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "",
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "",
|
||||
presence_entity: d.presence_entity || "" };
|
||||
birthday: (d.birthday || "").trim(), age_group: d.age_group || "", is_adult: !!d.is_adult,
|
||||
presence_entity: d.presence_entity || "",
|
||||
picture_entity: d.picture_entity || "" };
|
||||
const { ok, err, res } = await this._callWS(payload);
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
|
||||
// The PIN goes through its own command so it is hashed server-side and
|
||||
@@ -3572,6 +3580,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
this._zoneCache = {};
|
||||
this._applyDesign();
|
||||
this._bindHaPickers();
|
||||
this._mountBackdateCard();
|
||||
if (this._palette) this._focusPalette();
|
||||
return;
|
||||
}
|
||||
@@ -3632,6 +3641,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
}
|
||||
|
||||
this._applyDesign();
|
||||
this._mountBackdateCard();
|
||||
}
|
||||
|
||||
// A dialog plays its opening animation only when it first appears (#974).
|
||||
@@ -4492,7 +4502,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
<div class="tm-child-head">
|
||||
<div class="tm-avatar">${this._mdi(child.avatar)}</div>
|
||||
<div class="tm-child-name">
|
||||
<h3>${this._esc(child.name || this._t("panel.child_unnamed"))}</h3>
|
||||
<h3>${this._esc(child.name || this._t("panel.child_unnamed"))}${child.is_adult ? ` <span class="tm-pill tm-pill-accent">${this._t("panel.child_adult_pill")}</span>` : ""}</h3>
|
||||
${this._idBadge(child.id)}
|
||||
<div class="tm-meta">${child.availability_entity ? `<code title="${this._esc(child.availability_entity)}">${this._esc(child.availability_entity)}</code>${child.availability_inverted ? ` <em>${this._t("panel.child_inverted")}</em>` : ""}` : `${this._t("panel.child_no_availability")}`}${child.unavailability_entity ? ` · ${this._t("panel.child_busy_label")} <code title="${this._esc(child.unavailability_entity)}">${this._esc(child.unavailability_entity)}</code>` : ""}</div>
|
||||
</div>
|
||||
@@ -4507,6 +4517,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
<div class="tm-card-foot">
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="edit-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_edit")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="reorder-chores-for-child" data-id="${this._esc(child.id)}" title="${this._t("panel.chore_order_title")}">⇅ ${this._t("panel.chore_order_title")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm" data-act="backdate-open" data-id="${this._esc(child.id)}" title="${this._esc(this._t("panel.backdate_hint"))}">📅 ${this._t("panel.backdate_button")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_delete")}</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -6484,9 +6495,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
</div>
|
||||
<div class="tm-dash-tile-f">
|
||||
${found ? `<span class="tm-dash-made"><ha-icon icon="mdi:check"></ha-icon>${this._t("panel.dash_created")}</span>` : ""}
|
||||
<span class="tm-dash-sp"></span>
|
||||
${found ? `<button type="button" class="tm-btn tm-btn-sm" data-act="dash-open" data-v="${this._esc(found)}">${this._t("panel.dash_open")}</button>` : ""}
|
||||
<button type="button" class="tm-btn tm-btn-sm ${found ? "" : "tm-btn-raised"}" data-act="dash-new" data-v="${kind}" ${off ? "disabled" : ""}>${this._t("panel.dash_create_btn")}</button>
|
||||
<span class="tm-dash-acts">
|
||||
${found ? `<button type="button" class="tm-btn tm-btn-sm" data-act="dash-open" data-v="${this._esc(found)}">${this._t("panel.dash_open")}</button>` : ""}
|
||||
<button type="button" class="tm-btn tm-btn-sm ${found ? "" : "tm-btn-raised"}" data-act="dash-new" data-v="${kind}" ${off ? "disabled" : ""}>${this._t("panel.dash_create_btn")}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
@@ -6587,8 +6599,11 @@ class TaskMatePanel extends HTMLElement {
|
||||
.tm-dash-tile-b { flex: 1; padding: 12px 14px; }
|
||||
.tm-dash-tile-b p { margin: 0 0 8px; font-size: 13px; color: var(--tm-text-muted); }
|
||||
.tm-dash-tile-b small { color: var(--tm-text-faint); font-size: 12px; }
|
||||
.tm-dash-tile-f { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--tm-border-soft); }
|
||||
.tm-dash-sp { flex: 1; }
|
||||
/* Wraps rather than squeezing: with "Created" + Open + Create… a narrow
|
||||
tile drops the buttons to their own right-aligned line (#1059). */
|
||||
.tm-dash-tile-f { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--tm-border-soft); }
|
||||
.tm-dash-acts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-inline-start: auto; }
|
||||
.tm-dash-acts .tm-btn { flex: none; }
|
||||
.tm-dash-made { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--tm-positive); white-space: nowrap; --mdc-icon-size: 14px; }
|
||||
.tm-dash-kinds { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
|
||||
.tm-dash-kind { display: flex; align-items: center; gap: 10px; text-align: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--tm-border); background: var(--tm-surface-0); color: var(--tm-text); font-family: inherit; cursor: pointer; --mdc-icon-size: 22px; }
|
||||
@@ -9688,6 +9703,242 @@ class TaskMatePanel extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Log a past job: one done on an earlier day that nobody ticked. The
|
||||
// backend (complete_chore with completed_date) re-checks every rule here —
|
||||
// the last 7 days, the child's own non-rotating job, due that day, that
|
||||
// day's limit — so this list is a convenience, not the gate.
|
||||
// "Today" is Home Assistant's today: the backend checks the day against its
|
||||
// own clock and zone, so a browser in another zone (or a tablet with a wrong
|
||||
// clock) must not offer a day the backend would call today or too old.
|
||||
_backdateDays() {
|
||||
const tz = this._hass?.config?.time_zone;
|
||||
const lang = this._hass?.locale?.language || this._hass?.language || undefined;
|
||||
let todayKey;
|
||||
try {
|
||||
todayKey = new Date().toLocaleDateString("en-CA", tz ? { timeZone: tz } : undefined);
|
||||
} catch (err) {
|
||||
todayKey = new Date().toLocaleDateString("en-CA");
|
||||
}
|
||||
const [y, m, d0] = todayKey.split("-").map(Number);
|
||||
const days = [];
|
||||
for (let back = 1; back <= 7; back++) {
|
||||
// Plain calendar arithmetic on the date parts, at noon UTC so no zone
|
||||
// can push it onto a neighbouring day.
|
||||
const d = new Date(Date.UTC(y, m - 1, d0 - back, 12));
|
||||
days.push({
|
||||
v: d.toISOString().slice(0, 10),
|
||||
l: d.toLocaleDateString(lang, { weekday: "long", day: "numeric", month: "short", timeZone: "UTC" }),
|
||||
dow: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][d.getUTCDay()],
|
||||
});
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
_backdateChores(childId, dow, dayKey) {
|
||||
// Once TaskMate has described the day, offer exactly what it would accept
|
||||
// that hasn't already used up that week's quota. Until then, a first guess
|
||||
// from the weekday alone (logging still checks everything).
|
||||
const info = this._backdateInfo;
|
||||
if (info && info.day === dayKey) {
|
||||
const ids = (info.loggable && info.loggable[childId]) || [];
|
||||
const week = (info.week_progress && info.week_progress[childId]) || {};
|
||||
return (this._state.chores || []).filter(c => {
|
||||
if (!ids.includes(c.id)) return false;
|
||||
const target = Number(c.weekly_target) || 0;
|
||||
return !target || (Number(week[c.id]) || 0) < target;
|
||||
});
|
||||
}
|
||||
return (this._state.chores || []).filter(c => {
|
||||
if (c.enabled === false) return false;
|
||||
if ((c.disabled_for || []).includes(childId)) return false;
|
||||
if ((c.assigned_to || []).length && !(c.assigned_to || []).includes(childId)) return false;
|
||||
if ((c.assignment_mode || "everyone") !== "everyone") return false;
|
||||
// TaskMate refuses these for a past day: an open-ended job needs the
|
||||
// child's note and a price, and a team's members can't be reconstructed.
|
||||
if (c.open_ended || c.team_size >= 2) return false;
|
||||
// A weekly-target job has no fixed days: the child picks them.
|
||||
if (Number(c.weekly_target) > 0) return true;
|
||||
if ((c.schedule_mode || "specific_days") === "specific_days") {
|
||||
const due = c.due_days || [];
|
||||
return !due.length || due.includes(dow);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async _openBackdateDialog(childId) {
|
||||
const child = (this._state.children || []).find(c => c.id === childId);
|
||||
if (!child) return;
|
||||
const day = this._backdateDays()[0];
|
||||
const first = this._backdateChores(childId, day.dow, day.v)[0];
|
||||
this._backdateKey = "";
|
||||
this._backdateUseList = false;
|
||||
this._backdateCardReady = undefined;
|
||||
this._backdateInfo = null;
|
||||
this._openDialog({ kind: "backdate", data: { child_id: childId, day: day.v, chore_id: first ? first.id : "" } });
|
||||
this._backdateCardReady = await this._loadBackdateCard();
|
||||
if (this._dialog && this._dialog.kind === "backdate") this._render();
|
||||
}
|
||||
|
||||
// The day is shown with the dashboard's own sticker chart card, so jobs are
|
||||
// ticked exactly as the children tick them. The panel isn't a dashboard and
|
||||
// never loads dashboard resources, so the card (and the attribute helper it
|
||||
// reads the sensors through) are imported here, at the same versioned URLs
|
||||
// a dashboard uses — a browser that has both open runs each module once.
|
||||
// If the card can't load, the dialog falls back to a plain job list.
|
||||
async _loadBackdateCard() {
|
||||
if (customElements.get("taskmate-sticker-chart-card")) return true;
|
||||
try {
|
||||
await import(`/taskmate/taskmate-attr-resolver.js?v=${PANEL_VERSION}`);
|
||||
await import(`/taskmate/taskmate-sticker-chart-card.js?v=${PANEL_VERSION}`);
|
||||
return !!customElements.get("taskmate-sticker-chart-card");
|
||||
} catch (err) {
|
||||
console.warn("TaskMate: sticker chart card unavailable in the panel", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_overviewEntity() {
|
||||
const states = (this._hass && this._hass.states) || {};
|
||||
return Object.keys(states).find(id => {
|
||||
const a = id.startsWith("sensor.") && states[id].attributes;
|
||||
return a && a.points_name !== undefined && Array.isArray(a.children);
|
||||
}) || "";
|
||||
}
|
||||
|
||||
// Called after every render: the card element is created once and moved
|
||||
// into the dialog's placeholder, so a re-render never loses its state.
|
||||
//
|
||||
// Nothing here may throw: it runs inside the panel's render, so an error
|
||||
// would leave the whole panel dead until a reload. If the card can't be
|
||||
// built (its module hasn't defined it yet, or the overview sensor is
|
||||
// missing), the dialog falls back to the plain job list instead.
|
||||
_mountBackdateCard() {
|
||||
const host = this.querySelector(".tm-backdate-host");
|
||||
if (!host || !this._dialog || this._dialog.kind !== "backdate") return;
|
||||
// Not defined yet: _openBackdateDialog re-renders once the import settles.
|
||||
if (!customElements.get("taskmate-sticker-chart-card")) return;
|
||||
try {
|
||||
const entity = this._overviewEntity();
|
||||
if (!entity) throw new Error("no TaskMate overview sensor");
|
||||
const d = this._dialog.data;
|
||||
if (!this._backdateCard) {
|
||||
this._backdateCard = document.createElement("taskmate-sticker-chart-card");
|
||||
this._backdateCard.addEventListener("taskmate-day-changed", () => this._refreshBackdateDay());
|
||||
}
|
||||
const card = this._backdateCard;
|
||||
const key = `${d.child_id}|${d.day}`;
|
||||
if (this._backdateKey !== key) {
|
||||
card.setConfig({ entity, child_id: d.child_id, date: d.day, show_claim: false });
|
||||
this._backdateKey = key;
|
||||
// Nothing from the previous day may linger while this one loads.
|
||||
card.dayLoggable = undefined;
|
||||
card.dayWeekProgress = {};
|
||||
card.dayCompletions = [];
|
||||
this._refreshBackdateDay();
|
||||
// Changing the day isn't an unsaved edit: every tick is saved as it's made.
|
||||
this._dialogInitialHash = this._hashDialog();
|
||||
}
|
||||
card.hass = this._hass;
|
||||
if (card.parentNode !== host) host.appendChild(card);
|
||||
} catch (err) {
|
||||
console.warn("TaskMate: sticker chart card couldn't be shown in the panel", err);
|
||||
if (!this._backdateUseList) {
|
||||
this._backdateUseList = true;
|
||||
this._backdateCard = null;
|
||||
// Re-render on the next turn rather than from inside a render.
|
||||
setTimeout(() => { if (this._dialog && this._dialog.kind === "backdate") this._render(); }, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What TaskMate says about the chosen day: its completions, that week's
|
||||
// weekly-target progress and the chores it would accept. Feeds both the
|
||||
// card and the plain-list fallback.
|
||||
async _refreshBackdateDay() {
|
||||
const d = this._dialog && this._dialog.data;
|
||||
if (!d) return;
|
||||
const day = d.day;
|
||||
this._backdateInfoPending = day;
|
||||
const { ok, res, err } = await this._callWS({ type: "taskmate/day_completions", date: day });
|
||||
if (this._backdateInfoPending === day) this._backdateInfoPending = null;
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
|
||||
if (!this._dialog || this._dialog.kind !== "backdate" || this._dialog.data.day !== day) return;
|
||||
this._backdateInfo = {
|
||||
day,
|
||||
loggable: res.loggable || {},
|
||||
week_progress: res.week_progress || {},
|
||||
};
|
||||
const card = this._backdateCard;
|
||||
if (card) {
|
||||
card.dayLoggable = this._backdateInfo.loggable;
|
||||
card.dayWeekProgress = this._backdateInfo.week_progress;
|
||||
card.dayCompletions = res.completions || [];
|
||||
} else {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
_renderBackdateDialog() {
|
||||
const d = this._dialog.data;
|
||||
const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
|
||||
const name = child.name || this._t("panel.child_unnamed");
|
||||
const days = this._backdateDays();
|
||||
const day = days.find(x => x.v === d.day) || days[0];
|
||||
if (this._backdateCardReady !== false && !this._backdateUseList) {
|
||||
return this._dialogShell(
|
||||
this._t("panel.backdate_title", { name }),
|
||||
[
|
||||
`<p class="tm-field-hint">${this._esc(this._t("panel.backdate_hint"))}</p>`,
|
||||
this._select(this._t("panel.backdate_day"), "day", day.v, days, "", true),
|
||||
this._backdateCardReady ? `<div class="tm-backdate-host"></div>` : `<p class="tm-field-hint">…</p>`,
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn tm-btn-raised" data-act="close-backdate">${this._t("panel.backdate_finish")}</button>`
|
||||
);
|
||||
}
|
||||
// The plain list needs the day described too; fetched once per day.
|
||||
if ((!this._backdateInfo || this._backdateInfo.day !== day.v) && this._backdateInfoPending !== day.v) {
|
||||
setTimeout(() => this._refreshBackdateDay(), 0);
|
||||
}
|
||||
const chores = this._backdateChores(d.child_id, day.dow, day.v);
|
||||
if (!chores.some(c => c.id === d.chore_id)) d.chore_id = chores[0] ? chores[0].id : "";
|
||||
return this._dialogShell(
|
||||
this._t("panel.backdate_title", { name }),
|
||||
[
|
||||
`<p class="tm-field-hint">${this._esc(this._t("panel.backdate_hint_list"))}</p>`,
|
||||
this._select(this._t("panel.backdate_day"), "day", day.v, days, "", true),
|
||||
chores.length
|
||||
? this._select(this._t("panel.backdate_chore"), "chore_id", d.chore_id,
|
||||
chores.map(c => ({ v: c.id, l: `${c.name} (${c.points})` })))
|
||||
: `<p class="tm-field-hint">${this._esc(this._t("panel.backdate_none", { name }))}</p>`,
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
|
||||
<button type="button" class="tm-btn tm-btn-raised" data-act="save-backdate" ${chores.length ? "" : "disabled"}>${this._t("panel.backdate_save")}</button>`
|
||||
);
|
||||
}
|
||||
|
||||
async _saveBackdateDialog() {
|
||||
const d = this._dialog.data;
|
||||
if (!d.child_id || !d.chore_id || !d.day || this._backdateBusy) return;
|
||||
this._backdateBusy = true;
|
||||
try {
|
||||
const { ok, err } = await this._callService("complete_chore", {
|
||||
chore_id: d.chore_id, child_id: d.child_id, as_parent: true, completed_date: d.day,
|
||||
});
|
||||
if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
|
||||
const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
|
||||
const chore = (this._state.chores || []).find(c => c.id === d.chore_id) || {};
|
||||
const day = this._backdateDays().find(x => x.v === d.day);
|
||||
this._closeDialog(true);
|
||||
await this._fetchState();
|
||||
this._showToast("ok", this._t("panel.backdate_done", {
|
||||
chore: chore.name || "", name: child.name || "", day: day ? day.l : d.day,
|
||||
}));
|
||||
} finally {
|
||||
this._backdateBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
_openSwapDialog(choreId) {
|
||||
const c = (this._state.chores || []).find(x => x.id === choreId);
|
||||
if (!c) return;
|
||||
@@ -9729,6 +9980,7 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (this._dialog.kind === "swap") return this._renderSwapDialog();
|
||||
if (this._dialog.kind === "gift") return this._renderGiftDialog();
|
||||
if (this._dialog.kind === "adjust") return this._renderAdjustDialog();
|
||||
if (this._dialog.kind === "backdate") return this._renderBackdateDialog();
|
||||
if (this._dialog.kind === "child") return this._renderChildDialog();
|
||||
if (this._dialog.kind === "chore") return this._renderChoreDialog();
|
||||
if (this._dialog.kind === "reward") return this._renderRewardDialog();
|
||||
@@ -9820,6 +10072,9 @@ class TaskMatePanel extends HTMLElement {
|
||||
[
|
||||
this._field(this._t("panel.child_name_label"), "name", d.name, "text", this._t("panel.child_name_placeholder")),
|
||||
this._iconPickerField(this._t("panel.child_avatar_label"), "avatar", d.avatar),
|
||||
// A grown-up's profile (#1061): auto-approved, hidden from cards by default.
|
||||
this._switch(this._t("panel.child_adult_label"), "is_adult", !!d.is_adult,
|
||||
this._esc(this._t("panel.child_adult_hint"))),
|
||||
this._field(this._t("panel.child_birthday_label"), "birthday", d.birthday, "text",
|
||||
this._esc(this._t("panel.child_birthday_hint"))),
|
||||
// Setup wizard age group (#980): only steers chore suggestions.
|
||||
@@ -9842,6 +10097,10 @@ class TaskMatePanel extends HTMLElement {
|
||||
(this._haUsers || []).map(u => ({ v: u.id, l: u.is_admin ? `${u.name} (admin)` : u.name }))
|
||||
),
|
||||
this._t("panel.child_link_user_hint")),
|
||||
// Any entity with a picture: normally the child's person, but an image
|
||||
// or camera entity works the same way.
|
||||
this._entityPickerField(this._t("panel.child_picture_entity_label"), "picture_entity", d.picture_entity,
|
||||
[], this._t("panel.child_picture_entity_hint"), { withPicture: true }),
|
||||
this._kioskPinField(d),
|
||||
].join(""),
|
||||
`<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
|
||||
@@ -10912,13 +11171,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
else { (this._settingsEntityDraft = this._settingsEntityDraft || {})[field] = value; }
|
||||
}
|
||||
|
||||
_entityPickerField(label, name, value, domains, hintHtml = "") {
|
||||
_entityPickerField(label, name, value, domains, hintHtml = "", { withPicture = false } = {}) {
|
||||
const friendly = value && this._hass?.states?.[value]?.attributes?.friendly_name || "";
|
||||
const icon = value ? (this._hass?.states?.[value]?.attributes?.icon || this._entityDomainIcon(value)) : "";
|
||||
return `
|
||||
<div class="tm-field">
|
||||
<span class="tm-field-label">${this._esc(label)}</span>
|
||||
<div class="tm-ep" data-ep-field="${name}" data-ep-domains="${(domains || []).join(",")}">
|
||||
<div class="tm-ep" data-ep-field="${name}" data-ep-domains="${(domains || []).join(",")}"${withPicture ? ' data-ep-picture="1"' : ""}>
|
||||
${value
|
||||
? `<div class="tm-ep-selected">
|
||||
<ha-icon icon="${this._esc(icon)}" class="tm-ep-selected-icon"></ha-icon>
|
||||
@@ -10945,6 +11204,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
const entities = Object.entries(this._hass.states)
|
||||
.filter(([id, st]) => {
|
||||
if (domains.length && !domains.some(d => id.startsWith(d + "."))) return false;
|
||||
// A photo picker lists only entities that have a picture to show, and
|
||||
// never TaskMate's own child sensors: they show the photo chosen here,
|
||||
// so picking one would make the face its own source.
|
||||
if (picker.dataset.epPicture) {
|
||||
if (!st.attributes?.entity_picture) return false;
|
||||
if (this._hass.entities?.[id]?.platform === "taskmate") return false;
|
||||
}
|
||||
if (!query) return true;
|
||||
const fn = st.attributes?.friendly_name || "";
|
||||
return id.toLowerCase().includes(query) || fn.toLowerCase().includes(query);
|
||||
|
||||
Reference in New Issue
Block a user