Updated apps
This commit is contained in:
@@ -43,6 +43,9 @@ 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";
|
||||
import "./components/suggested-setups-dialog";
|
||||
import "./components/battery-fleet-section";
|
||||
import type { MaintenanceSuggestedSetupsDialog } from "./components/suggested-setups-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";
|
||||
@@ -129,6 +132,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@state() private _moreMenuOpen = false;
|
||||
@state() private _toastMessage = "";
|
||||
@state() private _toastUndo: (() => void) | null = null;
|
||||
@state() private _toastActionLabel = "";
|
||||
// Narrow-viewport disclosure (UX 2026-07): filters and create-actions are
|
||||
// occasional-use — collapsed behind two toggle buttons so the task list
|
||||
// starts above the fold on phones. Desktop renders them inline as before.
|
||||
@state() private _filtersOpen = false;
|
||||
@state() private _actionsMenuOpen = false;
|
||||
// Battery Fleet: offer one-click setup only when Battery Notes is present
|
||||
// and the fleet isn't set up yet.
|
||||
@state() private _batteryFleetSetupAvailable = false;
|
||||
private _toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private _dismissedSuggestions = new Set<string>();
|
||||
|
||||
@@ -368,6 +380,17 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
]);
|
||||
if (viewsResult) this._savedViews = (viewsResult as { views: SavedView[] }).views || [];
|
||||
if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects;
|
||||
// Battery Fleet availability (Battery Notes present + not yet set up).
|
||||
this.hass.connection
|
||||
.sendMessagePromise<{ available: boolean; configured: boolean }>({
|
||||
type: "maintenance_supporter/battery_fleet/overview",
|
||||
})
|
||||
.then((ov) => {
|
||||
this._batteryFleetSetupAvailable = !!ov.available && !ov.configured;
|
||||
})
|
||||
.catch(() => {
|
||||
this._batteryFleetSetupAvailable = false;
|
||||
});
|
||||
if (statsResult) this._stats = statsResult as StatisticsResponse;
|
||||
if (budgetResult) this._budget = budgetResult as BudgetStatus;
|
||||
if (groupsResult) this._groups = (groupsResult as { groups: Record<string, MaintenanceGroup> }).groups || {};
|
||||
@@ -852,15 +875,23 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private _showToast(msg: string): void {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
this._toastUndo = null;
|
||||
this._toastActionLabel = "";
|
||||
this._toastMessage = msg;
|
||||
this._toastTimer = setTimeout(() => { this._toastMessage = ""; this._toastTimer = null; }, 4000);
|
||||
}
|
||||
|
||||
/** A toast with an Undo action — used for reversible actions (archive) that
|
||||
* run immediately instead of behind a confirm dialog. Longer-lived so the
|
||||
* user has time to react; the undo callback dismisses it. */
|
||||
/** A toast with an action button (label defaults to Undo). Used for
|
||||
* reversible actions (archive) and follow-up shortcuts (configure the
|
||||
* freshly adopted task). Longer-lived so the user has time to react; the
|
||||
* callback dismisses it. */
|
||||
private _showActionToast(msg: string, label: string, action: () => void): void {
|
||||
this._showUndoToast(msg, action);
|
||||
this._toastActionLabel = label;
|
||||
}
|
||||
|
||||
private _showUndoToast(msg: string, undo: () => void): void {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
this._toastActionLabel = "";
|
||||
this._toastMessage = msg;
|
||||
this._toastUndo = undo;
|
||||
this._toastTimer = setTimeout(() => {
|
||||
@@ -987,9 +1018,57 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
?.open();
|
||||
}
|
||||
|
||||
private _onProblemSensorsAdopted(e: CustomEvent): void {
|
||||
private async _onProblemSensorsAdopted(e: CustomEvent): Promise<void> {
|
||||
const tasks = e.detail?.tasks_created ?? 0;
|
||||
this._showToast(t("adopt_problem_done", this._lang).replace("{tasks}", String(tasks)));
|
||||
const created = (e.detail?.created ?? []) as Array<{ entry_id: string; task_id: string; name: string }>;
|
||||
await this._loadData();
|
||||
const msg = t("adopt_problem_done", this._lang).replace("{tasks}", String(tasks));
|
||||
if (created.length > 0) {
|
||||
// Adopted tasks are fully configurable from day one (responsible user,
|
||||
// priority, documents) — surface that with a direct path to the first.
|
||||
this._showActionToast(msg, t("adopt_problem_configure", this._lang), () => {
|
||||
const ref = created[0];
|
||||
const obj = this._objects.find((o) => o.entry_id === ref.entry_id);
|
||||
const tk = obj?.tasks.find((task) => task.id === ref.task_id);
|
||||
if (obj && tk) {
|
||||
this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog")?.openEdit(ref.entry_id, tk);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._showToast(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Suggested setups (integration signatures, v2.28) ---
|
||||
|
||||
private async _setupBatteryFleet(): Promise<void> {
|
||||
try {
|
||||
const res = await this.hass.connection.sendMessagePromise<{ entry_id: string; task_id?: string }>({
|
||||
type: "maintenance_supporter/battery_fleet/setup",
|
||||
});
|
||||
this._batteryFleetSetupAvailable = false;
|
||||
await this._loadData();
|
||||
// Jump to the fleet task so the user lands on its battery detail section.
|
||||
const obj = this._objects.find((o) => o.entry_id === res.entry_id);
|
||||
const tk = obj?.tasks.find((t2) => t2.id === res.task_id) || obj?.tasks[0];
|
||||
if (obj && tk) {
|
||||
this._showTask(obj.entry_id, tk.id);
|
||||
}
|
||||
this._showToast(t("battery_fleet_setup_done", this._lang));
|
||||
} catch (e) {
|
||||
this._showToast(describeWsError(e, this._lang));
|
||||
}
|
||||
}
|
||||
|
||||
private _openSuggestedSetups(): void {
|
||||
this.shadowRoot!
|
||||
.querySelector<MaintenanceSuggestedSetupsDialog>("maintenance-suggested-setups-dialog")
|
||||
?.open();
|
||||
}
|
||||
|
||||
private _onSetupsAdopted(e: CustomEvent): void {
|
||||
const tasks = e.detail?.tasks_created ?? 0;
|
||||
this._showToast(t("setups_done", this._lang).replace("{tasks}", String(tasks)));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
@@ -1720,6 +1799,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
return `${link.quantity}× ${pt.name}${stock}${loc}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
// #99: editable per-completion parts selection (not on buy tasks — those
|
||||
// RESTOCK via the qty field instead of consuming).
|
||||
dlg.parts = tk?.part_ref ? [] : objParts;
|
||||
dlg.consumesParts = tk?.part_ref ? [] : (tk?.consumes_parts || []);
|
||||
dlg.open();
|
||||
}
|
||||
|
||||
@@ -1794,13 +1877,17 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@problem-sensors-adopted=${(e: CustomEvent) => this._onProblemSensorsAdopted(e)}
|
||||
></maintenance-adopt-problem-sensors-dialog>
|
||||
<maintenance-suggested-setups-dialog
|
||||
.hass=${this.hass}
|
||||
@integration-setups-adopted=${(e: CustomEvent) => this._onSetupsAdopted(e)}
|
||||
></maintenance-suggested-setups-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}
|
||||
${this._toastUndo ? html`<button class="toast-undo" @click=${() => this._runToastUndo()}>${this._toastActionLabel || t("undo", this._lang)}</button>` : nothing}
|
||||
</div>` : nothing}
|
||||
${this._renderPalette()}
|
||||
${this._renderTemplateGallery()}
|
||||
@@ -2046,10 +2133,39 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
(n, o) => n + o.tasks.filter((tk) => tk.archived).length, 0,
|
||||
);
|
||||
|
||||
// Filters actively narrowing the list — shown on the collapsed toggle so
|
||||
// "why is my list short?" has a visible answer even with filters hidden.
|
||||
const activeFilterCount =
|
||||
(this._filterStatus ? 1 : 0) +
|
||||
(this._filterUser ? 1 : 0) +
|
||||
(this._filterLabel ? 1 : 0) +
|
||||
(this._activeViewId ? 1 : 0);
|
||||
|
||||
return html`
|
||||
${this._features.budget ? this._renderBudgetBar() : nothing}
|
||||
|
||||
<div class="filter-bar">
|
||||
${this.narrow ? html`
|
||||
<div class="mobile-controls">
|
||||
<ha-button
|
||||
class="mobile-toggle ${this._filtersOpen ? "active" : ""}"
|
||||
@click=${() => { this._filtersOpen = !this._filtersOpen; }}
|
||||
>
|
||||
<ha-icon icon="mdi:filter-variant"></ha-icon>
|
||||
${t("filter_label", L)}${activeFilterCount > 0 ? ` (${activeFilterCount})` : ""}
|
||||
</ha-button>
|
||||
${!isOperator ? html`
|
||||
<ha-button
|
||||
class="mobile-toggle ${this._actionsMenuOpen ? "active" : ""}"
|
||||
@click=${() => { this._actionsMenuOpen = !this._actionsMenuOpen; }}
|
||||
>
|
||||
<ha-icon icon="mdi:plus"></ha-icon>
|
||||
${t("add", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
|
||||
<div class="filter-bar ${this.narrow && !this._filtersOpen ? "collapsed" : ""}">
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${t("views_label", L)}</span>
|
||||
<select
|
||||
@@ -2169,7 +2285,18 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${this._bulkMode ? t("cancel", L) : t("bulk_select", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
${!isOperator ? html`
|
||||
</div>
|
||||
|
||||
${!isOperator ? html`
|
||||
<div
|
||||
class="actions-bar ${this.narrow && !this._actionsMenuOpen ? "collapsed" : ""}"
|
||||
@click=${() => { if (this.narrow) this._actionsMenuOpen = false; }}
|
||||
>
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog")?.openCreate("", this._objects)}
|
||||
>
|
||||
${t("new_task", L)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog")?.openCreate()}
|
||||
>
|
||||
@@ -2181,13 +2308,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<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)}
|
||||
>
|
||||
${t("new_task", L)}
|
||||
<ha-button @click=${() => this._openSuggestedSetups()}>
|
||||
<ha-icon icon="mdi:auto-fix"></ha-icon> ${t("setups_button", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
</div>
|
||||
${this._batteryFleetSetupAvailable ? html`
|
||||
<ha-button @click=${() => this._setupBatteryFleet()}>
|
||||
<ha-icon icon="mdi:battery-sync"></ha-icon> ${t("battery_fleet_setup_button", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
|
||||
${rows.length === 0
|
||||
? html`
|
||||
@@ -2731,7 +2861,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const bars: { label: string; spent: number; budget: number }[] = [];
|
||||
if (b.monthly_budget > 0) bars.push({ label: t("budget_monthly", L), spent: b.monthly_spent, budget: b.monthly_budget });
|
||||
if (b.yearly_budget > 0) bars.push({ label: t("budget_yearly", L), spent: b.yearly_spent, budget: b.yearly_budget });
|
||||
if (bars.length === 0) return nothing;
|
||||
// #104: budget tracking enabled WITHOUT a maximum — the spent totals were
|
||||
// invisible (a bar needs a denominator). Show plain spent lines instead,
|
||||
// so "what did I spend" always has a dashboard answer.
|
||||
const spentOnly: { label: string; spent: number }[] = [];
|
||||
if (!(b.monthly_budget > 0)) spentOnly.push({ label: t("budget_monthly", L), spent: b.monthly_spent || 0 });
|
||||
if (!(b.yearly_budget > 0)) spentOnly.push({ label: t("budget_yearly", L), spent: b.yearly_spent || 0 });
|
||||
|
||||
return html`
|
||||
<div class="budget-bars">
|
||||
@@ -2750,6 +2885,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${spentOnly.map(
|
||||
(s) => html`
|
||||
<div class="budget-item budget-spent-only">
|
||||
<div class="budget-label">
|
||||
<span>${s.label}</span>
|
||||
<span>${s.spent.toFixed(2)} ${cs}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user