Updated apps

This commit is contained in:
2026-07-20 22:52:35 -04:00
parent 28a8cb98f6
commit a0c3271743
1164 changed files with 94781 additions and 6892 deletions
@@ -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>
`;
}