93 files
This commit is contained in:
@@ -287,6 +287,12 @@ STRATEGY_CHUNKS_URL = f"{STRATEGY_DIR_URL}/chunks"
|
||||
# STRATEGY_DIR_URL. The shim has no relative imports, so a flat URL is fine.
|
||||
STRATEGY_SHIM_URL = "/maintenance_supporter_strategy_shim.js"
|
||||
CALENDAR_CARD_URL = "/maintenance_supporter_calendar_card"
|
||||
# Panel code-split chunks (perf wave 2, item 5). The panel entry is served
|
||||
# from a versioned FILE url (PANEL_URL + content hash, see panel.py), which
|
||||
# gives relative imports nothing to resolve against — so esbuild rewrites
|
||||
# its chunk imports to ABSOLUTE urls under this prefix (publicPath in
|
||||
# esbuild.mjs; the two values must stay in sync).
|
||||
PANEL_CHUNKS_URL = "/maintenance_supporter_panelfiles/panel-chunks"
|
||||
# Runtime-loaded UI translations (frontend/locales/<lang>.json), served as a
|
||||
# directory so the panel/card fetch the active language on demand. Mirrors
|
||||
# LOCALES_BASE in frontend-src/styles.ts: only EN is bundled into the JS (as the
|
||||
|
||||
+71
@@ -167,3 +167,74 @@ describe("complete-dialog", () => {
|
||||
expect(completedEvent).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => {
|
||||
async function mountWithParts(over: Partial<MaintenanceCompleteDialog> = {}) {
|
||||
const { hass } = createMockHass({});
|
||||
const el = await fixture<MaintenanceCompleteDialog>(html`
|
||||
<maintenance-complete-dialog
|
||||
.hass=${hass}
|
||||
.entryId=${"entry1"}
|
||||
.taskId=${"task1"}
|
||||
.taskName=${"Uses Parts"}
|
||||
.lang=${"en"}
|
||||
></maintenance-complete-dialog>
|
||||
`);
|
||||
Object.assign(el, over);
|
||||
el.open();
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
}
|
||||
|
||||
const chip = (el: MaintenanceCompleteDialog) =>
|
||||
el.shadowRoot!.querySelector<HTMLButtonElement>(".cost-suggestion");
|
||||
|
||||
it("sums selected consumed parts (qty x unit cost) and fills on click", async () => {
|
||||
const el = await mountWithParts({
|
||||
parts: [
|
||||
{ id: "p1", name: "Filter", cost: 12.5 },
|
||||
{ id: "p2", name: "O-Ring", cost: 2.25 },
|
||||
{ id: "p3", name: "Unpriced", cost: null },
|
||||
] as never,
|
||||
consumesParts: [
|
||||
{ part_id: "p1", quantity: 1 },
|
||||
{ part_id: "p2", quantity: 2 },
|
||||
{ part_id: "p3", quantity: 1 },
|
||||
] as never,
|
||||
currencySymbol: "€",
|
||||
});
|
||||
const c = chip(el)!;
|
||||
expect(c, "suggestion chip rendered").to.exist;
|
||||
expect(c.textContent).to.include("17.00");
|
||||
expect(c.textContent).to.include("€");
|
||||
c.click();
|
||||
await el.updateComplete;
|
||||
const cost = [...el.shadowRoot!.querySelectorAll<HTMLInputElement>(".field-input")][1];
|
||||
expect(cost.value).to.equal("17.00");
|
||||
expect(chip(el), "chip hides once cost is set").to.equal(null);
|
||||
});
|
||||
|
||||
it("buy task: restock qty x unit cost, follows the qty field", async () => {
|
||||
const el = await mountWithParts({ restockDefault: 2, restockUnitCost: 4.5 });
|
||||
expect(chip(el)!.textContent).to.include("9.00");
|
||||
});
|
||||
|
||||
it("no chip when no involved part carries a price", async () => {
|
||||
const el = await mountWithParts({
|
||||
parts: [{ id: "p1", name: "Filter", cost: null }] as never,
|
||||
consumesParts: [{ part_id: "p1", quantity: 1 }] as never,
|
||||
});
|
||||
expect(chip(el)).to.equal(null);
|
||||
});
|
||||
|
||||
it("no chip once the user typed a cost themselves", async () => {
|
||||
const el = await mountWithParts({
|
||||
parts: [{ id: "p1", name: "Filter", cost: 5 }] as never,
|
||||
consumesParts: [{ part_id: "p1", quantity: 1 }] as never,
|
||||
});
|
||||
expect(chip(el)).to.exist;
|
||||
setInput(el, 1, "3.10");
|
||||
await el.updateComplete;
|
||||
expect(chip(el)).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
+14
-1
@@ -20,7 +20,9 @@ function setDeepLink(query: string) {
|
||||
}
|
||||
|
||||
async function settleRaf(el: { updateComplete: Promise<unknown> }) {
|
||||
// Deep-link dialog opens behind a requestAnimationFrame.
|
||||
// Deep-link dialog opens behind a requestAnimationFrame — and since the
|
||||
// dialogs became lazy code-split chunks, behind their dynamic import too.
|
||||
await customElements.whenDefined("maintenance-complete-dialog");
|
||||
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await (el as HTMLElement & { updateComplete: Promise<unknown> }).updateComplete;
|
||||
@@ -30,6 +32,15 @@ function completeDialog(el: HTMLElement): MaintenanceCompleteDialog | null {
|
||||
return sr(el).querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog");
|
||||
}
|
||||
|
||||
/** The dialogs are lazy code-split chunks — the open lands whenever the
|
||||
* whole lazy-UI group has loaded, so poll instead of guessing a delay. */
|
||||
async function waitForOpenCompleteDialog(el: HTMLElement): Promise<void> {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (completeDialog(el)?.shadowRoot?.querySelector("ha-dialog")) return;
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
}
|
||||
}
|
||||
|
||||
describe("panel deep links (QR scan routing)", () => {
|
||||
beforeEach(() => {
|
||||
resetTaskSeq();
|
||||
@@ -52,6 +63,7 @@ describe("panel deep links (QR scan routing)", () => {
|
||||
expect(sr(el).querySelector(".task-header"), "task detail rendered").to.exist;
|
||||
expect(sr(el).querySelector(".task-name-breadcrumb")!.textContent).to.include("Scan Me");
|
||||
// …with the complete dialog open and targeted at the scanned task.
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "complete dialog open").to.exist;
|
||||
expect(dlg.entryId).to.equal("e1");
|
||||
@@ -94,6 +106,7 @@ describe("panel deep links (QR scan routing)", () => {
|
||||
expect(
|
||||
sent.filter((m) => m.type === "maintenance_supporter/task/quick_complete").length,
|
||||
).to.equal(1);
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist;
|
||||
expect(dlg.taskName).to.equal("No Defaults");
|
||||
|
||||
+23
-7
@@ -29,6 +29,10 @@ interface BatteryRow {
|
||||
* the fleet floor, whichever is higher) — the level bar colors against
|
||||
* it, not against a fixed 20 %. */
|
||||
low_threshold?: number;
|
||||
/** B1: the predicted date has passed while the battery still reports
|
||||
* healthy. Never escalates to low/task — the row just shows the
|
||||
* discrepancy (usual cause: an unrecorded swap). */
|
||||
forecast_overdue?: boolean;
|
||||
}
|
||||
interface RosterRow extends BatteryRow {
|
||||
status: "low" | "soon" | "ok";
|
||||
@@ -466,13 +470,15 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
})()}
|
||||
${b.days_until != null
|
||||
? html`<span
|
||||
class="bf-predicted ${b.predicted_source === "trend" ? "bf-trend" : ""}"
|
||||
title=${b.predicted_source === "trend"
|
||||
? t("battery_fleet_predicted_trend", L)
|
||||
.replace("{date}", this._predictedDate(b.days_until))
|
||||
.replace("{confidence}", t("cal_confidence_" + (b.prediction_confidence || "medium"), L))
|
||||
: t("battery_fleet_predicted_on", L).replace("{date}", this._predictedDate(b.days_until))}
|
||||
>~${this._predictedDate(b.days_until)}</span
|
||||
class="bf-predicted ${b.predicted_source === "trend" ? "bf-trend" : ""} ${b.forecast_overdue ? "bf-overdue" : ""}"
|
||||
title=${b.forecast_overdue
|
||||
? t("battery_fleet_forecast_overdue", L)
|
||||
: b.predicted_source === "trend"
|
||||
? t("battery_fleet_predicted_trend", L)
|
||||
.replace("{date}", this._predictedDate(b.days_until))
|
||||
.replace("{confidence}", t("cal_confidence_" + (b.prediction_confidence || "medium"), L))
|
||||
: t("battery_fleet_predicted_on", L).replace("{date}", this._predictedDate(b.days_until))}
|
||||
>${b.forecast_overdue ? html`<ha-icon icon="mdi:calendar-alert"></ha-icon>` : nothing}~${this._predictedDate(b.days_until)}</span
|
||||
>`
|
||||
: nothing}
|
||||
<button
|
||||
@@ -770,6 +776,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
/* B1: passed prediction on a still-healthy battery — warn-tinted with a
|
||||
calendar-alert icon; the tooltip explains (record the swap / forecast
|
||||
was off). Deliberately NOT red: this is a discrepancy, not an alarm. */
|
||||
.bf-predicted.bf-overdue {
|
||||
color: var(--warning-color, #ff9800);
|
||||
}
|
||||
.bf-predicted.bf-overdue ha-icon {
|
||||
--mdc-icon-size: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.bf-total {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -21,6 +21,11 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
@property() public readingUnit = "";
|
||||
/** Buy task (part_ref): default restock quantity — shows an editable qty field. */
|
||||
@property({ attribute: false }) public restockDefault: number | null = null;
|
||||
/** #104 follow-up: the buy task's part unit cost — powers the cost
|
||||
* suggestion (restock qty × unit cost). */
|
||||
@property({ attribute: false }) public restockUnitCost: number | null = null;
|
||||
/** Currency symbol for the cost suggestion ("" = plain number). */
|
||||
@property() public currencySymbol = "";
|
||||
/** #99: the parts offered on completion — enables the editable "parts used"
|
||||
* section. Built by `partsForCompletion`: the object's own inventory plus
|
||||
* every shared pool this task links to (#111), each tagged with its owner. */
|
||||
@@ -215,6 +220,47 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
return this.requiredFields.includes(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
|
||||
}
|
||||
|
||||
/** #104 follow-up: suggested cost derived from the parts this completion
|
||||
* touches — the SELECTED "parts used" (qty × each part's unit cost) on a
|
||||
* consuming task, or restock qty × unit cost on a buy task. Null when no
|
||||
* involved part carries a price. Follows the live selection, so ticking
|
||||
* a part off updates the suggestion. */
|
||||
private _partsCostSuggestion(): number | null {
|
||||
if (this.restockDefault !== null) {
|
||||
const qty = parseFloat(this._restockQty);
|
||||
if (this.restockUnitCost == null || !Number.isFinite(qty) || qty <= 0) return null;
|
||||
return Math.round(this.restockUnitCost * qty * 100) / 100;
|
||||
}
|
||||
if (!this.parts.length) return null;
|
||||
let sum = 0;
|
||||
let priced = false;
|
||||
for (const link of Object.values(this._usedParts)) {
|
||||
const def = this.parts.find(
|
||||
(pt) => partLinkKey({ part_id: pt.id, entry_id: pt.entry_id }) === partLinkKey(link),
|
||||
);
|
||||
if (def?.cost != null) {
|
||||
sum += def.cost * (link.quantity || 1);
|
||||
priced = true;
|
||||
}
|
||||
}
|
||||
return priced ? Math.round(sum * 100) / 100 : null;
|
||||
}
|
||||
|
||||
/** The one-click "use ≈ X from parts" chip under the cost field. Hidden
|
||||
* once the user typed a cost themselves — a suggestion, never an
|
||||
* overwrite. */
|
||||
private _renderCostSuggestion(L: string) {
|
||||
if (this._cost.trim() !== "") return nothing;
|
||||
const suggestion = this._partsCostSuggestion();
|
||||
if (suggestion == null || suggestion <= 0) return nothing;
|
||||
const amount = `${suggestion.toFixed(2)}${this.currencySymbol ? ` ${this.currencySymbol}` : ""}`;
|
||||
return html`<button
|
||||
type="button"
|
||||
class="cost-suggestion"
|
||||
@click=${() => (this._cost = suggestion.toFixed(2))}
|
||||
>${t("cost_from_parts", L).replace("{amount}", amount)}</button>`;
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._open = false;
|
||||
}
|
||||
@@ -317,6 +363,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._cost}
|
||||
@input=${(e: Event) => (this._cost = (e.target as HTMLInputElement).value)} />
|
||||
${this._renderCostSuggestion(L)}
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${t("duration_minutes", L)}${this._req("duration")}</span>
|
||||
@@ -386,6 +433,19 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
|
||||
.cost-suggestion {
|
||||
align-self: flex-start;
|
||||
margin-top: 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -33,11 +33,22 @@ const common = {
|
||||
banner: { js: `/*! maintenance_supporter frontend ${manifestVersion} */` },
|
||||
};
|
||||
|
||||
// Panel
|
||||
// Panel — code-split (perf wave 2, item 5): the dialogs + settings view the
|
||||
// panel dynamic-imports land in frontend/panel-chunks/ as content-hashed
|
||||
// chunks, shrinking the entry's critical parse path. publicPath makes the
|
||||
// chunk imports ABSOLUTE, because the entry is served from a versioned
|
||||
// file URL (/maintenance_supporter_panel_<hash>) that has no directory for
|
||||
// relative imports to resolve against. Cache-safety (#124 class): the
|
||||
// entry URL changes with its content hash, and a fresh entry always names
|
||||
// exactly its own content-hashed chunks.
|
||||
rmSync("../frontend/panel-chunks", { recursive: true, force: true });
|
||||
await build({
|
||||
...common,
|
||||
entryPoints: ["maintenance-panel.ts"],
|
||||
outfile: "../frontend/maintenance-panel.js",
|
||||
outdir: "../frontend",
|
||||
splitting: true,
|
||||
chunkNames: "panel-chunks/[name]-[hash]",
|
||||
publicPath: "/maintenance_supporter_panelfiles",
|
||||
});
|
||||
|
||||
// Lovelace Card
|
||||
|
||||
@@ -16,4 +16,5 @@ export const LS_KEYS = {
|
||||
objectSort: "maintenance_supporter_object_sort",
|
||||
groupBy: "maintenance_supporter_groupby",
|
||||
objectView: "maintenance_supporter_object_view",
|
||||
objectsCache: "msp-objects-cache",
|
||||
} as const;
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flotila baterií",
|
||||
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
|
||||
"update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.",
|
||||
"update_reload": "Načíst znovu"
|
||||
"update_reload": "Načíst znovu",
|
||||
"battery_fleet_forecast_overdue": "Předpovězené datum uplynulo — baterie stále hlásí dobrý stav. Pokud jste ji vyměnili, zaznamenejte výměnu; jinak byla předpověď mylná.",
|
||||
"cost_from_parts": "Použít ≈ {amount} z dílů"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflåde",
|
||||
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
|
||||
"update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.",
|
||||
"update_reload": "Genindlæs"
|
||||
"update_reload": "Genindlæs",
|
||||
"battery_fleet_forecast_overdue": "Forudsagt dato er overskredet — batteriet melder stadig god tilstand. Hvis du har skiftet det, registrér udskiftningen; ellers ramte prognosen forbi.",
|
||||
"cost_from_parts": "Brug ≈ {amount} fra dele"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batterie-Flotte",
|
||||
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
|
||||
"update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.",
|
||||
"update_reload": "Neu laden"
|
||||
"update_reload": "Neu laden",
|
||||
"battery_fleet_forecast_overdue": "Prognosedatum überschritten — die Batterie meldet sich weiterhin gesund. Falls du sie gewechselt hast, trage den Wechsel nach; andernfalls lag die Prognose daneben.",
|
||||
"cost_from_parts": "≈ {amount} aus Teilen übernehmen"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Battery fleet",
|
||||
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
|
||||
"update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.",
|
||||
"update_reload": "Reload"
|
||||
"update_reload": "Reload",
|
||||
"battery_fleet_forecast_overdue": "Predicted date passed — the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",
|
||||
"cost_from_parts": "Use ≈ {amount} from parts"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flota de baterías",
|
||||
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
|
||||
"update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.",
|
||||
"update_reload": "Recargar"
|
||||
"update_reload": "Recargar",
|
||||
"battery_fleet_forecast_overdue": "Fecha prevista superada: la batería sigue informando buen estado. Si la cambiaste, registra el reemplazo; si no, la previsión falló.",
|
||||
"cost_from_parts": "Usar ≈ {amount} de las piezas"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Paristokanta",
|
||||
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
|
||||
"update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.",
|
||||
"update_reload": "Lataa uudelleen"
|
||||
"update_reload": "Lataa uudelleen",
|
||||
"battery_fleet_forecast_overdue": "Ennustettu päivä on ohitettu — akku ilmoittaa yhä hyvästä kunnosta. Jos vaihdoit sen, kirjaa vaihto; muuten ennuste oli pielessä.",
|
||||
"cost_from_parts": "Käytä ≈ {amount} osista"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Parc de piles",
|
||||
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
|
||||
"update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.",
|
||||
"update_reload": "Recharger"
|
||||
"update_reload": "Recharger",
|
||||
"battery_fleet_forecast_overdue": "Date prévue dépassée — la batterie se signale toujours en bon état. Si vous l'avez remplacée, enregistrez le remplacement ; sinon la prévision était erronée.",
|
||||
"cost_from_parts": "Reprendre ≈ {amount} des pièces"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "बैटरी फ्लीट",
|
||||
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
|
||||
"update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।",
|
||||
"update_reload": "पुनः लोड करें"
|
||||
"update_reload": "पुनः लोड करें",
|
||||
"battery_fleet_forecast_overdue": "अनुमानित तिथि बीत गई — बैटरी अब भी अच्छी स्थिति बता रही है। यदि आपने इसे बदला है, तो बदलाव दर्ज करें; अन्यथा पूर्वानुमान गलत था।",
|
||||
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Elemflotta",
|
||||
"battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.",
|
||||
"update_banner": "A Maintenance Supporter újabb verziója érhető el a szerveren — töltse újra az oldalt a panel frissítéséhez.",
|
||||
"update_reload": "Újratöltés"
|
||||
"update_reload": "Újratöltés",
|
||||
"battery_fleet_forecast_overdue": "Az előrejelzett dátum elmúlt — az elem továbbra is jó állapotot jelez. Ha kicserélted, rögzítsd a cserét; különben az előrejelzés tévedett.",
|
||||
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Parco batterie",
|
||||
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
|
||||
"update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.",
|
||||
"update_reload": "Ricarica"
|
||||
"update_reload": "Ricarica",
|
||||
"battery_fleet_forecast_overdue": "Data prevista superata — la batteria risulta ancora in buono stato. Se l'hai sostituita, registra la sostituzione; altrimenti la previsione era errata.",
|
||||
"cost_from_parts": "Usa ≈ {amount} dai ricambi"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "電池フリート",
|
||||
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
|
||||
"update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。",
|
||||
"update_reload": "再読み込み"
|
||||
"update_reload": "再読み込み",
|
||||
"battery_fleet_forecast_overdue": "予測日を過ぎましたが、電池はまだ正常と報告しています。交換済みなら交換を記録してください。そうでなければ予測が外れています。",
|
||||
"cost_from_parts": "部品から ≈ {amount} を適用"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "배터리 플릿",
|
||||
"battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.",
|
||||
"update_banner": "서버에 Maintenance Supporter의 새 버전이 있습니다 — 패널을 업데이트하려면 새로 고침하세요.",
|
||||
"update_reload": "새로 고침"
|
||||
"update_reload": "새로 고침",
|
||||
"battery_fleet_forecast_overdue": "예측 날짜가 지났지만 배터리는 여전히 정상으로 보고됩니다. 교체했다면 교체를 기록하세요. 아니라면 예측이 빗나간 것입니다.",
|
||||
"cost_from_parts": "부품에서 ≈ {amount} 적용"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflåte",
|
||||
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
|
||||
"update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.",
|
||||
"update_reload": "Last inn på nytt"
|
||||
"update_reload": "Last inn på nytt",
|
||||
"battery_fleet_forecast_overdue": "Forutsagt dato er passert — batteriet melder fortsatt god tilstand. Hvis du byttet det, registrer byttet; ellers bommet prognosen.",
|
||||
"cost_from_parts": "Bruk ≈ {amount} fra deler"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batterijvloot",
|
||||
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
|
||||
"update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.",
|
||||
"update_reload": "Herladen"
|
||||
"update_reload": "Herladen",
|
||||
"battery_fleet_forecast_overdue": "Voorspelde datum verstreken — de batterij meldt zich nog steeds gezond. Heb je hem vervangen, registreer dan de vervanging; anders zat de voorspelling ernaast.",
|
||||
"cost_from_parts": "≈ {amount} uit onderdelen overnemen"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flota baterii",
|
||||
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
|
||||
"update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.",
|
||||
"update_reload": "Załaduj ponownie"
|
||||
"update_reload": "Załaduj ponownie",
|
||||
"battery_fleet_forecast_overdue": "Przewidywana data minęła — bateria nadal zgłasza dobry stan. Jeśli ją wymieniono, zapisz wymianę; w przeciwnym razie prognoza była błędna.",
|
||||
"cost_from_parts": "Użyj ≈ {amount} z części"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.",
|
||||
"update_banner": "Há uma versão mais nova do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
|
||||
"update_reload": "Recarregar"
|
||||
"update_reload": "Recarregar",
|
||||
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria ainda reporta bom estado. Se você a trocou, registre a troca; caso contrário, a previsão errou.",
|
||||
"cost_from_parts": "Usar ≈ {amount} das peças"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
|
||||
"update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
|
||||
"update_reload": "Recarregar"
|
||||
"update_reload": "Recarregar",
|
||||
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria continua a reportar bom estado. Se a substituiu, registe a substituição; caso contrário, a previsão falhou.",
|
||||
"cost_from_parts": "Usar ≈ {amount} das peças"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
|
||||
"update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.",
|
||||
"update_reload": "Перезагрузить"
|
||||
"update_reload": "Перезагрузить",
|
||||
"battery_fleet_forecast_overdue": "Прогнозируемая дата прошла — батарея по-прежнему сообщает о хорошем состоянии. Если вы её заменили, зафиксируйте замену; иначе прогноз оказался неверным.",
|
||||
"cost_from_parts": "Взять ≈ {amount} из запчастей"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflotta",
|
||||
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
|
||||
"update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.",
|
||||
"update_reload": "Ladda om"
|
||||
"update_reload": "Ladda om",
|
||||
"battery_fleet_forecast_overdue": "Förutsagt datum har passerat — batteriet rapporterar fortfarande god status. Om du bytte det, registrera bytet; annars slog prognosen fel.",
|
||||
"cost_from_parts": "Använd ≈ {amount} från delar"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Pil filosu",
|
||||
"battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.",
|
||||
"update_banner": "Sunucuda Maintenance Supporter'ın daha yeni bir sürümü var — paneli güncellemek için sayfayı yeniden yükleyin.",
|
||||
"update_reload": "Yeniden yükle"
|
||||
"update_reload": "Yeniden yükle",
|
||||
"battery_fleet_forecast_overdue": "Öngörülen tarih geçti — pil hâlâ sağlıklı görünüyor. Değiştirdiyseniz değişimi kaydedin; aksi halde tahmin yanılmış demektir.",
|
||||
"cost_from_parts": "Parçalardan ≈ {amount} kullan"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
|
||||
"update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.",
|
||||
"update_reload": "Перезавантажити"
|
||||
"update_reload": "Перезавантажити",
|
||||
"battery_fleet_forecast_overdue": "Прогнозована дата минула — батарея й далі повідомляє про добрий стан. Якщо ви її замінили, зафіксуйте заміну; інакше прогноз не справдився.",
|
||||
"cost_from_parts": "Узяти ≈ {amount} із запчастин"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "电池群",
|
||||
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
|
||||
"update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。",
|
||||
"update_reload": "重新加载"
|
||||
"update_reload": "重新加载",
|
||||
"battery_fleet_forecast_overdue": "预测日期已过——电池仍报告状态良好。如果您已更换电池,请记录更换;否则说明预测有误。",
|
||||
"cost_from_parts": "采用配件合计 ≈ {amount}"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { mergeSubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { hydrateObjects } from "./helpers/hydrate-objects";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays } from "./styles";
|
||||
import type {
|
||||
@@ -156,10 +157,10 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
private async _loadData(): Promise<void> {
|
||||
try {
|
||||
const [objResult, statsResult] = await Promise.all([
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects", compact: true }),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }),
|
||||
]);
|
||||
this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects;
|
||||
this._objects = hydrateObjects((objResult as { objects: MaintenanceObjectResponse[] }).objects);
|
||||
this._stats = statsResult as StatisticsResponse;
|
||||
} catch {
|
||||
// WS not available yet
|
||||
@@ -275,14 +276,16 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
try {
|
||||
const unsub = await this.hass.connection.subscribeMessage(
|
||||
(msg: unknown) => {
|
||||
const next = mergeSubscriptionEvent(
|
||||
this._objects,
|
||||
msg as SubscriptionEvent<MaintenanceObjectResponse>,
|
||||
);
|
||||
const ev = msg as SubscriptionEvent<MaintenanceObjectResponse>;
|
||||
// Compact payloads: hydrate before merging (helpers/hydrate-objects).
|
||||
if (ev.objects) hydrateObjects(ev.objects);
|
||||
if (ev.delta) hydrateObjects(ev.delta);
|
||||
const next = mergeSubscriptionEvent(this._objects, ev);
|
||||
if (next !== null) this._objects = next;
|
||||
},
|
||||
// deltas: only changed entries arrive — see helpers/subscription-merge.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true }
|
||||
// compact: empty keys stripped server-side, hydrated above.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true, compact: true }
|
||||
);
|
||||
// Detached mid-subscribe → drop the orphaned subscription.
|
||||
if (!this.isConnected) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { isStaleBundle } from "./helpers/bundle-version";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence, setDateTimePrefs } from "./styles";
|
||||
import { LS_KEYS } from "./helpers/storage-keys";
|
||||
import { readObjectsCache, writeObjectsCache } from "./helpers/objects-cache";
|
||||
import { hydrateObjects } from "./helpers/hydrate-objects";
|
||||
import { daysProgress } from "./helpers/interval";
|
||||
import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
|
||||
import { warrantyStatus } from "./helpers/warranty";
|
||||
@@ -34,20 +36,18 @@ import type {
|
||||
} from "./types";
|
||||
import { StatisticsService } from "./statistics-service";
|
||||
import { UserService } from "./user-service";
|
||||
import "./components/object-dialog";
|
||||
// The heavy dialogs + the settings view are NOT imported here: they are
|
||||
// esbuild code-split chunks loaded by _ensureLazyUi() right after mount
|
||||
// (roadmap perf wave 2, item 5). Only their types are imported — esbuild
|
||||
// tree-shakes type-only imports, so they cost the entry bundle nothing.
|
||||
import type { MaintenanceObjectDialog } from "./components/object-dialog";
|
||||
import "./components/documents-section";
|
||||
import "./components/parts-section";
|
||||
import "./components/task-documents";
|
||||
import "./components/task-dialog";
|
||||
import type { MaintenanceTaskDialog } from "./components/task-dialog";
|
||||
import "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
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
|
||||
@@ -61,7 +61,6 @@ import type {
|
||||
} from "./components/history-edit-dialog";
|
||||
import "./components/confirm-dialog";
|
||||
import type { MaintenanceConfirmDialog } from "./components/confirm-dialog";
|
||||
import "./components/settings-view";
|
||||
import "./components/storage-section-card";
|
||||
import "./components/seasonal-overrides-dialog";
|
||||
import type { SeasonalOverridesDialog } from "./components/seasonal-overrides-dialog";
|
||||
@@ -236,8 +235,45 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
private _popstateHandler = (e: PopStateEvent) => this._onPopState(e);
|
||||
|
||||
/** Lazy UI chunks (perf wave 2, item 5): the six dialogs + the settings
|
||||
* view are code-split out of the entry bundle and fetched in parallel
|
||||
* right after mount — the critical parse path shrinks while every open
|
||||
* path goes through _ui(), so a click can never race a still-loading
|
||||
* chunk. */
|
||||
private _lazyUi: Promise<unknown> | null = null;
|
||||
|
||||
private _ensureLazyUi(): Promise<unknown> {
|
||||
if (!this._lazyUi) {
|
||||
this._lazyUi = Promise.all([
|
||||
import("./components/object-dialog"),
|
||||
import("./components/task-dialog"),
|
||||
import("./components/complete-dialog"),
|
||||
import("./components/qr-dialog"),
|
||||
import("./components/adopt-problem-sensors-dialog"),
|
||||
import("./components/suggested-setups-dialog"),
|
||||
import("./components/settings-view"),
|
||||
]).then(() => this.updateComplete);
|
||||
}
|
||||
return this._lazyUi;
|
||||
}
|
||||
|
||||
/** The requested dialog element, guaranteed upgraded (chunk loaded). */
|
||||
private async _ui<T extends Element>(tag: string): Promise<T | null> {
|
||||
await this._ensureLazyUi();
|
||||
return this.shadowRoot?.querySelector<T>(tag) ?? null;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Prefetch the lazy UI chunks AFTER the first paint settles (idle), not
|
||||
// here: an eager prefetch put 7 fetches + ~280 KB of parsing in direct
|
||||
// competition with the initial data load and measurably slowed first
|
||||
// paint. Deep links and early clicks stay safe either way — every open
|
||||
// path awaits _ui().
|
||||
const idle = (window as unknown as { requestIdleCallback?: (cb: () => void, o?: { timeout: number }) => void }).requestIdleCallback;
|
||||
const kick = () => this._ensureLazyUi();
|
||||
if (idle) idle(kick, { timeout: 3000 });
|
||||
else window.setTimeout(kick, 1500);
|
||||
window.addEventListener("popstate", this._popstateHandler);
|
||||
window.addEventListener("keydown", this._paletteKeydown);
|
||||
window.addEventListener("resize", this._onVirtualScroll, { passive: true });
|
||||
@@ -265,6 +301,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
} catch {
|
||||
// storage blocked — keep the defaults
|
||||
}
|
||||
// Skeleton from cache: paint the previous visit's task list immediately;
|
||||
// the live payload replaces it through the normal _objects assignment.
|
||||
if (this._objects.length === 0) {
|
||||
const cached = readObjectsCache<MaintenanceObjectResponse, StatisticsResponse>();
|
||||
if (cached) {
|
||||
this._objects = cached.objects;
|
||||
if (cached.stats) this._stats = cached.stats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@@ -378,7 +423,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const [objResult, statsResult, budgetResult, groupsResult, settingsResult, viewsResult] = await Promise.all([
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects", compact: true }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/groups" }).catch(() => null),
|
||||
@@ -386,7 +431,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/views/list" }).catch(() => null),
|
||||
]);
|
||||
if (viewsResult) this._savedViews = (viewsResult as { views: SavedView[] }).views || [];
|
||||
if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects;
|
||||
if (objResult) {
|
||||
this._objects = hydrateObjects((objResult as { objects: MaintenanceObjectResponse[] }).objects);
|
||||
writeObjectsCache(this._objects, (statsResult as StatisticsResponse | null) ?? this._stats ?? null);
|
||||
}
|
||||
// A data refresh means the open task's history may have grown (complete,
|
||||
// history edit) — the truncated list payload can't tell, so refetch.
|
||||
if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) {
|
||||
@@ -470,11 +518,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
if (msAction === "add_object") {
|
||||
this._deepLinkHandled = true;
|
||||
cleanMsActionUrl();
|
||||
requestAnimationFrame(() => {
|
||||
this.shadowRoot
|
||||
?.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog")
|
||||
?.openCreate();
|
||||
});
|
||||
void this._ui<MaintenanceObjectDialog>("maintenance-object-dialog")
|
||||
.then((d) => d?.openCreate());
|
||||
return;
|
||||
}
|
||||
if (msAction === "open_vacation"
|
||||
@@ -486,14 +531,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// Switch to the Settings tab — that's where Vacation/Budget/Groups live.
|
||||
this._overviewTab = "settings";
|
||||
// Hint the settings-view which sub-section to scroll to (its own
|
||||
// settings-view component reads this on attribute change).
|
||||
requestAnimationFrame(() => {
|
||||
// settings-view component reads this on attribute change). The view is
|
||||
// a lazy chunk — wait for it before poking at the element.
|
||||
void this._ensureLazyUi().then(() => requestAnimationFrame(() => {
|
||||
const settingsView = this.shadowRoot?.querySelector(
|
||||
"maintenance-settings-view",
|
||||
) as (HTMLElement & { scrollToSection?: (s: string) => void }) | null;
|
||||
const target = msAction.replace("open_", ""); // "vacation" / "budget" / "groups" / "settings"
|
||||
settingsView?.scrollToSection?.(target);
|
||||
});
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -601,15 +647,25 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
try {
|
||||
const unsub = await this.hass.connection.subscribeMessage(
|
||||
(msg: unknown) => {
|
||||
const next = mergeSubscriptionEvent(
|
||||
this._objects,
|
||||
msg as SubscriptionEvent<MaintenanceObjectResponse>,
|
||||
);
|
||||
if (next !== null) this._objects = next;
|
||||
const ev = msg as SubscriptionEvent<MaintenanceObjectResponse>;
|
||||
// Compact payloads: hydrate incoming entries before merging so
|
||||
// everything downstream keeps seeing the full shape.
|
||||
if (ev.objects) hydrateObjects(ev.objects);
|
||||
if (ev.delta) hydrateObjects(ev.delta);
|
||||
const next = mergeSubscriptionEvent(this._objects, ev);
|
||||
if (next !== null) {
|
||||
this._objects = next;
|
||||
// Full snapshots are rare (subscribe start) — keep the skeleton
|
||||
// cache fresh from them; per-delta writes would churn storage.
|
||||
if ((msg as SubscriptionEvent<MaintenanceObjectResponse>).objects) {
|
||||
writeObjectsCache(next, this._stats ?? null);
|
||||
}
|
||||
}
|
||||
},
|
||||
// deltas: only entries whose rebuilt response actually changed —
|
||||
// no-op timer waves send nothing, a real change ships one object.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true }
|
||||
// compact: empty keys stripped server-side, hydrated above.
|
||||
{ type: "maintenance_supporter/subscribe", deltas: true, compact: true }
|
||||
);
|
||||
// If the element was detached while the subscribe was in flight, drop the
|
||||
// now-orphaned subscription instead of storing it on a dead component.
|
||||
@@ -1051,9 +1107,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// --- Adopt problem sensors ---
|
||||
|
||||
private _openAdoptProblemSensors(): void {
|
||||
this.shadowRoot!
|
||||
.querySelector<MaintenanceAdoptProblemSensorsDialog>("maintenance-adopt-problem-sensors-dialog")
|
||||
?.open();
|
||||
void this._ui<MaintenanceAdoptProblemSensorsDialog>("maintenance-adopt-problem-sensors-dialog")
|
||||
.then((d) => d?.open());
|
||||
}
|
||||
|
||||
private async _onProblemSensorsAdopted(e: CustomEvent): Promise<void> {
|
||||
@@ -1069,7 +1124,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
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);
|
||||
void this._ui<MaintenanceTaskDialog>("maintenance-task-dialog")
|
||||
.then((d) => d?.openEdit(ref.entry_id, tk));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1100,9 +1156,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
|
||||
private _openSuggestedSetups(): void {
|
||||
this.shadowRoot!
|
||||
.querySelector<MaintenanceSuggestedSetupsDialog>("maintenance-suggested-setups-dialog")
|
||||
?.open();
|
||||
void this._ui<MaintenanceSuggestedSetupsDialog>("maintenance-suggested-setups-dialog")
|
||||
.then((d) => d?.open());
|
||||
}
|
||||
|
||||
private _onSetupsAdopted(e: CustomEvent): void {
|
||||
@@ -1850,8 +1905,11 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
|
||||
private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog");
|
||||
if (!dlg) return;
|
||||
void this._ui<MaintenanceCompleteDialog>("maintenance-complete-dialog")
|
||||
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled));
|
||||
}
|
||||
|
||||
private _fillAndOpenCompleteDialog(dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
dlg.entryId = entryId;
|
||||
dlg.taskId = taskId;
|
||||
dlg.taskName = taskName;
|
||||
@@ -1873,6 +1931,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
|
||||
const refPart = tk?.part_ref ? objParts.find((pt) => pt.id === tk.part_ref!.part_id) : undefined;
|
||||
dlg.restockDefault = tk?.part_ref ? (refPart?.restock_quantity ?? 1) : null;
|
||||
// #104 follow-up: parts carry unit costs — the dialog offers their sum
|
||||
// as a one-click cost suggestion (buy task: restock qty × unit cost).
|
||||
dlg.restockUnitCost = tk?.part_ref ? (refPart?.cost ?? null) : null;
|
||||
dlg.currencySymbol = this._budget?.currency_symbol || "";
|
||||
// #111: a link may point at another object's pool — name that object, and
|
||||
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
|
||||
dlg.consumesInfo = (tk?.consumes_parts || []).map((link) =>
|
||||
@@ -1888,13 +1950,13 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
|
||||
private _openQrForObject(entryId: string, objectName: string): void {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceQrDialog>("maintenance-qr-dialog");
|
||||
dlg?.openForObject(entryId, objectName);
|
||||
void this._ui<MaintenanceQrDialog>("maintenance-qr-dialog")
|
||||
.then((dlg) => dlg?.openForObject(entryId, objectName));
|
||||
}
|
||||
|
||||
private _openQrForTask(entryId: string, taskId: string, objectName: string, taskName: string): void {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceQrDialog>("maintenance-qr-dialog");
|
||||
dlg?.openForTask(entryId, taskId, objectName, taskName);
|
||||
void this._ui<MaintenanceQrDialog>("maintenance-qr-dialog")
|
||||
.then((dlg) => dlg?.openForTask(entryId, taskId, objectName, taskName));
|
||||
}
|
||||
|
||||
private _onDialogEvent = async (): Promise<void> => {
|
||||
@@ -2385,12 +2447,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
@click=${() => { if (this.narrow) this._actionsMenuOpen = false; }}
|
||||
>
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog")?.openCreate("", this._objects)}
|
||||
@click=${() => this._ui<MaintenanceTaskDialog>("maintenance-task-dialog").then((d) => d?.openCreate("", this._objects))}
|
||||
>
|
||||
${t("new_task", L)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog")?.openCreate()}
|
||||
@click=${() => this._ui<MaintenanceObjectDialog>("maintenance-object-dialog").then((d) => d?.openCreate())}
|
||||
>
|
||||
${t("new_object", L)}
|
||||
</ha-button>
|
||||
@@ -2422,7 +2484,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<ha-button appearance="filled" @click=${() => this._openTemplateGallery()}>
|
||||
<ha-icon icon="mdi:view-grid-plus-outline"></ha-icon> ${t("templates_from", L)}
|
||||
</ha-button>
|
||||
<ha-button appearance="plain" @click=${() => this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog")?.openCreate()}>
|
||||
<ha-button appearance="plain" @click=${() => this._ui<MaintenanceObjectDialog>("maintenance-object-dialog").then((d) => d?.openCreate())}>
|
||||
${t("new_object", L)}
|
||||
</ha-button>
|
||||
</div>
|
||||
@@ -2720,7 +2782,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
` : nothing}
|
||||
${!isOperator ? html`
|
||||
<ha-button
|
||||
@click=${() => this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog")?.openCreate()}
|
||||
@click=${() => this._ui<MaintenanceObjectDialog>("maintenance-object-dialog").then((d) => d?.openCreate())}
|
||||
>
|
||||
${t("new_object", L)}
|
||||
</ha-button>
|
||||
@@ -3115,12 +3177,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<div class="action-buttons">
|
||||
${!isOperator ? html`
|
||||
<ha-button appearance="filled" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog");
|
||||
dlg?.openCreate(obj.entry_id);
|
||||
void this._ui<MaintenanceTaskDialog>("maintenance-task-dialog")
|
||||
.then((d) => d?.openCreate(obj.entry_id));
|
||||
}}>${t("add_task", L)}</ha-button>
|
||||
<ha-button appearance="plain" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceObjectDialog>("maintenance-object-dialog");
|
||||
dlg?.openEdit(obj.entry_id, o);
|
||||
void this._ui<MaintenanceObjectDialog>("maintenance-object-dialog")
|
||||
.then((d) => d?.openEdit(obj.entry_id, o));
|
||||
}}>${t("edit", L)}</ha-button>
|
||||
` : nothing}
|
||||
<div class="more-menu-wrapper">
|
||||
@@ -3192,8 +3254,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
? html`<div class="empty-state-centered">
|
||||
<p class="empty">${t("no_tasks_yet", L)}</p>
|
||||
<ha-button appearance="filled" @click=${() => {
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog");
|
||||
dlg?.openCreate(obj.entry_id);
|
||||
void this._ui<MaintenanceTaskDialog>("maintenance-task-dialog")
|
||||
.then((d) => d?.openCreate(obj.entry_id));
|
||||
}}>${t("add_first_task", L)}</ha-button>
|
||||
</div>`
|
||||
: html`<div class="task-table">${[...visibleTasks].sort((a, b) => {
|
||||
@@ -3372,7 +3434,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
showObject: () => this._showObject(entryId),
|
||||
toggleMoreMenu: () => this._toggleMoreMenu(),
|
||||
closeMoreMenu: () => this._closeMoreMenu(),
|
||||
openEdit: (tk) => { this.shadowRoot!.querySelector<MaintenanceTaskDialog>("maintenance-task-dialog")?.openEdit(entryId, tk); },
|
||||
openEdit: (tk) => { void this._ui<MaintenanceTaskDialog>("maintenance-task-dialog").then((d) => d?.openEdit(entryId, tk)); },
|
||||
openComplete: (tk) => this._openCompleteDialog(entryId, taskId, tk.name, this._features.checklists ? tk.checklist : undefined, this._features.adaptive && !!tk.adaptive_config?.enabled),
|
||||
promptSkip: () => this._promptSkipTask(entryId, taskId),
|
||||
toggleArchive: (archived) => this._toggleArchiveTask(entryId, taskId, archived),
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface MaintenanceObject {
|
||||
/** Attached documents tagged as manuals — the fallback for the "manual"
|
||||
* column/header when documentation_url is unset. Computed server-side. */
|
||||
manual_docs?: ManualDocRef[];
|
||||
/** Battery-fleet markers (v2.53 field audit): true on THE fleet object;
|
||||
* excluded = entity_ids manually excluded from the fleet roster. */
|
||||
battery_fleet?: boolean;
|
||||
battery_fleet_excluded?: string[];
|
||||
}
|
||||
|
||||
/** Slim reference to a manual-tagged document (subset of MaintenanceDocument). */
|
||||
|
||||
@@ -15,6 +15,7 @@ from ..const import (
|
||||
CARD_URL,
|
||||
DOMAIN,
|
||||
LOCALES_URL,
|
||||
PANEL_CHUNKS_URL,
|
||||
STRATEGY_CHUNKS_URL,
|
||||
STRATEGY_SHIM_URL,
|
||||
STRATEGY_URL,
|
||||
@@ -78,6 +79,17 @@ async def async_register_card(hass: HomeAssistant) -> None:
|
||||
str(frontend_dir / "maintenance-calendar-card.js"),
|
||||
False,
|
||||
),
|
||||
# Panel code-split chunks (perf wave 2, item 5): the panel entry
|
||||
# imports them by ABSOLUTE URL (esbuild publicPath) because its own
|
||||
# module_url is a versioned file path with no directory to resolve
|
||||
# relative imports against. Chunk names are content-hashed, and the
|
||||
# entry URL re-hashes with every build — a stale entry can never
|
||||
# name a fresh chunk or vice versa (#124 class).
|
||||
StaticPathConfig(
|
||||
PANEL_CHUNKS_URL,
|
||||
str(frontend_dir / "panel-chunks"),
|
||||
False,
|
||||
),
|
||||
# Runtime-loaded UI translations: a directory of <lang>.json fetched by
|
||||
# the panel/card on demand (only EN is bundled into the JS). cache=False
|
||||
# so a translation edit shows up on reload without a version bump.
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flotila baterií",
|
||||
"battery_fleet_setup_done": "Flotila baterií nastavena — jeden úkol sleduje všechny.",
|
||||
"update_banner": "Na serveru je novější verze Maintenance Supporter — načtěte znovu pro aktualizaci panelu.",
|
||||
"update_reload": "Načíst znovu"
|
||||
"update_reload": "Načíst znovu",
|
||||
"battery_fleet_forecast_overdue": "Předpovězené datum uplynulo — baterie stále hlásí dobrý stav. Pokud jste ji vyměnili, zaznamenejte výměnu; jinak byla předpověď mylná.",
|
||||
"cost_from_parts": "Použít ≈ {amount} z dílů"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflåde",
|
||||
"battery_fleet_setup_done": "Batteriflåde opsat — én opgave følger alle batterier.",
|
||||
"update_banner": "En nyere version af Maintenance Supporter er på serveren — genindlæs for at opdatere panelet.",
|
||||
"update_reload": "Genindlæs"
|
||||
"update_reload": "Genindlæs",
|
||||
"battery_fleet_forecast_overdue": "Forudsagt dato er overskredet — batteriet melder stadig god tilstand. Hvis du har skiftet det, registrér udskiftningen; ellers ramte prognosen forbi.",
|
||||
"cost_from_parts": "Brug ≈ {amount} fra dele"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batterie-Flotte",
|
||||
"battery_fleet_setup_done": "Batterie-Flotte eingerichtet — ein Task überwacht alle Batterien.",
|
||||
"update_banner": "Auf dem Server läuft eine neuere Version von Maintenance Supporter — neu laden, um das Panel zu aktualisieren.",
|
||||
"update_reload": "Neu laden"
|
||||
"update_reload": "Neu laden",
|
||||
"battery_fleet_forecast_overdue": "Prognosedatum überschritten — die Batterie meldet sich weiterhin gesund. Falls du sie gewechselt hast, trage den Wechsel nach; andernfalls lag die Prognose daneben.",
|
||||
"cost_from_parts": "≈ {amount} aus Teilen übernehmen"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Battery fleet",
|
||||
"battery_fleet_setup_done": "Battery fleet set up — one task tracks all your batteries.",
|
||||
"update_banner": "A newer version of Maintenance Supporter is on the server — reload to update the panel.",
|
||||
"update_reload": "Reload"
|
||||
"update_reload": "Reload",
|
||||
"battery_fleet_forecast_overdue": "Predicted date passed — the battery still reports healthy. If you swapped it, record the replacement; otherwise the forecast was off.",
|
||||
"cost_from_parts": "Use ≈ {amount} from parts"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flota de baterías",
|
||||
"battery_fleet_setup_done": "Flota de baterías configurada — una tarea controla todas.",
|
||||
"update_banner": "Hay una versión más reciente de Maintenance Supporter en el servidor — recarga para actualizar el panel.",
|
||||
"update_reload": "Recargar"
|
||||
"update_reload": "Recargar",
|
||||
"battery_fleet_forecast_overdue": "Fecha prevista superada: la batería sigue informando buen estado. Si la cambiaste, registra el reemplazo; si no, la previsión falló.",
|
||||
"cost_from_parts": "Usar ≈ {amount} de las piezas"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Paristokanta",
|
||||
"battery_fleet_setup_done": "Paristokanta määritetty — yksi tehtävä seuraa kaikkia.",
|
||||
"update_banner": "Palvelimella on uudempi Maintenance Supporter -versio — lataa uudelleen päivittääksesi paneelin.",
|
||||
"update_reload": "Lataa uudelleen"
|
||||
"update_reload": "Lataa uudelleen",
|
||||
"battery_fleet_forecast_overdue": "Ennustettu päivä on ohitettu — akku ilmoittaa yhä hyvästä kunnosta. Jos vaihdoit sen, kirjaa vaihto; muuten ennuste oli pielessä.",
|
||||
"cost_from_parts": "Käytä ≈ {amount} osista"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Parc de piles",
|
||||
"battery_fleet_setup_done": "Parc de piles configuré — une tâche suit toutes vos piles.",
|
||||
"update_banner": "Une version plus récente de Maintenance Supporter est sur le serveur — rechargez pour mettre à jour le panneau.",
|
||||
"update_reload": "Recharger"
|
||||
"update_reload": "Recharger",
|
||||
"battery_fleet_forecast_overdue": "Date prévue dépassée — la batterie se signale toujours en bon état. Si vous l'avez remplacée, enregistrez le remplacement ; sinon la prévision était erronée.",
|
||||
"cost_from_parts": "Reprendre ≈ {amount} des pièces"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "बैटरी फ्लीट",
|
||||
"battery_fleet_setup_done": "बैटरी फ्लीट सेट — एक कार्य सभी बैटरियों को ट्रैक करता है।",
|
||||
"update_banner": "सर्वर पर Maintenance Supporter का नया संस्करण है — पैनल अपडेट करने के लिए पुनः लोड करें।",
|
||||
"update_reload": "पुनः लोड करें"
|
||||
"update_reload": "पुनः लोड करें",
|
||||
"battery_fleet_forecast_overdue": "अनुमानित तिथि बीत गई — बैटरी अब भी अच्छी स्थिति बता रही है। यदि आपने इसे बदला है, तो बदलाव दर्ज करें; अन्यथा पूर्वानुमान गलत था।",
|
||||
"cost_from_parts": "पुर्ज़ों से ≈ {amount} लें"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Elemflotta",
|
||||
"battery_fleet_setup_done": "Elemflotta beállítva — egyetlen feladat követi az összes elemet.",
|
||||
"update_banner": "A Maintenance Supporter újabb verziója érhető el a szerveren — töltse újra az oldalt a panel frissítéséhez.",
|
||||
"update_reload": "Újratöltés"
|
||||
"update_reload": "Újratöltés",
|
||||
"battery_fleet_forecast_overdue": "Az előrejelzett dátum elmúlt — az elem továbbra is jó állapotot jelez. Ha kicserélted, rögzítsd a cserét; különben az előrejelzés tévedett.",
|
||||
"cost_from_parts": "≈ {amount} átvétele az alkatrészekből"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Parco batterie",
|
||||
"battery_fleet_setup_done": "Parco batterie configurato — un'attività monitora tutte.",
|
||||
"update_banner": "Sul server è presente una versione più recente di Maintenance Supporter — ricarica per aggiornare il pannello.",
|
||||
"update_reload": "Ricarica"
|
||||
"update_reload": "Ricarica",
|
||||
"battery_fleet_forecast_overdue": "Data prevista superata — la batteria risulta ancora in buono stato. Se l'hai sostituita, registra la sostituzione; altrimenti la previsione era errata.",
|
||||
"cost_from_parts": "Usa ≈ {amount} dai ricambi"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "電池フリート",
|
||||
"battery_fleet_setup_done": "電池フリートを設定 — 1つのタスクで全電池を管理。",
|
||||
"update_banner": "サーバーに新しいバージョンの Maintenance Supporter があります — 再読み込みしてパネルを更新してください。",
|
||||
"update_reload": "再読み込み"
|
||||
"update_reload": "再読み込み",
|
||||
"battery_fleet_forecast_overdue": "予測日を過ぎましたが、電池はまだ正常と報告しています。交換済みなら交換を記録してください。そうでなければ予測が外れています。",
|
||||
"cost_from_parts": "部品から ≈ {amount} を適用"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "배터리 플릿",
|
||||
"battery_fleet_setup_done": "배터리 플릿이 설정되었습니다 — 하나의 작업이 모든 배터리를 추적합니다.",
|
||||
"update_banner": "서버에 Maintenance Supporter의 새 버전이 있습니다 — 패널을 업데이트하려면 새로 고침하세요.",
|
||||
"update_reload": "새로 고침"
|
||||
"update_reload": "새로 고침",
|
||||
"battery_fleet_forecast_overdue": "예측 날짜가 지났지만 배터리는 여전히 정상으로 보고됩니다. 교체했다면 교체를 기록하세요. 아니라면 예측이 빗나간 것입니다.",
|
||||
"cost_from_parts": "부품에서 ≈ {amount} 적용"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflåte",
|
||||
"battery_fleet_setup_done": "Batteriflåte satt opp — én oppgave følger alle batteriene.",
|
||||
"update_banner": "En nyere versjon av Maintenance Supporter er på serveren — last inn på nytt for å oppdatere panelet.",
|
||||
"update_reload": "Last inn på nytt"
|
||||
"update_reload": "Last inn på nytt",
|
||||
"battery_fleet_forecast_overdue": "Forutsagt dato er passert — batteriet melder fortsatt god tilstand. Hvis du byttet det, registrer byttet; ellers bommet prognosen.",
|
||||
"cost_from_parts": "Bruk ≈ {amount} fra deler"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batterijvloot",
|
||||
"battery_fleet_setup_done": "Batterijvloot ingesteld — één taak volgt al je batterijen.",
|
||||
"update_banner": "Er staat een nieuwere versie van Maintenance Supporter op de server — herlaad om het paneel bij te werken.",
|
||||
"update_reload": "Herladen"
|
||||
"update_reload": "Herladen",
|
||||
"battery_fleet_forecast_overdue": "Voorspelde datum verstreken — de batterij meldt zich nog steeds gezond. Heb je hem vervangen, registreer dan de vervanging; anders zat de voorspelling ernaast.",
|
||||
"cost_from_parts": "≈ {amount} uit onderdelen overnemen"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Flota baterii",
|
||||
"battery_fleet_setup_done": "Flota baterii skonfigurowana — jedno zadanie śledzi wszystkie.",
|
||||
"update_banner": "Na serwerze jest nowsza wersja Maintenance Supporter — załaduj ponownie, aby zaktualizować panel.",
|
||||
"update_reload": "Załaduj ponownie"
|
||||
"update_reload": "Załaduj ponownie",
|
||||
"battery_fleet_forecast_overdue": "Przewidywana data minęła — bateria nadal zgłasza dobry stan. Jeśli ją wymieniono, zapisz wymianę; w przeciwnym razie prognoza była błędna.",
|
||||
"cost_from_parts": "Użyj ≈ {amount} z części"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma única tarefa acompanha todas as suas baterias.",
|
||||
"update_banner": "Há uma versão mais nova do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
|
||||
"update_reload": "Recarregar"
|
||||
"update_reload": "Recarregar",
|
||||
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria ainda reporta bom estado. Se você a trocou, registre a troca; caso contrário, a previsão errou.",
|
||||
"cost_from_parts": "Usar ≈ {amount} das peças"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Frota de baterias",
|
||||
"battery_fleet_setup_done": "Frota de baterias configurada — uma tarefa acompanha todas.",
|
||||
"update_banner": "Há uma versão mais recente do Maintenance Supporter no servidor — recarregue para atualizar o painel.",
|
||||
"update_reload": "Recarregar"
|
||||
"update_reload": "Recarregar",
|
||||
"battery_fleet_forecast_overdue": "Data prevista ultrapassada — a bateria continua a reportar bom estado. Se a substituiu, registe a substituição; caso contrário, a previsão falhou.",
|
||||
"cost_from_parts": "Usar ≈ {amount} das peças"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей настроен — одна задача следит за всеми.",
|
||||
"update_banner": "На сервере более новая версия Maintenance Supporter — перезагрузите, чтобы обновить панель.",
|
||||
"update_reload": "Перезагрузить"
|
||||
"update_reload": "Перезагрузить",
|
||||
"battery_fleet_forecast_overdue": "Прогнозируемая дата прошла — батарея по-прежнему сообщает о хорошем состоянии. Если вы её заменили, зафиксируйте замену; иначе прогноз оказался неверным.",
|
||||
"cost_from_parts": "Взять ≈ {amount} из запчастей"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Batteriflotta",
|
||||
"battery_fleet_setup_done": "Batteriflotta konfigurerad — en uppgift följer alla batterier.",
|
||||
"update_banner": "En nyare version av Maintenance Supporter finns på servern — ladda om för att uppdatera panelen.",
|
||||
"update_reload": "Ladda om"
|
||||
"update_reload": "Ladda om",
|
||||
"battery_fleet_forecast_overdue": "Förutsagt datum har passerat — batteriet rapporterar fortfarande god status. Om du bytte det, registrera bytet; annars slog prognosen fel.",
|
||||
"cost_from_parts": "Använd ≈ {amount} från delar"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Pil filosu",
|
||||
"battery_fleet_setup_done": "Pil filosu kuruldu — tek bir görev tüm pillerinizi takip ediyor.",
|
||||
"update_banner": "Sunucuda Maintenance Supporter'ın daha yeni bir sürümü var — paneli güncellemek için sayfayı yeniden yükleyin.",
|
||||
"update_reload": "Yeniden yükle"
|
||||
"update_reload": "Yeniden yükle",
|
||||
"battery_fleet_forecast_overdue": "Öngörülen tarih geçti — pil hâlâ sağlıklı görünüyor. Değiştirdiyseniz değişimi kaydedin; aksi halde tahmin yanılmış demektir.",
|
||||
"cost_from_parts": "Parçalardan ≈ {amount} kullan"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "Парк батарей",
|
||||
"battery_fleet_setup_done": "Парк батарей налаштовано — одне завдання стежить за всіма.",
|
||||
"update_banner": "На сервері новіша версія Maintenance Supporter — перезавантажте, щоб оновити панель.",
|
||||
"update_reload": "Перезавантажити"
|
||||
"update_reload": "Перезавантажити",
|
||||
"battery_fleet_forecast_overdue": "Прогнозована дата минула — батарея й далі повідомляє про добрий стан. Якщо ви її замінили, зафіксуйте заміну; інакше прогноз не справдився.",
|
||||
"cost_from_parts": "Узяти ≈ {amount} із запчастин"
|
||||
}
|
||||
|
||||
@@ -833,5 +833,7 @@
|
||||
"battery_fleet_setup_button": "电池群",
|
||||
"battery_fleet_setup_done": "电池群已设置 — 一个任务跟踪所有电池。",
|
||||
"update_banner": "服务器上有更新版本的 Maintenance Supporter——请重新加载以更新面板。",
|
||||
"update_reload": "重新加载"
|
||||
"update_reload": "重新加载",
|
||||
"battery_fleet_forecast_overdue": "预测日期已过——电池仍报告状态良好。如果您已更换电池,请记录更换;否则说明预测有误。",
|
||||
"cost_from_parts": "采用配件合计 ≈ {amount}"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
var S="2.52.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
var S="2.53.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
|
||||
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as d}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MEKM6THN.js";import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-URNT5464.js";import{a,b as _,c as t,f as l,g as h,i as g,j as n,n as i,p as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var r=class extends h{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._users=[];this._responsible="";this._localeReady=!1;this._userService=null;this._toggle=s=>{let o=new Set(this._selected);o.has(s)?o.delete(s):o.add(s),this._selected=o};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(s=>s.entity_id))};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let s=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:e.suggested_entry_id??void 0,object_name:e.suggested_object_name,device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0})),o=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/adopt",selections:s});this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:o})),this._open=!1}catch(s){this._error=d(s,this._lang)}finally{this._adopting=!1}}}}get _lang(){return this.hass?.language||"en"}updated(s){s.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,v(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._responsible="";try{this._userService?this._userService.updateHass(this.hass):this._userService=new u(this.hass);let[s,o]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/discover"}),this._userService.getUsers().catch(()=>[])]);this._sensors=s.sensors||[],this._selected=new Set(this._sensors.map(e=>e.entity_id)),this._users=o}catch(s){this._error=d(s,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return t``;let s=this._lang,o=this._sensors.length>0&&this._selected.size===this._sensors.length;return t`
|
||||
<div class="overlay" @click=${this._close}>
|
||||
<div class="card" @click=${e=>e.stopPropagation()}>
|
||||
<div class="title">${i("adopt_problem_title",s)}</div>
|
||||
<div class="hint">${i("adopt_problem_hint",s)}</div>
|
||||
${this._error?t`<div class="error">${this._error}</div>`:l}
|
||||
|
||||
${this._loading?t`<div class="loading">…</div>`:this._sensors.length===0?t`<div class="empty">${i("adopt_problem_none",s)}</div>`:t`
|
||||
<label class="select-all">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${o}
|
||||
@change=${this._toggleAll}
|
||||
/>
|
||||
<span>${i("selected",s)}: ${this._selected.size} / ${this._sensors.length}</span>
|
||||
</label>
|
||||
<div class="list">
|
||||
${this._sensors.map(e=>{let m=this._selected.has(e.entity_id),p=e.state==="on",c=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return t`
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${m}
|
||||
@change=${()=>this._toggle(e.entity_id)}
|
||||
/>
|
||||
<div class="row-main">
|
||||
<div class="row-top">
|
||||
<span class="row-name">${e.name}</span>
|
||||
<span class="chip ${p?"chip-active":"chip-ok"}">
|
||||
${p?i("adopt_problem_active",s):i("adopt_problem_ok",s)}
|
||||
</span>
|
||||
</div>
|
||||
${c?t`<div class="row-sub">${c}</div>`:l}
|
||||
<div class="row-target">
|
||||
→ ${e.suggested_object_name}${e.suggested_entry_id?l:t` <span class="new-tag">${i("adopt_problem_new_object",s)}</span>`}
|
||||
</div>
|
||||
${e.suggested_part_name?t`<div class="row-part">
|
||||
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
|
||||
${i("adopt_problem_part",s).replace("{name}",e.suggested_part_name)}
|
||||
</div>`:l}
|
||||
</div>
|
||||
</label>
|
||||
`})}
|
||||
</div>
|
||||
`}
|
||||
|
||||
${!this._loading&&this._sensors.length>0&&this._users.length>0?t`
|
||||
<label class="responsible">
|
||||
<span>${i("adopt_problem_responsible",s)}</span>
|
||||
<select
|
||||
.value=${this._responsible}
|
||||
@change=${e=>{this._responsible=e.target.value}}
|
||||
>
|
||||
<option value="" ?selected=${!this._responsible}>${i("no_user_assigned",s)}</option>
|
||||
${this._users.map(e=>t`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
`:l}
|
||||
|
||||
<div class="actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${i("cancel",s)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._adopt}
|
||||
.disabled=${this._selected.size===0||this._adopting}
|
||||
>
|
||||
${i("adopt_problem_adopt",s)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}};r.styles=_`
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 560px;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.hint {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
.loading,
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-all input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row input {
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.row-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
.row-sub {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-target {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-part {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.row-part ha-icon {
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
.new-tag {
|
||||
font-style: italic;
|
||||
}
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip-active {
|
||||
background: var(--error-color, #f44336);
|
||||
color: #fff;
|
||||
}
|
||||
.chip-ok {
|
||||
background: var(--divider-color);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.responsible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.responsible select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
`,a([g({attribute:!1})],r.prototype,"hass",2),a([n()],r.prototype,"_open",2),a([n()],r.prototype,"_loading",2),a([n()],r.prototype,"_adopting",2),a([n()],r.prototype,"_error",2),a([n()],r.prototype,"_sensors",2),a([n()],r.prototype,"_selected",2),a([n()],r.prototype,"_users",2),a([n()],r.prototype,"_responsible",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",r);export{r as MaintenanceAdoptProblemSensorsDialog};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{n as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";function _(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let t=!!e.entry_id&&e.entry_id!==r,a=t?e.entry_id:r,o=s.find(p=>p.entry_id===a),n=(o?.parts||[]).find(p=>p.id===e.part_id)||null,d=t&&o?.object?.name||"",i=n?.name||u("shared_part_unknown",c);return{part:n,foreign:t,ownerName:d,label:d?`${i} (${d})`:i}}function f(e,r,s,c){let{part:t,label:a}=l(e,r,s,c),o=t&&t.stock!==null&&t.stock!==void 0?` (${t.stock}${t.unit?" "+t.unit:""})`:"",n=t?.storage_location?` \u2014 ${t.storage_location}`:"";return`${e.quantity}\xD7 ${a}${o}${n}`}function g(e,r,s,c){let a=(s.find(n=>n.entry_id===r)?.parts||[]).map(n=>({...n})),o=new Set(a.map(n=>_({part_id:n.id})));for(let n of e?.consumes_parts||[]){if(!n.entry_id||n.entry_id===r)continue;let d=_(n);if(o.has(d))continue;o.add(d);let{part:i,ownerName:p}=l(n,r,s,c);a.push({id:n.part_id,name:i?.name||u("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:n.entry_id,owner_name:p})}return a}export{_ as a,f as b,g as c};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
var o=["notes","cost","duration","photo","user"],t={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{o as a,t as b};
|
||||
+8
-8
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as t,b as a,c as i,f as l,g as p,i as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var e=class extends p{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(n){let o=n.target.value;this.value=o,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:o}}))}render(){return i`
|
||||
<label class="field">
|
||||
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:l}</span>`:l}
|
||||
<input
|
||||
.value=${this.value??""}
|
||||
.type=${this.type}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
step=${this.step??l}
|
||||
min=${this.min??l}
|
||||
max=${this.max??l}
|
||||
pattern=${this.pattern??l}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
/>
|
||||
${this.helper?i`<span class="helper">${this.helper}</span>`:l}
|
||||
</label>
|
||||
`}};e.styles=a`
|
||||
:host { display: block; }
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color, #888);
|
||||
font-weight: 500;
|
||||
}
|
||||
.req { color: var(--error-color, #f44336); margin-left: 2px; }
|
||||
input {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
`,t([r()],e.prototype,"label",2),t([r()],e.prototype,"value",2),t([r()],e.prototype,"placeholder",2),t([r()],e.prototype,"type",2),t([r({type:Boolean})],e.prototype,"required",2),t([r({type:Boolean})],e.prototype,"disabled",2),t([r()],e.prototype,"step",2),t([r()],e.prototype,"min",2),t([r()],e.prototype,"max",2),t([r()],e.prototype,"pattern",2),t([r()],e.prototype,"helper",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e);
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
var i=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],s=i.map(n=>n.key),l=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function c(n){if(!Array.isArray(n))return[...l];let o=new Set,e=[];for(let a of n)typeof a=="string"&&s.includes(a)&&!o.has(a)&&(o.add(a),e.push(a));return e.length?(e.includes("name")||e.unshift("name"),e):[...l]}function u(n,o,e){let a=new Blob([n],{type:e}),r=URL.createObjectURL(a),t=document.createElement("a");t.href=r,t.download=o,t.target="_blank",t.rel="noopener",t.style.display="none",document.body.appendChild(t),t.dispatchEvent(new MouseEvent("click")),document.body.removeChild(t),setTimeout(()=>URL.revokeObjectURL(r),6e4)}function y(n,o){let e=document.createElement("a");e.href=n,e.download=o,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}export{i as a,l as b,c,u as d,y as e};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{n as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var s={name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_for_minutes:"trigger_for_minutes"};function c(r,o){let e=s[r];if(!e)return r;let t=a(e,o);return t&&t!==e?t:r}function d(r){let e=r.match(/data\['([^']+)'\]/)?.[1],t;return(t=r.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=r.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=r.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=r.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(r)?{field:e,rule:"required"}:(t=r.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(r)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(r)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function g(r,o,e){if(e=e??a("action_error",o),typeof r=="string")return r;if(typeof r!="object"||r===null)return e;let t=r,_=t.message||t.error?.message||"";if(!_)return e;let i=d(_),l=i.field?c(i.field,o):"",n=u=>a(u,o).replace("{field}",l).replace("{n}",i.param??"");switch(i.rule){case"too_long":return n("err_too_long");case"too_short":return n("err_too_short");case"value_too_high":return n("err_value_too_high");case"value_too_low":return n("err_value_too_low");case"required":return n("err_required");case"wrong_type":return n("err_wrong_type").replace("{type}",i.param??"");case"invalid_choice":return n("err_invalid_choice");case"invalid_value":return n("err_invalid_value");default:return _||e}}export{g as a};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
var r=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{r as a};
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{b as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4AV2K4W7.js";import{a as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FYWLAW5.js";import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MEKM6THN.js";import{a as s,b,c as a,f as d,g as v,i as n,j as l,n as r,x as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var i=class extends v{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[_(e),{...e}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,o=t.files?.[0];if(t.value="",!!o){this._photoUploading=!0,this._error="";try{let c=new FormData;c.append("entry_id",this.entryId),c.append("tags","photo"),c.append("file",o,o.name);let p=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:c});if(!p.ok){this._error=p.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let u=await p.json();u.id&&(this._photoDocId=u.id,this._photoPreview=URL.createObjectURL(o))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=g(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?a`<span class="req-mark" aria-hidden="true">*</span>`:d}_partsCostSuggestion(){if(this.restockDefault!==null){let o=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(o)||o<=0?null:Math.round(this.restockUnitCost*o*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let o of Object.values(this._usedParts)){let c=this.parts.find(p=>_({part_id:p.id,entry_id:p.entry_id})===_(o));c?.cost!=null&&(e+=c.cost*(o.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return d;let t=this._partsCostSuggestion();if(t==null||t<=0)return d;let o=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return a`<button
|
||||
type="button"
|
||||
class="cost-suggestion"
|
||||
@click=${()=>this._cost=t.toFixed(2)}
|
||||
>${r("cost_from_parts",e).replace("{amount}",o)}</button>`}_close(){this._open=!1}render(){if(!this._open)return a``;let e=this.lang||this.hass?.language||"en";return a`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${r("complete_title",e)}${this.taskName}</div>
|
||||
<div class="content">
|
||||
${this._error?a`<div class="error">${this._error}</div>`:d}
|
||||
${this.checklist.length>0?a`
|
||||
<div class="checklist-section">
|
||||
<label class="checklist-label">${r("checklist",e)}</label>
|
||||
${this.checklist.map((t,o)=>a`
|
||||
<label class="checklist-item" @click=${()=>this._toggleCheck(o)}>
|
||||
<input type="checkbox" .checked=${!!this._checklistState[String(o)]} />
|
||||
<span>${t}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
`:d}
|
||||
${this.taskType==="reading"?a`
|
||||
<label class="field">
|
||||
<span class="field-label">${r("reading_value_label",e)}${this.readingUnit?` (${this.readingUnit})`:""}</span>
|
||||
<input type="number" step="any" class="field-input"
|
||||
.value=${this._readingValue}
|
||||
@input=${t=>this._readingValue=t.target.value} />
|
||||
</label>`:d}
|
||||
${this.parts.length?a`<div class="used-parts">
|
||||
<span class="field-label">${r("complete_parts_used",e)}</span>
|
||||
${this.parts.map(t=>{let o=_({part_id:t.id,entry_id:t.entry_id}),c=this._usedParts[o],p=c!==void 0,u=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return a`<div class="used-part-row">
|
||||
<label class="used-part-check">
|
||||
<input type="checkbox" .checked=${p}
|
||||
@change=${f=>{let h={...this._usedParts};f.target.checked?h[o]=h[o]||u:delete h[o],this._usedParts=h}} />
|
||||
<span
|
||||
>${t.name}${t.owner_name?a`<span class="used-part-owner"> (${t.owner_name})</span>`:d}${t.stock!==null&&t.stock!==void 0?` (${t.stock}${t.unit?" "+t.unit:""})`:""}</span
|
||||
>
|
||||
</label>
|
||||
${p?a`<input class="used-part-qty" type="number" min="0.01" max="999" step="0.01"
|
||||
.value=${String(c.quantity)}
|
||||
@input=${f=>{let h=parseFloat(f.target.value);this._usedParts={...this._usedParts,[o]:{...u,quantity:Number.isFinite(h)&&h>=.01?h:1}}}} />`:d}
|
||||
</div>`})}
|
||||
</div>`:this.consumesInfo.length?a`<div class="consumes-hint">
|
||||
${this.consumesInfo.map(t=>a`<div>${t}</div>`)}
|
||||
</div>`:d}
|
||||
${this.restockDefault!==null?a`
|
||||
<label class="field">
|
||||
<span class="field-label">${r("restock_quantity_label",e)}</span>
|
||||
<input type="number" step="0.01" min="0.01" class="field-input"
|
||||
.value=${this._restockQty}
|
||||
@input=${t=>this._restockQty=t.target.value} />
|
||||
</label>`:d}
|
||||
<!-- Native <input>s rather than <ha-textfield>: when this dialog
|
||||
is opened from a Lovelace card via dialog-mount, ha-textfield
|
||||
isn't yet registered (HA loads it lazily when its own panels
|
||||
need it) so the elements render with zero height and the user
|
||||
only sees the title + Cancel/Complete buttons — the original
|
||||
bug report. Native inputs always render. -->
|
||||
<label class="field">
|
||||
<span class="field-label">${r("notes_optional",e)}${this._req("notes")}</span>
|
||||
<input type="text" class="field-input"
|
||||
.value=${this._notes}
|
||||
@input=${t=>this._notes=t.target.value} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${r("cost_optional",e)}${this._req("cost")}</span>
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._cost}
|
||||
@input=${t=>this._cost=t.target.value} />
|
||||
${this._renderCostSuggestion(e)}
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${r("duration_minutes",e)}${this._req("duration")}</span>
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._duration}
|
||||
@input=${t=>this._duration=t.target.value} />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">${r("completion_photo_optional",e)}${this._req("photo")}</span>
|
||||
${this._photoPreview?a`
|
||||
<div class="photo-preview">
|
||||
<img src=${this._photoPreview} alt="" />
|
||||
<button type="button" class="photo-remove" @click=${this._removePhoto}
|
||||
title="${r("remove",e)}">✕</button>
|
||||
</div>`:a`
|
||||
<label class="photo-pick">
|
||||
<ha-icon icon="mdi:camera"></ha-icon>
|
||||
<span>${this._photoUploading?r("uploading",e):r("add_photo",e)}</span>
|
||||
<input type="file" accept="image/*" capture="environment"
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>`}
|
||||
</div>
|
||||
${this.adaptiveEnabled?a`
|
||||
<div class="feedback-section">
|
||||
<label class="feedback-label">${r("was_maintenance_needed",e)}</label>
|
||||
<div class="feedback-buttons">
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="needed"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("needed")}
|
||||
>${r("feedback_needed",e)}</button>
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="not_needed"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("not_needed")}
|
||||
>${r("feedback_not_needed",e)}</button>
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="not_sure"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("not_sure")}
|
||||
>${r("feedback_not_sure",e)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`:d}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${r("cancel",e)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._complete}
|
||||
.disabled=${this._loading||this._missingRequired.length>0}
|
||||
title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(m[t]??t,e))).join(" \xB7 "):""}
|
||||
>
|
||||
${this._loading?r("completing",e):r("complete",e)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};i.styles=[k,b`
|
||||
.req-mark {
|
||||
color: var(--error-color, #f44336);
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
|
||||
.cost-suggestion {
|
||||
align-self: flex-start;
|
||||
margin-top: 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.consumes-hint {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
border-left: 3px solid var(--primary-color);
|
||||
padding: 4px 8px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
/* #99: editable per-completion parts selection */
|
||||
.used-parts { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.used-part-row { display: flex; align-items: center; gap: 8px; }
|
||||
.used-part-check {
|
||||
display: flex; align-items: center; gap: 6px; flex: 1;
|
||||
font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.used-part-check input { cursor: pointer; }
|
||||
/* #111: whose stock this row draws on. Muted but never omitted — an
|
||||
unlabelled foreign pool is indistinguishable from an own part. */
|
||||
.used-part-owner { color: var(--secondary-text-color); }
|
||||
.used-part-qty {
|
||||
width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
/* .field/.field-label/.field-input come from nativeFieldStyles */
|
||||
.photo-pick {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px dashed var(--divider-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
width: fit-content;
|
||||
}
|
||||
.photo-pick:hover { border-color: var(--primary-color); }
|
||||
.photo-pick input[type="file"] { display: none; }
|
||||
.photo-preview {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
.photo-preview img {
|
||||
max-width: 160px;
|
||||
max-height: 160px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--error-color, #db4437);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.checklist-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.checklist-label {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.checklist-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.feedback-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.feedback-label {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.feedback-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.feedback-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 8px;
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.feedback-btn:hover {
|
||||
background: var(--secondary-background-color, #f5f5f5);
|
||||
}
|
||||
.feedback-btn.selected {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color, #fff);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
`],s([n({attribute:!1})],i.prototype,"hass",2),s([n()],i.prototype,"entryId",2),s([n()],i.prototype,"taskId",2),s([n()],i.prototype,"taskName",2),s([n()],i.prototype,"lang",2),s([n({type:Array})],i.prototype,"checklist",2),s([n({type:Boolean})],i.prototype,"adaptiveEnabled",2),s([n()],i.prototype,"taskType",2),s([n()],i.prototype,"readingUnit",2),s([n({attribute:!1})],i.prototype,"restockDefault",2),s([n({attribute:!1})],i.prototype,"restockUnitCost",2),s([n()],i.prototype,"currencySymbol",2),s([n({attribute:!1})],i.prototype,"parts",2),s([n({attribute:!1})],i.prototype,"consumesParts",2),s([n({type:Array})],i.prototype,"consumesInfo",2),s([n({type:Array})],i.prototype,"requiredFields",2),s([l()],i.prototype,"_open",2),s([l()],i.prototype,"_notes",2),s([l()],i.prototype,"_cost",2),s([l()],i.prototype,"_duration",2),s([l()],i.prototype,"_loading",2),s([l()],i.prototype,"_error",2),s([l()],i.prototype,"_checklistState",2),s([l()],i.prototype,"_feedback",2),s([l()],i.prototype,"_photoDocId",2),s([l()],i.prototype,"_photoPreview",2),s([l()],i.prototype,"_photoUploading",2),s([l()],i.prototype,"_readingValue",2),s([l()],i.prototype,"_restockQty",2),s([l()],i.prototype,"_usedParts",2),s([n({attribute:!1})],i.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",i);export{i as MaintenanceCompleteDialog};
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-6CM3ZIHM.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MEKM6THN.js";import{a as r,b as _,c as l,f as o,g as p,i as d,j as s,n as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var e=class extends p{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return this.hass?.language??navigator.language.split("-")[0]??"en"}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(a,n){this._entryId=a,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(a){this._error=h(a,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(a=>a.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let a=this._lang,n=this._entryId?i("edit_object",a):i("new_object",a);return l`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${n}</div>
|
||||
<div class="content">
|
||||
${this._error?l`<div class="error">${this._error}</div>`:o}
|
||||
<ms-textfield
|
||||
label="${i("name",a)}"
|
||||
required
|
||||
.value=${this._name}
|
||||
@input=${t=>this._name=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("manufacturer_optional",a)}"
|
||||
.value=${this._manufacturer}
|
||||
@input=${t=>this._manufacturer=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("model_optional",a)}"
|
||||
.value=${this._model}
|
||||
@input=${t=>this._model=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("serial_number_optional",a)}"
|
||||
.value=${this._serialNumber}
|
||||
@input=${t=>this._serialNumber=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("documentation_url_optional",a)}"
|
||||
type="url"
|
||||
.value=${this._documentationUrl}
|
||||
@input=${t=>this._documentationUrl=t.target.value}
|
||||
></ms-textfield>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
label="${i("area_id_optional",a)}"
|
||||
.value=${this._areaId}
|
||||
@value-changed=${t=>this._areaId=t.detail.value||""}
|
||||
></ha-area-picker>
|
||||
<ms-textfield
|
||||
label="${i("installation_date_optional",a)}"
|
||||
type="date"
|
||||
.value=${this._installationDate}
|
||||
@input=${t=>this._installationDate=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("warranty_expiry_optional",a)}"
|
||||
type="date"
|
||||
.value=${this._warrantyExpiry}
|
||||
@input=${t=>this._warrantyExpiry=t.target.value}
|
||||
></ms-textfield>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${{device:this._haDeviceId||void 0}}
|
||||
.schema=${[{name:"device",selector:{device:{}}}]}
|
||||
.computeLabel=${()=>i("link_device_optional",a)}
|
||||
@value-changed=${t=>this._haDeviceId=t.detail.value?.device||""}
|
||||
></ha-form>
|
||||
${this._parentChoices().length?l`<label class="textarea-field">
|
||||
<span class="textarea-label">${i("parent_object_optional",a)}</span>
|
||||
<select
|
||||
class="parent-select"
|
||||
.value=${this._parentEntryId}
|
||||
@change=${t=>this._parentEntryId=t.target.value}
|
||||
>
|
||||
<option value="" ?selected=${!this._parentEntryId}>
|
||||
${i("parent_none",a)}
|
||||
</option>
|
||||
${this._parentChoices().map(t=>l`<option
|
||||
value=${t.entry_id}
|
||||
?selected=${this._parentEntryId===t.entry_id}
|
||||
>${t.object.name}</option>`)}
|
||||
</select>
|
||||
</label>`:o}
|
||||
<label class="textarea-field">
|
||||
<span class="textarea-label">${i("object_notes_optional",a)}</span>
|
||||
<textarea
|
||||
rows="3"
|
||||
.value=${this._notes}
|
||||
@input=${t=>this._notes=t.target.value}
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${i("cancel",this._lang)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${this._loading||!this._name.trim()}
|
||||
>
|
||||
${this._loading?i("saving",this._lang):i("save",this._lang)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};e.styles=_`
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
ms-textfield {
|
||||
display: block;
|
||||
}
|
||||
.textarea-field {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.textarea-label {
|
||||
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
|
||||
}
|
||||
.textarea-field textarea {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
.textarea-field textarea:focus {
|
||||
outline: none; border-color: var(--primary-color);
|
||||
}
|
||||
.parent-select {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
`,r([d({attribute:!1})],e.prototype,"hass",2),r([d({attribute:!1})],e.prototype,"objects",2),r([s()],e.prototype,"_open",2),r([s()],e.prototype,"_loading",2),r([s()],e.prototype,"_error",2),r([s()],e.prototype,"_name",2),r([s()],e.prototype,"_manufacturer",2),r([s()],e.prototype,"_model",2),r([s()],e.prototype,"_serialNumber",2),r([s()],e.prototype,"_areaId",2),r([s()],e.prototype,"_installationDate",2),r([s()],e.prototype,"_warrantyExpiry",2),r([s()],e.prototype,"_documentationUrl",2),r([s()],e.prototype,"_notes",2),r([s()],e.prototype,"_haDeviceId",2),r([s()],e.prototype,"_parentEntryId",2),r([s()],e.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",e);export{e as MaintenanceObjectDialog};
|
||||
@@ -0,0 +1,213 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a,b as v,c as n,f as g,g as b,i as m,j as c,n as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";function p(l){return l.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function x(l){return!l.startsWith("data:image/svg+xml,")&&!l.startsWith("data:image/png;base64,")?"":p(l)}function $(l){return l.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var r=class extends b{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,i){this._entryId=e,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,i,o,s){this._entryId=e,this._taskId=i,this._objectName=o,this._taskName=s,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let o=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&o.push(this.hass.connection.sendMessagePromise({...i,action:"complete"}));let s=await Promise.all(o);if(e!==this._generateSeq)return;this._viewResult=s[0],s.length>1&&(this._completeResult=s[1])}catch(i){if(e!==this._generateSeq)return;let o=i?.code,s=i?.message;this._error=o==="no_url"||typeof s=="string"&&s.includes("No Home Assistant URL")?t("qr_error_no_url",this.lang):t("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,i=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,o=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),s=window.open("","_blank","width=600,height=500");if(!s)return;let h=this.lang||"en",d=p(i),u=p(o),_=!!this._completeResult,f=p(t("qr_action_view",h)),w=p(t("qr_action_complete",h));s.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${d}</title>
|
||||
<style>
|
||||
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
|
||||
carry their own white quiet zone and stay scannable either way, but the
|
||||
labels below are explicit dark greys and would vanish on a WebView's dark
|
||||
canvas. Same reasoning as helpers/report.ts. */
|
||||
:root{color-scheme:light}
|
||||
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
|
||||
h2{margin:0 0 4px}
|
||||
.sub{color:#666;font-size:14px;margin-bottom:16px}
|
||||
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
|
||||
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||
.qr-col img{width:${_?"200px":"280px"}}
|
||||
.qr-label{font-size:13px;font-weight:500;color:#333}
|
||||
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
|
||||
</style></head><body>
|
||||
<h2>${d}</h2>
|
||||
${u?`<div class="sub">${u}</div>`:""}
|
||||
<div class="qr-row">
|
||||
<div class="qr-col">
|
||||
<img src="${x(this._viewResult.svg_data_uri)}" alt="QR Info" />
|
||||
<div class="qr-label">${f}</div>
|
||||
</div>
|
||||
${_?`<div class="qr-col">
|
||||
<img src="${x(this._completeResult.svg_data_uri)}" alt="QR Complete" />
|
||||
<div class="qr-label">${w}</div>
|
||||
</div>`:""}
|
||||
</div>
|
||||
<div class="url">${p(this._viewResult.url)}</div>
|
||||
<script>setTimeout(()=>window.print(),300)<\/script>
|
||||
</body></html>`),s.document.close()}_downloadSvg(e,i){let o=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),s=new Blob([o],{type:"image/svg+xml"}),h=URL.createObjectURL(s),d=document.createElement("a");d.href=h;let u=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;d.download=`qr-${$(u)}-${i}.svg`,d.click(),URL.revokeObjectURL(h)}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en",i=this._taskName?`${t("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${t("qr_code",e)}: ${this._objectName}`,o=!!this._viewResult;return n`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${i}</div>
|
||||
<div class="content">
|
||||
${this._loading?n`<div class="loading">${t("qr_generating",e)}</div>`:this._error?n`<div class="error">${this._error}</div>`:o?n`
|
||||
<div class="qr-pair">
|
||||
<div class="qr-item">
|
||||
<img
|
||||
class="qr-image ${this._completeResult?"small":""}"
|
||||
src="${this._viewResult.svg_data_uri}"
|
||||
alt="QR Info"
|
||||
/>
|
||||
<div class="qr-item-label">${t("qr_action_view",e)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${()=>this._downloadSvg(this._viewResult,"info")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${t("qr_download",e)}
|
||||
</button>
|
||||
</div>
|
||||
${this._completeResult?n`
|
||||
<div class="qr-item">
|
||||
<img
|
||||
class="qr-image small"
|
||||
src="${this._completeResult.svg_data_uri}"
|
||||
alt="QR Complete"
|
||||
/>
|
||||
<div class="qr-item-label">${t("qr_action_complete",e)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${()=>this._downloadSvg(this._completeResult,"complete")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${t("qr_download",e)}
|
||||
</button>
|
||||
</div>
|
||||
`:g}
|
||||
</div>
|
||||
<div class="url-display">${this._viewResult.url}</div>
|
||||
`:g}
|
||||
<div class="action-row">
|
||||
<label>${t("qr_url_mode",e)}</label>
|
||||
<div class="action-toggle">
|
||||
<button class="toggle-btn ${this._urlMode==="companion"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("companion")}>${t("qr_mode_companion",e)}</button>
|
||||
<button class="toggle-btn ${this._urlMode==="local"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("local")}>${t("qr_mode_local",e)}</button>
|
||||
<button class="toggle-btn ${this._urlMode==="server"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("server")}>${t("qr_mode_server",e)}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${t("cancel",e)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._print}
|
||||
.disabled=${!o}
|
||||
>
|
||||
${t("qr_print",e)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};r.styles=v`
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.qr-pair {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.qr-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.qr-image {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.qr-image.small {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
.qr-item-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
.dl-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color, #e0e0e0);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 6px 14px;
|
||||
border-radius: 18px;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
.dl-btn:hover {
|
||||
background: var(--secondary-background-color, #f5f5f5);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.dl-btn ha-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.url-display {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
.loading {
|
||||
padding: 40px 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.error {
|
||||
padding: 20px 0;
|
||||
color: var(--error-color, #f44336);
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
.action-row label {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.action-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--primary-text-color);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.toggle-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color, #fff);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
`,a([m({attribute:!1})],r.prototype,"hass",2),a([m()],r.prototype,"lang",2),a([c()],r.prototype,"_open",2),a([c()],r.prototype,"_loading",2),a([c()],r.prototype,"_error",2),a([c()],r.prototype,"_viewResult",2),a([c()],r.prototype,"_completeResult",2),a([c()],r.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",r);export{r as MaintenanceQrDialog};
|
||||
+1141
File diff suppressed because it is too large
Load Diff
+146
@@ -0,0 +1,146 @@
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MEKM6THN.js";import{a as l,b as m,c as i,f as h,g as v,i as u,j as d,n as p,p as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4MVSRJ3Y.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._setups=[];this._selected=new Set;this._baselines=new Map;this._targets=new Map;this._objects=[];this._localeReady=!1;this._toggle=t=>{let e=new Set(this._selected);e.has(t)?e.delete(t):e.add(t),this._selected=e};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/adopt",selections:[...this._selected].map(e=>{let r={device_id:e},c=this._targets.get(e);c&&(r.entry_id=c);let s=this._setups.find(n=>n.device_id===e);for(let n of s?.tasks??[]){let o=this._baselines.get(`${e} ${n.task_name}`),_=o?parseFloat(o):NaN;!isNaN(_)&&_>=0&&((r.baselines??={})[n.task_name]=_)}return r})});this.dispatchEvent(new CustomEvent("integration-setups-adopted",{bubbles:!0,composed:!0,detail:t})),this._open=!1}catch(t){this._error=g(t,this._lang)}finally{this._adopting=!1}}}}get _lang(){return this.hass?.language||"en"}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._setups=[],this._selected=new Set;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/discover"});this._setups=t.setups||[],this._selected=new Set(this._setups.map(e=>e.device_id)),this._baselines=new Map,this._targets=new Map;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._objects=(e.objects||[]).map(r=>({entry_id:r.entry_id,name:r.object?.name||r.entry_id})).sort((r,c)=>r.name.localeCompare(c.name))}catch{this._objects=[]}}catch(t){this._error=g(t,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return i``;let t=this._lang;return i`
|
||||
<div class="overlay" @click=${this._close}>
|
||||
<div class="card" @click=${e=>e.stopPropagation()}>
|
||||
<div class="title">${p("setups_title",t)}</div>
|
||||
<div class="hint">${p("setups_hint",t)}</div>
|
||||
${this._error?i`<div class="error">${this._error}</div>`:h}
|
||||
|
||||
${this._loading?i`<div class="loading">…</div>`:this._setups.length===0?i`<div class="empty">${p("setups_none",t)}</div>`:i`
|
||||
<div class="list">
|
||||
${this._setups.map(e=>{let r=this._selected.has(e.device_id),c=[e.integration_name,e.area_name].filter(Boolean).join(" \xB7 ");return i`
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${r}
|
||||
@change=${()=>this._toggle(e.device_id)}
|
||||
/>
|
||||
<div class="row-main">
|
||||
<div class="row-top">
|
||||
<span class="row-name">${e.device_name}</span>
|
||||
</div>
|
||||
<div class="row-sub">${c}</div>
|
||||
<div class="row-target" @click=${s=>s.preventDefault()}>
|
||||
→
|
||||
${r&&this._objects.length>0?i`
|
||||
<select
|
||||
class="target-select"
|
||||
@change=${s=>{let n=new Map(this._targets),o=s.target.value;o?n.set(e.device_id,o):n.delete(e.device_id),this._targets=n}}
|
||||
>
|
||||
<option value="" ?selected=${!this._targets.get(e.device_id)}>
|
||||
${e.suggested_entry_id?e.suggested_object_name:p("setups_target_new",t).replace("{name}",e.suggested_object_name)}
|
||||
</option>
|
||||
${this._objects.filter(s=>s.entry_id!==e.suggested_entry_id).map(s=>i`<option
|
||||
value=${s.entry_id}
|
||||
?selected=${this._targets.get(e.device_id)===s.entry_id}
|
||||
>
|
||||
${s.name}
|
||||
</option>`)}
|
||||
</select>
|
||||
`:i`${e.suggested_object_name}${e.suggested_entry_id?h:i` <span class="new-tag">${p("adopt_problem_new_object",t)}</span>`}`}
|
||||
</div>
|
||||
<div class="row-tasks">
|
||||
${e.tasks.map(s=>i`<span class="chip" title=${s.entity_ids.join(", ")}>
|
||||
<ha-icon icon="mdi:link-variant"></ha-icon>${s.task_name_localized||s.task_name}
|
||||
</span>`)}
|
||||
</div>
|
||||
${r?e.tasks.filter(s=>s.direction==="usage_delta").map(s=>{let n=`${e.device_id} ${s.task_name}`;return i`
|
||||
<div class="baseline-field" @click=${o=>o.preventDefault()}>
|
||||
<span class="baseline-label"
|
||||
>${s.task_name_localized||s.task_name} —
|
||||
${p("setups_baseline_hint",t)}</span
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
min="0"
|
||||
.value=${this._baselines.get(n)??""}
|
||||
@click=${o=>o.preventDefault()}
|
||||
@input=${o=>{let _=new Map(this._baselines);_.set(n,o.target.value),this._baselines=_}}
|
||||
/>
|
||||
</div>
|
||||
`}):h}
|
||||
</div>
|
||||
</label>
|
||||
`})}
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${p("cancel",t)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._adopt}
|
||||
.disabled=${this._selected.size===0||this._adopting}
|
||||
>
|
||||
${p("setups_adopt",t)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}};a.styles=m`
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 560px;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.title { font-size: 18px; font-weight: 500; }
|
||||
.hint { color: var(--secondary-text-color); font-size: 13px; }
|
||||
.error { color: var(--error-color, #f44336); font-size: 13px; }
|
||||
.loading, .empty { color: var(--secondary-text-color); font-size: 14px; padding: 12px 0; }
|
||||
.list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 50vh; }
|
||||
.row {
|
||||
display: flex; align-items: flex-start; gap: 10px; padding: 8px;
|
||||
border: 1px solid var(--divider-color); border-radius: 6px; cursor: pointer;
|
||||
}
|
||||
.row input { margin-top: 2px; cursor: pointer; }
|
||||
.row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
|
||||
.row-name { font-weight: 500; font-size: 13px; }
|
||||
.row-sub, .row-target { color: var(--secondary-text-color); font-size: 12px; }
|
||||
.new-tag { font-style: italic; }
|
||||
.row-tasks { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
||||
background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
|
||||
color: var(--primary-text-color); white-space: nowrap;
|
||||
}
|
||||
.chip ha-icon { --mdc-icon-size: 12px; color: var(--primary-color); }
|
||||
.target-select {
|
||||
font-size: 12px; padding: 2px 4px; max-width: 100%;
|
||||
border: 1px solid var(--divider-color); border-radius: 4px;
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.baseline-field {
|
||||
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||||
margin-top: 4px; font-size: 12px; color: var(--secondary-text-color);
|
||||
}
|
||||
.baseline-field input {
|
||||
width: 110px; padding: 3px 6px; font-size: 12px;
|
||||
border: 1px solid var(--divider-color); border-radius: 4px;
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
|
||||
`,l([u({attribute:!1})],a.prototype,"hass",2),l([d()],a.prototype,"_open",2),l([d()],a.prototype,"_loading",2),l([d()],a.prototype,"_adopting",2),l([d()],a.prototype,"_error",2),l([d()],a.prototype,"_setups",2),l([d()],a.prototype,"_selected",2),l([d()],a.prototype,"_baselines",2),l([d()],a.prototype,"_targets",2),l([d()],a.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",a);export{a as MaintenanceSuggestedSetupsDialog};
|
||||
+983
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*! maintenance_supporter frontend 2.46.0 */
|
||||
import{a as v}from"./chunk-V27MBWM3.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-DM35MPY4.js";import{a as n}from"./chunk-JSL2OXKU.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as v}from"./chunk-CRNPDKEQ.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-HC6XJZMT.js";import{a as n}from"./chunk-5HT73ALV.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
import{a as v}from"./chunk-7IBGRLM5.js";import{a as u,b as o,d as h,e as b,f as y,g as l,h as g,i as r,k as m,t as p}from"./chunk-C6VY6OOC.js";import{a as n}from"./chunk-D4IFN5R3.js";var x=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=parseFloat(this._localMonthly),a=parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(a)&&a>=0&&(i.budget_yearly=a),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,a=this._status;if(!a)return o`<ha-card><div class="loading">${r("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=a.currency_symbol||g,_=a.alert_threshold_pct??x,f=[{label:r("budget_monthly",t)||"Monthly",spent:a.monthly_spent||0,budget:a.monthly_budget||0},{label:r("budget_yearly",t)||"Yearly",spent:a.yearly_spent||0,budget:a.yearly_budget||0}];return o`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="emoji">💰</span>
|
||||
<span>${this._config.title||r("settings_budget",t)||"Budget"}</span>
|
||||
</div>
|
||||
<span class="currency">${i}</span>
|
||||
</div>
|
||||
|
||||
${this._error?o`<div class="error">${this._error}</div>`:h}
|
||||
|
||||
${f.map(e=>{if(!(e.budget>0))return o`
|
||||
<div class="track spent-only">
|
||||
<div class="track-label-row">
|
||||
<label>${e.label}</label>
|
||||
<span class="track-numbers ok">${e.spent.toFixed(0)} ${i}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;let d=Math.min(100,Math.max(0,e.spent/e.budget*100)),c=d>=100?"danger":d>=_?"warning":"ok";return o`
|
||||
<div class="track">
|
||||
<div class="track-label-row">
|
||||
<label>${e.label}</label>
|
||||
<span class="track-numbers ${c}">
|
||||
${e.spent.toFixed(0)} / ${e.budget.toFixed(0)} ${i}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar"><div class="bar-fill ${c}" style="width:${d}%"></div></div>
|
||||
</div>
|
||||
`})}
|
||||
|
||||
${this._isAdmin?o`
|
||||
<div class="inputs-row">
|
||||
<div class="input-field">
|
||||
<label>${r("budget_monthly_set",t)||"Set monthly"}</label>
|
||||
<div class="input-wrap">
|
||||
<input type="number" min="0" step="1"
|
||||
.value=${this._localMonthly}
|
||||
?disabled=${this._busy}
|
||||
@input=${e=>{this._localMonthly=e.target.value,this._dirty=!0}} />
|
||||
<span class="input-suffix">${i}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<label>${r("budget_yearly_set",t)||"Set yearly"}</label>
|
||||
<div class="input-wrap">
|
||||
<input type="number" min="0" step="1"
|
||||
.value=${this._localYearly}
|
||||
?disabled=${this._busy}
|
||||
@input=${e=>{this._localYearly=e.target.value,this._dirty=!0}} />
|
||||
<span class="input-suffix">${i}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn ${this._dirty?"primary":"muted"}"
|
||||
@click=${this._save}
|
||||
?disabled=${this._busy||!this._dirty}>
|
||||
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
|
||||
${this._dirty?r("save",t)||"Save":r("saved",t)||"Saved"}
|
||||
</button>
|
||||
<button class="btn link" @click=${this._onDeepLink}>
|
||||
${r("budget_advanced",t)||"Currency, alerts\u2026"}
|
||||
</button>
|
||||
</div>
|
||||
`:o`
|
||||
<button class="btn link" @click=${this._onDeepLink}>
|
||||
${r("budget_open_panel",t)||"Open in panel"}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}};s.styles=[v,u`
|
||||
.currency {
|
||||
font-size: 14px; font-weight: 600;
|
||||
color: var(--secondary-text-color);
|
||||
background: var(--secondary-background-color);
|
||||
padding: 2px 10px; border-radius: 999px;
|
||||
}
|
||||
.track { display: flex; flex-direction: column; gap: 4px; }
|
||||
.track-label-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.track-label-row label {
|
||||
font-size: 12px; color: var(--secondary-text-color);
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.track-numbers { font-size: 13px; font-weight: 600; }
|
||||
.track-numbers.ok { color: var(--primary-text-color); }
|
||||
.track-numbers.warning { color: #ff9800; }
|
||||
.track-numbers.danger { color: var(--error-color, #f44336); }
|
||||
.bar {
|
||||
height: 6px; background: var(--secondary-background-color);
|
||||
border-radius: 3px; overflow: hidden;
|
||||
}
|
||||
.bar-fill { height: 100%; transition: width 0.3s; border-radius: 3px; }
|
||||
.bar-fill.ok { background: var(--primary-color); }
|
||||
.bar-fill.warning { background: #ff9800; }
|
||||
.bar-fill.danger { background: var(--error-color, #f44336); }
|
||||
.inputs-row {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
|
||||
padding-top: 4px; border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.input-field { display: flex; flex-direction: column; gap: 4px; }
|
||||
.input-field label {
|
||||
font-size: 11px; color: var(--secondary-text-color);
|
||||
text-transform: uppercase; letter-spacing: 0.3px;
|
||||
}
|
||||
.input-wrap { position: relative; display: flex; align-items: center; }
|
||||
.input-wrap input {
|
||||
flex: 1; padding: 6px 32px 6px 8px; font-size: 13px;
|
||||
background: var(--secondary-background-color, #2c2c2c);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.input-suffix {
|
||||
position: absolute; right: 8px;
|
||||
color: var(--secondary-text-color); font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.actions { display: flex; gap: 8px; align-items: center; }
|
||||
`],n([y({attribute:!1})],s.prototype,"hass",2),n([l()],s.prototype,"_config",2),n([l()],s.prototype,"_status",2),n([l()],s.prototype,"_busy",2),n([l()],s.prototype,"_error",2),n([l()],s.prototype,"_localMonthly",2),n([l()],s.prototype,"_localYearly",2),n([l()],s.prototype,"_dirty",2);customElements.get("maintenance-budget-section-card")||customElements.define("maintenance-budget-section-card",s);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-budget-section-card",name:"Maintenance Supporter \u2014 Budget",description:"Inline monthly + yearly budget editor",preview:!1});export{s as MaintenanceBudgetSectionCard};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
var s=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var t=(a,r,c,o)=>{for(var e=o>1?void 0:o?l(r,c):r,i=a.length-1,d;i>=0;i--)(d=a[i])&&(e=(o?d(r,c,e):d(e))||e);return o&&e&&s(r,c,e),e};var m={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},v={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};export{t as a,m as b,v as c};
|
||||
@@ -1,60 +0,0 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
import{a as r}from"./chunk-C6VY6OOC.js";var a=r`
|
||||
ha-card { overflow: hidden; }
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.title {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 16px; font-weight: 500;
|
||||
}
|
||||
.emoji { font-size: 20px; }
|
||||
|
||||
/* Button family — primary action / muted-saved-state / link / icon-with-text */
|
||||
.btn {
|
||||
padding: 6px 12px; font-size: 13px;
|
||||
border-radius: 6px; cursor: pointer;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--secondary-background-color, transparent);
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 500;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); }
|
||||
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn.primary {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color, white);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.btn.primary[disabled] { opacity: 0.6; }
|
||||
.btn.muted {
|
||||
background: transparent;
|
||||
color: var(--secondary-text-color);
|
||||
border-style: dashed;
|
||||
}
|
||||
.btn.muted[disabled] { opacity: 1; cursor: default; }
|
||||
.btn.muted ha-icon, .btn.primary ha-icon { --mdc-icon-size: 14px; }
|
||||
.btn.link {
|
||||
background: transparent; border: none; padding: 6px 4px;
|
||||
color: var(--primary-color); margin-left: auto;
|
||||
}
|
||||
.btn.link:hover { background: transparent; text-decoration: underline; }
|
||||
|
||||
/* Error + loading states */
|
||||
.error {
|
||||
padding: 8px; border-radius: 6px;
|
||||
background: rgba(211, 47, 47, 0.1);
|
||||
color: var(--error-color, #d32f2f); font-size: 13px;
|
||||
}
|
||||
.loading {
|
||||
padding: 24px; text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;export{a};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*! maintenance_supporter frontend 2.46.0 */
|
||||
import{a as r}from"./chunk-DM35MPY4.js";var a=r`
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as r}from"./chunk-HC6XJZMT.js";var a=r`
|
||||
ha-card { overflow: hidden; }
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
/*! maintenance_supporter frontend 2.46.0 */
|
||||
var s=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var t=(a,r,c,o)=>{for(var e=o>1?void 0:o?l(r,c):r,i=a.length-1,d;i>=0;i--)(d=a[i])&&(e=(o?d(r,c,e):d(e))||e);return o&&e&&s(r,c,e),e};var m={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},v={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};export{t as a,m as b,v as c};
|
||||
-2435
File diff suppressed because one or more lines are too long
+239
-221
File diff suppressed because one or more lines are too long
-137
@@ -1,137 +0,0 @@
|
||||
/*! maintenance_supporter frontend 2.46.0 */
|
||||
import{a as m}from"./chunk-V27MBWM3.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-DM35MPY4.js";import{a}from"./chunk-JSL2OXKU.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="emoji">🏷️</span>
|
||||
<span>${this._config.title||i("groups",t)||"Groups"}</span>
|
||||
<span class="count">${r.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._error?s`<div class="error">${this._error}</div>`:l}
|
||||
|
||||
${r.length===0?s`<div class="empty">${i("groups_empty",t)||"No groups yet."}</div>`:s`
|
||||
<div class="group-list">
|
||||
${r.map(n=>{let d=this._groups[n],v=d.task_refs?.length??0,b=this._editingId===n;return s`
|
||||
<div class="group-row">
|
||||
${b?s`
|
||||
<input class="edit-input" type="text"
|
||||
.value=${this._editingName}
|
||||
?disabled=${this._busy}
|
||||
@input=${c=>{this._editingName=c.target.value}}
|
||||
@keydown=${c=>this._onKeyDown(c,this._saveEdit.bind(this))} />
|
||||
<button class="btn small primary"
|
||||
@click=${this._saveEdit}
|
||||
?disabled=${this._busy||!this._editingName.trim()}>
|
||||
${i("save",t)||"Save"}
|
||||
</button>
|
||||
<button class="btn small"
|
||||
@click=${()=>{this._editingId=null}}>
|
||||
${i("cancel",t)||"Cancel"}
|
||||
</button>
|
||||
`:s`
|
||||
<span class="group-name">${d.name||"Unnamed"}</span>
|
||||
<span class="task-count">${v}</span>
|
||||
${this._isAdmin?s`
|
||||
<button class="icon-btn"
|
||||
title="${i("edit",t)||"Edit"}"
|
||||
@click=${()=>this._startEdit(n)}
|
||||
?disabled=${this._busy}>
|
||||
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||
</button>
|
||||
<button class="icon-btn danger"
|
||||
title="${i("delete",t)||"Delete"}"
|
||||
@click=${()=>this._deleteGroup(n,d.name||"Unnamed")}
|
||||
?disabled=${this._busy}>
|
||||
<ha-icon icon="mdi:delete"></ha-icon>
|
||||
</button>
|
||||
`:l}
|
||||
`}
|
||||
</div>
|
||||
`})}
|
||||
</div>
|
||||
`}
|
||||
|
||||
${this._isAdmin?s`
|
||||
<div class="add-row">
|
||||
<input type="text"
|
||||
placeholder="${i("group_new_placeholder",t)||"Add group\u2026"}"
|
||||
.value=${this._newName}
|
||||
?disabled=${this._busy}
|
||||
@input=${n=>{this._newName=n.target.value}}
|
||||
@keydown=${n=>this._onKeyDown(n,this._addGroup.bind(this))} />
|
||||
<button class="btn primary"
|
||||
@click=${this._addGroup}
|
||||
?disabled=${this._busy||!this._newName.trim()}>
|
||||
<ha-icon icon="mdi:plus"></ha-icon>
|
||||
${i("add",t)||"Add"}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn link" @click=${this._onDeepLink}>
|
||||
${i("groups_manage_tasks",t)||"Manage task assignments\u2026"}
|
||||
</button>
|
||||
`:s`
|
||||
<button class="btn link" @click=${this._onDeepLink}>
|
||||
${i("groups_open_panel",t)||"Open in panel"}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}};e.styles=[m,u`
|
||||
.count {
|
||||
font-size: 12px; color: var(--secondary-text-color);
|
||||
background: var(--secondary-background-color);
|
||||
padding: 2px 8px; border-radius: 999px;
|
||||
}
|
||||
.empty {
|
||||
padding: 16px; text-align: center;
|
||||
color: var(--secondary-text-color); font-style: italic;
|
||||
}
|
||||
.group-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.group-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 8px; border-radius: 6px;
|
||||
background: var(--secondary-background-color, rgba(255,255,255,0.03));
|
||||
}
|
||||
.group-name { flex: 1; font-size: 14px; }
|
||||
.task-count {
|
||||
font-size: 11px; color: var(--secondary-text-color);
|
||||
background: var(--card-background-color, rgba(0,0,0,0.2));
|
||||
padding: 1px 8px; border-radius: 999px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.edit-input {
|
||||
flex: 1; padding: 4px 8px; font-size: 14px;
|
||||
background: var(--card-background-color, #1c1c1c);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--primary-color); border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.icon-btn {
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
color: var(--secondary-text-color); padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
background: var(--state-icon-color, rgba(255,255,255,0.06));
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.icon-btn.danger:hover { color: var(--error-color); }
|
||||
.icon-btn ha-icon { --mdc-icon-size: 18px; }
|
||||
.add-row {
|
||||
display: flex; gap: 6px;
|
||||
padding-top: 8px; border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.add-row input {
|
||||
flex: 1; padding: 6px 8px; font-size: 13px;
|
||||
background: var(--secondary-background-color, #2c2c2c);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
/* Card-specific overrides on the shared .btn */
|
||||
.btn.small { padding: 4px 8px; font-size: 12px; }
|
||||
.btn ha-icon { --mdc-icon-size: 16px; }
|
||||
`],a([g({attribute:!1})],e.prototype,"hass",2),a([o()],e.prototype,"_config",2),a([o()],e.prototype,"_groups",2),a([o()],e.prototype,"_loaded",2),a([o()],e.prototype,"_busy",2),a([o()],e.prototype,"_error",2),a([o()],e.prototype,"_newName",2),a([o()],e.prototype,"_editingId",2),a([o()],e.prototype,"_editingName",2);customElements.get("maintenance-groups-section-card")||customElements.define("maintenance-groups-section-card",e);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-groups-section-card",name:"Maintenance Supporter \u2014 Groups",description:"Inline group CRUD",preview:!1});export{e as MaintenanceGroupsSectionCard};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
import{a as m}from"./chunk-7IBGRLM5.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-C6VY6OOC.js";import{a}from"./chunk-D4IFN5R3.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as m}from"./chunk-CRNPDKEQ.js";import{a as u,b as s,d as l,e as h,f as g,g as o,i,k as _,t as p}from"./chunk-HC6XJZMT.js";import{a}from"./chunk-5HT73ALV.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),_(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*! maintenance_supporter frontend 2.52.0 */
|
||||
import{a as m}from"./chunk-7IBGRLM5.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-C6VY6OOC.js";import{a as i}from"./chunk-D4IFN5R3.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
|
||||
/*! maintenance_supporter frontend 2.53.0 */
|
||||
import{a as m}from"./chunk-CRNPDKEQ.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-HC6XJZMT.js";import{a as i}from"./chunk-5HT73ALV.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
/*! maintenance_supporter frontend 2.46.0 */
|
||||
import{a as m}from"./chunk-V27MBWM3.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,k as f,t as l}from"./chunk-DM35MPY4.js";import{a as i}from"./chunk-JSL2OXKU.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return this.hass?.language||"en"}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),f(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,b=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",g=p?"active":d?"scheduled":"inactive";return n`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="emoji">🏖️</span>
|
||||
<span>${this._config.title||e("vacation_mode",t)||"Vacation mode"}</span>
|
||||
</div>
|
||||
<span class="status-pill ${g}">${b}</span>
|
||||
</div>
|
||||
|
||||
${this._error?n`<div class="error">${this._error}</div>`:c}
|
||||
|
||||
${this._isAdmin?n`
|
||||
<div class="row toggle-row">
|
||||
<label>${e("enable",t)||"Enable"}</label>
|
||||
<ha-switch
|
||||
.checked=${d}
|
||||
.disabled=${this._busy}
|
||||
@change=${o=>this._toggleEnabled(o.target.checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
|
||||
<div class="dates-row">
|
||||
<div class="date-field">
|
||||
<label>${e("vacation_start",t)||"Start"}</label>
|
||||
<input type="date" .value=${this._localStart}
|
||||
?disabled=${this._busy}
|
||||
@input=${o=>{this._localStart=o.target.value,this._dirty=!0}} />
|
||||
</div>
|
||||
<div class="date-field">
|
||||
<label>${e("vacation_end",t)||"End"}</label>
|
||||
<input type="date" .value=${this._localEnd}
|
||||
?disabled=${this._busy}
|
||||
@input=${o=>{this._localEnd=o.target.value,this._dirty=!0}} />
|
||||
</div>
|
||||
<div class="date-field buffer">
|
||||
<label>${e("vacation_buffer",t)||"Buffer days"}</label>
|
||||
<input type="number" min="0" max="14"
|
||||
.value=${String(this._localBuffer)}
|
||||
?disabled=${this._busy}
|
||||
@input=${o=>{this._localBuffer=parseInt(o.target.value,10)||0,this._dirty=!0}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn ${this._dirty?"primary":"muted"}"
|
||||
@click=${this._save}
|
||||
?disabled=${this._busy||!this._dirty}>
|
||||
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
|
||||
${this._dirty?e("save",t)||"Save":e("saved",t)||"Saved"}
|
||||
</button>
|
||||
${p?n`<button class="btn"
|
||||
@click=${this._endNow}
|
||||
?disabled=${this._busy}>
|
||||
${e("vacation_end_now",t)||"End now"}
|
||||
</button>`:c}
|
||||
${u>0?n`<button class="btn link"
|
||||
@click=${this._onDeepLink}>
|
||||
${u} ${e("vacation_exempt_count",t)||"exempt"}…
|
||||
</button>`:n`<button class="btn link"
|
||||
@click=${this._onDeepLink}>
|
||||
${e("vacation_advanced",t)||"Advanced\u2026"}
|
||||
</button>`}
|
||||
</div>
|
||||
`:n`
|
||||
<div class="readonly">
|
||||
${d&&s.start&&s.end?n`<div>${s.start} → ${s.end}</div>`:c}
|
||||
<button class="btn link" @click=${this._onDeepLink}>
|
||||
${e("vacation_open_panel",t)||"Open in panel"}
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}};a.styles=[m,h`
|
||||
.status-pill {
|
||||
font-size: 11px; font-weight: 600;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.status-pill.active {
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
color: #4caf50;
|
||||
}
|
||||
.status-pill.scheduled {
|
||||
background: rgba(255, 152, 0, 0.15);
|
||||
color: #ff9800;
|
||||
}
|
||||
.status-pill.inactive {
|
||||
background: rgba(158, 158, 158, 0.15);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.row.toggle-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.row.toggle-row label {
|
||||
font-size: 14px; color: var(--primary-text-color);
|
||||
}
|
||||
.dates-row {
|
||||
display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px;
|
||||
}
|
||||
.date-field.buffer label { white-space: nowrap; }
|
||||
.date-field { display: flex; flex-direction: column; gap: 4px; }
|
||||
.date-field label {
|
||||
font-size: 11px; color: var(--secondary-text-color);
|
||||
text-transform: uppercase; letter-spacing: 0.3px;
|
||||
}
|
||||
.date-field input {
|
||||
padding: 6px 8px; font-size: 13px;
|
||||
background: var(--secondary-background-color, #2c2c2c);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.date-field input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.actions {
|
||||
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
||||
}
|
||||
.readonly { display: flex; flex-direction: column; gap: 8px; }
|
||||
`],i([v({attribute:!1})],a.prototype,"hass",2),i([r()],a.prototype,"_config",2),i([r()],a.prototype,"_state",2),i([r()],a.prototype,"_busy",2),i([r()],a.prototype,"_error",2),i([r()],a.prototype,"_localStart",2),i([r()],a.prototype,"_localEnd",2),i([r()],a.prototype,"_localBuffer",2),i([r()],a.prototype,"_dirty",2);customElements.get("maintenance-vacation-section-card")||customElements.define("maintenance-vacation-section-card",a);window.customCards=window.customCards||[];window.customCards.push({type:"maintenance-vacation-section-card",name:"Maintenance Supporter \u2014 Vacation",description:"Inline vacation mode toggle + dates",preview:!1});export{a as MaintenanceVacationSectionCard};
|
||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -231,12 +231,21 @@ def build_overview(
|
||||
# so they get a ~date only when the trend has earned one.
|
||||
trend = (trend_predictions or {}).get(bat.entity_id)
|
||||
if trend is not None:
|
||||
days: int | None = max(0, trend[0])
|
||||
days_raw: int | None = trend[0]
|
||||
source, confidence = "trend", trend[1]
|
||||
else:
|
||||
pred = None if rechargeable else _predicted_date(bat)
|
||||
days = (pred - today).days if pred is not None else None
|
||||
days_raw = (pred - today).days if pred is not None else None
|
||||
source, confidence = "typical", None
|
||||
# B1 (decided 2026-08): a PASSED prediction while the battery still
|
||||
# reports healthy is "forecast overdue" — it stays in `soon` (clamped
|
||||
# to 0 days, no negative countdowns) and NEVER escalates into `low`
|
||||
# or the task trigger: the forecast has error bars, the sensor says
|
||||
# fine, and the usual cause is a swap that was never recorded (the
|
||||
# roster's unrecorded-swap hint covers exactly that). Reality fires
|
||||
# the task; predictions only shop.
|
||||
overdue = days_raw is not None and days_raw < 0
|
||||
days = max(0, days_raw) if days_raw is not None else None
|
||||
if bat.low:
|
||||
ov.low.append(_row(bat, t, None, rechargeable=rechargeable))
|
||||
if not rechargeable:
|
||||
@@ -245,10 +254,10 @@ def build_overview(
|
||||
ov.all.append({**_row(bat, t, None, rechargeable=rechargeable), "status": "low"})
|
||||
continue
|
||||
if days is not None and days <= horizon_days:
|
||||
ov.soon.append(_row(bat, t, days, source, confidence, rechargeable=rechargeable))
|
||||
ov.soon.append(_row(bat, t, days, source, confidence, rechargeable=rechargeable, forecast_overdue=overdue))
|
||||
if not rechargeable:
|
||||
ov.needs_soon[t] = ov.needs_soon.get(t, 0) + bat.quantity
|
||||
ov.all.append({**_row(bat, t, days, source, confidence, rechargeable=rechargeable), "status": "soon"})
|
||||
ov.all.append({**_row(bat, t, days, source, confidence, rechargeable=rechargeable, forecast_overdue=overdue), "status": "soon"})
|
||||
continue
|
||||
ov.all.append({**_row(bat, t, days, source, confidence, rechargeable=rechargeable), "status": "ok"})
|
||||
|
||||
@@ -267,6 +276,7 @@ def _row(
|
||||
prediction_confidence: str | None = None,
|
||||
*,
|
||||
rechargeable: bool = False,
|
||||
forecast_overdue: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"entity_id": bat.entity_id,
|
||||
@@ -285,6 +295,11 @@ def _row(
|
||||
"rechargeable": rechargeable,
|
||||
# This battery's own low threshold — the level bars color against it.
|
||||
"low_threshold": bat.low_threshold,
|
||||
# B1: the predicted date has PASSED while the battery still reports
|
||||
# healthy. Deliberately NOT low and NOT the task trigger — a forecast
|
||||
# carries error bars and the sensor says fine — but the roster shows
|
||||
# the discrepancy (common cause: a swap that was never recorded).
|
||||
"forecast_overdue": forecast_overdue,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"requirements": [
|
||||
"pypdf>=4.3.0"
|
||||
],
|
||||
"version": "2.52.0"
|
||||
"version": "2.53.0"
|
||||
}
|
||||
|
||||
@@ -261,8 +261,33 @@ def _build_task_summary(
|
||||
}
|
||||
|
||||
|
||||
def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_data: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Build a full object response dict."""
|
||||
_EMPTY_LIST: list[Any] = []
|
||||
_EMPTY_DICT: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _strip_empty(d: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Drop keys whose value is None, [] or {} (compact mode, perf wave 2 #3).
|
||||
|
||||
Measured on a 121-task instance: 52 % of the objects payload was keys
|
||||
carrying one of these three empties. Only clients that OPT IN via
|
||||
``compact: true`` get stripped responses — they hydrate the handful of
|
||||
list/dict-typed keys back client-side (helpers/hydrate-objects.ts; the
|
||||
two lists are pinned against each other by test_ws_compact_mode).
|
||||
Scalars stay droppable without a table because absent and null read the
|
||||
same through JS ``== null`` / ``||`` access. False, 0 and "" are kept —
|
||||
they are meaningful values, not absences.
|
||||
"""
|
||||
return {k: v for k, v in d.items() if not (v is None or v in (_EMPTY_LIST, _EMPTY_DICT))}
|
||||
|
||||
|
||||
def _build_object_response(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
coordinator_data: dict[str, Any] | None,
|
||||
*,
|
||||
compact: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a full object response dict (compact: empty keys stripped)."""
|
||||
from ..const import slugify_object_name
|
||||
|
||||
obj_data = entry.data.get(CONF_OBJECT, {})
|
||||
@@ -311,7 +336,7 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
resp: dict[str, Any] = {
|
||||
"entry_id": entry.entry_id,
|
||||
"object": {
|
||||
"id": obj_data.get("id", ""),
|
||||
@@ -344,6 +369,12 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
# v2.20 (N1) replace-flow lineage, both directions.
|
||||
"predecessor_entry_id": obj_data.get("predecessor_entry_id"),
|
||||
"replaced_by_entry_id": obj_data.get("replaced_by_entry_id"),
|
||||
# Battery-fleet markers (field-completeness audit, #50 class):
|
||||
# the fleet flag existed only task-level in the response while
|
||||
# the OBJECT flag drives find_fleet_entry — consumers (and our
|
||||
# own visual harness) had to detect the fleet via a task.
|
||||
"battery_fleet": obj_data.get("battery_fleet", False),
|
||||
"battery_fleet_excluded": obj_data.get("battery_fleet_excluded", []),
|
||||
# (roadmap P2) count of attached documents (files + web-links) for
|
||||
# the objects-table paperclip badge; computed, not persisted.
|
||||
"document_count": document_count,
|
||||
@@ -365,6 +396,11 @@ def _build_object_response(hass: HomeAssistant, entry: ConfigEntry, coordinator_
|
||||
"tasks": tasks,
|
||||
"parts": parts_payload,
|
||||
}
|
||||
if compact:
|
||||
resp["object"] = _strip_empty(resp["object"])
|
||||
resp["tasks"] = [_strip_empty(t) for t in tasks]
|
||||
resp = _strip_empty(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def _get_global_entry(hass: HomeAssistant) -> ConfigEntry | None:
|
||||
|
||||
@@ -292,6 +292,10 @@ async def ws_get_statistics(
|
||||
vol.Required("type"): "maintenance_supporter/subscribe",
|
||||
# 2.52 delta protocol opt-in — see the handler docstring.
|
||||
vol.Optional("deltas", default=False): bool,
|
||||
# Compact payloads (perf wave 2 #3): same opt-in + hydration contract
|
||||
# as the `objects` read — empty keys stripped from every snapshot and
|
||||
# delta this subscription ships.
|
||||
vol.Optional("compact", default=False): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -320,6 +324,7 @@ async def ws_subscribe(
|
||||
import json
|
||||
|
||||
deltas: bool = msg.get("deltas", False)
|
||||
compact: bool = msg.get("compact", False)
|
||||
attached_entry_ids: set[str] = set()
|
||||
unsub_callbacks: list[Callable[[], None]] = []
|
||||
dirty: set[str] = set()
|
||||
@@ -330,7 +335,7 @@ async def ws_subscribe(
|
||||
def _build(entry: Any) -> dict[str, Any]:
|
||||
rd = _get_runtime_data(hass, entry.entry_id)
|
||||
coord_data = rd.coordinator.data if rd and rd.coordinator else None
|
||||
return _build_object_response(hass, entry, coord_data)
|
||||
return _build_object_response(hass, entry, coord_data, compact=compact)
|
||||
|
||||
def _hash(resp: dict[str, Any]) -> int:
|
||||
return hash(json.dumps(resp, sort_keys=True, default=str))
|
||||
|
||||
@@ -128,7 +128,16 @@ def _validate_device_link(
|
||||
return True
|
||||
|
||||
|
||||
@websocket_api.websocket_command({vol.Required("type"): "maintenance_supporter/objects"})
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "maintenance_supporter/objects",
|
||||
# Opt-in (perf wave 2 #3): strip keys whose value is None/[]/{} from
|
||||
# the object + task summaries. Own panel/card pass this and hydrate
|
||||
# the list/dict keys back; consumers that don't ask keep the full,
|
||||
# every-field shape (#50 contract untouched).
|
||||
vol.Optional("compact", default=False): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_get_objects(
|
||||
hass: HomeAssistant,
|
||||
@@ -136,12 +145,13 @@ async def ws_get_objects(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return all maintenance objects with tasks and computed status."""
|
||||
compact = bool(msg.get("compact", False))
|
||||
entries = _get_object_entries(hass)
|
||||
result = []
|
||||
for entry in entries:
|
||||
rd = _get_runtime_data(hass, entry.entry_id)
|
||||
coord_data = rd.coordinator.data if rd and rd.coordinator else None
|
||||
result.append(_build_object_response(hass, entry, coord_data))
|
||||
result.append(_build_object_response(hass, entry, coord_data, compact=compact))
|
||||
|
||||
connection.send_result(msg["id"], {"objects": result})
|
||||
|
||||
|
||||
+20
-20
@@ -15,19 +15,19 @@
|
||||
"latest_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota",
|
||||
"latest_release_notes": null
|
||||
},
|
||||
"power": 0.4,
|
||||
"power": 0.5,
|
||||
"linkquality": 116,
|
||||
"current": 0.03,
|
||||
"power_on_behavior": "on"
|
||||
},
|
||||
"0xffffb40e0607af27": {
|
||||
"state": "ON",
|
||||
"voltage": 120.9,
|
||||
"voltage": 120.4,
|
||||
"ac_frequency": 60,
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"power": 2.1,
|
||||
"power": 2.4,
|
||||
"current": 0.12,
|
||||
"energy": 26.63,
|
||||
"power_factor": 0.14,
|
||||
@@ -47,7 +47,7 @@
|
||||
"countdown_to_turn_off": 0,
|
||||
"voltage": 120.4,
|
||||
"countdown_to_turn_on": 0,
|
||||
"energy": 49.45,
|
||||
"energy": 49.47,
|
||||
"power_factor": 0.2,
|
||||
"ac_frequency": 60,
|
||||
"update": {
|
||||
@@ -58,8 +58,8 @@
|
||||
"latest_release_notes": null
|
||||
},
|
||||
"linkquality": 134,
|
||||
"power": 0.2,
|
||||
"current": 0.01,
|
||||
"power": 4.2,
|
||||
"current": 0.16,
|
||||
"power_on_behavior": "on"
|
||||
},
|
||||
"0xb40e060fffe031e3": {
|
||||
@@ -77,8 +77,8 @@
|
||||
"voltage": 118.9,
|
||||
"state": "ON",
|
||||
"ac_frequency": 60,
|
||||
"energy": 100.44,
|
||||
"power": 95.7,
|
||||
"energy": 100.49,
|
||||
"power": 94.7,
|
||||
"current": 0.91,
|
||||
"power_factor": 0.94,
|
||||
"update": {
|
||||
@@ -95,13 +95,13 @@
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"countdown_to_turn_on": 0,
|
||||
"voltage": 120,
|
||||
"energy": 43.06,
|
||||
"voltage": 120.5,
|
||||
"energy": 43.07,
|
||||
"state": "ON",
|
||||
"power": 20.2,
|
||||
"current": 0.29,
|
||||
"power": 23.5,
|
||||
"current": 0.31,
|
||||
"ac_frequency": 60,
|
||||
"power_factor": 0.58,
|
||||
"power_factor": 0.54,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
"installed_version": 268513381,
|
||||
@@ -114,12 +114,12 @@
|
||||
},
|
||||
"0xffffb40e060895b3": {
|
||||
"state": "ON",
|
||||
"voltage": 120.5,
|
||||
"voltage": 120.9,
|
||||
"ac_frequency": 60,
|
||||
"energy": 6.21,
|
||||
"current": 0.01,
|
||||
"power": 0.2,
|
||||
"power_factor": 0,
|
||||
"power": 0.1,
|
||||
"power_factor": 0.11,
|
||||
"linkquality": 123,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
@@ -136,14 +136,14 @@
|
||||
"0xffffb40e0608864e": {
|
||||
"led_brightness": 100,
|
||||
"countdown_to_turn_off": 0,
|
||||
"voltage": 120.5,
|
||||
"voltage": 120.9,
|
||||
"energy": 17.34,
|
||||
"countdown_to_turn_on": 0,
|
||||
"state": "ON",
|
||||
"current": 0.02,
|
||||
"ac_frequency": 60,
|
||||
"power": 0.4,
|
||||
"power_factor": 0.2,
|
||||
"power": 0.3,
|
||||
"power_factor": 0.14,
|
||||
"update": {
|
||||
"state": "idle",
|
||||
"installed_version": 268513381,
|
||||
@@ -188,7 +188,7 @@
|
||||
"latest_release_notes": null
|
||||
},
|
||||
"power_factor": 0.06,
|
||||
"power": 0.1
|
||||
"power": 0.3
|
||||
},
|
||||
"0xa4c1380d0679ffff": {
|
||||
"battery": 100,
|
||||
|
||||
Reference in New Issue
Block a user