Updated Scheduler and Maintainance Apps
This commit is contained in:
@@ -777,6 +777,9 @@
|
||||
"panel.settings_chore_rotation_title": "Aufgabenrotation",
|
||||
"panel.settings_currency_hint": "Wie Punkte benannt und in der App angezeigt werden.",
|
||||
"panel.settings_currency_title": "Währung",
|
||||
"panel.settings_parents_title": "Eltern (ohne Admin-Rechte)",
|
||||
"panel.settings_parents_hint": "Geben Sie einem Haushaltsmitglied ohne Admin-Rechte die tägliche Kontrolle — Aufgaben bestätigen, Punkte anpassen, Belohnungen freigeben — ohne Home-Assistant-Administratorrechte. Dieses Admin-Panel können sie weiterhin nicht öffnen und die Konfiguration nicht ändern.",
|
||||
"panel.settings_parents_empty": "Keine Home-Assistant-Benutzer ohne Admin-Rechte gefunden. Erstellen Sie zuerst einen normalen HA-Benutzer und wählen Sie ihn dann hier aus.",
|
||||
"panel.settings_evening_hint": "Standard 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Abend",
|
||||
"panel.settings_history_title": "Verlauf & Serien",
|
||||
|
||||
@@ -815,6 +815,9 @@
|
||||
"panel.settings_chore_rotation_title": "Chore Rotation",
|
||||
"panel.settings_currency_hint": "How points are named and shown across the app.",
|
||||
"panel.settings_currency_title": "Currency",
|
||||
"panel.settings_parents_title": "Parents (no admin rights)",
|
||||
"panel.settings_parents_hint": "Give a non-admin household member day-to-day control — approve chores, adjust points, confirm rewards — without Home Assistant admin rights. They still can't open this admin panel or change configuration.",
|
||||
"panel.settings_parents_empty": "No non-admin Home Assistant users found. Create a standard HA user first, then tick them here.",
|
||||
"panel.settings_evening_hint": "Default 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Evening",
|
||||
"panel.settings_history_title": "History & streaks",
|
||||
|
||||
@@ -815,6 +815,9 @@
|
||||
"panel.settings_chore_rotation_title": "Chore Rotation",
|
||||
"panel.settings_currency_hint": "How points are named and shown across the app.",
|
||||
"panel.settings_currency_title": "Currency",
|
||||
"panel.settings_parents_title": "Parents (no admin rights)",
|
||||
"panel.settings_parents_hint": "Give a non-admin household member day-to-day control — approve chores, adjust points, confirm rewards — without Home Assistant admin rights. They still can't open this admin panel or change configuration.",
|
||||
"panel.settings_parents_empty": "No non-admin Home Assistant users found. Create a standard HA user first, then tick them here.",
|
||||
"panel.settings_evening_hint": "Default 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Evening",
|
||||
"panel.settings_history_title": "History & streaks",
|
||||
|
||||
@@ -777,6 +777,9 @@
|
||||
"panel.settings_chore_rotation_title": "Rotation des tâches",
|
||||
"panel.settings_currency_hint": "Comment les points sont nommés et affichés dans l'application.",
|
||||
"panel.settings_currency_title": "Monnaie",
|
||||
"panel.settings_parents_title": "Parents (sans droits admin)",
|
||||
"panel.settings_parents_hint": "Donnez à un membre du foyer non-administrateur le contrôle quotidien — valider les tâches, ajuster les points, confirmer les récompenses — sans droits d’administrateur Home Assistant. Il ne pourra toujours pas ouvrir ce panneau d’administration ni modifier la configuration.",
|
||||
"panel.settings_parents_empty": "Aucun utilisateur Home Assistant non-administrateur trouvé. Créez d’abord un utilisateur HA standard, puis cochez-le ici.",
|
||||
"panel.settings_evening_hint": "Par défaut 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Soir",
|
||||
"panel.settings_history_title": "Historique et séries",
|
||||
|
||||
@@ -777,6 +777,9 @@
|
||||
"panel.settings_chore_rotation_title": "Oppgaverotasjon",
|
||||
"panel.settings_currency_hint": "Hvordan poeng navngis og vises i appen.",
|
||||
"panel.settings_currency_title": "Valuta",
|
||||
"panel.settings_parents_title": "Foreldre (uten adminrettigheter)",
|
||||
"panel.settings_parents_hint": "Gi et husstandsmedlem uten adminrettigheter daglig kontroll — godkjenne oppgaver, justere poeng, bekrefte belønninger — uten Home Assistant-adminrettigheter. De kan fortsatt ikke åpne dette adminpanelet eller endre konfigurasjonen.",
|
||||
"panel.settings_parents_empty": "Fant ingen Home Assistant-brukere uten adminrettigheter. Opprett en vanlig HA-bruker først, og huk deretter av her.",
|
||||
"panel.settings_evening_hint": "Standard 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Kveld",
|
||||
"panel.settings_history_title": "Historikk og serier",
|
||||
|
||||
@@ -777,6 +777,9 @@
|
||||
"panel.settings_chore_rotation_title": "Oppgåverotasjon",
|
||||
"panel.settings_currency_hint": "Korleis poeng vert namngjeve og viste i appen.",
|
||||
"panel.settings_currency_title": "Valuta",
|
||||
"panel.settings_parents_title": "Foreldre (utan adminrettar)",
|
||||
"panel.settings_parents_hint": "Gi eit husstandsmedlem utan adminrettar dagleg kontroll — godkjenne oppgåver, justere poeng, stadfeste løningar — utan Home Assistant-adminrettar. Dei kan framleis ikkje opne dette adminpanelet eller endre konfigurasjonen.",
|
||||
"panel.settings_parents_empty": "Fann ingen Home Assistant-brukarar utan adminrettar. Opprett ein vanleg HA-brukar først, og hak deretter av her.",
|
||||
"panel.settings_evening_hint": "Standard 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Kveld",
|
||||
"panel.settings_history_title": "Historikk og rekkjer",
|
||||
|
||||
@@ -777,6 +777,9 @@
|
||||
"panel.settings_chore_rotation_title": "Rotação de Tarefas",
|
||||
"panel.settings_currency_hint": "Como os pontos são nomeados e exibidos no app.",
|
||||
"panel.settings_currency_title": "Moeda",
|
||||
"panel.settings_parents_title": "Pais (sem direitos de administrador)",
|
||||
"panel.settings_parents_hint": "Dê a um membro da família sem permissões de administrador o controle do dia a dia — aprovar tarefas, ajustar pontos, confirmar recompensas — sem direitos de administrador do Home Assistant. Ele ainda não poderá abrir este painel de administração nem alterar a configuração.",
|
||||
"panel.settings_parents_empty": "Nenhum usuário do Home Assistant sem permissões de administrador foi encontrado. Crie primeiro um usuário HA padrão e depois marque-o aqui.",
|
||||
"panel.settings_evening_hint": "Padrão 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Noite",
|
||||
"panel.settings_history_title": "Histórico e sequências",
|
||||
|
||||
@@ -782,6 +782,9 @@
|
||||
"panel.settings_chore_rotation_title": "Rotação de Tarefas",
|
||||
"panel.settings_currency_hint": "Como os pontos são nomeados e apresentados na aplicação.",
|
||||
"panel.settings_currency_title": "Moeda",
|
||||
"panel.settings_parents_title": "Pais (sem direitos de administrador)",
|
||||
"panel.settings_parents_hint": "Dê a um membro do agregado sem permissões de administrador o controlo do dia a dia — aprovar tarefas, ajustar pontos, confirmar recompensas — sem direitos de administrador do Home Assistant. Continuará sem poder abrir este painel de administração nem alterar a configuração.",
|
||||
"panel.settings_parents_empty": "Não foram encontrados utilizadores do Home Assistant sem permissões de administrador. Crie primeiro um utilizador HA normal e depois selecione-o aqui.",
|
||||
"panel.settings_evening_hint": "Predefinição 17:00 – 21:00",
|
||||
"panel.settings_evening_label": "Noite",
|
||||
"panel.settings_history_title": "Histórico e sequências",
|
||||
|
||||
@@ -66,6 +66,35 @@
|
||||
|
||||
window.__taskmate_attrs = resolveAttrs;
|
||||
|
||||
/**
|
||||
* Non-admin parent role (#661). True for Home Assistant admins and for users
|
||||
* the admin has designated as TaskMate parents (published as the
|
||||
* parent_user_ids attribute on sensor.taskmate_overview). Parents get the
|
||||
* day-to-day controls (approve/reject, complete-on-behalf, undo) without HA
|
||||
* admin rights. Admin remains the only tier that can open the admin panel or
|
||||
* change configuration.
|
||||
*/
|
||||
function isTaskmateParent(hass) {
|
||||
if (!hass || !hass.user) return false;
|
||||
if (hass.user.is_admin) return true;
|
||||
let ids;
|
||||
const ov = hass.states && hass.states["sensor.taskmate_overview"];
|
||||
if (ov && ov.attributes && Array.isArray(ov.attributes.parent_user_ids)) {
|
||||
ids = ov.attributes.parent_user_ids;
|
||||
} else if (hass.states) {
|
||||
// Overview sensor renamed: find whichever taskmate sensor carries the list.
|
||||
for (const st of Object.values(hass.states)) {
|
||||
if (st && st.attributes && Array.isArray(st.attributes.parent_user_ids)) {
|
||||
ids = st.attributes.parent_user_ids;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.isArray(ids) && ids.includes(hass.user.id);
|
||||
}
|
||||
|
||||
window.__taskmate_is_parent = isTaskmateParent;
|
||||
|
||||
/**
|
||||
* Event-driven update guard for LitElement cards.
|
||||
*
|
||||
|
||||
@@ -3385,8 +3385,8 @@ class TaskMateChildCard extends LitElement {
|
||||
if (this._loading[bonusKey]) return;
|
||||
|
||||
// Undoing removes already-awarded points, so it is parent-only — short-circuit
|
||||
// for non-admins with one friendly message instead of a doomed service call.
|
||||
if (this.hass.user && !this.hass.user.is_admin) {
|
||||
// for non-parents with one friendly message instead of a doomed service call.
|
||||
if (!window.__taskmate_is_parent(this.hass)) {
|
||||
this._notifyUndo(this._t("child.undo_not_allowed"));
|
||||
return;
|
||||
}
|
||||
@@ -3839,11 +3839,11 @@ class TaskMateChildCard extends LitElement {
|
||||
}
|
||||
|
||||
// Undoing a completion removes already-awarded points, so it is parent-only
|
||||
// (this mirrors the admin gate on the reject_chore service). For a non-admin
|
||||
// (this mirrors the parent gate on the reject_chore service). For a non-parent
|
||||
// user the call always fails with a raw "Unauthorized" — HA shows its own
|
||||
// snackbar and we used to add an error notification on top. Short-circuit
|
||||
// with one clear, friendly message and never fire the doomed call.
|
||||
if (this.hass.user && !this.hass.user.is_admin) {
|
||||
if (!window.__taskmate_is_parent(this.hass)) {
|
||||
this._notifyUndo(this._t("child.undo_not_allowed"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
pendingApprovals = (attrs.chore_completions || completions.filter(c => !c.approved)).length;
|
||||
}
|
||||
|
||||
const isAdmin = !!(this.hass && this.hass.user && this.hass.user.is_admin);
|
||||
const isParent = window.__taskmate_is_parent(this.hass);
|
||||
|
||||
// Aggregate today's progress across all children
|
||||
let doneTotal = 0;
|
||||
@@ -555,9 +555,9 @@ class TaskMateOverviewCard extends LitElement {
|
||||
<div class="tmd-bd">
|
||||
${pendingApprovals > 0 ? this._ovAlert(design, pendingApprovals) : ""}
|
||||
<div class="ov-kids">
|
||||
${kids.map((k) => this._ovKid(design, k, isAdmin, pointsIcon))}
|
||||
${kids.map((k) => this._ovKid(design, k, isParent, pointsIcon))}
|
||||
</div>
|
||||
${isAdmin ? kids.filter(k => this._expanded[k.child.id]).map(k => this._ovBehalf(k, pointsIcon)) : ""}
|
||||
${isParent ? kids.filter(k => this._expanded[k.child.id]).map(k => this._ovBehalf(k, pointsIcon)) : ""}
|
||||
${this._ovToday(design, doneTotal, choreTotal, overallPct)}
|
||||
</div>
|
||||
</ha-card>`;
|
||||
@@ -630,11 +630,11 @@ class TaskMateOverviewCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_ovKid(design, k, isAdmin, pointsIcon) {
|
||||
_ovKid(design, k, isParent, pointsIcon) {
|
||||
const cls = design === "console" ? "cn" : design === "cleanpro" ? "cp" : "";
|
||||
const nameUpper = design === "console" ? `${k.child.name.toUpperCase()} · ${k.pct}%` : k.child.name;
|
||||
const isOpen = !!this._expanded[k.child.id];
|
||||
const expandable = isAdmin;
|
||||
const expandable = isParent;
|
||||
return html`
|
||||
<div class="ov-kid ${cls} ${expandable ? 'tm-clickable' : ''}" style="--ac:${k.tone}"
|
||||
@click="${expandable ? () => { this._expanded = { ...this._expanded, [k.child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
@@ -737,7 +737,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
// Pending approvals for this child
|
||||
const childPending = childCompletions.filter(c => !c.approved).length;
|
||||
|
||||
const isAdmin = !!(this.hass && this.hass.user && this.hass.user.is_admin);
|
||||
const isParent = window.__taskmate_is_parent(this.hass);
|
||||
const outstanding = childChores.filter(c => {
|
||||
const doneToday = completions.filter(
|
||||
x => x.child_id === child.id && x.chore_id === c.id && !x.bonus_subtask_id
|
||||
@@ -751,8 +751,8 @@ class TaskMateOverviewCard extends LitElement {
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${avatar}"></ha-icon>
|
||||
</div>
|
||||
<div class="child-main ${isAdmin ? 'tm-expandable' : ''}"
|
||||
@click="${isAdmin ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
<div class="child-main ${isParent ? 'tm-expandable' : ''}"
|
||||
@click="${isParent ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
<div class="child-name-row">
|
||||
<span class="child-name">${child.name}</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;flex-shrink:0;">
|
||||
@@ -787,7 +787,7 @@ class TaskMateOverviewCard extends LitElement {
|
||||
` : html`
|
||||
<div style="font-size:0.8rem;color:var(--secondary-text-color);opacity:0.7;">${this._t('common.no_chores_today')}</div>
|
||||
`}
|
||||
${isAdmin && isOpen ? html`
|
||||
${isParent && isOpen ? html`
|
||||
<div class="tm-outstanding" @click="${(e) => e.stopPropagation()}">
|
||||
<div class="tm-outstanding-hdr">${this._t('common.complete_on_behalf_heading')}</div>
|
||||
${outstanding.length === 0 ? html`
|
||||
|
||||
@@ -412,7 +412,12 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (!t) return;
|
||||
const act = t.dataset.act;
|
||||
|
||||
if (act === "tab") { this._activeTab = t.dataset.tab; this._filter = ""; this._render(); return; }
|
||||
if (act === "tab") {
|
||||
this._activeTab = t.dataset.tab; this._filter = ""; this._render();
|
||||
// Settings tab lists HA users for the parent-role picker (#661); load then repaint.
|
||||
if (this._activeTab === "settings") this._ensureHaUsers().then(() => this._render());
|
||||
return;
|
||||
}
|
||||
if (act === "close-dialog") { this._closeDialog(); return; }
|
||||
if (act === "clear-field") { this._setEntityField(t.dataset.field, ""); this._render(); return; }
|
||||
if (act === "pick-entity") { this._setEntityField(t.dataset.field, t.dataset.value); this._closeEntityDropdown(); this._render(); return; }
|
||||
@@ -1899,6 +1904,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
root.querySelectorAll("ha-icon-picker[data-setting]").forEach(el => {
|
||||
payload[el.dataset.setting] = el.value || "";
|
||||
});
|
||||
// Non-admin parent role (#661): collect the ticked HA users.
|
||||
const parentBoxes = root.querySelectorAll("input[type=checkbox][data-parent-user]");
|
||||
if (parentBoxes.length) {
|
||||
payload.parent_user_ids = Array.from(parentBoxes)
|
||||
.filter(el => el.checked)
|
||||
.map(el => el.dataset.parentUser);
|
||||
}
|
||||
if (this._settingsEntityDraft) {
|
||||
Object.assign(payload, this._settingsEntityDraft);
|
||||
}
|
||||
@@ -3721,6 +3733,21 @@ class TaskMatePanel extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tm-section">
|
||||
<div class="tm-section-head"><div>
|
||||
<h3>${this._t("panel.settings_parents_title")}</h3>
|
||||
<p class="tm-meta">${this._t("panel.settings_parents_hint")}</p>
|
||||
</div></div>
|
||||
<div class="tm-section-body">
|
||||
${(this._haUsers || []).filter(u => !u.is_admin).map(u => `
|
||||
<label class="tm-setting-row" style="cursor:pointer">
|
||||
<div class="tm-setting-label">${this._esc(u.name)}</div>
|
||||
<input type="checkbox" data-parent-user="${this._esc(u.id)}" ${(s.parent_user_ids || []).includes(u.id) ? "checked" : ""}>
|
||||
</label>`).join("")
|
||||
|| `<p class="tm-meta">${this._t("panel.settings_parents_empty")}</p>`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tm-section">
|
||||
<div class="tm-section-head"><div><h3>${this._t("panel.settings_history_title")}</h3></div></div>
|
||||
<div class="tm-section-body">
|
||||
|
||||
@@ -763,7 +763,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
if (design === "cleanpro") {
|
||||
const isAdmin = !!(this.hass && this.hass.user && this.hass.user.is_admin);
|
||||
const isParent = window.__taskmate_is_parent(this.hass);
|
||||
return html`
|
||||
<div class="row pd-ov-cp" style="--ac:${tone}">
|
||||
${this._av(child.name, child.avatar, tone, 36)}
|
||||
@@ -774,7 +774,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
</div>
|
||||
<div class="bar" style="margin-top:7px;height:8px"><i style="width:${pct}%"></i></div>
|
||||
</div>
|
||||
${isAdmin ? html`<button class="btn ghost round sm"
|
||||
${isParent ? html`<button class="btn ghost round sm"
|
||||
title="${this._t('dashboard.tab_points')}"
|
||||
@click="${() => this._switchTab('points')}">+</button>` : ""}
|
||||
</div>
|
||||
@@ -982,7 +982,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
const isComplete = total > 0 && approved >= total;
|
||||
const cls = isComplete ? "complete" : pct > 0 ? "partial" : "none";
|
||||
|
||||
const isAdmin = !!(this.hass && this.hass.user && this.hass.user.is_admin);
|
||||
const isParent = window.__taskmate_is_parent(this.hass);
|
||||
const outstanding = childChores.filter(c => {
|
||||
const doneToday = completions.filter(
|
||||
x => x.child_id === child.id && x.chore_id === c.id && !x.bonus_subtask_id
|
||||
@@ -992,12 +992,12 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
const isOpen = !!this._expanded[child.id];
|
||||
|
||||
return html`
|
||||
<div class="child-tile ${isAdmin ? 'tm-expandable' : ''}">
|
||||
<div class="child-tile ${isParent ? 'tm-expandable' : ''}">
|
||||
<div class="child-avatar">
|
||||
<ha-icon icon="${child.avatar || 'mdi:account-circle'}"></ha-icon>
|
||||
</div>
|
||||
<div class="child-tile-main"
|
||||
@click="${isAdmin ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
@click="${isParent ? () => { this._expanded = { ...this._expanded, [child.id]: !isOpen }; this.requestUpdate(); } : null}">
|
||||
<div class="child-tile-header">
|
||||
<span class="child-tile-name">${child.name}</span>
|
||||
<span class="points-pill">
|
||||
@@ -1011,7 +1011,7 @@ class TaskMateParentDashboardCard extends LitElement {
|
||||
</div>
|
||||
<span class="progress-label">${approved}/${total}</span>
|
||||
</div>
|
||||
${isAdmin && isOpen ? html`
|
||||
${isParent && isOpen ? html`
|
||||
<div class="tm-outstanding" @click="${(e) => e.stopPropagation()}">
|
||||
<div class="tm-outstanding-hdr">${this._t('common.complete_on_behalf_heading')}</div>
|
||||
${outstanding.length === 0 ? html`
|
||||
|
||||
Reference in New Issue
Block a user