278 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-13 20:38:58 +00:00
1 parent d7a0a08372
commit 1b4f5f68c6
278 files changed
+36892 -7150

No files matched your search

@@ -3,11 +3,12 @@
import { LitElement, html, nothing, type TemplateResult } from "lit";
import { isSafeHttpUrl } from "./helpers/url";
import { queryTokens, scoreFields } from "./helpers/search-match";
import { objectRef, parseRef, renderRefChip, taskRef } from "./helpers/reference";
import { objectRef, parseRef, renderRefChip, taskRef, type HasRef } from "./helpers/reference";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
import { isStaleBundle } from "./helpers/bundle-version";
import { customElement, property, state } from "lit/decorators.js";
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setProfilePrefs, langOf, formatNumber, formatCost } from "./styles";
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, STATUS_ICONS, currencySymbolOf, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setProfilePrefs, langOf, formatCost, syncCurrencyDecimals} from "./styles";
import { OVERVIEW_TABS, type OverviewTab } from "./helpers/overview-tabs";
import { LS_KEYS, lsGet, lsSet } from "./helpers/storage-keys";
import { openHtmlInNewTab, openSignedDocument, signApiPath } from "./helpers/document-url";
import { readObjectsCache, writeObjectsCache } from "./helpers/objects-cache";
@@ -119,8 +120,9 @@ import "./components/task-detail-view";
import { computeWindow, VIRTUAL_MIN_ROWS } from "./helpers/virtual-window";
import { INITIAL_STICKY, nextStickyState, stickyStateOnSelect, stickyTop, type StickyState } from "./helpers/sticky-pane";
import { invalidateSettingsCache } from "./helpers/settings-cache";
import { historyPhotoIds } from "./helpers/history-photos";
import { entryReadingValues, readingSlotDelta } from "./helpers/reading-slots";
import { buildHistoryEntryDraft } from "./helpers/history-draft";
import { readingSlotDelta } from "./helpers/reading-slots";
import { runWs } from "./helpers/ws-run";
type View = "overview" | "object" | "task" | "all_objects" | "all_parts";
@@ -151,14 +153,14 @@ interface PartsOverviewRow {
type SortMode = "due_date" | "object" | "type" | "task_name" | "area" | "assigned_user" | "group";
type ObjectSortMode = "alphabetical" | "due_soonest" | "task_count";
type GroupByMode = "none" | "area" | "group" | "user" | "object";
type OverviewTab = "today" | "dashboard" | "calendar" | "settings";
// OverviewTab + OVERVIEW_TABS live in helpers/overview-tabs.ts — shared with the panel card's tab preset (#174).
// Value lists for everything that arrives as a free string — localStorage,
// a saved view, a deep-link query — and must be validated before it lands in
// state (an unknown mode would be persisted and silently break sorting).
const SORT_MODES: readonly SortMode[] = ["due_date", "object", "type", "task_name", "area", "assigned_user", "group"];
const GROUP_BY_MODES: readonly GroupByMode[] = ["none", "area", "group", "user", "object"];
const OVERVIEW_TABS: readonly OverviewTab[] = ["today", "dashboard", "calendar", "settings"];
// (OVERVIEW_TABS: imported from helpers/overview-tabs.ts.)
/** The dashboard list's status filter options (the <select> in the toolbar). */
const STATUS_FILTERS: readonly string[] = ["overdue", "due_soon", "triggered", "ok"];
@@ -179,6 +181,26 @@ export class MaintenanceSupporterPanel extends LitElement {
@property({ type: Boolean, reflect: true }) public split = false;
private _tightObserver: ResizeObserver | null = null;
@property({ attribute: false }) public panel: Record<string, unknown> = {};
/** #174: mounted inside `maintenance-supporter-panel-card` rather than as
* the sidebar panel — no hamburger (the dashboard has its own header), no
* safe-area padding (the dashboard wrapper applies it), deep links read
* from whatever path the dashboard has. Reflected for the CSS. */
@property({ type: Boolean, reflect: true }) public embedded = false;
/** #174: the card's opening tab / saved view — applied once on the initial
* load when the URL carries no `tab` / `view` of its own, WITHOUT
* persisting (the sidebar panel keeps its remembered tab). */
@property({ attribute: false }) public presets: { tab?: string; view?: string } = {};
/** Once per element instance — deliberately NOT reset on disconnect: HA
* re-attaches the card element on edit-mode toggles and view switches,
* and a reset flipped a user who had moved off the preset tab back onto
* it every time (bug audit 2026-09-12). */
private _presetsApplied = false;
/** #174: the pathname the panel was mounted on. Embedded, deep links are
* only ours while the page is still on that path — HA fires
* `location-changed` before it swaps the DOM, so a card that is about to
* be detached would otherwise consume another view's `?tab=` (bug audit
* 2026-09-12). */
private _mountPath: string | null = null;
@state() private _objects: MaintenanceObjectResponse[] = [];
@state() private _stats: StatisticsResponse | null = null;
@@ -212,7 +234,7 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _budget: BudgetStatus | null = null;
private get _currencySymbol(): string {
return this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL;
return currencySymbolOf(this._budget);
}
@state() private _groups: Record<string, MaintenanceGroup> = {};
@@ -230,6 +252,8 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _defaultWarningDays = 7;
/** #145: global "Task row actions" style (buttons_compact | buttons | icons). */
@state() private _rowActionStyle = "buttons_compact";
/** #170: reference numbers in front of names in the lists (global setting). */
@state() private _refsInLists = false;
/** #145: one-time "rows look different now" notice for existing installs. */
@state() private _rowActionNotice = false;
@state() private _actionLoading = false;
@@ -389,6 +413,7 @@ export class MaintenanceSupporterPanel extends LitElement {
connectedCallback(): void {
super.connectedCallback();
this._mountPath = window.location.pathname;
// Prefetch the lazy UI chunks AFTER the first paint settles (idle), not
// here: an eager prefetch put 7 fetches + ~280 KB of parsing in direct
// competition with the initial data load and measurably slowed first
@@ -471,6 +496,14 @@ export class MaintenanceSupporterPanel extends LitElement {
this._initialLoadDone = false;
this._lastConnection = null;
this._deepLinkHandled = false;
// `_presetsApplied` intentionally survives (see its declaration).
// Pending timers must not fire into a detached element (bug audit
// 2026-09-12): a debounced search would send a WS query for a closed
// palette, and a toast timer would leave `_toastMessage` in whatever
// state it was in when the element comes back.
if (this._searchTimer) { clearTimeout(this._searchTimer); this._searchTimer = null; }
if (this._toastTimer) { clearTimeout(this._toastTimer); this._toastTimer = null; }
this._toastMessage = ""; this._toastUndo = null; this._toastActionLabel = "";
this._statsService?.clearCache();
this._statsService = null;
}
@@ -703,14 +736,14 @@ export class MaintenanceSupporterPanel extends LitElement {
});
}
if (statsResult) this._stats = statsResult as StatisticsResponse;
if (budgetResult) this._budget = budgetResult as BudgetStatus;
if (budgetResult) { this._budget = budgetResult as BudgetStatus; syncCurrencyDecimals(this._budget); }
if (groupsResult) this._groups = (groupsResult as { groups: Record<string, MaintenanceGroup> }).groups || {};
if (settingsResult) {
const sr = settingsResult as {
features: AdvancedFeatures;
admin_panel_user_ids?: string[];
operator_write_enabled?: boolean;
general?: { default_warning_days?: number; row_action_style?: string; row_action_notice_pending?: boolean };
general?: { default_warning_days?: number; row_action_style?: string; row_action_notice_pending?: boolean; ref_numbers_in_lists?: boolean };
objects_table_columns?: string[];
};
this._features = sr.features;
@@ -723,6 +756,7 @@ export class MaintenanceSupporterPanel extends LitElement {
const ras = sr.general?.row_action_style;
this._rowActionStyle = ras === "icons" || ras === "buttons" ? ras : "buttons_compact";
this._rowActionNotice = sr.general?.row_action_notice_pending === true;
this._refsInLists = sr.general?.ref_numbers_in_lists === true;
this._objectsTableColumns = sanitizeColumns(sr.objects_table_columns);
}
@@ -751,9 +785,16 @@ export class MaintenanceSupporterPanel extends LitElement {
* params from it. Navigations away from the panel are not ours. */
private _onLocationChanged(): void {
if (!this._initialLoadDone || !window.location.search) return;
const base = `/${typeof this.panel?.url_path === "string" ? this.panel.url_path : "maintenance-supporter"}`;
const path = window.location.pathname;
if (path !== base && !path.startsWith(`${base}/`)) return;
// Embedded in a card (#174): the dashboard's path is ours, whatever it
// is — but only the one we were mounted on. A navigation to ANOTHER
// view carrying `?tab=` fires while this element is still attached and
// must be left alone (bug audit 2026-09-12).
if (this.embedded && this._mountPath !== null && window.location.pathname !== this._mountPath) return;
if (!this.embedded) {
const base = `/${typeof this.panel?.url_path === "string" ? this.panel.url_path : "maintenance-supporter"}`;
const path = window.location.pathname;
if (path !== base && !path.startsWith(`${base}/`)) return;
}
this._deepLinkHandled = false;
// HA's navigate() already pushed the entry for this URL (state null), so
// the show*() helpers must take that entry over instead of stacking a
@@ -835,6 +876,25 @@ export class MaintenanceSupporterPanel extends LitElement {
// ignored. Like a tap, the tab (also the implied one) and the sort are
// remembered for the next plain visit. Consumed once and stripped like
// every other param.
// #174: the card's presets open a tab / saved view once, without the
// persistence a tap or a `?tab=` link has — a dashboard subview that
// starts on Today must not rewrite the sidebar panel's remembered tab.
if (this.embedded && !this._presetsApplied) {
this._presetsApplied = true;
const pt = this.presets?.tab ?? "";
const pv = (this.presets?.view ?? "").trim();
if (!params.has("tab") && (OVERVIEW_TABS as readonly string[]).includes(pt) && (pt !== "settings" || this.hass?.user?.is_admin)) {
this._overviewTab = pt as OverviewTab;
}
if (!params.has("view") && pv) {
const wanted = pv.toLowerCase();
const match = this._savedViews.find((v) => v.id === pv) ?? this._savedViews.find((v) => v.name.trim().toLowerCase() === wanted);
if (match) {
this._overviewTab = "dashboard";
this._applyView(match.id);
}
}
}
const tab = params.get("tab");
const view = params.get("view");
const sort = params.get("sort");
@@ -1046,7 +1106,7 @@ export class MaintenanceSupporterPanel extends LitElement {
]);
if (!this.isConnected) return;
if (statsResult) this._stats = statsResult as StatisticsResponse;
if (budgetResult) this._budget = budgetResult as BudgetStatus;
if (budgetResult) { this._budget = budgetResult as BudgetStatus; syncCurrencyDecimals(this._budget); }
} while (this._kpiRefreshPending);
} finally {
this._kpiRefreshInFlight = false;
@@ -1170,6 +1230,18 @@ export class MaintenanceSupporterPanel extends LitElement {
// --- Navigation ---
/** #170: the "#8.3" chip in front of a list row's name, when the setting is on. */
private _listRef(entryId: string, taskId: string) {
if (!this._refsInLists) return nothing;
const obj = this._getObject(entryId);
return renderRefChip(taskRef(obj?.object, obj?.tasks.find((tk) => tk.id === taskId)));
}
/** The "#8" chip in front of an object's name in cards / tables, when the setting is on. */
private _objRef(obj: HasRef | null | undefined) {
return this._refsInLists ? renderRefChip(objectRef(obj)) : nothing;
}
/** Push a browser history entry so the back button navigates within the
* panel. An in-app deep link arrives on the entry HA's navigate() just
* pushed — that one is taken over instead (see _onLocationChanged). */
@@ -1387,6 +1459,7 @@ export class MaintenanceSupporterPanel extends LitElement {
this._selectedTaskId = taskId;
this._activeTab = "overview";
this._historyFilter = null;
this._historySearch = "";
this._resetStickyPane();
this._fetchFullHistory(entryId, taskId);
const task = this._getTask(entryId, taskId);
@@ -1401,6 +1474,10 @@ export class MaintenanceSupporterPanel extends LitElement {
this._selectedTaskId = taskId;
this._activeTab = "overview";
this._historyFilter = null;
// The notes filter belongs to ONE task: the global search pre-fills it
// for the history hit it opened (after this call), and it must not leak
// into every task opened afterwards (bug audit 2026-09-12).
this._historySearch = "";
this._scrollContentToTop();
// Payload diet: list responses carry only the most recent history
// window — the detail's full timeline/charts load here, on demand.
@@ -1513,7 +1590,15 @@ export class MaintenanceSupporterPanel extends LitElement {
}).then((res) => {
if (seq !== this._searchSeq || !this._paletteOpen) return;
this._searchRemote = { query: q, documents: res.documents || [], history: res.history || [] };
}).catch(() => { /* the local groups still answer; the server groups just stay empty */ });
}).catch(() => {
// The local groups still answer; the server groups just stay empty.
// Record the (empty) answer for this query — `waiting` is derived
// from `_searchRemote.query === q`, so a swallowed error left the
// "Searching…" footer / placeholder up forever (bug audit
// 2026-09-12).
if (seq !== this._searchSeq || !this._paletteOpen) return;
this._searchRemote = { query: q, documents: [], history: [] };
});
}, SEARCH_DEBOUNCE_MS);
}
@@ -1540,12 +1625,16 @@ export class MaintenanceSupporterPanel extends LitElement {
const o = obj.object;
if (o.ref_no !== ref.object) continue;
const oref = objectRef(o)!;
// A number on paper outlives the archive: an archived object or task
// still answers to its reference (with an "Archived" note), while the
// text search below keeps hiding archived things.
const archivedNote = (flag: boolean | undefined, base: string) => (flag ? [base, t("archived", L)].filter(Boolean).join(" · ") : base);
if (ref.task == null) {
objects.push({ kind: "object", entryId: obj.entry_id, label: o.name || "", sub: t("object", L), score: 1000, icon: "mdi:package-variant-closed", ref: oref });
objects.push({ kind: "object", entryId: obj.entry_id, label: o.name || "", sub: archivedNote(o.archived, t("object", L)), score: 1000, icon: "mdi:package-variant-closed", ref: oref });
}
for (const task of obj.tasks) {
if (task.archived || (ref.task != null && task.ref_no !== ref.task)) continue;
tasks.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: task.name || "", sub: o.name || "", score: ref.task == null ? 900 : 1000, icon: "mdi:clipboard-check-outline", ref: taskRef(o, task) });
if (ref.task != null && task.ref_no !== ref.task) continue;
tasks.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: task.name || "", sub: archivedNote(task.archived || o.archived, o.name || ""), score: ref.task == null ? 900 : 1000, icon: "mdi:clipboard-check-outline", ref: taskRef(o, task) });
}
}
if (objects.length || tasks.length) {
@@ -2636,7 +2725,7 @@ export class MaintenanceSupporterPanel extends LitElement {
return html`
<div class="header">
${this.narrow ? html`<ha-menu-button .hass=${this.hass} .narrow=${this.narrow}></ha-menu-button>` : nothing}
${this.narrow && !this.embedded ? html`<ha-menu-button .hass=${this.hass} .narrow=${this.narrow}></ha-menu-button>` : nothing}
${this._view !== "overview"
? html`<ha-icon-button
.path=${"M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"}
@@ -2845,25 +2934,13 @@ export class MaintenanceSupporterPanel extends LitElement {
<div class="today-row" @click=${() => this._showTask(row.entry_id, row.task_id)}>
<span class="today-dot ${row.trigger_active ? "triggered" : row.status}"></span>
<div class="today-main">
<div class="today-task">${row.task_name}</div>
<div class="today-task">${this._listRef(row.entry_id, row.task_id)}${row.task_name}</div>
<div class="today-object">
<span class="today-object-text">${row.object_name} · ${formatDueDays(row.days_until_due, L)}</span>
${renderPersonChip(personOf(row), "today-person")}
</div>
</div>
${this._actionStyle() === "icons"
? html`
<mwc-icon-button class="btn-complete" title="${t("complete", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>`
: html`
<ha-button size="small" appearance="accent" variant="success" class="today-complete" title="${t("complete", L)}"
@click=${(e: Event) => { e.stopPropagation(); this._openCompleteDialogForRow(row); }}>
${this._actionStyle() === "buttons_compact" && (this.narrow || this.tight)
? html`<ha-icon icon="mdi:check"></ha-icon>`
: html`<ha-icon slot="start" icon="mdi:check"></ha-icon>${t("complete", L)}`}
</ha-button>`}
${this._renderRowActions(L, () => this._openCompleteDialogForRow(row), undefined, false)}
</div>
`)}
</div>
@@ -3327,7 +3404,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<div class="object-card${overdue ? ' object-card-overdue' : ''}" @click=${() => this._showObject(obj.entry_id)}>
${overdue ? html`<span class="overdue-dot" title="${t("has_overdue", L)}"></span>` : nothing}
<div class="object-card-header">
<span class="object-card-name">${obj.object.name}</span>
<span class="object-card-name">${this._objRef(obj.object)}${obj.object.name}</span>
${obj.object.paused
? html`<span class="paused-badge" title="${t("object_paused_badge", L)}${obj.object.paused_until ? ` — ${obj.object.paused_until}` : ""}">
<ha-icon icon="mdi:pause-circle-outline"></ha-icon>
@@ -3491,7 +3568,7 @@ export class MaintenanceSupporterPanel extends LitElement {
${rows.map((row) => html`
<tr class="objects-table-row" @click=${() => this._showObject(row.entry_id)}>
<td>
<span class="objects-table-name">${row.name}</span>
<span class="objects-table-name">${this._objRef(this._getObject(row.entry_id)?.object)}${row.name}</span>
${row.low
? html`<ha-icon class="part-low-icon" icon="mdi:cart-arrow-down"
title="${t("part_reorder_threshold", L)}: ${row.reorder_threshold}"></ha-icon>`
@@ -3500,7 +3577,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<td>${row.object_name || "—"}</td>
<td>${row.stock !== null ? `${row.stock}${row.unit ? ` ${row.unit}` : ""}` : "—"}</td>
<td>${row.reorder_threshold ?? "—"}</td>
<td>${row.cost != null ? `${row.cost} ${currency}`.trim() : "—"}</td>
<td>${row.cost != null ? formatCost(row.cost, currency, L) : "—"}</td>
<td>${row.storage_location || "—"}</td>
<td>
${row.consumers.length === 0
@@ -3544,21 +3621,16 @@ export class MaintenanceSupporterPanel extends LitElement {
esc(row.consumers.map((c) => `${c.object_name ?? ""}/${c.task_name ?? c.task_id}×${c.quantity}`).join(" | ")),
].join(","));
}
const ts = new Date().toISOString().slice(0, 10);
const ts = isoDateLocal(new Date());
downloadTextFile(lines.join("\n"), `maintenance_parts_${ts}.csv`, "text/csv;charset=utf-8");
}
// (#67 / Phase 3) Download all objects as a one-row-per-object CSV.
private async _exportObjectsCsv(): Promise<void> {
try {
const result = await this.hass.connection.sendMessagePromise({
type: "maintenance_supporter/objects/csv",
}) as { csv: string };
const ts = new Date().toISOString().slice(0, 10);
downloadTextFile(result.csv, `maintenance_objects_${ts}.csv`, "text/csv;charset=utf-8");
} catch {
this._showToast(t("action_error", this._lang));
}
const result = await runWs<{ csv: string }>(this, { type: "maintenance_supporter/objects/csv" }, { onError: (m) => this._showToast(m) });
if (!result) return;
const ts = isoDateLocal(new Date());
downloadTextFile(result.csv, `maintenance_objects_${ts}.csv`, "text/csv;charset=utf-8");
}
// (#67) Tabular All-Objects view honouring the configured columns. Desktop
@@ -3596,7 +3668,7 @@ export class MaintenanceSupporterPanel extends LitElement {
switch (key) {
case "name":
return html`<td class="oc-name">
<span class="objects-table-name">${o.name}</span>
<span class="objects-table-name">${this._objRef(o)}${o.name}</span>
${o.document_count
? html`<span class="doc-badge" title="${o.document_count} ${t("documents", L)}">
<ha-icon icon="mdi:paperclip"></ha-icon>${o.document_count}
@@ -3753,9 +3825,9 @@ export class MaintenanceSupporterPanel extends LitElement {
const pct = Math.min(100, Math.max(0, (spent / budget) * 100));
const color = pct >= 100 ? "var(--error-color, #f44336)" : pct >= b.alert_threshold_pct ? "var(--warning-color, #ff9800)" : "var(--success-color, #4caf50)";
return html`
<div class="stat-item budget-tile" title="${label}: ${formatNumber(spent, L, 2)} / ${formatCost(budget, cs, L)}">
<div class="stat-item budget-tile" title="${label}: ${formatCost(spent, cs, L)} / ${formatCost(budget, cs, L)}">
<span class="stat-value budget-tile-value">${formatCost(spent, cs, L)}</span>
<span class="budget-tile-max">/ ${formatCost(budget, cs, L, 0)}</span>
<span class="budget-tile-max">/ ${formatCost(budget, cs, L)}</span>
<div class="budget-tile-bar"><div style="width:${pct}%; background:${color}"></div></div>
<span class="stat-label">${label}</span>
</div>
@@ -3810,7 +3882,7 @@ export class MaintenanceSupporterPanel extends LitElement {
</span>
<span class="row-head">
<span class="cell object-name" @click=${(e: Event) => { e.stopPropagation(); this._showObject(row.entry_id); }}>${row.object_name}</span>
<span class="cell task-name" @click=${() => this._showTask(row.entry_id, row.task_id)}>${row.task_name}</span>
<span class="cell task-name" @click=${() => this._showTask(row.entry_id, row.task_id)}>${this._listRef(row.entry_id, row.task_id)}${row.task_name}</span>
</span>
<span class="task-sub${hasSub ? '' : ' task-sub-empty'}">
${row.group_names.length > 0 ? html`
@@ -3858,20 +3930,17 @@ export class MaintenanceSupporterPanel extends LitElement {
private async _dismissRowActionNotice(backToIcons: boolean): Promise<void> {
const settings: Record<string, unknown> = { row_action_notice_pending: false };
if (backToIcons) settings.row_action_style = "icons";
try {
await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/global/update", settings });
this._rowActionNotice = false;
if (backToIcons) this._rowActionStyle = "icons";
invalidateSettingsCache();
} catch (err) {
console.warn("[maintenance-supporter] row-action notice update failed", err);
}
const ok = await runWs(this, { type: "maintenance_supporter/global/update", settings }, { onError: (m) => this._showToast(m) });
if (ok === undefined) return;
this._rowActionNotice = false;
if (backToIcons) this._rowActionStyle = "icons";
invalidateSettingsCache();
}
/** #145: row actions as HA buttons — labelled (accent/success + outlined
* warning, the task-detail pairing) or, for "buttons_compact" on narrow screens,
* icon-only — or the classic icon pair when the household chose "icons". */
private _renderRowActions(L: string, onComplete: () => void, onSkip: () => void, allowSkip = true): TemplateResult {
private _renderRowActions(L: string, onComplete: () => void, onSkip?: () => void, allowSkip = true): TemplateResult {
const style = this._actionStyle();
if (style === "buttons" || style === "buttons_compact") {
const iconOnly = style === "buttons_compact" && (this.narrow || this.tight);
@@ -3886,7 +3955,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<ha-icon icon="mdi:check"></ha-icon>
</ha-button>
${allowSkip ? html`
<ha-button size="small" appearance="outlined" variant="warning" title="${t("skip", L)}" aria-label="${t("skip", L)}" ?disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<ha-button size="small" appearance="outlined" variant="warning" title="${t("skip", L)}" aria-label="${t("skip", L)}" ?disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip?.(); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</ha-button>` : nothing}
</span>`;
@@ -3897,7 +3966,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<ha-icon slot="start" icon="mdi:check"></ha-icon>${t("complete", L)}
</ha-button>
${allowSkip ? html`
<ha-button size="small" appearance="outlined" variant="warning" title="${t("skip", L)}" ?disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<ha-button size="small" appearance="outlined" variant="warning" title="${t("skip", L)}" ?disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip?.(); }}>
<ha-icon slot="start" icon="mdi:skip-next"></ha-icon>${t("skip", L)}
</ha-button>` : nothing}
</span>`;
@@ -3908,7 +3977,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<ha-icon icon="mdi:check"></ha-icon>
</mwc-icon-button>
${allowSkip ? html`
<mwc-icon-button class="btn-skip" title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
<mwc-icon-button class="btn-skip" title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip?.(); }}>
<ha-icon icon="mdi:skip-next"></ha-icon>
</mwc-icon-button>` : nothing}
</span>`;
@@ -4044,7 +4113,7 @@ export class MaintenanceSupporterPanel extends LitElement {
? html`<span class="doc-badge" title="${task.document_count} ${t("documents", L)}"><ha-icon icon="mdi:paperclip"></ha-icon>${task.document_count}</span>`
: nothing}
</span>
<span class="cell task-name" @click=${() => this._showTask(obj.entry_id, task.id)}>${task.name}</span>
<span class="cell task-name" @click=${() => this._showTask(obj.entry_id, task.id)}>${this._listRef(obj.entry_id, task.id)}${task.name}</span>
<span class="task-sub${task.responsible_user_id ? '' : ' task-sub-empty'}">${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null, (id) => this._userService?.getPerson(id) ?? null)}</span>
<span class="cell type">${t(task.type, L)}</span>
<span class="due-cell" @click=${() => this._showTask(obj.entry_id, task.id)}>
@@ -4434,24 +4503,7 @@ export class MaintenanceSupporterPanel extends LitElement {
private _openHistoryEdit(entry: HistoryEntry): void {
if (!this._selectedEntryId || !this._selectedTaskId) return;
const editTask = this._getTask(this._selectedEntryId, this._selectedTaskId);
const draft: HistoryEntryDraft = {
entry_id: this._selectedEntryId,
task_id: this._selectedTaskId,
original_timestamp: entry.timestamp,
type: entry.type,
timestamp: entry.timestamp,
notes: entry.notes ?? null,
cost: entry.cost ?? null,
duration: entry.duration ?? null,
completed_by: entry.completed_by ?? null,
used_parts: entry.used_parts ?? null,
photo_doc_ids: historyPhotoIds(entry),
reading_value: entry.reading_value ?? null,
reading_values: entryReadingValues(entry),
readings: editTask?.readings ?? [],
task_type: editTask?.type ?? null,
reading_unit: editTask?.reading_unit ?? null,
};
const draft: HistoryEntryDraft = buildHistoryEntryDraft(this._selectedEntryId, this._selectedTaskId, entry, editTask);
this.shadowRoot
?.querySelector<MaintenanceHistoryEditDialog>("maintenance-history-edit-dialog")
?.openEdit(draft);