184 files
This commit is contained in:
1 parent
7b47edbf8d
commit
0c64e47969
184 files changed
+9608
-11572
No files matched your search
@@ -75,7 +75,7 @@ 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";
|
||||
import { renderTriggerProgress, renderMiniSparkline, computeTrend } from "./renderers/progress";
|
||||
import { type HistoryContext } from "./renderers/history";
|
||||
import { renderUserBadge, type TaskDetailContext } from "./renderers/task-detail";
|
||||
// The task-detail sub-view as a web component (light-DOM, panel styles apply).
|
||||
@@ -112,7 +112,7 @@ 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";
|
||||
type GroupByMode = "none" | "area" | "group" | "user" | "object";
|
||||
|
||||
// Chart dimension constants for mini sparklines (overview)
|
||||
|
||||
@@ -125,6 +125,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
* The wide row grid then cannot hold labelled action buttons, so the
|
||||
* compact (icon-only) form applies here too. Reflected for the CSS. */
|
||||
@property({ type: Boolean, reflect: true }) public tight = false;
|
||||
/** ≥1500px panel width (2026-09-01): master-detail split — the dashboard
|
||||
* keeps the task list on the left and docks the task detail on the right
|
||||
* (a mail client's preview pane). Reflected for the CSS. */
|
||||
@property({ type: Boolean, reflect: true }) public split = false;
|
||||
private _tightObserver: ResizeObserver | null = null;
|
||||
@property({ attribute: false }) public panel: Record<string, unknown> = {};
|
||||
|
||||
@@ -340,6 +344,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// fragments. Below ~1000 the narrow layout is the honest choice;
|
||||
// from there up the chips track keeps ≥150 px of slack.
|
||||
if (w > 0) this.tight = w < 1000;
|
||||
if (w > 0) this.split = w >= 1500;
|
||||
});
|
||||
this._tightObserver.observe(this);
|
||||
}
|
||||
@@ -358,7 +363,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._objectSortMode = savedObj as ObjectSortMode;
|
||||
}
|
||||
const savedGroup = lsGet(LS_KEYS.groupBy);
|
||||
if (savedGroup && ["none", "area", "group", "user"].includes(savedGroup)) {
|
||||
if (savedGroup && ["none", "area", "group", "user", "object"].includes(savedGroup)) {
|
||||
this._groupByMode = savedGroup as GroupByMode;
|
||||
}
|
||||
const savedView = lsGet(LS_KEYS.objectView);
|
||||
@@ -822,6 +827,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
entry_id: obj.entry_id,
|
||||
task_id: task.id,
|
||||
object_name: obj.object.name,
|
||||
allow_skip: task.allow_skip !== false,
|
||||
task_name: task.name,
|
||||
type: task.type,
|
||||
schedule_type: task.schedule_type,
|
||||
@@ -1023,7 +1029,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
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)) {
|
||||
if (["none", "area", "group", "user", "object"].includes(f.group_by)) {
|
||||
this._groupByMode = f.group_by as GroupByMode;
|
||||
}
|
||||
// Persist sort/group like the manual controls do, so they stick after reload.
|
||||
@@ -1077,7 +1083,28 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._scrollContentToTop();
|
||||
}
|
||||
|
||||
/** Master-detail split is active: wide panel, dashboard tab, no bulk. */
|
||||
private _splitActive(): boolean {
|
||||
return this.split && !this.narrow && !this.tight && this._view === "overview" && this._overviewTab === "dashboard" && !this._bulkMode;
|
||||
}
|
||||
|
||||
private _showTask(entryId: string, taskId: string): void {
|
||||
// Master-detail split (2026-09-01): same data prep, but no view switch
|
||||
// and no history entry — the detail docks on the right like a mail
|
||||
// client's preview pane. The detail's breadcrumb (showTaskView) still
|
||||
// opens the full page.
|
||||
if (this._splitActive()) {
|
||||
this._selectedEntryId = entryId;
|
||||
this._selectedTaskId = taskId;
|
||||
this._activeTab = "overview";
|
||||
this._historyFilter = null;
|
||||
this._fetchFullHistory(entryId, taskId);
|
||||
const task = this._getTask(entryId, taskId);
|
||||
if (task?.trigger_config?.entity_id) {
|
||||
this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config));
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._pushPanelState("task", entryId, taskId);
|
||||
this._view = "task";
|
||||
this._selectedEntryId = entryId;
|
||||
@@ -2496,6 +2523,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<option value="area" ?selected=${this._groupByMode === "area"}>${t("groupby_area", L)}</option>
|
||||
${this._features.groups ? html`<option value="group" ?selected=${this._groupByMode === "group"}>${t("groupby_group", L)}</option>` : nothing}
|
||||
<option value="user" ?selected=${this._groupByMode === "user"}>${t("groupby_user", L)}</option>
|
||||
<option value="object" ?selected=${this._groupByMode === "object"}>${t("groupby_object", L)}</option>
|
||||
</select>
|
||||
</label>
|
||||
${archivedCount > 0 ? html`
|
||||
@@ -2541,7 +2569,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
`
|
||||
: html`
|
||||
${this._bulkMode ? this._renderBulkBar(rows, L) : nothing}
|
||||
${this._groupByMode === "none"
|
||||
${this._splitActive()
|
||||
? html`
|
||||
<div class="split-layout">
|
||||
<div class="split-list">
|
||||
${this._groupByMode === "none" ? this._renderTaskTable(rows) : this._renderGroupedTasks(rows, L)}
|
||||
</div>
|
||||
<div class="split-pane">
|
||||
${this._selectedEntryId && this._selectedTaskId && this._getTask(this._selectedEntryId, this._selectedTaskId)
|
||||
? this._renderTaskDetail()
|
||||
: html`<div class="split-pane-empty"><ha-icon icon="mdi:cursor-default-click-outline"></ha-icon><p>${t("split_select_hint", L)}</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: this._groupByMode === "none"
|
||||
? this._renderTaskTable(rows)
|
||||
: this._renderGroupedTasks(rows, L)}
|
||||
`}
|
||||
@@ -2661,6 +2702,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
keys = [u || noneLabel];
|
||||
} else if (this._groupByMode === "group") {
|
||||
keys = row.group_names.length > 0 ? row.group_names : [noneLabel];
|
||||
} else if (this._groupByMode === "object") {
|
||||
// #150(3): group the dashboard by the owning object/device.
|
||||
keys = [row.object_name];
|
||||
}
|
||||
for (const k of keys) {
|
||||
if (!groups.has(k)) groups.set(k, []);
|
||||
@@ -2675,6 +2719,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
});
|
||||
const icon = this._groupByMode === "area" ? "mdi:map-marker-outline"
|
||||
: this._groupByMode === "group" ? "mdi:folder-outline"
|
||||
: this._groupByMode === "object" ? "mdi:cube-outline"
|
||||
: "mdi:account-outline";
|
||||
return html`
|
||||
${sorted.map(([key, taskRows]) => html`
|
||||
@@ -3220,7 +3265,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
const bulkSelected = this._bulkMode && this._bulkSelected.has(this._bulkKey(row));
|
||||
return html`
|
||||
<div class="task-row${!row.enabled ? ' task-disabled' : ''}${bulkSelected ? ' bulk-selected' : ''}">
|
||||
<div class="task-row${!row.enabled ? ' task-disabled' : ''}${bulkSelected ? ' bulk-selected' : ''}${this._splitActive() && row.entry_id === this._selectedEntryId && row.task_id === this._selectedTaskId ? ' selected' : ''}">
|
||||
${this._bulkMode ? html`
|
||||
<label class="cell bulk-check" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<input type="checkbox" .checked=${bulkSelected} @change=${() => this._toggleBulkRow(row)} />
|
||||
@@ -3260,13 +3305,13 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
? html`<div class="days-bar"><div class="days-bar-fill${daysOverflow ? " overflow" : ""}" style="width:${pct}%;background:${barColor}"></div></div>`
|
||||
: nothing}
|
||||
${row.trigger_config
|
||||
? renderTriggerProgress(row)
|
||||
? renderTriggerProgress(row, { trend: computeTrend(row, this._miniStatsData), lang: L })
|
||||
: !hasDaysBar && row.trigger_active
|
||||
? html`<span style="color:var(--maint-triggered-color);font-weight:600">⚡</span>`
|
||||
: nothing}
|
||||
${renderMiniSparkline(row, this._miniStatsData, this._lang)}
|
||||
</span>
|
||||
${this._renderRowActions(L, () => this._openCompleteDialogForRow(row), () => this._promptSkipTask(row.entry_id, row.task_id))}
|
||||
${this._renderRowActions(L, () => this._openCompleteDialogForRow(row), () => this._promptSkipTask(row.entry_id, row.task_id), row.allow_skip)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -3296,7 +3341,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
/** #145: row actions as HA buttons — labelled (accent/success + plain, 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): 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);
|
||||
@@ -3308,9 +3353,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<ha-button size="small" appearance="accent" variant="success" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
</ha-button>
|
||||
<ha-icon-button .label=${t("skip", L)} title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
|
||||
<ha-icon icon="mdi:skip-next"></ha-icon>
|
||||
</ha-icon-button>
|
||||
${allowSkip ? html`
|
||||
<ha-icon-button .label=${t("skip", L)} title="${t("skip", L)}" .disabled=${this._actionLoading} @click=${(e: Event) => { e.stopPropagation(); onSkip(); }}>
|
||||
<ha-icon icon="mdi:skip-next"></ha-icon>
|
||||
</ha-icon-button>` : nothing}
|
||||
</span>`;
|
||||
}
|
||||
return html`
|
||||
@@ -3318,9 +3364,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<ha-button size="small" appearance="accent" variant="success" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
|
||||
<ha-icon slot="start" icon="mdi:check"></ha-icon>${t("complete", L)}
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="plain" variant="neutral" 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>
|
||||
${allowSkip ? html`
|
||||
<ha-button size="small" appearance="outlined" variant="neutral" 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>`;
|
||||
}
|
||||
return html`
|
||||
@@ -3328,9 +3375,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<mwc-icon-button class="btn-complete" title="${t("complete", L)}" @click=${(e: Event) => { e.stopPropagation(); onComplete(); }}>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
</mwc-icon-button>
|
||||
<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>
|
||||
${allowSkip ? html`
|
||||
<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>`;
|
||||
}
|
||||
|
||||
@@ -3470,7 +3518,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<span class="due-cell" @click=${() => this._showTask(obj.entry_id, task.id)}>
|
||||
<span class="due-text">${formatDueDays(task.days_until_due, L)}</span>
|
||||
${task.trigger_config
|
||||
? renderTriggerProgress(task)
|
||||
? renderTriggerProgress(task, { trend: computeTrend(task, this._miniStatsData), lang: L })
|
||||
: nothing}
|
||||
${renderMiniSparkline(task, this._miniStatsData, this._lang)}
|
||||
</span>
|
||||
@@ -3478,6 +3526,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
L,
|
||||
() => this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled),
|
||||
() => this._promptSkipTask(obj.entry_id, task.id),
|
||||
task.allow_skip,
|
||||
)}
|
||||
</div>
|
||||
`)}</div>`}
|
||||
|
||||
Reference in new issue
Block a user