348 files
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+23
-16
@@ -5,8 +5,8 @@
|
||||
* behaviour the extraction must preserve:
|
||||
* - header renders name / object breadcrumb / status chip / actions
|
||||
* - Complete / Skip route to the panel callbacks with the task
|
||||
* - operator mode hides archive + the more-menu (read-only surface)
|
||||
* - the more-menu items (edit/duplicate/reset/snooze/delete) fire callbacks
|
||||
* - operator mode: only Complete/Skip; the more-menu carries just QR + worksheet
|
||||
* - the more-menu items (edit/qr/worksheet/duplicate/…/archive/delete) fire callbacks
|
||||
* - tab bar switches via setActiveTab; history tab renders the timeline
|
||||
* - KPI bar shows warning days + currency; user badge resolves names
|
||||
*/
|
||||
@@ -154,16 +154,18 @@ describe("task-detail renderer", () => {
|
||||
expect(skipped).to.equal(1);
|
||||
});
|
||||
|
||||
it("operator mode hides archive button and the more-menu", () => {
|
||||
const host = mount(task(), ctx({ isOperator: true }));
|
||||
expect(host.querySelector(".more-menu-wrapper")).to.be.null;
|
||||
// Only Complete / Skip / QR remain.
|
||||
const labels = [...host.querySelectorAll(".task-header-actions ha-button")]
|
||||
.map((b) => b.textContent || "");
|
||||
expect(labels.some((l) => /archive/i.test(l))).to.be.false;
|
||||
it("operator mode: only Complete/Skip buttons; menu carries just QR + worksheet", () => {
|
||||
const host = mount(task(), ctx({ isOperator: true, moreMenuOpen: true }));
|
||||
// The ⋮ stays (QR + worksheet moved into it), but write actions don't.
|
||||
expect(host.querySelector(".more-menu-wrapper"), "menu present").to.exist;
|
||||
const buttons = [...host.querySelectorAll(".task-header-actions ha-button")];
|
||||
expect(buttons.length).to.equal(2); // Complete + Skip only
|
||||
const items = [...host.querySelectorAll(".popup-menu-item")].map((i) => i.textContent?.trim() || "");
|
||||
expect(items.length).to.equal(2);
|
||||
expect(items.join(" ")).to.not.match(/edit|archive|delete|duplicate|reset/i);
|
||||
});
|
||||
|
||||
it("open more-menu lists edit/duplicate/reset/postpone/snooze/worksheet/delete and fires callbacks", () => {
|
||||
it("open more-menu lists edit/qr/worksheet/duplicate/reset/postpone/snooze/archive/delete and fires callbacks", () => {
|
||||
const calls: string[] = [];
|
||||
const host = mount(task(), ctx({
|
||||
moreMenuOpen: true,
|
||||
@@ -172,14 +174,19 @@ describe("task-detail renderer", () => {
|
||||
promptPostpone: () => calls.push("postpone"),
|
||||
snoozeTask: () => calls.push("snooze"),
|
||||
printWorksheet: () => calls.push("worksheet"),
|
||||
toggleArchive: () => calls.push("archive"),
|
||||
}));
|
||||
const items = [...host.querySelectorAll(".popup-menu-item")];
|
||||
expect(items.length).to.equal(7);
|
||||
(items[3] as HTMLElement).click(); // postpone
|
||||
(items[4] as HTMLElement).click(); // snooze
|
||||
(items[5] as HTMLElement).click(); // work sheet (v2.21)
|
||||
(items[6] as HTMLElement).click(); // delete (danger)
|
||||
expect(calls).to.deep.equal(["close", "postpone", "close", "snooze", "close", "worksheet", "close", "delete"]);
|
||||
// edit, qr, worksheet, duplicate, reset, postpone, snooze, archive, delete
|
||||
expect(items.length).to.equal(9);
|
||||
(items[5] as HTMLElement).click(); // postpone
|
||||
(items[6] as HTMLElement).click(); // snooze
|
||||
(items[2] as HTMLElement).click(); // work sheet (v2.21)
|
||||
(items[7] as HTMLElement).click(); // archive (demoted from the header)
|
||||
(items[8] as HTMLElement).click(); // delete (danger)
|
||||
expect(calls).to.deep.equal([
|
||||
"close", "postpone", "close", "snooze", "close", "worksheet", "close", "archive", "close", "delete",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows a postponed badge when the task has a due_override, and none otherwise", () => {
|
||||
|
||||
+7
-3
@@ -177,8 +177,12 @@ describe("maintenance-task-detail-view", () => {
|
||||
expect(el.querySelector(".detail-section")).to.equal(null);
|
||||
});
|
||||
|
||||
it("hides the more-menu for operators (read-only surface preserved)", async () => {
|
||||
const el = await mount(task(), ctx({ isOperator: true }));
|
||||
expect(el.querySelector(".more-menu-wrapper")).to.equal(null);
|
||||
it("operators get the more-menu with only the read-safe items (QR + worksheet)", async () => {
|
||||
const el = await mount(task(), ctx({ isOperator: true, moreMenuOpen: true }));
|
||||
expect(el.querySelector(".more-menu-wrapper"), "menu present for operators").to.exist;
|
||||
const labels = [...el.querySelectorAll(".popup-menu-item")].map((i) => i.textContent?.trim());
|
||||
expect(labels.length).to.equal(2);
|
||||
// No write actions leak into the operator menu.
|
||||
expect(labels.join(" ")).to.not.match(/edit|archive|delete|duplicate|reset/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@state() private _defaultWarningDays = 7;
|
||||
@state() private _actionLoading = false;
|
||||
@state() private _moreMenuOpen = false;
|
||||
@state() private _objMenuOpen = false;
|
||||
@state() private _toastMessage = "";
|
||||
@state() private _toastUndo: (() => void) | null = null;
|
||||
@state() private _toastActionLabel = "";
|
||||
@@ -3014,34 +3015,34 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<h2>${o.name}</h2>
|
||||
<div class="action-buttons">
|
||||
${!isOperator ? html`
|
||||
<ha-button appearance="plain" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog");
|
||||
dlg?.openEdit(obj.entry_id, o);
|
||||
}}>${t("edit", L)}</ha-button>
|
||||
<ha-button appearance="filled" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog");
|
||||
dlg?.openCreate(obj.entry_id);
|
||||
}}>${t("add_task", L)}</ha-button>
|
||||
<ha-button appearance="plain" .disabled=${this._actionLoading} @click=${() => this._duplicateObject(obj.entry_id)}>
|
||||
<ha-icon icon="mdi:content-copy"></ha-icon> ${t("duplicate", L)}
|
||||
</ha-button>
|
||||
<ha-button appearance="plain" @click=${() => this._toggleArchiveObject(obj.entry_id, !!o.archived)}>
|
||||
<ha-icon icon="${o.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
|
||||
${o.archived ? t("unarchive_object", L) : t("archive_object", L)}
|
||||
</ha-button>
|
||||
${!o.archived ? html`
|
||||
<ha-button appearance="plain" @click=${() => this._togglePauseObject(obj.entry_id, !!o.paused)}>
|
||||
<ha-icon icon="${o.paused ? 'mdi:play-circle-outline' : 'mdi:pause-circle-outline'}"></ha-icon>
|
||||
${o.paused ? t("resume_object", L) : t("pause_object", L)}
|
||||
</ha-button>
|
||||
<ha-button appearance="plain" .disabled=${this._actionLoading} @click=${() => this._replaceObject(obj.entry_id, o.name)}>
|
||||
<ha-icon icon="mdi:swap-horizontal"></ha-icon> ${t("replace_object", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
<ha-button variant="danger" appearance="plain" @click=${() => this._deleteObject(obj.entry_id)}>${t("delete", L)}</ha-button>
|
||||
<ha-button appearance="plain" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog");
|
||||
dlg?.openEdit(obj.entry_id, o);
|
||||
}}>${t("edit", L)}</ha-button>
|
||||
` : nothing}
|
||||
<ha-button appearance="plain" @click=${() => this._openQrForObject(obj.entry_id, o.name)}><ha-icon icon="mdi:qrcode"></ha-icon> ${t("qr_code", L)}</ha-button>
|
||||
<ha-button appearance="plain" @click=${() => this._printObjectReport(obj.entry_id)}><ha-icon icon="mdi:file-document-outline"></ha-icon> ${t("report_button", L)}</ha-button>
|
||||
<div class="more-menu-wrapper">
|
||||
<ha-icon-button .disabled=${this._actionLoading} .path=${"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"} @click=${() => this._toggleObjMenu()}></ha-icon-button>
|
||||
${this._objMenuOpen ? html`
|
||||
<div class="popup-menu" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._openQrForObject(obj.entry_id, o.name); }}>${t("qr_code", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._printObjectReport(obj.entry_id); }}>${t("report_button", L)}</div>
|
||||
${!isOperator ? html`
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._duplicateObject(obj.entry_id); }}>${t("duplicate", L)}</div>
|
||||
${!o.archived ? html`
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._togglePauseObject(obj.entry_id, !!o.paused); }}>${o.paused ? t("resume_object", L) : t("pause_object", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._replaceObject(obj.entry_id, o.name); }}>${t("replace_object", L)}</div>
|
||||
` : nothing}
|
||||
<div class="popup-menu-item" @click=${() => { this._closeObjMenu(); this._toggleArchiveObject(obj.entry_id, !!o.archived); }}>${o.archived ? t("unarchive_object", L) : t("archive_object", L)}</div>
|
||||
<div class="popup-menu-divider"></div>
|
||||
<div class="popup-menu-item danger" @click=${() => { this._closeObjMenu(); this._deleteObject(obj.entry_id); }}>${t("delete", L)}</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${o.paused
|
||||
@@ -3141,6 +3142,21 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
/**
|
||||
* Render compact task header with status chip and action buttons.
|
||||
*/
|
||||
// Object-detail ⋮ menu — same click-away pattern as the task-detail menu.
|
||||
private _toggleObjMenu(): void {
|
||||
this._objMenuOpen = !this._objMenuOpen;
|
||||
if (this._objMenuOpen) {
|
||||
setTimeout(() => {
|
||||
const handler = () => { this._objMenuOpen = false; document.removeEventListener("click", handler); };
|
||||
document.addEventListener("click", handler);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private _closeObjMenu(): void {
|
||||
this._objMenuOpen = false;
|
||||
}
|
||||
|
||||
private _toggleMoreMenu(): void {
|
||||
this._moreMenuOpen = !this._moreMenuOpen;
|
||||
if (this._moreMenuOpen) {
|
||||
|
||||
@@ -121,30 +121,27 @@ function renderTaskHeader(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
<div class="task-header-actions">
|
||||
<ha-button appearance="filled" @click=${() => ctx.openComplete(task)}>${t("complete", L)}</ha-button>
|
||||
<ha-button appearance="plain" .disabled=${ctx.actionLoading} @click=${() => ctx.promptSkip()}>${t("skip", L)}</ha-button>
|
||||
${!isOperator ? html`
|
||||
<ha-button appearance="plain" @click=${() => ctx.toggleArchive(!!task.archived)}>
|
||||
<ha-icon icon="${task.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
|
||||
${task.archived ? t("unarchive", L) : t("archive", L)}
|
||||
</ha-button>
|
||||
` : nothing}
|
||||
<ha-button appearance="plain" @click=${() => ctx.openQr(task.name)}><ha-icon icon="mdi:qrcode"></ha-icon> ${t("qr_code", L)}</ha-button>
|
||||
${!isOperator ? html`
|
||||
<div class="more-menu-wrapper">
|
||||
<ha-icon-button .disabled=${ctx.actionLoading} .path=${"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"} @click=${() => ctx.toggleMoreMenu()}></ha-icon-button>
|
||||
${ctx.moreMenuOpen ? html`
|
||||
<div class="popup-menu" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="more-menu-wrapper">
|
||||
<ha-icon-button .disabled=${ctx.actionLoading} .path=${"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"} @click=${() => ctx.toggleMoreMenu()}></ha-icon-button>
|
||||
${ctx.moreMenuOpen ? html`
|
||||
<div class="popup-menu" @click=${(e: Event) => e.stopPropagation()}>
|
||||
${!isOperator ? html`
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.openEdit(task); }}>${t("edit", L)}</div>
|
||||
` : nothing}
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.openQr(task.name); }}>${t("qr_code", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.printWorksheet(); }}>${t("worksheet", L)}</div>
|
||||
${!isOperator ? html`
|
||||
<div class="popup-menu-item" @click=${() => ctx.duplicateTask()}>${t("duplicate", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.promptReset(); }}>${t("reset", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.promptPostpone(); }}>${t("postpone", L)}…</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.snoozeTask(); }}>${t("snooze", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.printWorksheet(); }}>${t("worksheet", L)}</div>
|
||||
<div class="popup-menu-item" @click=${() => { ctx.closeMoreMenu(); ctx.toggleArchive(!!task.archived); }}>${task.archived ? t("unarchive", L) : t("archive", L)}</div>
|
||||
<div class="popup-menu-divider"></div>
|
||||
<div class="popup-menu-item danger" @click=${() => { ctx.closeMoreMenu(); ctx.deleteTask(); }}>${t("delete", L)}</div>
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -373,10 +370,13 @@ export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext)
|
||||
|
||||
return html`
|
||||
<div class="tab-content overview-tab">
|
||||
${task.battery_fleet_task
|
||||
? html`<maintenance-battery-fleet-section .hass=${ctx.hass}></maintenance-battery-fleet-section>`
|
||||
: nothing}
|
||||
${renderKPIBar(task, ctx)}
|
||||
${renderTaskMeta(task, ctx)}
|
||||
${task.battery_fleet_task
|
||||
? html`<maintenance-battery-fleet-section .hass=${ctx.hass}></maintenance-battery-fleet-section>`
|
||||
? nothing
|
||||
: html`
|
||||
${renderDaysProgress(task, ctx.lang)}
|
||||
${renderTriggerSection(task, ctx.sparkline)}
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -22,5 +22,5 @@
|
||||
"requirements": [
|
||||
"pypdf>=4.3.0"
|
||||
],
|
||||
"version": "2.38.3"
|
||||
"version": "2.39.0"
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user