182 files
This commit is contained in:
+2
-2
@@ -9,7 +9,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { UserService } from "../user-service";
|
||||
import type { HAUser, HomeAssistant } from "../types";
|
||||
@@ -55,7 +55,7 @@ export class MaintenanceAdoptProblemSensorsDialog extends LitElement {
|
||||
private _userService: UserService | null = null;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>): void {
|
||||
|
||||
+6
-15
@@ -6,7 +6,8 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { LS_KEYS, lsGet, lsSet } from "../helpers/storage-keys";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -84,7 +85,7 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
private _localeReady = false;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -232,24 +233,14 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
private static readonly _SORT_KEY = "ms_bf_roster_sort";
|
||||
|
||||
private static _storedSort(): "name" | "urgency" {
|
||||
try {
|
||||
const v = localStorage.getItem(MaintenanceBatteryFleetSection._SORT_KEY);
|
||||
return v === "name" ? "name" : "urgency";
|
||||
} catch {
|
||||
return "urgency";
|
||||
}
|
||||
return lsGet(LS_KEYS.batteryRosterSort) === "name" ? "name" : "urgency";
|
||||
}
|
||||
|
||||
private _setSort(mode: "name" | "urgency"): void {
|
||||
this._rosterSort = mode;
|
||||
try {
|
||||
localStorage.setItem(MaintenanceBatteryFleetSection._SORT_KEY, mode);
|
||||
} catch {
|
||||
// storage unavailable — the toggle still works for this visit
|
||||
}
|
||||
// Storage may be unavailable — the toggle still works for this visit.
|
||||
lsSet(LS_KEYS.batteryRosterSort, mode);
|
||||
}
|
||||
|
||||
/** Urgency (the default, issue #123): low rows first — emptiest first —
|
||||
|
||||
+4
-5
@@ -8,7 +8,8 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale, DEFAULT_CURRENCY_SYMBOL } from "../styles";
|
||||
import { t, ensureLocale, DEFAULT_CURRENCY_SYMBOL, langOf } from "../styles";
|
||||
import { registerCustomCard } from "../helpers/register-card";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { sectionCardSharedStyles } from "./section-card-shared-styles";
|
||||
import type { BudgetStatus, HomeAssistant } from "../types";
|
||||
@@ -40,7 +41,7 @@ export class MaintenanceBudgetSectionCard extends LitElement {
|
||||
}
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
private get _isAdmin(): boolean {
|
||||
@@ -277,9 +278,7 @@ if (!customElements.get("maintenance-budget-section-card")) {
|
||||
);
|
||||
}
|
||||
|
||||
(window as { customCards?: unknown[] }).customCards =
|
||||
(window as { customCards?: unknown[] }).customCards || [];
|
||||
((window as { customCards?: unknown[] }).customCards!).push({
|
||||
registerCustomCard({
|
||||
type: "maintenance-budget-section-card",
|
||||
name: "Maintenance Supporter — Budget",
|
||||
description: "Inline monthly + yearly budget editor",
|
||||
|
||||
+7
-31
@@ -10,10 +10,12 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { downloadSignedDocument, openSignedDocument, signDocumentPath } from "../helpers/document-url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface MaintenanceDocument {
|
||||
@@ -28,16 +30,6 @@ interface MaintenanceDocument {
|
||||
added_at?: string;
|
||||
}
|
||||
|
||||
const CATEGORIES = ["manual", "warranty", "invoice", "spare_parts", "photo", "other"] as const;
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
manual: "mdi:book-open-variant",
|
||||
warranty: "mdi:shield-check",
|
||||
invoice: "mdi:receipt-text-outline",
|
||||
spare_parts: "mdi:cog-outline",
|
||||
photo: "mdi:image-outline",
|
||||
other: "mdi:file-document-outline",
|
||||
};
|
||||
|
||||
export class MaintenanceDocumentsSection extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public entryId!: string;
|
||||
@@ -67,16 +59,11 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
}
|
||||
|
||||
private async _sign(doc: MaintenanceDocument): Promise<string> {
|
||||
const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 300,
|
||||
});
|
||||
return signed.path;
|
||||
return signDocumentPath(this.hass, doc.id);
|
||||
}
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>): void {
|
||||
@@ -210,12 +197,7 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
|
||||
private async _download(doc: MaintenanceDocument): Promise<void> {
|
||||
try {
|
||||
const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 30,
|
||||
});
|
||||
downloadUrl(signed.path, doc.filename || doc.title || "document");
|
||||
await downloadSignedDocument(this.hass, doc.id, doc.filename || doc.title || "document");
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
@@ -228,15 +210,9 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
this._lightboxUrl = this._thumbs[doc.id] || (await this._sign(doc));
|
||||
return;
|
||||
}
|
||||
// Open the tab synchronously (in the click gesture) so it isn't popup-blocked,
|
||||
// then point it at the freshly signed URL once it resolves.
|
||||
const win = window.open("about:blank", "_blank");
|
||||
try {
|
||||
const url = await this._sign(doc);
|
||||
// Absolute URL so it always resolves against the blank popup (about:blank).
|
||||
if (win) win.location.href = new URL(url, window.location.origin).href;
|
||||
await openSignedDocument(this.hass, doc.id);
|
||||
} catch (e) {
|
||||
if (win) win.close();
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t } from "../styles";
|
||||
import { t, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import "./ms-textfield";
|
||||
import type {
|
||||
@@ -26,7 +26,7 @@ export class MaintenanceGroupDialog extends LitElement {
|
||||
@state() private _selected: Set<string> = new Set(); // "entry_id:task_id"
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language ?? navigator.language.split("-")[0] ?? "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public openCreate(): void {
|
||||
|
||||
+4
-5
@@ -8,7 +8,8 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { registerCustomCard } from "../helpers/register-card";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { sectionCardSharedStyles } from "./section-card-shared-styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -47,7 +48,7 @@ export class MaintenanceGroupsSectionCard extends LitElement {
|
||||
}
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
private get _isAdmin(): boolean {
|
||||
@@ -330,9 +331,7 @@ if (!customElements.get("maintenance-groups-section-card")) {
|
||||
);
|
||||
}
|
||||
|
||||
(window as { customCards?: unknown[] }).customCards =
|
||||
(window as { customCards?: unknown[] }).customCards || [];
|
||||
((window as { customCards?: unknown[] }).customCards!).push({
|
||||
registerCustomCard({
|
||||
type: "maintenance-groups-section-card",
|
||||
name: "Maintenance Supporter — Groups",
|
||||
description: "Inline group CRUD",
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t } from "../styles";
|
||||
import { t, langOf } from "../styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
|
||||
@@ -50,7 +50,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
private _originalSnapshot: HistoryEntryDraft | null = null;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
// #130: selectable parts + the edited selection (part key -> quantity;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { signDocumentPath } from "../helpers/document-url";
|
||||
|
||||
export class MaintenanceHistoryPhoto extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -29,12 +30,7 @@ export class MaintenanceHistoryPhoto extends LitElement {
|
||||
|
||||
private async _sign(): Promise<void> {
|
||||
try {
|
||||
const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${this.docId}`,
|
||||
expires: 300,
|
||||
});
|
||||
this._url = signed.path;
|
||||
this._url = await signDocumentPath(this.hass, this.docId);
|
||||
} catch {
|
||||
this._failed = true;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant, MaintenanceObject, MaintenanceObjectResponse } from "../types";
|
||||
import { t } from "../styles";
|
||||
import { t, langOf } from "../styles";
|
||||
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import "./ms-textfield";
|
||||
@@ -33,7 +33,7 @@ export class MaintenanceObjectDialog extends LitElement {
|
||||
@state() private _entryId: string | null = null; // null = create, string = update
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language ?? navigator.language.split("-")[0] ?? "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public openCreate(): void {
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, STATUS_COLORS } from "../styles";
|
||||
import { t, STATUS_COLORS, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant, MaintenanceObject, MaintenanceTask } from "../types";
|
||||
|
||||
@@ -31,7 +31,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
@state() private _error = "";
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public async openFor(entryId: string): Promise<void> {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant, MaintenancePart } from "../types";
|
||||
// Per-part document links (v2.26) — the task-documents component in part mode.
|
||||
@@ -70,7 +70,7 @@ export class MaintenancePartsSection extends LitElement {
|
||||
@state() private _docsFor: string | null = null;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.locale?.language || this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant, SavedView, SavedViewFilters } from "../types";
|
||||
|
||||
@@ -33,7 +33,7 @@ export class MaintenanceSavedViewsDialog extends LitElement {
|
||||
private _localeReady = false;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>): void {
|
||||
@@ -171,7 +171,7 @@ export class MaintenanceSavedViewsDialog extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 340px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 480px;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t } from "../styles";
|
||||
import { t, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SeasonalOverridesDialog extends LitElement {
|
||||
@state() private _values: string[] = new Array(12).fill("");
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language ?? navigator.language.split("-")[0] ?? "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public open(entryId: string, taskId: string, currentOverrides: Record<number, number> | null | undefined): void {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import type { HomeAssistant, AdvancedFeatures, BudgetStatus, HAUser } from "../types";
|
||||
import { t } from "../styles";
|
||||
import { t, langOf } from "../styles";
|
||||
import { signApiPath } from "../helpers/document-url";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { UserService } from "../user-service";
|
||||
import { OBJECT_COLUMNS, sanitizeColumns } from "../helpers/object-columns";
|
||||
import { downloadTextFile } from "../helpers/download";
|
||||
@@ -160,7 +162,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
private _userService: UserService | null = null;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changedProps: Map<string, unknown>): void {
|
||||
@@ -1430,6 +1432,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<button @click=${this._exportJson}>${t("settings_export_json", L)}</button>
|
||||
<button @click=${this._exportYaml}>${t("settings_export_yaml", L)}</button>
|
||||
<button @click=${this._exportCsv}>${t("settings_export_csv", L)}</button>
|
||||
<button @click=${this._exportSettings}>${t("settings_export_settings", L)}</button>
|
||||
</div>
|
||||
<div class="settings-actions docs-archive-block">
|
||||
<h4>${t("settings_docs_archive", L)}</h4>
|
||||
@@ -1510,6 +1513,22 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** The SECOND export: the global entry's settings (groups, saved views,
|
||||
* vacation, notification/budget settings, feature toggles) — the objects
|
||||
* export deliberately excludes them. Re-import via the regular import. */
|
||||
private async _exportSettings(): Promise<void> {
|
||||
try {
|
||||
const result = await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/settings/export",
|
||||
}) as { data: string };
|
||||
const ts = new Date().toISOString().slice(0, 10);
|
||||
this._downloadFile(result.data, `maintenance_settings_${ts}.json`, "application/json");
|
||||
this._showToast(t("settings_export_success", this._lang));
|
||||
} catch {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
}
|
||||
}
|
||||
|
||||
private async _exportYaml(): Promise<void> {
|
||||
try {
|
||||
const ids = this._selectedEntryIds;
|
||||
@@ -1573,17 +1592,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
try {
|
||||
const raw = this._selectedEntryIds;
|
||||
const q = raw ? `?entry_ids=${encodeURIComponent(raw.join(","))}` : "";
|
||||
const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/documents/archive${q}`,
|
||||
expires: 300,
|
||||
});
|
||||
const a = document.createElement("a");
|
||||
a.href = signed.path;
|
||||
a.download = "maintenance-documents.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
const signed = await signApiPath(this.hass, `/api/maintenance_supporter/documents/archive${q}`);
|
||||
downloadUrl(signed, "maintenance-documents.zip");
|
||||
} catch {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
}
|
||||
|
||||
+4
-11
@@ -8,8 +8,9 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { openSignedDocument } from "../helpers/document-url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -55,7 +56,7 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
private _searchTimer = 0;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>): void {
|
||||
@@ -137,17 +138,9 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
window.open(doc.url, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
const win = window.open("about:blank", "_blank");
|
||||
try {
|
||||
const s = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 300,
|
||||
});
|
||||
// Absolute URL so it navigates the blank popup reliably (about:blank base).
|
||||
if (win) win.location.href = new URL(s.path, window.location.origin).href;
|
||||
await openSignedDocument(this.hass, doc.id);
|
||||
} catch (e) {
|
||||
if (win) win.close();
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -61,7 +61,7 @@ export class MaintenanceSuggestedSetupsDialog extends LitElement {
|
||||
private _localeReady = false;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, t, weekdayName } from "../styles";
|
||||
import { formatDate, t, weekdayName, langOf } from "../styles";
|
||||
import { UserService } from "../user-service";
|
||||
import { partLinkKey } from "../helpers/shared-parts";
|
||||
import {
|
||||
@@ -24,6 +24,8 @@ const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"];
|
||||
// The type selector additionally offers "compound" (a group of conditions
|
||||
// joined by AND/OR); its per-condition sub-type is limited to the flat kinds.
|
||||
const TRIGGER_TYPE_KEYS_WITH_COMPOUND = [...TRIGGER_TYPE_KEYS, "compound"];
|
||||
// Backend defaults for the adaptive tuning fields (ewa_alpha / min / max days).
|
||||
const ADAPTIVE_DEFAULTS = { alpha: "0.3", min: "7", max: "365" } as const;
|
||||
|
||||
/** One condition of a compound trigger — a flat trigger the user edits inline.
|
||||
* String-typed like the top-level fields (form inputs); coerced on save. */
|
||||
@@ -279,9 +281,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// Adaptive tuning (parity with the options flow's adaptive step) — Store-
|
||||
// managed like the environmental binding, saved through task/set_adaptive.
|
||||
@state() private _adaptiveEnabled = false;
|
||||
@state() private _adaptiveAlpha = "0.3";
|
||||
@state() private _adaptiveMin = "7";
|
||||
@state() private _adaptiveMax = "365";
|
||||
@state() private _adaptiveAlpha: string = ADAPTIVE_DEFAULTS.alpha;
|
||||
@state() private _adaptiveMin: string = ADAPTIVE_DEFAULTS.min;
|
||||
@state() private _adaptiveMax: string = ADAPTIVE_DEFAULTS.max;
|
||||
@state() private _adaptiveSeasonal = true;
|
||||
@state() private _adaptivePrediction = true;
|
||||
private _adaptiveInitial = "";
|
||||
@@ -295,7 +297,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
private _userService: UserService | null = null;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language ?? navigator.language.split("-")[0] ?? "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
public async openCreate(entryId: string, objects?: Array<{ entry_id: string; object: { name: string } }>): Promise<void> {
|
||||
@@ -404,9 +406,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._environmentalInitial = this._environmentalEntity;
|
||||
this._environmentalAttributeInitial = this._environmentalAttribute;
|
||||
this._adaptiveEnabled = !!ac.enabled;
|
||||
this._adaptiveAlpha = (ac.ewa_alpha ?? 0.3).toString();
|
||||
this._adaptiveMin = (ac.min_interval_days ?? 7).toString();
|
||||
this._adaptiveMax = (ac.max_interval_days ?? 365).toString();
|
||||
this._adaptiveAlpha = ac.ewa_alpha?.toString() ?? ADAPTIVE_DEFAULTS.alpha;
|
||||
this._adaptiveMin = ac.min_interval_days?.toString() ?? ADAPTIVE_DEFAULTS.min;
|
||||
this._adaptiveMax = ac.max_interval_days?.toString() ?? ADAPTIVE_DEFAULTS.max;
|
||||
this._adaptiveSeasonal = ac.seasonal_enabled !== false;
|
||||
this._adaptivePrediction = ac.sensor_prediction_enabled !== false;
|
||||
this._adaptiveInitial = this._adaptiveSnapshot();
|
||||
@@ -497,9 +499,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._environmentalInitial = "";
|
||||
this._environmentalAttributeInitial = "";
|
||||
this._adaptiveEnabled = false;
|
||||
this._adaptiveAlpha = "0.3";
|
||||
this._adaptiveMin = "7";
|
||||
this._adaptiveMax = "365";
|
||||
this._adaptiveAlpha = ADAPTIVE_DEFAULTS.alpha;
|
||||
this._adaptiveMin = ADAPTIVE_DEFAULTS.min;
|
||||
this._adaptiveMax = ADAPTIVE_DEFAULTS.max;
|
||||
this._adaptiveSeasonal = true;
|
||||
this._adaptivePrediction = true;
|
||||
this._adaptiveInitial = this._adaptiveSnapshot();
|
||||
@@ -1314,34 +1316,13 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
</select>
|
||||
</div>
|
||||
` : nothing}
|
||||
${this._availableAttributes.length > 0
|
||||
? html`
|
||||
<div class="select-row">
|
||||
<label>${t("attribute_optional", L)}</label>
|
||||
<select
|
||||
.value=${this._triggerAttribute}
|
||||
@change=${(e: Event) => (this._triggerAttribute = (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="" ?selected=${!this._triggerAttribute}>${t("use_entity_state", L)}</option>
|
||||
${this._suggestedAttributes.map(
|
||||
(attr) => html`<option value=${attr} ?selected=${attr === this._triggerAttribute}>${attr} ★</option>`
|
||||
)}
|
||||
${this._availableAttributes
|
||||
.filter((a) => !this._suggestedAttributes.includes(a.name))
|
||||
.map(
|
||||
(a) => html`<option value=${a.name} ?selected=${a.name === this._triggerAttribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ms-textfield
|
||||
label="${t("attribute_optional", L)}"
|
||||
.value=${this._triggerAttribute}
|
||||
@input=${(e: Event) => (this._triggerAttribute = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
`
|
||||
}
|
||||
${this._renderAttributeSelect({
|
||||
label: t("attribute_optional", L),
|
||||
value: this._triggerAttribute,
|
||||
suggested: this._suggestedAttributes,
|
||||
available: this._availableAttributes,
|
||||
onSelect: (v) => (this._triggerAttribute = v),
|
||||
})}
|
||||
${this._renderTriggerTypeFields()}
|
||||
${this._renderTriggerLiveHint()}
|
||||
`}
|
||||
@@ -1576,76 +1557,73 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** Shared attribute selector: "use entity state" + suggested★ + remaining
|
||||
* attributes (non-numeric flagged), textfield fallback when none known. */
|
||||
private _renderAttributeSelect(cfg: {
|
||||
label: string;
|
||||
value: string;
|
||||
suggested: string[];
|
||||
available: { name: string; numeric: boolean }[];
|
||||
onSelect: (value: string) => void;
|
||||
}) {
|
||||
const L = this._lang;
|
||||
if (cfg.available.length > 0) {
|
||||
return html`
|
||||
<div class="select-row">
|
||||
<label>${cfg.label}</label>
|
||||
<select
|
||||
.value=${cfg.value}
|
||||
@change=${(e: Event) => cfg.onSelect((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="" ?selected=${!cfg.value}>${t("use_entity_state", L)}</option>
|
||||
${cfg.suggested.map(
|
||||
(attr) => html`<option value=${attr} ?selected=${attr === cfg.value}>${attr} ★</option>`
|
||||
)}
|
||||
${cfg.available
|
||||
.filter((a) => !cfg.suggested.includes(a.name))
|
||||
.map(
|
||||
(a) => html`<option value=${a.name} ?selected=${a.name === cfg.value}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ms-textfield
|
||||
label="${cfg.label}"
|
||||
.value=${cfg.value}
|
||||
@input=${(e: Event) => cfg.onSelect((e.target as HTMLInputElement).value.trim())}
|
||||
></ms-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
/** Environmental attribute — the same live-fetched dropdown the flat and
|
||||
* compound attribute fields use, keyed by the environmental entity. */
|
||||
private _renderEnvironmentalAttribute(L: string) {
|
||||
this._fetchConditionAttributes(this._environmentalEntity);
|
||||
const opts = this._conditionAttrOptions[this._environmentalEntity];
|
||||
if (opts && opts.available.length > 0) {
|
||||
return html`
|
||||
<div class="select-row">
|
||||
<label>${t("environmental_attribute_optional", L)}</label>
|
||||
<select
|
||||
.value=${this._environmentalAttribute}
|
||||
@change=${(e: Event) => (this._environmentalAttribute = (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="" ?selected=${!this._environmentalAttribute}>${t("use_entity_state", L)}</option>
|
||||
${opts.suggested.map(
|
||||
(attr) => html`<option value=${attr} ?selected=${attr === this._environmentalAttribute}>${attr} ★</option>`
|
||||
)}
|
||||
${opts.available
|
||||
.filter((a) => !opts.suggested.includes(a.name))
|
||||
.map(
|
||||
(a) => html`<option value=${a.name} ?selected=${a.name === this._environmentalAttribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ms-textfield
|
||||
label="${t("environmental_attribute_optional", L)}"
|
||||
.value=${this._environmentalAttribute}
|
||||
@input=${(e: Event) => (this._environmentalAttribute = (e.target as HTMLInputElement).value.trim())}
|
||||
></ms-textfield>
|
||||
`;
|
||||
return this._renderAttributeSelect({
|
||||
label: t("environmental_attribute_optional", L),
|
||||
value: this._environmentalAttribute,
|
||||
suggested: opts?.suggested ?? [],
|
||||
available: opts?.available ?? [],
|
||||
onSelect: (v) => (this._environmentalAttribute = v),
|
||||
});
|
||||
}
|
||||
|
||||
/** Attribute selector for one compound condition — the same live-fetched
|
||||
* dropdown the flat editor has, keyed by the condition's first entity. */
|
||||
private _renderConditionAttribute(c: CompoundConditionDraft, i: number) {
|
||||
const L = this._lang;
|
||||
const firstId = c.entityIds.split(",")[0]?.trim() || "";
|
||||
if (firstId) this._fetchConditionAttributes(firstId);
|
||||
const opts = firstId ? this._conditionAttrOptions[firstId] : undefined;
|
||||
if (opts && opts.available.length > 0) {
|
||||
return html`
|
||||
<div class="select-row">
|
||||
<label>${t("attribute_optional", L)}</label>
|
||||
<select
|
||||
.value=${c.attribute}
|
||||
@change=${(e: Event) => this._patchCondition(i, { attribute: (e.target as HTMLSelectElement).value })}
|
||||
>
|
||||
<option value="" ?selected=${!c.attribute}>${t("use_entity_state", L)}</option>
|
||||
${opts.suggested.map(
|
||||
(attr) => html`<option value=${attr} ?selected=${attr === c.attribute}>${attr} ★</option>`
|
||||
)}
|
||||
${opts.available
|
||||
.filter((a) => !opts.suggested.includes(a.name))
|
||||
.map(
|
||||
(a) => html`<option value=${a.name} ?selected=${a.name === c.attribute}>${a.name}${a.numeric ? "" : " (non-numeric)"}</option>`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ms-textfield
|
||||
label="${t("attribute_optional", L)}"
|
||||
.value=${c.attribute}
|
||||
@input=${(e: Event) => this._patchCondition(i, { attribute: (e.target as HTMLInputElement).value.trim() })}
|
||||
></ms-textfield>
|
||||
`;
|
||||
return this._renderAttributeSelect({
|
||||
label: t("attribute_optional", this._lang),
|
||||
value: c.attribute,
|
||||
suggested: opts?.suggested ?? [],
|
||||
available: opts?.available ?? [],
|
||||
onSelect: (v) => this._patchCondition(i, { attribute: v }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Type-specific inputs for a single compound condition (mirrors the flat
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { downloadSignedDocument, openSignedDocument } from "../helpers/document-url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface Doc {
|
||||
@@ -31,16 +33,6 @@ interface Doc {
|
||||
part_ids?: string[];
|
||||
}
|
||||
|
||||
const CATEGORIES = ["manual", "warranty", "invoice", "spare_parts", "photo", "other"] as const;
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
manual: "mdi:book-open-variant",
|
||||
warranty: "mdi:shield-check",
|
||||
invoice: "mdi:receipt-text-outline",
|
||||
spare_parts: "mdi:cog-outline",
|
||||
photo: "mdi:image-outline",
|
||||
other: "mdi:file-document-outline",
|
||||
};
|
||||
|
||||
export class MaintenanceTaskDocuments extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public entryId!: string;
|
||||
@@ -59,7 +51,7 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
private _localeReady = false;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
/** The id linked docs reference — a task id or (part mode) a part id. */
|
||||
@@ -158,19 +150,9 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
// A per-task page hint jumps straight to the relevant page via the PDF
|
||||
// viewer's #page=N fragment (client-side, so it never breaks the signature).
|
||||
const page = this._pageFor(doc);
|
||||
const frag = page ? `#page=${page}` : "";
|
||||
const win = window.open("about:blank", "_blank");
|
||||
try {
|
||||
const s = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 300,
|
||||
});
|
||||
// Absolute URL: a fragment on a *root-relative* path won't resolve against
|
||||
// the blank popup's about:blank base, so it would silently stay blank.
|
||||
if (win) win.location.href = new URL(s.path + frag, window.location.origin).href;
|
||||
await openSignedDocument(this.hass, doc.id, page ? `#page=${page}` : "");
|
||||
} catch (e) {
|
||||
if (win) win.close();
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
}
|
||||
@@ -196,12 +178,7 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
|
||||
private async _download(doc: Doc): Promise<void> {
|
||||
try {
|
||||
const s = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 30,
|
||||
});
|
||||
downloadUrl(s.path, doc.filename || doc.title || "document");
|
||||
await downloadSignedDocument(this.hass, doc.id, doc.filename || doc.title || "document");
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence } from "../styles";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
@@ -62,7 +62,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
private _featuresLoaded = false;
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
/** Open the dialog. Loads fresh data from /object via WS so dialog stays in
|
||||
|
||||
+4
-5
@@ -13,7 +13,8 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale } from "../styles";
|
||||
import { t, ensureLocale, langOf } from "../styles";
|
||||
import { registerCustomCard } from "../helpers/register-card";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { sectionCardSharedStyles } from "./section-card-shared-styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -55,7 +56,7 @@ export class MaintenanceVacationSectionCard extends LitElement {
|
||||
}
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
return langOf(this.hass);
|
||||
}
|
||||
|
||||
private get _isAdmin(): boolean {
|
||||
@@ -317,9 +318,7 @@ if (!customElements.get("maintenance-vacation-section-card")) {
|
||||
);
|
||||
}
|
||||
|
||||
(window as { customCards?: unknown[] }).customCards =
|
||||
(window as { customCards?: unknown[] }).customCards || [];
|
||||
((window as { customCards?: unknown[] }).customCards!).push({
|
||||
registerCustomCard({
|
||||
type: "maintenance-vacation-section-card",
|
||||
name: "Maintenance Supporter — Vacation",
|
||||
description: "Inline vacation mode toggle + dates",
|
||||
|
||||
Reference in New Issue
Block a user