updated apps
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
/** Maintenance Supporter Sidebar Panel. */
|
||||
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence } from "./styles";
|
||||
import { LS_KEYS } from "./helpers/storage-keys";
|
||||
import { daysProgress } from "./helpers/interval";
|
||||
import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
|
||||
import { warrantyStatus } from "./helpers/warranty";
|
||||
@@ -23,12 +25,15 @@ import type {
|
||||
HistoryEntry,
|
||||
TriggerConfig,
|
||||
StatisticsPoint,
|
||||
SavedView,
|
||||
SavedViewFilters,
|
||||
} from "./types";
|
||||
import { StatisticsService } from "./statistics-service";
|
||||
import { UserService } from "./user-service";
|
||||
import "./components/object-dialog";
|
||||
import type { MaintenanceObjectDialog } from "./components/object-dialog";
|
||||
import "./components/documents-section";
|
||||
import "./components/parts-section";
|
||||
import "./components/task-documents";
|
||||
import "./components/task-dialog";
|
||||
import type { MaintenanceTaskDialog } from "./components/task-dialog";
|
||||
@@ -36,6 +41,8 @@ import "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
import "./components/qr-dialog";
|
||||
import type { MaintenanceQrDialog } from "./components/qr-dialog";
|
||||
import "./components/adopt-problem-sensors-dialog";
|
||||
import type { MaintenanceAdoptProblemSensorsDialog } from "./components/adopt-problem-sensors-dialog";
|
||||
// v2.0.0: panel uses the extracted Calendar Card instead of its own
|
||||
// _renderCalendar() method — single source of truth for the calendar view.
|
||||
import "./maintenance-calendar-card";
|
||||
@@ -53,6 +60,8 @@ import "./components/seasonal-overrides-dialog";
|
||||
import type { SeasonalOverridesDialog } from "./components/seasonal-overrides-dialog";
|
||||
import "./components/group-dialog";
|
||||
import type { MaintenanceGroupDialog } from "./components/group-dialog";
|
||||
import "./components/saved-views-dialog";
|
||||
import type { MaintenanceSavedViewsDialog } from "./components/saved-views-dialog";
|
||||
import { type SparklineContext } from "./renderers/sparkline";
|
||||
import { buildCalendarBuckets, isoDateLocal, type CalendarEvent } from "./helpers/calendar-bucket";
|
||||
import { renderTriggerProgress, renderMiniSparkline } from "./renderers/progress";
|
||||
@@ -84,17 +93,22 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@state() private _selectedTaskId: string | null = null;
|
||||
@state() private _filterStatus = "";
|
||||
@state() private _filterUser: string | null = null;
|
||||
@state() private _filterLabel: string | null = null;
|
||||
// v2.24: shared saved filter views + the id of the one currently applied ("" =
|
||||
// none; cleared the moment the user hand-edits any filter control).
|
||||
@state() private _savedViews: SavedView[] = [];
|
||||
@state() private _activeViewId = "";
|
||||
@state() private _unsub: (() => void) | null = null;
|
||||
@state() private _chartRangeDays = (() => {
|
||||
try {
|
||||
const v = parseInt(localStorage.getItem("msp-chart-range") || "", 10);
|
||||
const v = parseInt(localStorage.getItem(LS_KEYS.chartRange) || "", 10);
|
||||
return [7, 30, 90, 365].includes(v) ? v : 30;
|
||||
} catch {
|
||||
return 30;
|
||||
}
|
||||
})();
|
||||
@state() private _hideOutliers = (() => {
|
||||
try { return localStorage.getItem("msp-chart-hide-outliers") === "1"; } catch { return false; }
|
||||
try { return localStorage.getItem(LS_KEYS.chartHideOutliers) === "1"; } catch { return false; }
|
||||
})();
|
||||
@state() private _historyFilter: string | null = null;
|
||||
@state() private _budget: BudgetStatus | null = null;
|
||||
@@ -121,7 +135,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// Dashboard redesign state
|
||||
@state() private _overviewTab: "today" | "dashboard" | "calendar" | "settings" = (() => {
|
||||
try {
|
||||
const v = localStorage.getItem("msp-overview-tab");
|
||||
const v = localStorage.getItem(LS_KEYS.overviewTab);
|
||||
return v === "today" || v === "calendar" ? v : "dashboard";
|
||||
} catch { return "dashboard"; }
|
||||
})();
|
||||
@@ -153,7 +167,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// v2.15.0: collapsed analysis sections on the task-detail overview tab,
|
||||
// remembered per section across visits.
|
||||
@state() private _collapsedSections: Set<string> = (() => {
|
||||
try { return new Set(JSON.parse(localStorage.getItem("msp-collapsed-sections") || "[]")); }
|
||||
try { return new Set(JSON.parse(localStorage.getItem(LS_KEYS.collapsedSections) || "[]")); }
|
||||
catch { return new Set(); }
|
||||
})();
|
||||
// v2.15.0: command palette ("/" since 2.18.1 — Ctrl+K clashed with HA's own
|
||||
@@ -208,21 +222,29 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
window.addEventListener("popstate", this._popstateHandler);
|
||||
window.addEventListener("keydown", this._paletteKeydown);
|
||||
window.addEventListener("resize", this._onVirtualScroll, { passive: true });
|
||||
const saved = localStorage.getItem("maintenance_supporter_sort");
|
||||
if (saved && ["due_date", "object", "type", "task_name", "area", "assigned_user", "group"].includes(saved)) {
|
||||
this._sortMode = saved as SortMode;
|
||||
}
|
||||
const savedObj = localStorage.getItem("maintenance_supporter_object_sort");
|
||||
if (savedObj && ["alphabetical", "due_soonest", "task_count"].includes(savedObj)) {
|
||||
this._objectSortMode = savedObj as ObjectSortMode;
|
||||
}
|
||||
const savedGroup = localStorage.getItem("maintenance_supporter_groupby");
|
||||
if (savedGroup && ["none", "area", "group", "user"].includes(savedGroup)) {
|
||||
this._groupByMode = savedGroup as GroupByMode;
|
||||
}
|
||||
const savedView = localStorage.getItem("maintenance_supporter_object_view");
|
||||
if (savedView === "cards" || savedView === "table") {
|
||||
this._objectViewMode = savedView;
|
||||
// localStorage.getItem can THROW (Safari private mode / locked-down policies
|
||||
// where storage access raises rather than returning null) — an unguarded
|
||||
// read here would abort connectedCallback and leave the panel blank. Every
|
||||
// other storage access in this file is wrapped; wrap these too.
|
||||
try {
|
||||
const saved = localStorage.getItem(LS_KEYS.taskSort);
|
||||
if (saved && ["due_date", "object", "type", "task_name", "area", "assigned_user", "group"].includes(saved)) {
|
||||
this._sortMode = saved as SortMode;
|
||||
}
|
||||
const savedObj = localStorage.getItem(LS_KEYS.objectSort);
|
||||
if (savedObj && ["alphabetical", "due_soonest", "task_count"].includes(savedObj)) {
|
||||
this._objectSortMode = savedObj as ObjectSortMode;
|
||||
}
|
||||
const savedGroup = localStorage.getItem(LS_KEYS.groupBy);
|
||||
if (savedGroup && ["none", "area", "group", "user"].includes(savedGroup)) {
|
||||
this._groupByMode = savedGroup as GroupByMode;
|
||||
}
|
||||
const savedView = localStorage.getItem(LS_KEYS.objectView);
|
||||
if (savedView === "cards" || savedView === "table") {
|
||||
this._objectViewMode = savedView;
|
||||
}
|
||||
} catch {
|
||||
// storage blocked — keep the defaults
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,13 +356,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const [objResult, statsResult, budgetResult, groupsResult, settingsResult] = await Promise.all([
|
||||
const [objResult, statsResult, budgetResult, groupsResult, settingsResult, viewsResult] = await Promise.all([
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/groups" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/settings" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/views/list" }).catch(() => null),
|
||||
]);
|
||||
if (viewsResult) this._savedViews = (viewsResult as { views: SavedView[] }).views || [];
|
||||
if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects;
|
||||
if (statsResult) this._stats = statsResult as StatisticsResponse;
|
||||
if (budgetResult) this._budget = budgetResult as BudgetStatus;
|
||||
@@ -479,7 +503,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private _setChartRange(days: number): void {
|
||||
if (days === this._chartRangeDays) return;
|
||||
this._chartRangeDays = days;
|
||||
try { localStorage.setItem("msp-chart-range", String(days)); } catch { /* private mode */ }
|
||||
try { localStorage.setItem(LS_KEYS.chartRange, String(days)); } catch { /* private mode */ }
|
||||
const task = this._selectedEntryId && this._selectedTaskId
|
||||
? this._getTask(this._selectedEntryId, this._selectedTaskId)
|
||||
: null;
|
||||
@@ -498,7 +522,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// Outlier filtering is client-side on the already-fetched series, so just
|
||||
// flip the flag and let renderChart re-filter — no re-fetch needed.
|
||||
this._hideOutliers = hide;
|
||||
try { localStorage.setItem("msp-chart-hide-outliers", hide ? "1" : "0"); } catch { /* private mode */ }
|
||||
try { localStorage.setItem(LS_KEYS.chartHideOutliers, hide ? "1" : "0"); } catch { /* private mode */ }
|
||||
}
|
||||
|
||||
private async _fetchMiniStatsForOverview(): Promise<void> {
|
||||
@@ -557,6 +581,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
if (task.responsible_user_id !== userId) continue;
|
||||
}
|
||||
|
||||
// Label filter (v2.26 — also captured by saved views)
|
||||
if (this._filterLabel && !(task.labels || []).includes(this._filterLabel)) continue;
|
||||
|
||||
// Collect groups that contain this task
|
||||
const groupNames: string[] = [];
|
||||
for (const group of Object.values(this._groups)) {
|
||||
@@ -695,6 +722,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
* the task list. Empty string clears the filter (used by "Tasks" KPI). */
|
||||
private _filterByStatus(status: string): void {
|
||||
this._filterStatus = status;
|
||||
this._activeViewId = "";
|
||||
// Make sure we're on the dashboard tab so the task list is actually
|
||||
// visible — pointless to filter on the calendar/settings tabs.
|
||||
if (this._overviewTab !== "dashboard") {
|
||||
@@ -703,6 +731,75 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._scrollContentToTop();
|
||||
}
|
||||
|
||||
// ── v2.24: saved filter views ──────────────────────────────────────────────
|
||||
|
||||
/** Distinct labels across all loaded tasks (for the label-filter dropdown). */
|
||||
private get _allLabels(): string[] {
|
||||
const seen = new Set<string>();
|
||||
for (const obj of this._objects) {
|
||||
for (const task of obj.tasks) {
|
||||
for (const lb of task.labels || []) seen.add(lb);
|
||||
}
|
||||
}
|
||||
return [...seen].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/** The panel's current task-list filter state, in the shape a view stores. */
|
||||
private get _currentFilters(): SavedViewFilters {
|
||||
return {
|
||||
status: this._filterStatus,
|
||||
user_id: this._filterUser,
|
||||
label: this._filterLabel,
|
||||
archived: this._showArchived,
|
||||
sort_mode: this._sortMode,
|
||||
group_by: this._groupByMode,
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply a saved view's filters to the live controls (or clear on ""). */
|
||||
private _applyView(viewId: string): void {
|
||||
this._activeViewId = viewId;
|
||||
if (!viewId) return;
|
||||
const view = this._savedViews.find((v) => v.id === viewId);
|
||||
if (!view) return;
|
||||
const f = view.filters;
|
||||
this._filterStatus = f.status || "";
|
||||
this._filterUser = f.user_id || null;
|
||||
this._filterLabel = f.label || null;
|
||||
this._showArchived = !!f.archived;
|
||||
// Validate against the current enums before assigning — a view saved by an
|
||||
// older/renamed build could carry a mode no longer valid, which would then
|
||||
// be persisted to localStorage and silently break sorting/grouping.
|
||||
if (["due_date", "object", "type", "task_name", "area", "assigned_user", "group"].includes(f.sort_mode)) {
|
||||
this._sortMode = f.sort_mode as SortMode;
|
||||
}
|
||||
if (["none", "area", "group", "user"].includes(f.group_by)) {
|
||||
this._groupByMode = f.group_by as GroupByMode;
|
||||
}
|
||||
// Persist sort/group like the manual controls do, so they stick after reload.
|
||||
try {
|
||||
localStorage.setItem(LS_KEYS.taskSort, this._sortMode);
|
||||
localStorage.setItem(LS_KEYS.groupBy, this._groupByMode);
|
||||
} catch {
|
||||
// ignore private-mode storage errors
|
||||
}
|
||||
if (this._overviewTab !== "dashboard") this._overviewTab = "dashboard";
|
||||
}
|
||||
|
||||
private _openSavedViewsDialog(): void {
|
||||
this.shadowRoot!
|
||||
.querySelector<MaintenanceSavedViewsDialog>("maintenance-saved-views-dialog")
|
||||
?.open(this._currentFilters, this._savedViews);
|
||||
}
|
||||
|
||||
private _onSavedViewsChanged(e: CustomEvent<{ views: SavedView[] }>): void {
|
||||
this._savedViews = e.detail.views || [];
|
||||
// If the applied view was deleted, drop the stale selection.
|
||||
if (this._activeViewId && !this._savedViews.some((v) => v.id === this._activeViewId)) {
|
||||
this._activeViewId = "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset scroll on the panel's .content container. Used by every
|
||||
* navigation action so the user always lands at the top of the new
|
||||
* view, never at a stale scroll position from the previous view.
|
||||
@@ -880,6 +977,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Adopt problem sensors ---
|
||||
|
||||
private _openAdoptProblemSensors(): void {
|
||||
this.shadowRoot!
|
||||
.querySelector<MaintenanceAdoptProblemSensorsDialog>("maintenance-adopt-problem-sensors-dialog")
|
||||
?.open();
|
||||
}
|
||||
|
||||
private _onProblemSensorsAdopted(e: CustomEvent): void {
|
||||
const tasks = e.detail?.tasks_created ?? 0;
|
||||
this._showToast(t("adopt_problem_done", this._lang).replace("{tasks}", String(tasks)));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
// --- Template gallery ---
|
||||
|
||||
private async _openTemplateGallery(): Promise<void> {
|
||||
@@ -1545,7 +1656,19 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
never: t("worksheet_never", L),
|
||||
typeLabel: (ty: string) => t(ty, L),
|
||||
statusLabel: (st: string) => t(st, L),
|
||||
parts: t("consumes_parts_label", L),
|
||||
};
|
||||
// Required parts as checkable lines: qty × name (stock unit) — location.
|
||||
const wsParts = obj.parts || [];
|
||||
const partsLines = (task.consumes_parts || [])
|
||||
.map((link) => {
|
||||
const pt = wsParts.find((x) => x.id === link.part_id);
|
||||
if (!pt) return "";
|
||||
const stock = pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : "";
|
||||
const loc = pt.storage_location ? ` — ${pt.storage_location}` : "";
|
||||
return `${link.quantity}× ${pt.name}${stock}${loc}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
const html = buildTaskWorksheetHtml(
|
||||
task, obj.object.name, labels,
|
||||
(iso) => formatDate(iso, L),
|
||||
@@ -1554,6 +1677,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
qrComplete?.svg_data_uri || null,
|
||||
excerpt,
|
||||
new Date().toISOString(),
|
||||
partsLines,
|
||||
);
|
||||
const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
|
||||
window.open(url, "_blank");
|
||||
@@ -1579,6 +1703,21 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
?.tasks.find((tsk) => tsk.id === taskId);
|
||||
dlg.taskType = tk?.type || "";
|
||||
dlg.readingUnit = tk?.reading_unit || "";
|
||||
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
||||
// task shows what it will decrement (incl. the storage location).
|
||||
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
|
||||
const partById = new Map(objParts.map((pt) => [pt.id, pt]));
|
||||
const refPart = tk?.part_ref ? partById.get(tk.part_ref.part_id) : undefined;
|
||||
dlg.restockDefault = tk?.part_ref ? (refPart?.restock_quantity ?? 1) : null;
|
||||
dlg.consumesInfo = (tk?.consumes_parts || [])
|
||||
.map((link) => {
|
||||
const pt = partById.get(link.part_id);
|
||||
if (!pt) return "";
|
||||
const loc = pt.storage_location ? ` — ${pt.storage_location}` : "";
|
||||
const stock = pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : "";
|
||||
return `${link.quantity}× ${pt.name}${stock}${loc}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
dlg.open();
|
||||
}
|
||||
|
||||
@@ -1649,6 +1788,14 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.objects=${this._objects}
|
||||
@group-saved=${this._onDialogEvent}
|
||||
></maintenance-group-dialog>
|
||||
<maintenance-adopt-problem-sensors-dialog
|
||||
.hass=${this.hass}
|
||||
@problem-sensors-adopted=${(e: CustomEvent) => this._onProblemSensorsAdopted(e)}
|
||||
></maintenance-adopt-problem-sensors-dialog>
|
||||
<maintenance-saved-views-dialog
|
||||
.hass=${this.hass}
|
||||
@saved-views-changed=${(e: CustomEvent<{ views: SavedView[] }>) => this._onSavedViewsChanged(e)}
|
||||
></maintenance-saved-views-dialog>
|
||||
${this._toastMessage ? html`<div class="toast">
|
||||
<span>${this._toastMessage}</span>
|
||||
${this._toastUndo ? html`<button class="toast-undo" @click=${() => this._runToastUndo()}>${t("undo", this._lang)}</button>` : nothing}
|
||||
@@ -1828,7 +1975,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
private _setOverviewTab(tab: "today" | "dashboard" | "calendar" | "settings"): void {
|
||||
this._overviewTab = tab;
|
||||
try { localStorage.setItem("msp-overview-tab", tab); } catch { /* private mode */ }
|
||||
try { localStorage.setItem(LS_KEYS.overviewTab, tab); } catch { /* private mode */ }
|
||||
this._scrollContentToTop();
|
||||
}
|
||||
|
||||
@@ -1901,11 +2048,32 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${this._features.budget ? this._renderBudgetBar() : nothing}
|
||||
|
||||
<div class="filter-bar">
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${t("views_label", L)}</span>
|
||||
<select
|
||||
.value=${this._activeViewId}
|
||||
@change=${(e: Event) => this._applyView((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">${t("views_none", L)}</option>
|
||||
${this._savedViews.map(
|
||||
(v) => html`<option value=${v.id} ?selected=${this._activeViewId === v.id}>${v.name}</option>`,
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
${!isOperator ? html`
|
||||
<ha-icon-button
|
||||
class="views-save-btn"
|
||||
.path=${"M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z"}
|
||||
.label=${t("views_manage", L)}
|
||||
title=${t("views_manage", L)}
|
||||
@click=${() => this._openSavedViewsDialog()}
|
||||
></ha-icon-button>
|
||||
` : nothing}
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${t("filter_label", L)}</span>
|
||||
<select
|
||||
.value=${this._filterStatus}
|
||||
@change=${(e: Event) => (this._filterStatus = (e.target as HTMLSelectElement).value)}
|
||||
@change=${(e: Event) => { this._filterStatus = (e.target as HTMLSelectElement).value; this._activeViewId = ""; }}
|
||||
>
|
||||
<option value="">${t("all", L)}</option>
|
||||
<option value="overdue">${t("overdue", L)}</option>
|
||||
@@ -1921,19 +2089,39 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@change=${(e: Event) => {
|
||||
const val = (e.target as HTMLSelectElement).value;
|
||||
this._filterUser = val || null;
|
||||
this._activeViewId = "";
|
||||
}}
|
||||
>
|
||||
<option value="">${t("all_users", L)}</option>
|
||||
<option value="current_user">${t("my_tasks", L)}</option>
|
||||
</select>
|
||||
</label>
|
||||
${this._allLabels.length > 0 ? html`
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${t("label_filter", L)}</span>
|
||||
<select
|
||||
.value=${this._filterLabel || ""}
|
||||
@change=${(e: Event) => {
|
||||
const val = (e.target as HTMLSelectElement).value;
|
||||
this._filterLabel = val || null;
|
||||
this._activeViewId = "";
|
||||
}}
|
||||
>
|
||||
<option value="">${t("all_labels", L)}</option>
|
||||
${this._allLabels.map(
|
||||
(lb) => html`<option value=${lb} ?selected=${this._filterLabel === lb}>${lb}</option>`
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
` : nothing}
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${t("sort_label", L)}</span>
|
||||
<select
|
||||
.value=${this._sortMode}
|
||||
@change=${(e: Event) => {
|
||||
this._sortMode = (e.target as HTMLSelectElement).value as SortMode;
|
||||
localStorage.setItem("maintenance_supporter_sort", this._sortMode);
|
||||
this._activeViewId = "";
|
||||
try { localStorage.setItem(LS_KEYS.taskSort, this._sortMode); } catch { /* private mode */ }
|
||||
}}
|
||||
>
|
||||
<option value="due_date" ?selected=${this._sortMode === "due_date"}>${t("sort_due_date", L)}</option>
|
||||
@@ -1951,7 +2139,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.value=${this._groupByMode}
|
||||
@change=${(e: Event) => {
|
||||
this._groupByMode = (e.target as HTMLSelectElement).value as GroupByMode;
|
||||
localStorage.setItem("maintenance_supporter_groupby", this._groupByMode);
|
||||
this._activeViewId = "";
|
||||
try { localStorage.setItem(LS_KEYS.groupBy, this._groupByMode); } catch { /* private mode */ }
|
||||
}}
|
||||
>
|
||||
<option value="none" ?selected=${this._groupByMode === "none"}>${t("groupby_none", L)}</option>
|
||||
@@ -1963,7 +2152,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${archivedCount > 0 ? html`
|
||||
<ha-button
|
||||
class="archived-toggle ${this._showArchived ? "active" : ""}"
|
||||
@click=${() => { this._showArchived = !this._showArchived; }}
|
||||
@click=${() => { this._showArchived = !this._showArchived; this._activeViewId = ""; }}
|
||||
>
|
||||
<ha-icon icon="mdi:archive-outline"></ha-icon>
|
||||
${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedCount})`}
|
||||
@@ -1987,6 +2176,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<ha-button @click=${() => this._openTemplateGallery()}>
|
||||
<ha-icon icon="mdi:view-grid-plus-outline"></ha-icon> ${t("templates_from", L)}
|
||||
</ha-button>
|
||||
<ha-button @click=${() => this._openAdoptProblemSensors()}>
|
||||
<ha-icon icon="mdi:alert-circle-check-outline"></ha-icon> ${t("adopt_problem_button", L)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog")?.openCreate("", this._objects)}
|
||||
>
|
||||
@@ -2265,7 +2457,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.value=${this._objectSortMode}
|
||||
@change=${(e: Event) => {
|
||||
this._objectSortMode = (e.target as HTMLSelectElement).value as ObjectSortMode;
|
||||
localStorage.setItem("maintenance_supporter_object_sort", this._objectSortMode);
|
||||
localStorage.setItem(LS_KEYS.objectSort, this._objectSortMode);
|
||||
}}
|
||||
>
|
||||
<option value="alphabetical" ?selected=${this._objectSortMode === "alphabetical"}>${t("sort_alphabetical", L)}</option>
|
||||
@@ -2294,7 +2486,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.value=${this._groupByMode}
|
||||
@change=${(e: Event) => {
|
||||
this._groupByMode = (e.target as HTMLSelectElement).value as GroupByMode;
|
||||
localStorage.setItem("maintenance_supporter_groupby", this._groupByMode);
|
||||
try { localStorage.setItem(LS_KEYS.groupBy, this._groupByMode); } catch { /* private mode */ }
|
||||
}}
|
||||
>
|
||||
<option value="none" ?selected=${this._groupByMode === "none"}>${t("groupby_none", L)}</option>
|
||||
@@ -2343,7 +2535,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
private _setObjectViewMode(mode: "cards" | "table"): void {
|
||||
this._objectViewMode = mode;
|
||||
localStorage.setItem("maintenance_supporter_object_view", mode);
|
||||
localStorage.setItem(LS_KEYS.objectView, mode);
|
||||
}
|
||||
|
||||
// (#67 / Phase 3) Download all objects as a one-row-per-object CSV.
|
||||
@@ -2417,7 +2609,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
case "documentation_url":
|
||||
return html`<td class="oc-documentation_url">${
|
||||
o.documentation_url && /^https?:\/\//i.test(o.documentation_url)
|
||||
isSafeHttpUrl(o.documentation_url)
|
||||
? html`<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer"
|
||||
@click=${(e: Event) => e.stopPropagation()}><ha-icon icon="mdi:file-document-outline"></ha-icon></a>`
|
||||
: "—"
|
||||
@@ -2716,7 +2908,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
? html`<p class="meta">${[o.manufacturer, o.model].filter(Boolean).join(" ")}</p>`
|
||||
: nothing}
|
||||
${o.serial_number ? html`<p class="meta">${t("serial_number_label", L)}: ${o.serial_number}</p>` : nothing}
|
||||
${o.documentation_url && /^https?:\/\//i.test(o.documentation_url)
|
||||
${isSafeHttpUrl(o.documentation_url)
|
||||
? html`<p class="meta">${t("documentation_url_label", L)}:
|
||||
<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer">${o.documentation_url}</a>
|
||||
</p>`
|
||||
@@ -2730,12 +2922,6 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<maintenance-documents-section
|
||||
.hass=${this.hass}
|
||||
.entryId=${obj.entry_id}
|
||||
.canWrite=${!isOperator}
|
||||
></maintenance-documents-section>
|
||||
|
||||
<h3>${t("tasks", L)} (${visibleTasks.length})${archivedInObj > 0 ? html`
|
||||
<ha-button
|
||||
class="archived-toggle ${this._showArchived ? "active" : ""}"
|
||||
@@ -2762,6 +2948,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${this._statusBadge(!!task.archived, !!task.is_done, task.status)}
|
||||
${!task.enabled ? html`<span class="badge-disabled">${t("disabled", L)}</span>` : nothing}
|
||||
${task.nfc_tag_id ? html`<span class="nfc-badge" title="${t("nfc_linked", L)}"><ha-icon icon="mdi:nfc-variant"></ha-icon></span>` : nothing}
|
||||
${task.document_count
|
||||
? 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="task-sub${task.responsible_user_id ? '' : ' task-sub-empty'}">${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null)}</span>
|
||||
@@ -2783,6 +2972,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
</span>
|
||||
</div>
|
||||
`)}</div>`}
|
||||
|
||||
<maintenance-documents-section
|
||||
.hass=${this.hass}
|
||||
.entryId=${obj.entry_id}
|
||||
.canWrite=${!isOperator}
|
||||
></maintenance-documents-section>
|
||||
|
||||
<maintenance-parts-section
|
||||
.hass=${this.hass}
|
||||
.entryId=${obj.entry_id}
|
||||
.parts=${obj.parts || []}
|
||||
.canWrite=${!isOperator}
|
||||
@parts-changed=${() => this._loadData()}
|
||||
></maintenance-parts-section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2820,7 +3023,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const next = new Set(this._collapsedSections);
|
||||
if (next.has(key)) next.delete(key); else next.add(key);
|
||||
this._collapsedSections = next;
|
||||
try { localStorage.setItem("msp-collapsed-sections", JSON.stringify([...next])); } catch { /* private mode */ }
|
||||
try { localStorage.setItem(LS_KEYS.collapsedSections, JSON.stringify([...next])); } catch { /* private mode */ }
|
||||
}
|
||||
|
||||
/** Build the context the history renderers need from panel state. */
|
||||
|
||||
Reference in New Issue
Block a user