From 4e0539a155172ed9e8cc74e280a5d2c3484c9639 Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Thu, 13 Aug 2026 23:38:20 +0000 Subject: [PATCH] 108 files --- .ha_run.lock | 2 +- .storage/lovelace_resources | 50 +- .../maintenance_supporter/coordinator.py | 32 +- .../maintenance_supporter/export.py | 4 + .../components/history-edit-dialog.ts | 136 + .../components/task-quick-actions-dialog.ts | 1 + .../frontend-src/locales/cs.json | 4 +- .../frontend-src/locales/da.json | 4 +- .../frontend-src/locales/de.json | 4 +- .../frontend-src/locales/en.json | 4 +- .../frontend-src/locales/es.json | 4 +- .../frontend-src/locales/fi.json | 4 +- .../frontend-src/locales/fr.json | 4 +- .../frontend-src/locales/hi.json | 4 +- .../frontend-src/locales/hu.json | 4 +- .../frontend-src/locales/it.json | 4 +- .../frontend-src/locales/ja.json | 4 +- .../frontend-src/locales/ko.json | 4 +- .../frontend-src/locales/nb.json | 4 +- .../frontend-src/locales/nl.json | 4 +- .../frontend-src/locales/pl.json | 4 +- .../frontend-src/locales/pt-br.json | 4 +- .../frontend-src/locales/pt.json | 4 +- .../frontend-src/locales/ru.json | 4 +- .../frontend-src/locales/sv.json | 4 +- .../frontend-src/locales/tr.json | 4 +- .../frontend-src/locales/uk.json | 4 +- .../frontend-src/locales/zh.json | 4 +- .../maintenance-dashboard-strategy.ts | 1 + .../frontend-src/maintenance-panel.ts | 157 +- .../frontend-src/panel-styles.ts | 37 + .../frontend-src/types.ts | 2 + .../frontend/locales/cs.json | 4 +- .../frontend/locales/da.json | 4 +- .../frontend/locales/de.json | 4 +- .../frontend/locales/en.json | 4 +- .../frontend/locales/es.json | 4 +- .../frontend/locales/fi.json | 4 +- .../frontend/locales/fr.json | 4 +- .../frontend/locales/hi.json | 4 +- .../frontend/locales/hu.json | 4 +- .../frontend/locales/it.json | 4 +- .../frontend/locales/ja.json | 4 +- .../frontend/locales/ko.json | 4 +- .../frontend/locales/nb.json | 4 +- .../frontend/locales/nl.json | 4 +- .../frontend/locales/pl.json | 4 +- .../frontend/locales/pt-br.json | 4 +- .../frontend/locales/pt.json | 4 +- .../frontend/locales/ru.json | 4 +- .../frontend/locales/sv.json | 4 +- .../frontend/locales/tr.json | 4 +- .../frontend/locales/uk.json | 4 +- .../frontend/locales/zh.json | 4 +- .../frontend/maintenance-calendar-card.js | 4 +- .../frontend/maintenance-card.js | 449 +-- .../frontend/maintenance-panel.js | 1284 +++++---- .../frontend/maintenance-strategy-shim.js | 4 +- ... adopt-problem-sensors-dialog-CHO2L6HX.js} | 4 +- .../adopt-problem-sensors-dialog-UBDFZUSU.js | 222 -- .../frontend/panel-chunks/chunk-4HD7ODUX.js | 2 - .../frontend/panel-chunks/chunk-6RMRSFSY.js | 2 - .../{chunk-SD6IEJBA.js => chunk-AIMCOREG.js} | 4 +- .../{chunk-I7J3AORE.js => chunk-GKQ6LXK5.js} | 2 +- .../frontend/panel-chunks/chunk-NGMG4DEY.js | 2 - .../frontend/panel-chunks/chunk-NHOMCQWL.js | 1201 -------- .../{chunk-LJXSDCLS.js => chunk-NU5DR7VT.js} | 4 +- .../{chunk-7QFSK25W.js => chunk-RCMO6YCL.js} | 2 +- .../{chunk-DV4UHMJC.js => chunk-T5QK5YQR.js} | 2 +- .../{chunk-C5W5B43R.js => chunk-TSHS2WDI.js} | 4 +- .../{chunk-BDAGEP22.js => chunk-VNISEOIC.js} | 4 +- .../frontend/panel-chunks/chunk-XYTY2SBA.js | 2 - .../frontend/panel-chunks/chunk-ZIQ7JY7R.js | 2 - .../frontend/panel-chunks/chunk-ZK3W7TF6.js | 54 - .../panel-chunks/complete-dialog-DYCBJTZV.js | 292 -- ...N3A2ME6.js => complete-dialog-F36PO7JG.js} | 4 +- ...-HNU4YSB4.js => object-dialog-2N2IPXHV.js} | 4 +- .../panel-chunks/object-dialog-YRX6AWLQ.js | 143 - ...alog-ONGEWKLY.js => qr-dialog-JCQF4P3S.js} | 4 +- .../panel-chunks/qr-dialog-QWUERDTM.js | 213 -- ...-2CBM3PKW.js => settings-view-GW5S7FSI.js} | 4 +- .../panel-chunks/settings-view-IR7NAWZ4.js | 1141 -------- .../suggested-setups-dialog-7XGBTDCC.js | 146 - ...js => suggested-setups-dialog-WJDDJQVZ.js} | 4 +- .../panel-chunks/task-dialog-FU3K4IJH.js | 983 ------- ...og-FGXLL4GU.js => task-dialog-GP2HET5N.js} | 4 +- .../chunks/budget-section-card-6U2VKRSH.js | 125 - ...VDV.js => budget-section-card-IGYCV4B2.js} | 4 +- .../{chunk-QCBC6YX3.js => chunk-6INP67SU.js} | 4 +- .../{chunk-TRW3CRLS.js => chunk-CWQRMTXS.js} | 4 +- .../{chunk-LPNFL3AF.js => chunk-HQWGP2HE.js} | 2 +- .../strategy/chunks/chunk-RNKC43AV.js | 1201 -------- .../strategy/chunks/chunk-UHANDS57.js | 2 - .../strategy/chunks/chunk-XWMJNIQI.js | 60 - ...t-W7QTL5QF.js => dialog-mount-NTYI3TJN.js} | 455 +-- .../strategy/chunks/dialog-mount-P5FL3UNR.js | 2453 ----------------- ...GRV.js => groups-section-card-R3BSNA5O.js} | 4 +- .../chunks/groups-section-card-SOUOTT4X.js | 137 - ...X.js => vacation-section-card-542QWPHZ.js} | 4 +- .../chunks/vacation-section-card-UIZAZBZW.js | 122 - .../maintenance-dashboard-strategy.js | 6 +- .../maintenance_supporter/manifest.json | 2 +- .../maintenance_supporter/parts_runtime.py | 88 + .../websocket/__init__.py | 2 + .../maintenance_supporter/websocket/io.py | 18 + .../maintenance_supporter/websocket/parts.py | 63 + .../websocket/tasks_history.py | 37 +- zigbee2mqtt/state.json | 34 +- 108 files changed, 1977 insertions(+), 9644 deletions(-) rename custom_components/maintenance_supporter/frontend/panel-chunks/{adopt-problem-sensors-dialog-2KGMOVVK.js => adopt-problem-sensors-dialog-CHO2L6HX.js} (97%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/adopt-problem-sensors-dialog-UBDFZUSU.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-4HD7ODUX.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-6RMRSFSY.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-SD6IEJBA.js => chunk-AIMCOREG.js} (90%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-I7J3AORE.js => chunk-GKQ6LXK5.js} (96%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-NGMG4DEY.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-NHOMCQWL.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-LJXSDCLS.js => chunk-NU5DR7VT.js} (95%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-7QFSK25W.js => chunk-RCMO6YCL.js} (78%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-DV4UHMJC.js => chunk-T5QK5YQR.js} (96%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-C5W5B43R.js => chunk-TSHS2WDI.js} (60%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{chunk-BDAGEP22.js => chunk-VNISEOIC.js} (95%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-XYTY2SBA.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-ZIQ7JY7R.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/chunk-ZK3W7TF6.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/complete-dialog-DYCBJTZV.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{complete-dialog-AN3A2ME6.js => complete-dialog-F36PO7JG.js} (97%) rename custom_components/maintenance_supporter/frontend/panel-chunks/{object-dialog-HNU4YSB4.js => object-dialog-2N2IPXHV.js} (96%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/object-dialog-YRX6AWLQ.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{qr-dialog-ONGEWKLY.js => qr-dialog-JCQF4P3S.js} (98%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/qr-dialog-QWUERDTM.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{settings-view-2CBM3PKW.js => settings-view-GW5S7FSI.js} (99%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/settings-view-IR7NAWZ4.js delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/suggested-setups-dialog-7XGBTDCC.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{suggested-setups-dialog-2MBPABZN.js => suggested-setups-dialog-WJDDJQVZ.js} (98%) delete mode 100644 custom_components/maintenance_supporter/frontend/panel-chunks/task-dialog-FU3K4IJH.js rename custom_components/maintenance_supporter/frontend/panel-chunks/{task-dialog-FGXLL4GU.js => task-dialog-GP2HET5N.js} (99%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/budget-section-card-6U2VKRSH.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{budget-section-card-237GPVDV.js => budget-section-card-IGYCV4B2.js} (97%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-QCBC6YX3.js => chunk-6INP67SU.js} (95%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-TRW3CRLS.js => chunk-CWQRMTXS.js} (61%) rename custom_components/maintenance_supporter/frontend/strategy/chunks/{chunk-LPNFL3AF.js => chunk-HQWGP2HE.js} (94%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-RNKC43AV.js delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-UHANDS57.js delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/chunk-XWMJNIQI.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{dialog-mount-W7QTL5QF.js => dialog-mount-NTYI3TJN.js} (82%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/dialog-mount-P5FL3UNR.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{groups-section-card-HS2WQGRV.js => groups-section-card-R3BSNA5O.js} (97%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/groups-section-card-SOUOTT4X.js rename custom_components/maintenance_supporter/frontend/strategy/chunks/{vacation-section-card-GKIEFNUX.js => vacation-section-card-542QWPHZ.js} (97%) delete mode 100644 custom_components/maintenance_supporter/frontend/strategy/chunks/vacation-section-card-UIZAZBZW.js diff --git a/.ha_run.lock b/.ha_run.lock index 5d888d26..2d3e9720 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786576922.6768394} \ No newline at end of file +{"pid": 72, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786664044.1505892} \ No newline at end of file diff --git a/.storage/lovelace_resources b/.storage/lovelace_resources index ada39a00..351e88bb 100644 --- a/.storage/lovelace_resources +++ b/.storage/lovelace_resources @@ -81,117 +81,117 @@ }, { "id": "88b90f2b1d6b45f0b44bee786ee1a525", - "url": "/taskmate/taskmate-attr-resolver.js?v=5.1.0", + "url": "/taskmate/taskmate-attr-resolver.js?v=5.1.1", "type": "module" }, { "id": "0796c92931dd458e81dc9c2677248274", - "url": "/taskmate/taskmate-localize.js?v=5.1.0", + "url": "/taskmate/taskmate-localize.js?v=5.1.1", "type": "module" }, { "id": "d2e281920ac7409583eafff1f1db69bd", - "url": "/taskmate/taskmate-design.js?v=5.1.0", + "url": "/taskmate/taskmate-design.js?v=5.1.1", "type": "module" }, { "id": "9eae9632f0734480be5edd784b030673", - "url": "/taskmate/taskmate-badges-card.js?v=5.1.0", + "url": "/taskmate/taskmate-badges-card.js?v=5.1.1", "type": "module" }, { "id": "5df49cd8a4eb420497b5df22cb31362c", - "url": "/taskmate/taskmate-child-card.js?v=5.1.0", + "url": "/taskmate/taskmate-child-card.js?v=5.1.1", "type": "module" }, { "id": "b44c87d0d74f468cb96c5eabc340f9ee", - "url": "/taskmate/taskmate-rewards-card.js?v=5.1.0", + "url": "/taskmate/taskmate-rewards-card.js?v=5.1.1", "type": "module" }, { "id": "787a96f9c30548708165a48457c99feb", - "url": "/taskmate/taskmate-approvals-card.js?v=5.1.0", + "url": "/taskmate/taskmate-approvals-card.js?v=5.1.1", "type": "module" }, { "id": "84031597de424ad68e7c2bc66d06b72e", - "url": "/taskmate/taskmate-points-card.js?v=5.1.0", + "url": "/taskmate/taskmate-points-card.js?v=5.1.1", "type": "module" }, { "id": "02613225844746b699bfbfac9642eca2", - "url": "/taskmate/taskmate-reorder-card.js?v=5.1.0", + "url": "/taskmate/taskmate-reorder-card.js?v=5.1.1", "type": "module" }, { "id": "7a06a07e17ba4b31b712bc91b77360b4", - "url": "/taskmate/taskmate-overview-card.js?v=5.1.0", + "url": "/taskmate/taskmate-overview-card.js?v=5.1.1", "type": "module" }, { "id": "08b42b53b33b4fa88db932455cf8d5aa", - "url": "/taskmate/taskmate-activity-card.js?v=5.1.0", + "url": "/taskmate/taskmate-activity-card.js?v=5.1.1", "type": "module" }, { "id": "15907eff51784acdba88ff694b574509", - "url": "/taskmate/taskmate-streak-card.js?v=5.1.0", + "url": "/taskmate/taskmate-streak-card.js?v=5.1.1", "type": "module" }, { "id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39", - "url": "/taskmate/taskmate-weekly-card.js?v=5.1.0", + "url": "/taskmate/taskmate-weekly-card.js?v=5.1.1", "type": "module" }, { "id": "289ce5943f5a4972a593697db4c96ece", - "url": "/taskmate/taskmate-graph-card.js?v=5.1.0", + "url": "/taskmate/taskmate-graph-card.js?v=5.1.1", "type": "module" }, { "id": "d2904acee4c74a398dcc7b996c23e7e7", - "url": "/taskmate/taskmate-reward-progress-card.js?v=5.1.0", + "url": "/taskmate/taskmate-reward-progress-card.js?v=5.1.1", "type": "module" }, { "id": "46d378736fd54a26be253f2dfb27202e", - "url": "/taskmate/taskmate-leaderboard-card.js?v=5.1.0", + "url": "/taskmate/taskmate-leaderboard-card.js?v=5.1.1", "type": "module" }, { "id": "db11ae36457842fa826c7991e1d9c6f4", - "url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.1.0", + "url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.1.1", "type": "module" }, { "id": "ce85f301b59a412e9abfe5ffd51cb9a2", - "url": "/taskmate/taskmate-penalties-card.js?v=5.1.0", + "url": "/taskmate/taskmate-penalties-card.js?v=5.1.1", "type": "module" }, { "id": "1959e1629de2422daa8adaf87069c288", - "url": "/taskmate/taskmate-bonuses-card.js?v=5.1.0", + "url": "/taskmate/taskmate-bonuses-card.js?v=5.1.1", "type": "module" }, { "id": "2548524b269c4959bc7e0e967df943af", - "url": "/taskmate/taskmate-points-display-card.js?v=5.1.0", + "url": "/taskmate/taskmate-points-display-card.js?v=5.1.1", "type": "module" }, { "id": "36ada49368db4187a283fbbbef45454f", - "url": "/taskmate/taskmate-calendar-card.js?v=5.1.0", + "url": "/taskmate/taskmate-calendar-card.js?v=5.1.1", "type": "module" }, { "id": "67c32687acd2497b90030ef141f4b6db", - "url": "/taskmate/taskmate-photo-gallery-card.js?v=5.1.0", + "url": "/taskmate/taskmate-photo-gallery-card.js?v=5.1.1", "type": "module" }, { "id": "6f0c8348a91f4b74a9b846ab3f62621f", - "url": "/taskmate/taskmate-family-goal-card.js?v=5.1.0", + "url": "/taskmate/taskmate-family-goal-card.js?v=5.1.1", "type": "module" }, { @@ -281,7 +281,7 @@ }, { "id": "73dbd2de03de4530b451a55bfe6c46cf", - "url": "/taskmate/taskmate-routine-card.js?v=5.1.0", + "url": "/taskmate/taskmate-routine-card.js?v=5.1.1", "type": "module" }, { @@ -310,7 +310,7 @@ "type": "module" }, { - "id": "5ba9f58b81f54f7a947d2d74df45025c", + "id": "8b8ada495c23458095cf669821c07524", "url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1", "type": "module" } diff --git a/custom_components/maintenance_supporter/coordinator.py b/custom_components/maintenance_supporter/coordinator.py index 3702a37f..a10b79e2 100644 --- a/custom_components/maintenance_supporter/coordinator.py +++ b/custom_components/maintenance_supporter/coordinator.py @@ -1025,17 +1025,39 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]): actual_interval = None # #99: enrich the per-completion parts selection with names so the - # history entry is readable without a part-id lookup. + # history entry is readable without a part-id lookup. Since #130 the + # AUTOMATIC path records too: with no explicit selection, the task's + # consumes_parts links — exactly what async_handle_completion_parts + # will consume below — go on the record, so completions from the + # no-dialog surfaces (service, button, QR, to-do, voice, recovery) + # stay correctable through the history editor like dialog ones. + record_links = ( + used_parts if used_parts is not None else (merged[task_id].get("consumes_parts") or []) + ) enriched_used: list[dict[str, Any]] | None = None - if used_parts is not None: - parts_catalog = self.entry.data.get("parts") or {} + if used_parts is not None or record_links: + own_catalog = self.entry.data.get("parts") or {} + + def _part_name(link: dict[str, Any]) -> str: + owner_id = link.get("entry_id") + catalog: dict[str, Any] = own_catalog + if owner_id and owner_id != self.entry.entry_id: + owner = self.hass.config_entries.async_get_entry(owner_id) + catalog = (owner.data.get("parts") or {}) if owner else {} + name = (catalog.get(link["part_id"]) or {}).get("name") + return str(name) if name else str(link["part_id"]) + enriched_used = [ { "part_id": link["part_id"], - "name": (parts_catalog.get(link["part_id"]) or {}).get("name") or link["part_id"], + "name": _part_name(link), "quantity": link.get("quantity", 1), + # #130: keep the pool owner on the record so a later + # history edit can resolve the part without guessing. + **({"entry_id": link["entry_id"]} if link.get("entry_id") else {}), } - for link in used_parts + for link in record_links + if isinstance(link, dict) and link.get("part_id") ] task.complete( diff --git a/custom_components/maintenance_supporter/export.py b/custom_components/maintenance_supporter/export.py index bc308d29..46ccdf57 100644 --- a/custom_components/maintenance_supporter/export.py +++ b/custom_components/maintenance_supporter/export.py @@ -126,6 +126,10 @@ def _build_export_object( "on_complete_action": tdata.get("on_complete_action"), "quick_complete_defaults": tdata.get("quick_complete_defaults"), "assignee_pool": tdata.get("assignee_pool") or [], + # v2.44: demanded completion details — the import has mirrored this + # key from day one; the export builder lost it (found by the #130 + # export audit), so backups silently dropped the requirement. + "required_completion_fields": tdata.get("required_completion_fields"), "rotation_strategy": tdata.get("rotation_strategy"), "reading_unit": tdata.get("reading_unit"), # Spare parts: consumption links + the auto-buy-task marker. diff --git a/custom_components/maintenance_supporter/frontend-src/components/history-edit-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/history-edit-dialog.ts index fbfcdc2d..b0ec1ab8 100644 --- a/custom_components/maintenance_supporter/frontend-src/components/history-edit-dialog.ts +++ b/custom_components/maintenance_supporter/frontend-src/components/history-edit-dialog.ts @@ -23,6 +23,19 @@ export interface HistoryEntryDraft { cost: number | null; duration: number | null; completed_by: string | null; + // #130: the entry's recorded part consumption ({part_id, name, quantity, + // entry_id? for pooled parts}); absent/empty = nothing consumed. + used_parts?: Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null; +} + +/** One selectable part option in the edit dialog — the object's own parts + * plus pooled parts this task links to, fetched via parts/overview. */ +interface PartOption { + part_id: string; + name: string; + entry_id: string; // owning object + foreign: boolean; // pooled (#111) — carried into the saved link + object_name: string | null; } export class MaintenanceHistoryEditDialog extends LitElement { @@ -40,6 +53,12 @@ export class MaintenanceHistoryEditDialog extends LitElement { return this.hass?.language || "en"; } + // #130: selectable parts + the edited selection (part key -> quantity; + // 0/absent = not consumed). Key = `${entry_id}:${part_id}` (pool-safe). + @state() private _partOptions: PartOption[] | null = null; + @state() private _partQty: Record = {}; + private _partQtyOriginal = ""; + /** Open the dialog with the given history-entry data. The caller must * pass `original_timestamp` (the entry's current timestamp before edit) * so the backend can find the entry. */ @@ -48,6 +67,71 @@ export class MaintenanceHistoryEditDialog extends LitElement { this._originalSnapshot = { ...draft }; this._error = ""; this._open = true; + this._partOptions = null; + this._partQty = {}; + this._partQtyOriginal = ""; + void this._loadPartOptions(); + } + + /** The object's own parts + pooled parts this task draws on — from the + * instance-wide overview so pooled owners resolve without extra calls. */ + private async _loadPartOptions(): Promise { + const draft = this._draft; + if (!draft) return; + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/parts/overview", + }) as { + parts: Array<{ + part_id: string; name: string; entry_id: string; object_name: string | null; + consumers: Array<{ entry_id: string; task_id: string }>; + }>; + }; + const options: PartOption[] = []; + for (const row of result.parts || []) { + const own = row.entry_id === draft.entry_id; + const linked = row.consumers.some((c) => c.entry_id === draft.entry_id && c.task_id === draft.task_id); + if (!own && !linked) continue; + options.push({ + part_id: row.part_id, + name: row.name, + entry_id: row.entry_id, + foreign: !own, + object_name: row.object_name, + }); + } + // Recorded parts whose catalog entry vanished stay selectable so a + // correction can still zero them out. + for (const link of draft.used_parts || []) { + const owner = link.entry_id || draft.entry_id; + if (!options.some((o) => o.part_id === link.part_id && o.entry_id === owner)) { + options.push({ + part_id: link.part_id, + name: link.name || link.part_id, + entry_id: owner, + foreign: owner !== draft.entry_id, + object_name: null, + }); + } + } + const qty: Record = {}; + for (const link of draft.used_parts || []) { + qty[`${link.entry_id || draft.entry_id}:${link.part_id}`] = link.quantity ?? 1; + } + this._partOptions = options; + this._partQty = qty; + this._partQtyOriginal = this._partSelectionKey(); + } catch { + this._partOptions = []; // parts UI unavailable — the rest still edits + } + } + + private _partSelectionKey(): string { + return JSON.stringify( + Object.entries(this._partQty) + .filter(([, q]) => q > 0) + .sort(([a], [b]) => a.localeCompare(b)), + ); } public close(): void { @@ -92,6 +176,17 @@ export class MaintenanceHistoryEditDialog extends LitElement { if (this._draft.completed_by !== this._originalSnapshot.completed_by) { patch.completed_by = this._draft.completed_by; } + // #130: send the parts selection only when it actually changed — the + // backend reconciles stock by the delta, so a no-op must stay silent. + if (this._partOptions !== null && this._partSelectionKey() !== this._partQtyOriginal) { + patch.used_parts = (this._partOptions || []) + .filter((o) => (this._partQty[`${o.entry_id}:${o.part_id}`] || 0) > 0) + .map((o) => ({ + part_id: o.part_id, + quantity: this._partQty[`${o.entry_id}:${o.part_id}`], + ...(o.foreign ? { entry_id: o.entry_id } : {}), + })); + } // Nothing changed → close without WS call const changedKeys = Object.keys(patch).filter( (k) => !["type", "entry_id", "task_id", "original_timestamp"].includes(k), @@ -173,6 +268,33 @@ export class MaintenanceHistoryEditDialog extends LitElement { }} /> + ${this._partOptions && this._partOptions.length > 0 ? html` +
+ ${t("complete_parts_used", L)} + ${this._partOptions.map((o) => { + const key = `${o.entry_id}:${o.part_id}`; + const qty = this._partQty[key] || 0; + return html` + + `; + })} +
+ ` : nothing} ${this._error ? html`
${this._error}
` : nothing}
`:u} + `:_}
✓ ${r("card_all_caught_up",e)}
@@ -4028,10 +4059,10 @@ ${h?`
${h}
`:""}
${r("card_no_tasks_title",e)}
${r("card_no_tasks_cta",e)} `:o` -
- ${c.map(({entry_id:_,object_name:v,task:f})=>o` +
+ ${c.map(({entry_id:u,object_name:v,task:f})=>o`
this._openTaskDetail(_,f.id)} + @click=${()=>this._openTaskDetail(u,f.id)} title="${r("open_task",e)||"Open task"}">
@@ -4041,18 +4072,18 @@ ${h?`
${h}
`:""} class="postponed-icon" icon="mdi:calendar-clock" title="${r("postponed",e)||"Postponed"}" - >`:u} + >`:_}
- ${d?this._assigneeName(f)?o`
+ ${l?this._assigneeName(f)?o`
${this._assigneeName(f)} -
`:u:o`
+
`:_:o`
${v} · ${r(f.type,e)}${this._assigneeName(f)?o` · ${this._assigneeName(f)}`:u} + >`:_}
`}
- ${this._docsFor(_,f).length?o`
- ${this._docsFor(_,f).map(y=>o` + ${this._docsFor(u,f).length?o`
+ ${this._docsFor(u,f).map(y=>o`
`:""} ${y.title} `)} -
`:u} +
`:_}
${f.days_until_due!==null&&f.days_until_due!==void 0?f.days_until_due<0?o`${at(f.days_until_due,e)}`:at(f.days_until_due,e):f.trigger_active?"\u26A1":"\u2014"}
@@ -4071,11 +4102,11 @@ ${h?`
${h}
`:""} {y.stopPropagation();let g=this.shadowRoot.querySelector("maintenance-complete-dialog");g.entryId=_,g.taskId=f.id,g.taskName=f.name,g.checklist=f.checklist||[],g.adaptiveEnabled=!!f.adaptive_config?.enabled,g.taskType=f.type||"",g.readingUnit=f.reading_unit||"",g.requiredFields=f.required_completion_fields||[],g.lang=e;let x=!!f.part_ref;g.parts=x?[]:ii(f,_,this._objects,e),g.consumesParts=x?[]:f.consumes_parts||[],g.open()}} + @click=${y=>{y.stopPropagation();let g=this.shadowRoot.querySelector("maintenance-complete-dialog");g.entryId=u,g.taskId=f.id,g.taskName=f.name,g.checklist=f.checklist||[],g.adaptiveEnabled=!!f.adaptive_config?.enabled,g.taskType=f.type||"",g.readingUnit=f.reading_unit||"",g.requiredFields=f.required_completion_fields||[],g.lang=e;let x=!!f.part_ref;g.parts=x?[]:ii(f,u,this._objects,e),g.consumesParts=x?[]:f.consumes_parts||[],g.open()}} > - `:u} + `:_}
`)}
@@ -4085,7 +4116,7 @@ ${h?`
${h}
`:""} .hass=${this.hass} @task-completed=${this._onCompleted} > - `}};M.styles=[qe,E` + `}};M.styles=[Oe,E` ha-card { overflow: hidden; } .card-header { @@ -4217,7 +4248,7 @@ ${h?`
${h}
`:""} --mdc-icon-size: 18px; color: var(--primary-color); } - `],l([b({attribute:!1})],M.prototype,"hass",2),l([p()],M.prototype,"_config",2),l([p()],M.prototype,"_objects",2),l([p()],M.prototype,"_stats",2),l([p()],M.prototype,"_unsub",2),l([p()],M.prototype,"_viewFilters",2),l([p()],M.prototype,"_userNames",2),l([p()],M.prototype,"_taskDocs",2);customElements.get("maintenance-supporter-card")||customElements.define("maintenance-supporter-card",M);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-supporter-card",name:"Maintenance Supporter",description:"Overview of your maintenance tasks with quick actions.",preview:!0});export{M as MaintenanceSupporterCard}; + `],d([b({attribute:!1})],M.prototype,"hass",2),d([h()],M.prototype,"_config",2),d([h()],M.prototype,"_objects",2),d([h()],M.prototype,"_stats",2),d([h()],M.prototype,"_unsub",2),d([h()],M.prototype,"_viewFilters",2),d([h()],M.prototype,"_userNames",2),d([h()],M.prototype,"_taskDocs",2);customElements.get("maintenance-supporter-card")||customElements.define("maintenance-supporter-card",M);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-supporter-card",name:"Maintenance Supporter",description:"Overview of your maintenance tasks with quick actions.",preview:!0});export{M as MaintenanceSupporterCard}; /*! Bundled license information: @lit/reactive-element/css-tag.js: diff --git a/custom_components/maintenance_supporter/frontend/maintenance-panel.js b/custom_components/maintenance_supporter/frontend/maintenance-panel.js index 3d63be45..16980129 100644 --- a/custom_components/maintenance_supporter/frontend/maintenance-panel.js +++ b/custom_components/maintenance_supporter/frontend/maintenance-panel.js @@ -1,16 +1,16 @@ -/*! maintenance_supporter frontend 2.56.0 */ -import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZK3W7TF6.js";import{b as Ut,c as re}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-SD6IEJBA.js";import{a as E}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-LJXSDCLS.js";import{a as se,b as ae,c as ne,d as oe,e as At}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-DV4UHMJC.js";import{a as le}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-I7J3AORE.js";import{a as g,b as D,c as o,d as P,f as d,g as C,h as ee,i as T,j as m,k as St,l as Mt,m as ut,n as s,o as Et,p as q,q as Dt,r as Y,s as Ct,t as gt,v as xt,w as mt,x as ie,y as Lt}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C5W5B43R.js";function it(n){return!!n&&/^https?:\/\//i.test(n)}var He=["assignee_pool","required_completion_fields","checklist","labels","history"],Fe=["checklist_progress"],Be=["tasks","parts"],Ve=["manual_docs","battery_fleet_excluded"];function Wt(n,r,t=[]){for(let e of r)n[e]===void 0&&(n[e]=[]);for(let e of t)n[e]===void 0&&(n[e]={})}function Ne(n){let r=n;Wt(r,Be),r.object&&typeof r.object=="object"&&Wt(r.object,Ve);for(let t of r.tasks)Wt(t,He,Fe);return n}function $t(n){for(let r of n)Ne(r);return n}function Ue(n,r){if(r.objects)return r.objects;let t=r.delta||[],e=r.removed||[];if(!t.length&&!e.length)return null;let i=new Map(n.map(a=>[a.entry_id,a]));for(let a of t)i.set(a.entry_id,a);for(let a of e)i.delete(a);return[...i.values()]}function ce(n,r){return r.objects&&$t(r.objects),r.delta&&$t(r.delta),Ue(n,r)}var Rt="2.56.0";function de(n,r=Rt){return!n||!r||r==="dev"?!1:n!==r}var F={overviewTab:"msp-overview-tab",collapsedSections:"msp-collapsed-sections",chartRange:"msp-chart-range",chartHideOutliers:"msp-chart-hide-outliers",taskSort:"maintenance_supporter_sort",objectSort:"maintenance_supporter_object_sort",groupBy:"maintenance_supporter_groupby",objectView:"maintenance_supporter_object_view",objectsCache:"msp-objects-cache",gettingStartedDismissed:"msp-gs-dismissed"};var We=168*3600*1e3;function pe(){try{let n=localStorage.getItem(F.objectsCache);if(!n)return null;let r=JSON.parse(n);return r.v!==Rt||!Number.isFinite(r.at)||Date.now()-r.at>We||!Array.isArray(r.objects)||r.objects.length===0?null:{objects:r.objects,stats:r.stats??null}}catch{return null}}function qt(n,r){if(!(!Array.isArray(n)||n.length===0))try{let t={v:Rt,at:Date.now(),objects:n,stats:r};localStorage.setItem(F.objectsCache,JSON.stringify(t))}catch{}}var qe={days:1,weeks:7,months:30.4368,years:365.25};function Yt(n,r){return!n||n<=0?0:n*(qe[r||"days"]??1)}function Ot(n,r,t){let e=Yt(n,t);if(e<=0||r==null)return{pct:0,overflow:!1};let i=(e-r)/e*100;return{pct:Math.max(0,Math.min(100,i)),overflow:i>100}}function A(n){return String(n??"").replace(/[&<>"']/g,r=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[r])}function he(n,r,t,e,i,a){let l=[[t.manufacturer,n.manufacturer],[t.model,n.model],[t.serial,n.serial_number],[t.installed,n.installation_date?e(n.installation_date):null],[t.warranty,n.warranty_expiry?e(n.warranty_expiry):null]].filter(([,p])=>!!p),h=r.map(p=>{let u=t.scheduleLabel(p);return` - ${A(p.name)} - ${A(t.typeLabel(p.type))} - ${A(t.statusLabel(p.status))} - ${A(u)} - ${A(p.last_performed?e(p.last_performed):t.none)} - ${A(p.next_due?e(p.next_due):t.none)} - ${p.times_performed??0} - ${(p.total_cost??0).toFixed(2)} ${A(i)} - `}).join(""),c=r.reduce((p,u)=>p+(u.total_cost??0),0);return` +/*! maintenance_supporter frontend 2.57.0 */ +import"/maintenance_supporter_panelfiles/panel-chunks/chunk-VNISEOIC.js";import{b as qt,c as oe}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-AIMCOREG.js";import{a as E}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NU5DR7VT.js";import{a as ae,b as ne,c as re,d as Ut,e as Lt}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-T5QK5YQR.js";import{a as le}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GKQ6LXK5.js";import{a as _,b as D,c as r,d as P,f as d,g as A,h as ie,i as T,j as g,k as St,l as Mt,m as ut,n as s,o as Et,p as W,q as Dt,r as Y,s as At,t as _t,v as xt,w as mt,x as se,y as Ct}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-TSHS2WDI.js";function it(n){return!!n&&/^https?:\/\//i.test(n)}var He=["assignee_pool","required_completion_fields","checklist","labels","history"],Fe=["checklist_progress"],Be=["tasks","parts"],Ve=["manual_docs","battery_fleet_excluded"];function Wt(n,o,t=[]){for(let e of o)n[e]===void 0&&(n[e]=[]);for(let e of t)n[e]===void 0&&(n[e]={})}function Ne(n){let o=n;Wt(o,Be),o.object&&typeof o.object=="object"&&Wt(o.object,Ve);for(let t of o.tasks)Wt(t,He,Fe);return n}function $t(n){for(let o of n)Ne(o);return n}function Ue(n,o){if(o.objects)return o.objects;let t=o.delta||[],e=o.removed||[];if(!t.length&&!e.length)return null;let i=new Map(n.map(a=>[a.entry_id,a]));for(let a of t)i.set(a.entry_id,a);for(let a of e)i.delete(a);return[...i.values()]}function ce(n,o){return o.objects&&$t(o.objects),o.delta&&$t(o.delta),Ue(n,o)}var Rt="2.57.0";function de(n,o=Rt){return!n||!o||o==="dev"?!1:n!==o}var F={overviewTab:"msp-overview-tab",collapsedSections:"msp-collapsed-sections",chartRange:"msp-chart-range",chartHideOutliers:"msp-chart-hide-outliers",taskSort:"maintenance_supporter_sort",objectSort:"maintenance_supporter_object_sort",groupBy:"maintenance_supporter_groupby",objectView:"maintenance_supporter_object_view",objectsCache:"msp-objects-cache",gettingStartedDismissed:"msp-gs-dismissed"};var qe=168*3600*1e3;function pe(){try{let n=localStorage.getItem(F.objectsCache);if(!n)return null;let o=JSON.parse(n);return o.v!==Rt||!Number.isFinite(o.at)||Date.now()-o.at>qe||!Array.isArray(o.objects)||o.objects.length===0?null:{objects:o.objects,stats:o.stats??null}}catch{return null}}function Yt(n,o){if(!(!Array.isArray(n)||n.length===0))try{let t={v:Rt,at:Date.now(),objects:n,stats:o};localStorage.setItem(F.objectsCache,JSON.stringify(t))}catch{}}var We={days:1,weeks:7,months:30.4368,years:365.25};function Kt(n,o){return!n||n<=0?0:n*(We[o||"days"]??1)}function Ot(n,o,t){let e=Kt(n,t);if(e<=0||o==null)return{pct:0,overflow:!1};let i=(e-o)/e*100;return{pct:Math.max(0,Math.min(100,i)),overflow:i>100}}function L(n){return String(n??"").replace(/[&<>"']/g,o=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[o])}function he(n,o,t,e,i,a){let l=[[t.manufacturer,n.manufacturer],[t.model,n.model],[t.serial,n.serial_number],[t.installed,n.installation_date?e(n.installation_date):null],[t.warranty,n.warranty_expiry?e(n.warranty_expiry):null]].filter(([,h])=>!!h),p=o.map(h=>{let u=t.scheduleLabel(h);return` + ${L(h.name)} + ${L(t.typeLabel(h.type))} + ${L(t.statusLabel(h.status))} + ${L(u)} + ${L(h.last_performed?e(h.last_performed):t.none)} + ${L(h.next_due?e(h.next_due):t.none)} + ${h.times_performed??0} + ${(h.total_cost??0).toFixed(2)} ${L(i)} + `}).join(""),c=o.reduce((h,u)=>h+(u.total_cost??0),0);return` -${A(t.title)} \u2014 ${A(n.name)} +${L(t.title)} \u2014 ${L(n.name)} -

${A(n.name)}

-

${A(t.title)} \xB7 ${A(t.generated)}: ${A(e(a))}

- ${l.length?`
${l.map(([p,u])=>`
${A(p)}
${A(u)}
`).join("")}
`:""} -

${A(t.tasksHeading)} (${r.length})

+

${L(n.name)}

+

${L(t.title)} \xB7 ${L(t.generated)}: ${L(e(a))}

+ ${l.length?`
${l.map(([h,u])=>`
${L(h)}
${L(u)}
`).join("")}
`:""} +

${L(t.tasksHeading)} (${o.length})

- - - + + + - ${h||``} - + ${p||``} +
${A(t.colTask)}${A(t.colType)}${A(t.colStatus)}${A(t.colSchedule)}${A(t.colLastDone)}${A(t.colNextDue)}${A(t.colTimes)}${A(t.colCost)}${L(t.colTask)}${L(t.colType)}${L(t.colStatus)}${L(t.colSchedule)}${L(t.colLastDone)}${L(t.colNextDue)}${L(t.colTimes)}${L(t.colCost)}
${A(t.none)}
${A(t.totalCost)}${c.toFixed(2)} ${A(i)}
${L(t.none)}
${L(t.totalCost)}${c.toFixed(2)} ${L(i)}
- ${n.notes?`
${A(t.notes)}: -${A(n.notes)}
`:""} -`}function Kt(n,r=new Date){if(!n)return{kind:"none",days:null,date:null};let t=new Date(`${n}T00:00:00`);if(isNaN(t.getTime()))return{kind:"none",days:null,date:null};let e=Date.UTC(r.getFullYear(),r.getMonth(),r.getDate()),i=Date.UTC(t.getFullYear(),t.getMonth(),t.getDate()),a=Math.round((i-e)/864e5);return a<0?{kind:"expired",days:a,date:n}:a<=60?{kind:"expiring",days:a,date:n}:{kind:"valid",days:a,date:n}}var z=n=>String(n??"").replace(/[&<>"']/g,r=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[r]);function ue(n,r,t,e,i,a,l,h,c,p=[]){let u=[[t.object,z(r)],[t.type,z(t.typeLabel(n.type))],[t.interval,z(i(n))],[t.nextDue,n.next_due?z(e(n.next_due)):"\u2014"],[t.lastDone,n.last_performed?z(e(n.last_performed)):z(t.never)]];n.priority&&n.priority!=="normal"&&u.push([t.priority,z(n.priority)]);let _=(n.checklist||[]).map(b=>`
  • ${z(b)}
  • `).join(""),v=(b,x)=>b?`
    ${z(x)}
    `:"";return` + ${n.notes?`
    ${L(t.notes)}: +${L(n.notes)}
    `:""} +`}function Gt(n,o=new Date){if(!n)return{kind:"none",days:null,date:null};let t=new Date(`${n}T00:00:00`);if(isNaN(t.getTime()))return{kind:"none",days:null,date:null};let e=Date.UTC(o.getFullYear(),o.getMonth(),o.getDate()),i=Date.UTC(t.getFullYear(),t.getMonth(),t.getDate()),a=Math.round((i-e)/864e5);return a<0?{kind:"expired",days:a,date:n}:a<=60?{kind:"expiring",days:a,date:n}:{kind:"valid",days:a,date:n}}var z=n=>String(n??"").replace(/[&<>"']/g,o=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[o]);function ue(n,o,t,e,i,a,l,p,c,h=[]){let u=[[t.object,z(o)],[t.type,z(t.typeLabel(n.type))],[t.interval,z(i(n))],[t.nextDue,n.next_due?z(e(n.next_due)):"\u2014"],[t.lastDone,n.last_performed?z(e(n.last_performed)):z(t.never)]];n.priority&&n.priority!=="normal"&&u.push([t.priority,z(n.priority)]);let m=(n.checklist||[]).map(b=>`
  • ${z(b)}
  • `).join(""),v=(b,x)=>b?`
    ${z(x)}
    `:"";return` ${z(n.name)} \u2014 ${z(t.title)} -

    ${d}

    -${u?`
    ${u}
    `:""} -
    -
    - QR Info -
    ${f}
    -
    - ${_?`
    - QR Complete -
    ${w}
    -
    `:""} -
    -
    ${p(this._viewResult.url)}
    -