278 files
This commit is contained in:
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);
|
||||
|
||||
Reference in new issue
Block a user