456 files
This commit is contained in:
+1
-1
@@ -285,6 +285,6 @@ if (!customElements.get("maintenance-budget-section-card")) {
|
||||
registerCustomCard({
|
||||
type: "maintenance-budget-section-card",
|
||||
name: "Maintenance Supporter — Budget",
|
||||
description: "Inline monthly + yearly budget editor",
|
||||
description: "Edit the monthly and yearly budget right on the dashboard.",
|
||||
preview: false,
|
||||
});
|
||||
|
||||
+1
-1
@@ -323,6 +323,6 @@ if (!customElements.get("maintenance-groups-section-card")) {
|
||||
registerCustomCard({
|
||||
type: "maintenance-groups-section-card",
|
||||
name: "Maintenance Supporter — Groups",
|
||||
description: "Inline group CRUD",
|
||||
description: "Create, rename and delete task groups right on the dashboard.",
|
||||
preview: false,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/** Dialog for generating, printing, and downloading QR codes.
|
||||
*
|
||||
* For tasks: shows two QR codes side-by-side — "Info" (ℹ) and "Complete" (✓)
|
||||
* with embedded icons. For objects: shows a single "Info" QR.
|
||||
* with embedded icons, plus the lightning-bolt "Quick-complete" code when the
|
||||
* task has quick-complete defaults (#192: the code existed since 1.3.0 but no
|
||||
* dialog ever offered it). For objects: shows a single "Info" QR.
|
||||
*/
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
@@ -48,6 +50,7 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
@state() private _error = "";
|
||||
@state() private _viewResult: QrResult | null = null;
|
||||
@state() private _completeResult: QrResult | null = null;
|
||||
@state() private _quickResult: QrResult | null = null;
|
||||
@state() private _urlMode: "companion" | "local" | "server" = "companion";
|
||||
|
||||
private _entryId = "";
|
||||
@@ -65,6 +68,7 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._viewResult = null;
|
||||
this._completeResult = null;
|
||||
this._quickResult = null;
|
||||
this._open = true;
|
||||
this._generate();
|
||||
}
|
||||
@@ -83,6 +87,7 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._viewResult = null;
|
||||
this._completeResult = null;
|
||||
this._quickResult = null;
|
||||
this._open = true;
|
||||
this._generate();
|
||||
}
|
||||
@@ -93,6 +98,7 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._viewResult = null;
|
||||
this._completeResult = null;
|
||||
this._quickResult = null;
|
||||
try {
|
||||
const base: Record<string, unknown> = {
|
||||
type: "maintenance_supporter/qr/generate",
|
||||
@@ -109,6 +115,13 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
promises.push(
|
||||
this.hass.connection.sendMessagePromise({ ...base, action: "complete" }),
|
||||
);
|
||||
// The one-tap code only when the task has defaults to record — for
|
||||
// any other task it would just open the complete dialog.
|
||||
if (await this._hasQuickCompleteDefaults()) {
|
||||
promises.push(
|
||||
this.hass.connection.sendMessagePromise({ ...base, action: "quick_complete" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
@@ -118,6 +131,9 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
if (results.length > 1) {
|
||||
this._completeResult = results[1] as QrResult;
|
||||
}
|
||||
if (results.length > 2) {
|
||||
this._quickResult = results[2] as QrResult;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (seq !== this._generateSeq) return;
|
||||
const code = (err as Record<string, unknown>)?.code;
|
||||
@@ -130,6 +146,18 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _hasQuickCompleteDefaults(): Promise<boolean> {
|
||||
try {
|
||||
const obj = await this.hass.connection.sendMessagePromise<{
|
||||
tasks?: Array<{ id: string; quick_complete_defaults?: Record<string, unknown> | null }>;
|
||||
}>({ type: "maintenance_supporter/object", entry_id: this._entryId });
|
||||
const task = (obj.tasks || []).find((tk) => tk.id === this._taskId);
|
||||
return !!task?.quick_complete_defaults && Object.keys(task.quick_complete_defaults).length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _setUrlMode(mode: "companion" | "local" | "server"): void {
|
||||
if (this._urlMode === mode) return;
|
||||
this._urlMode = mode;
|
||||
@@ -152,8 +180,10 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
const safeSub = escapeHtml(subtitle);
|
||||
|
||||
const hasComplete = !!this._completeResult;
|
||||
const hasQuick = !!this._quickResult;
|
||||
const viewLabel = escapeHtml(t("qr_action_view", L));
|
||||
const completeLabel = escapeHtml(t("qr_action_complete", L));
|
||||
const quickLabel = escapeHtml(t("qr_action_quick_complete", L));
|
||||
|
||||
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
@@ -169,7 +199,7 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
.sub{color:#666;font-size:14px;margin-bottom:16px}
|
||||
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
|
||||
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||
.qr-col img{width:${hasComplete ? "200px" : "280px"}}
|
||||
.qr-col img{width:${hasQuick ? "170px" : hasComplete ? "200px" : "280px"}}
|
||||
.qr-label{font-size:13px;font-weight:500;color:#333}
|
||||
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
|
||||
</style></head><body>
|
||||
@@ -184,6 +214,10 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
<img src="${sanitizeDataUri(this._completeResult!.svg_data_uri)}" alt="QR Complete" />
|
||||
<div class="qr-label">${completeLabel}</div>
|
||||
</div>` : ""}
|
||||
${hasQuick ? `<div class="qr-col">
|
||||
<img src="${sanitizeDataUri(this._quickResult!.svg_data_uri)}" alt="QR Quick-complete" />
|
||||
<div class="qr-label">${quickLabel}</div>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
<div class="url">${escapeHtml(this._viewResult.url)}</div>
|
||||
<script>setTimeout(()=>window.print(),300)<\/script>
|
||||
@@ -208,6 +242,7 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
this._open = false;
|
||||
this._viewResult = null;
|
||||
this._completeResult = null;
|
||||
this._quickResult = null;
|
||||
this._error = "";
|
||||
this._loading = false;
|
||||
}
|
||||
@@ -260,6 +295,23 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this._quickResult
|
||||
? html`
|
||||
<div class="qr-item quick">
|
||||
<img
|
||||
class="qr-image small"
|
||||
src="${this._quickResult.svg_data_uri}"
|
||||
alt="QR Quick-complete"
|
||||
/>
|
||||
<div class="qr-item-label">${t("qr_action_quick_complete", L)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${() => this._downloadSvg(this._quickResult!, "quick-complete")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${t("qr_download", L)}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="url-display">${this._viewResult!.url}</div>
|
||||
`
|
||||
@@ -312,6 +364,7 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
}
|
||||
.qr-pair {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* three codes (quick-complete, #192) wrap on a phone */
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
@@ -332,6 +385,7 @@ ${safeSub ? `<div class="sub">${safeSub}</div>` : ""}
|
||||
height: 180px;
|
||||
}
|
||||
.qr-item-label {
|
||||
max-width: 180px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -55,6 +55,8 @@ interface SettingsResponse {
|
||||
shopping_list_entity?: string;
|
||||
panel_enabled: boolean;
|
||||
panel_title: string;
|
||||
/** 2.94: the sidebar title an empty field means (server language). */
|
||||
panel_title_default?: string;
|
||||
/** Opt-in copy of the shipped Assist sentences into the config dir. */
|
||||
install_assist_sentences?: boolean;
|
||||
/** #146: household "low" floors (percent) for discovery + battery fleet. */
|
||||
@@ -207,7 +209,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
@state() private _vacSaving = false;
|
||||
|
||||
// Print QR codes section state
|
||||
@state() private _qrObjects: Array<{ entry_id: string; name: string; task_count: number }> = [];
|
||||
@state() private _qrObjects: Array<{ entry_id: string; name: string; task_count: number; quick_count: number }> = [];
|
||||
@state() private _qrSelectedEntries = new Set<string>();
|
||||
@state() private _qrActions = new Set<string>(["view"]);
|
||||
@state() private _qrUrlMode: "server" | "local" | "companion" = "companion";
|
||||
@@ -220,7 +222,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
@state() private _qrObjectsLoaded = false;
|
||||
|
||||
// Export/Import selective-object picker state
|
||||
@state() private _exportObjects: Array<{ entry_id: string; name: string; task_count: number }> = [];
|
||||
@state() private _exportObjects: Array<{ entry_id: string; name: string; task_count: number; quick_count: number }> = [];
|
||||
@state() private _exportSelectedEntries = new Set<string>();
|
||||
@state() private _exportObjectsLoaded = false;
|
||||
@state() private _docArchiveLoading = false;
|
||||
@@ -1063,7 +1065,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_panel_title", L)}</span>
|
||||
<input type="text" .value=${g.panel_title ?? ""}
|
||||
placeholder="Maintenance"
|
||||
placeholder=${g.panel_title_default || "Maintenance"}
|
||||
maxlength="50"
|
||||
@change=${(e: Event) => this._updateSetting("panel_title", (e.target as HTMLInputElement).value.trim())} />
|
||||
</label>
|
||||
@@ -1684,10 +1686,22 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
|
||||
// --- Section: Print QR codes (v1.1.0) ---
|
||||
|
||||
/** The objects the batch prints for: the ticked ones, or all when none is. */
|
||||
private _qrSelectedRows(): Array<{ entry_id: string; task_count: number; quick_count: number }> {
|
||||
return this._qrSelectedEntries.size === 0 ? this._qrObjects : this._qrObjects.filter((o) => this._qrSelectedEntries.has(o.entry_id));
|
||||
}
|
||||
|
||||
private _renderPrintQr(L: string) {
|
||||
const selectedCount = this._qrSelectedEntries.size || this._qrObjects.length;
|
||||
const rows = this._qrSelectedRows();
|
||||
const taskCount = rows.reduce((n, o) => n + o.task_count, 0);
|
||||
// #192: the quick-complete code only exists for tasks with defaults —
|
||||
// offered when the chosen objects have any (not tied to the completion-
|
||||
// actions switch: defaults set before it was turned off still work).
|
||||
const quickCount = rows.reduce((n, o) => n + o.quick_count, 0);
|
||||
// One code per TASK and action (the estimate multiplied OBJECTS, so the
|
||||
// 200 warning came too late and the server refused the batch).
|
||||
const estimatedQrs = [...this._qrActions].reduce((n, a) => n + (a === "quick_complete" ? quickCount : taskCount), 0);
|
||||
const actionCount = this._qrActions.size;
|
||||
const estimatedQrs = selectedCount * actionCount;
|
||||
const overLimit = estimatedQrs > 200;
|
||||
|
||||
return html`
|
||||
@@ -1721,15 +1735,21 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<div class="qr-filter-group">
|
||||
<div class="qr-filter-label">${t("qr_print_actions", L)}</div>
|
||||
<div class="qr-action-chips">
|
||||
${["view", "complete", "skip"].map((a) => html`
|
||||
<label class="qr-action-chip ${this._qrActions.has(a) ? "active" : ""}">
|
||||
${["view", "complete", "skip", "quick_complete"].map((a) => {
|
||||
const quickNone = a === "quick_complete" && quickCount === 0;
|
||||
return html`
|
||||
<label class="qr-action-chip ${this._qrActions.has(a) && !quickNone ? "active" : ""} ${quickNone ? "disabled" : ""}"
|
||||
title=${quickNone ? t("qr_print_quick_none", L) : nothing}>
|
||||
<input type="checkbox"
|
||||
.checked=${this._qrActions.has(a)}
|
||||
.checked=${this._qrActions.has(a) && !quickNone}
|
||||
.disabled=${quickNone}
|
||||
@change=${(e: Event) => this._toggleQrAction(a, (e.target as HTMLInputElement).checked)} />
|
||||
${t("qr_action_" + a, L)}
|
||||
${t("qr_action_" + a, L)}${a === "quick_complete" && quickCount > 0 ? ` (${quickCount})` : ""}
|
||||
</label>
|
||||
`)}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${quickCount === 0 ? html`<div class="hint qr-quick-hint">${t("qr_print_quick_none", L)}</div>` : nothing}
|
||||
</div>
|
||||
|
||||
<div class="qr-filter-group">
|
||||
@@ -1787,10 +1807,14 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
this._qrObjectsLoaded = true;
|
||||
}
|
||||
|
||||
/** {entry_id, name, task_count} of every object, name-sorted — the QR
|
||||
* batch and the export section both pick objects from this list. */
|
||||
private async _loadObjectRows(): Promise<Array<{ entry_id: string; name: string; task_count: number }> | undefined> {
|
||||
const result = await this._ws<{ objects: Array<{ entry_id: string; object: { name: string }; tasks: unknown[] }> }>({
|
||||
/** {entry_id, name, task_count, quick_count} of every object, name-sorted —
|
||||
* the QR batch and the export section both pick objects from this list.
|
||||
* ``quick_count``: tasks with quick-complete defaults (#192), the only
|
||||
* ones a quick-complete code is printed for. */
|
||||
private async _loadObjectRows(): Promise<Array<{ entry_id: string; name: string; task_count: number; quick_count: number }> | undefined> {
|
||||
const result = await this._ws<{
|
||||
objects: Array<{ entry_id: string; object: { name: string }; tasks: Array<{ quick_complete_defaults?: Record<string, unknown> | null }> }>;
|
||||
}>({
|
||||
type: "maintenance_supporter/objects",
|
||||
});
|
||||
if (!result) return undefined;
|
||||
@@ -1798,6 +1822,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
entry_id: o.entry_id,
|
||||
name: o.object.name,
|
||||
task_count: (o.tasks || []).length,
|
||||
quick_count: (o.tasks || []).filter((tk) => tk.quick_complete_defaults && Object.keys(tk.quick_complete_defaults).length > 0).length,
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
@@ -2596,6 +2621,10 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.qr-action-chip.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
.qr-action-chip.active {
|
||||
background: var(--primary-color, #03a9f4);
|
||||
color: #fff;
|
||||
|
||||
+109
-1
@@ -14,6 +14,11 @@
|
||||
* under another name starts unticked with that name, tasks it has by name are
|
||||
* listed as already there, and a before/after line says what adopting does.
|
||||
* Picking another target asks the server again (integration_setups/preview).
|
||||
*
|
||||
* 2.95: a duty whose integration counts the consumable itself names the
|
||||
* reset button completing it will press; tasks adopted before (or wired by
|
||||
* hand) are offered the same in their own section, with a before/after line
|
||||
* (integration_setups/reset_offers + wire_resets).
|
||||
*/
|
||||
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -30,8 +35,25 @@ interface SetupTask {
|
||||
threshold: number;
|
||||
direction: string;
|
||||
covered_by?: { task_id: string; name: string; reason: string } | null;
|
||||
/** 2.95: the integration's reset button completing this task presses. */
|
||||
reset?: { entity_id: string; name: string; disabled: boolean } | null;
|
||||
}
|
||||
|
||||
interface ResetOffer {
|
||||
entry_id: string;
|
||||
object_name: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
integration_name: string;
|
||||
button_entity_id: string;
|
||||
button_name: string;
|
||||
button_disabled: boolean;
|
||||
/** Adopted before the fingerprint and renamed since — offered unticked. */
|
||||
renamed?: boolean;
|
||||
}
|
||||
|
||||
const offerKey = (o: ResetOffer): string => `${o.entry_id}/${o.task_id}`;
|
||||
|
||||
interface SuggestedSetup {
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
@@ -74,6 +96,10 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
// Ticked tasks per device (task_name keys).
|
||||
@state() private _tasks: Map<string, Set<string>> = new Map();
|
||||
@state() private _objects: Array<{ entry_id: string; name: string }> = [];
|
||||
// 2.95: existing tasks that could also reset the integration's counter.
|
||||
@state() private _offers: ResetOffer[] = [];
|
||||
@state() private _offerTicks: Set<string> = new Set();
|
||||
@state() private _wiring = false;
|
||||
|
||||
private _localeReady = false;
|
||||
|
||||
@@ -99,6 +125,8 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._setups = [];
|
||||
this._selected = new Set();
|
||||
this._offers = [];
|
||||
void this._loadOffers();
|
||||
const resp = await runWs<{ setups: SuggestedSetup[] }>(
|
||||
this,
|
||||
{ type: "maintenance_supporter/integration_setups/discover" },
|
||||
@@ -129,6 +157,70 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private async _loadOffers(): Promise<void> {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{ offers: ResetOffer[] }>({
|
||||
type: "maintenance_supporter/integration_setups/reset_offers",
|
||||
});
|
||||
this._offers = r.offers || [];
|
||||
this._offerTicks = new Set(this._offers.filter((o) => !o.renamed).map(offerKey));
|
||||
} catch {
|
||||
this._offers = []; // an older backend has no offers
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleOffer(o: ResetOffer): void {
|
||||
const next = new Set(this._offerTicks);
|
||||
const key = offerKey(o);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
this._offerTicks = next;
|
||||
}
|
||||
|
||||
private _wire = async (): Promise<void> => {
|
||||
if (this._wiring) return;
|
||||
const items = this._offers.filter((o) => this._offerTicks.has(offerKey(o))).map((o) => ({ entry_id: o.entry_id, task_id: o.task_id }));
|
||||
if (items.length === 0) return;
|
||||
this._error = "";
|
||||
const result = await runWs<{ wired: number }>(
|
||||
this,
|
||||
{ type: "maintenance_supporter/integration_setups/wire_resets", items },
|
||||
{ busy: (b) => { this._wiring = b; }, onError: (m) => { this._error = m; } },
|
||||
);
|
||||
if (result === undefined) return;
|
||||
this.dispatchEvent(new CustomEvent("reset-counters-wired", { bubbles: true, composed: true, detail: result }));
|
||||
await this._loadOffers();
|
||||
};
|
||||
|
||||
private _renderOffers(L: string) {
|
||||
if (this._offers.length === 0) return nothing;
|
||||
const ticked = this._offers.filter((o) => this._offerTicks.has(offerKey(o))).length;
|
||||
return html`
|
||||
<div class="offers">
|
||||
<div class="offers-title">${t("reset_offers_title", L)}</div>
|
||||
<div class="hint">${t("reset_offers_hint", L)}</div>
|
||||
${this._offers.map(
|
||||
(o) => html`
|
||||
<label class="offer" data-task=${o.task_id}>
|
||||
<input type="checkbox" class="offer-check" .checked=${this._offerTicks.has(offerKey(o))} @change=${() => this._toggleOffer(o)} />
|
||||
<span class="task-text">
|
||||
<span>${o.task_name} · ${o.object_name}</span>
|
||||
${o.renamed ? html`<span class="offer-renamed">${t("reset_offers_renamed", L)}</span>` : nothing}
|
||||
<span class="offer-before">${t("reset_offers_before", L).replace("{integration}", o.integration_name)}</span>
|
||||
<span class="offer-after">
|
||||
${t("reset_offers_after", L).replace("{button}", o.button_name)}${o.button_disabled ? ` ${t("setups_reset_enable", L)}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
`,
|
||||
)}
|
||||
<div class="offers-actions">
|
||||
<ha-button @click=${this._wire} .disabled=${ticked === 0 || this._wiring}>${t("reset_offers_apply", L)}</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggle = (deviceId: string): void => {
|
||||
const next = new Set(this._selected);
|
||||
if (next.has(deviceId)) next.delete(deviceId);
|
||||
@@ -251,10 +343,11 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
<div class="hint">${t("setups_hint", L)}</div>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
|
||||
${this._renderOffers(L)}
|
||||
${this._loading
|
||||
? html`<div class="loading">…</div>`
|
||||
: this._setups.length === 0
|
||||
? html`<div class="empty">${t("setups_none", L)}</div>`
|
||||
? this._offers.length === 0 ? html`<div class="empty">${t("setups_none", L)}</div>` : nothing
|
||||
: html`
|
||||
<div class="list">
|
||||
${this._setups.map((s) => {
|
||||
@@ -292,6 +385,9 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
${task.covered_by
|
||||
? html`<span class="maybe">${t("setups_maybe_covered", L).replace("{name}", task.covered_by.name)}</span>`
|
||||
: nothing}
|
||||
${task.reset
|
||||
? html`<span class="reset">↻ ${t("setups_reset_line", L).replace("{button}", task.reset.name)}${task.reset.disabled ? ` ${t("setups_reset_enable", L)}` : ""}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
</label>
|
||||
${task.direction === "usage_delta" && ticks.has(task.task_name)
|
||||
@@ -397,6 +493,18 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
.task-text { display: flex; flex-direction: column; min-width: 0; }
|
||||
.maybe { font-size: 11px; color: var(--warning-color, #ff9800); }
|
||||
.already { font-size: 11px; color: var(--secondary-text-color); margin-top: 2px; }
|
||||
.reset { font-size: 11px; color: var(--secondary-text-color); }
|
||||
.offers {
|
||||
display: flex; flex-direction: column; gap: 6px; padding: 8px;
|
||||
border: 1px solid var(--primary-color); border-radius: 6px;
|
||||
overflow-y: auto; max-height: 40vh; flex-shrink: 0;
|
||||
}
|
||||
.offers-title { font-weight: 500; font-size: 14px; }
|
||||
.offer { display: flex; align-items: flex-start; gap: 6px; font-size: 13px; cursor: pointer; }
|
||||
.offer-before { font-size: 11px; color: var(--secondary-text-color); }
|
||||
.offer-after { font-size: 11px; color: var(--primary-color); }
|
||||
.offer-renamed { font-size: 11px; color: var(--warning-color, #ff9800); }
|
||||
.offers-actions { display: flex; justify-content: flex-end; }
|
||||
.before-after {
|
||||
font-size: 12px; font-weight: 500; color: var(--primary-color);
|
||||
border-top: 1px dashed var(--divider-color); padding-top: 4px; margin-top: 2px;
|
||||
|
||||
@@ -366,6 +366,12 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// so ha-form can drive the data fields when the service schema is known.
|
||||
@state() private _actionService = "";
|
||||
@state() private _actionTargetEntity = "";
|
||||
/** 2.95: not when the task completes itself (a reset wired by discovery). */
|
||||
@state() private _actionSkipAuto = false;
|
||||
/** 2.95: the task already has an action (suggested setups wire counter
|
||||
* resets without the feature toggle) — its section shows regardless, so
|
||||
* what runs on completion stays visible and removable. */
|
||||
@state() private _actionPresent = false;
|
||||
@state() private _actionData: Record<string, unknown> = {};
|
||||
@state() private _actionDataJsonFallback = "";
|
||||
@state() private _actionTesting = false;
|
||||
@@ -540,11 +546,15 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._actionTargetEntity = Array.isArray(tgt) ? (tgt[0] || "") : (tgt || "");
|
||||
this._actionData = (oca.data && typeof oca.data === "object") ? { ...oca.data } : {};
|
||||
this._actionDataJsonFallback = "";
|
||||
this._actionSkipAuto = oca.skip_auto === true;
|
||||
this._actionPresent = true;
|
||||
} else {
|
||||
this._actionService = "";
|
||||
this._actionTargetEntity = "";
|
||||
this._actionData = {};
|
||||
this._actionDataJsonFallback = "";
|
||||
this._actionSkipAuto = false;
|
||||
this._actionPresent = false;
|
||||
}
|
||||
const qcd = task.quick_complete_defaults;
|
||||
this._qcNotes = qcd?.notes || "";
|
||||
@@ -663,6 +673,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._actionTargetEntity = "";
|
||||
this._actionData = {};
|
||||
this._actionDataJsonFallback = "";
|
||||
this._actionSkipAuto = false;
|
||||
this._actionPresent = false;
|
||||
this._actionTesting = false;
|
||||
this._actionTestResult = "";
|
||||
this._qcNotes = "";
|
||||
@@ -857,7 +869,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _renderCompletionActionsSection(L: string) {
|
||||
if (!this.completionActionsEnabled) return nothing;
|
||||
if (!this.completionActionsEnabled && !this._actionPresent) return nothing;
|
||||
const schema = this._serviceSchema();
|
||||
return html`
|
||||
<details class="ca-section">
|
||||
@@ -894,6 +906,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<p class="field-help ca-domain-hint">
|
||||
${t("on_complete_action_target_hint", L)}
|
||||
</p>
|
||||
<label class="ca-skip-auto">
|
||||
<input type="checkbox" .checked=${this._actionSkipAuto}
|
||||
@change=${(e: Event) => { this._actionSkipAuto = (e.target as HTMLInputElement).checked; }} />
|
||||
<span>${t("on_complete_action_skip_auto", L)}</span>
|
||||
</label>
|
||||
${schema
|
||||
? html`
|
||||
<ha-form
|
||||
@@ -933,7 +950,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="ca-section">
|
||||
${this.completionActionsEnabled ? html`<details class="ca-section">
|
||||
<summary>${t("quick_complete_defaults_title", L)}</summary>
|
||||
<p class="field-help">${t("quick_complete_defaults_desc", L)}</p>
|
||||
<ms-textfield
|
||||
@@ -960,7 +977,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<option value="needed">${t("quick_complete_defaults_feedback_needed", L)}</option>
|
||||
<option value="not_needed">${t("quick_complete_defaults_feedback_not_needed", L)}</option>
|
||||
</select>
|
||||
</details>
|
||||
</details>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1668,8 +1685,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.checklist = items.length ? items : null;
|
||||
}
|
||||
|
||||
// v1.3.0: on_complete_action + quick_complete_defaults (gated)
|
||||
if (this.completionActionsEnabled) {
|
||||
// v1.3.0: on_complete_action + quick_complete_defaults (gated; the
|
||||
// action also when the task already had one — 2.95)
|
||||
if (this.completionActionsEnabled || this._actionPresent) {
|
||||
const svc = this._actionService.trim();
|
||||
if (svc && /^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) {
|
||||
const action: Record<string, unknown> = { service: svc };
|
||||
@@ -1679,11 +1697,13 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
if (Object.keys(dataDict).length > 0) {
|
||||
action.data = dataDict;
|
||||
}
|
||||
if (this._actionSkipAuto) action.skip_auto = true;
|
||||
data.on_complete_action = action;
|
||||
} else {
|
||||
data.on_complete_action = null;
|
||||
}
|
||||
|
||||
}
|
||||
if (this.completionActionsEnabled) {
|
||||
const qcd: Record<string, unknown> = {};
|
||||
if (this._qcNotes.trim()) qcd.notes = this._qcNotes.trim();
|
||||
const cost = parseFloat(this._qcCost);
|
||||
|
||||
+1
-1
@@ -333,6 +333,6 @@ if (!customElements.get("maintenance-vacation-section-card")) {
|
||||
registerCustomCard({
|
||||
type: "maintenance-vacation-section-card",
|
||||
name: "Maintenance Supporter — Vacation",
|
||||
description: "Inline vacation mode toggle + dates",
|
||||
description: "Switch vacation mode on or off and set its dates right on the dashboard.",
|
||||
preview: false,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user