429 files
This commit is contained in:
1 parent
472c19bceb
commit
963cce81a7
566 files changed
+50311
-47616
No files matched your search
+24
@@ -273,3 +273,27 @@ describe("complete-dialog cost suggestion from parts (#104 follow-up)", () => {
|
||||
expect(chip(el)).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete-dialog require_tag_scan note", () => {
|
||||
it("shows the scan hint when the task demands proof of presence", async () => {
|
||||
const { hass } = createMockHass({});
|
||||
const el = await fixture<MaintenanceCompleteDialog>(html`
|
||||
<maintenance-complete-dialog
|
||||
.hass=${hass}
|
||||
.entryId=${"entry1"}
|
||||
.taskId=${"task1"}
|
||||
.taskName=${"Filter Wechsel"}
|
||||
.lang=${"en"}
|
||||
.requireTagScan=${true}
|
||||
></maintenance-complete-dialog>
|
||||
`);
|
||||
el.open();
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.exist;
|
||||
});
|
||||
|
||||
it("stays silent for ordinary tasks", async () => {
|
||||
const { el } = await mount();
|
||||
expect(el.shadowRoot!.querySelector(".scan-required-note")).to.equal(null);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { formatDate, formatDateTime, setDateTimePrefs } from "../styles";
|
||||
|
||||
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined });
|
||||
const reset = () => setDateTimePrefs({ date_format: undefined, time_format: undefined }, null);
|
||||
|
||||
describe("formatDate with HA profile date_format (#97)", () => {
|
||||
afterEach(reset);
|
||||
@@ -66,3 +66,39 @@ describe("formatDateTime with HA profile time_format (#97)", () => {
|
||||
expect(out.toLowerCase()).to.match(/pm|nachm/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("server country regionalizes the language default (#140)", () => {
|
||||
afterEach(reset);
|
||||
|
||||
it('"en" + AU renders dd/mm/yyyy without any profile setting', () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
|
||||
});
|
||||
|
||||
it('"en" + US keeps mm/dd/yyyy; "de" + AT stays dd.mm.yyyy', () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "US");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "language" }, "AT");
|
||||
expect(formatDate("2026-08-10", "de")).to.equal("10.08.2026");
|
||||
});
|
||||
|
||||
it("an explicit profile format always beats the country", () => {
|
||||
setDateTimePrefs({ date_format: "MDY" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "YMD" }, "AU");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("2026-08-10");
|
||||
});
|
||||
|
||||
it("garbage/unset country falls back to the language mapping", () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "!!");
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
setDateTimePrefs({ date_format: "language" }, null);
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("08/10/2026");
|
||||
});
|
||||
|
||||
it("country survives a later prefs update that omits it (undefined ≠ clear)", () => {
|
||||
setDateTimePrefs({ date_format: "language" }, "AU");
|
||||
setDateTimePrefs({ date_format: "language" });
|
||||
expect(formatDate("2026-08-10", "en")).to.equal("10/08/2026");
|
||||
});
|
||||
});
|
||||
@@ -114,6 +114,83 @@ describe("panel deep links (QR scan routing)", () => {
|
||||
expect(dlg.taskName).to.equal("No Defaults");
|
||||
});
|
||||
|
||||
it("quick_complete refused for missing details falls back to the dialog WITH the scan flag", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el, sent } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Needs Notes", require_tag_scan: true })])],
|
||||
{
|
||||
"maintenance_supporter/task/quick_complete": () => {
|
||||
throw { code: "completion_details_required", message: "notes required" };
|
||||
},
|
||||
},
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.shadowRoot!.querySelector("ha-dialog"), "fallback dialog open").to.exist;
|
||||
expect(dlg.taskName).to.equal("Needs Notes");
|
||||
// The scan already happened: the gate is met, the note is not shown …
|
||||
expect(dlg.viaTagScan).to.equal(true);
|
||||
expect(dlg.requireTagScan).to.equal(true);
|
||||
expect(dlg.shadowRoot!.querySelector(".scan-required-note")).to.be.null;
|
||||
// … and the completion carries via_tag_scan for the backend's gate.
|
||||
const buttons = [...dlg.shadowRoot!.querySelectorAll(".dialog-actions ha-button")];
|
||||
(buttons[buttons.length - 1] as HTMLElement).click();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
const complete = sent.find((m) => m.type === "maintenance_supporter/task/complete");
|
||||
expect(complete, "task/complete sent").to.exist;
|
||||
expect(complete!.via_tag_scan).to.equal(true);
|
||||
});
|
||||
|
||||
it("a plain complete (no scan) opens the dialog WITHOUT the scan flag", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=complete");
|
||||
const { el } = await mountPanel([obj("e1", [task({ name: "Gated", require_tag_scan: true })])]);
|
||||
await settleRaf(el);
|
||||
await waitForOpenCompleteDialog(el);
|
||||
const dlg = completeDialog(el)!;
|
||||
expect(dlg.viaTagScan).to.equal(false);
|
||||
expect(dlg.shadowRoot!.querySelector(".scan-required-note"), "note shown").to.exist;
|
||||
});
|
||||
|
||||
it("any other quick_complete refusal relays the server's message", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Early" })])],
|
||||
{
|
||||
"maintenance_supporter/task/quick_complete": () => {
|
||||
throw { code: "too_early", message: "Earliest completion is 2026-09-01" };
|
||||
},
|
||||
},
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
expect((el as unknown as { _toastMessage: string })._toastMessage).to.equal(
|
||||
"Earliest completion is 2026-09-01",
|
||||
);
|
||||
const dlg = completeDialog(el);
|
||||
expect(dlg?.shadowRoot?.querySelector("ha-dialog") ?? null, "no dialog").to.be.null;
|
||||
});
|
||||
|
||||
it("a silent quick_complete success reloads the data", async () => {
|
||||
setDeepLink("entry_id=e1&task_id=t1&action=quick_complete");
|
||||
const { el, sent } = await mountPanel(
|
||||
[obj("e1", [task({ name: "Quick" })])],
|
||||
{ "maintenance_supporter/task/quick_complete": () => ({ success: true, via: "quick" }) },
|
||||
);
|
||||
await settleRaf(el);
|
||||
await settleRaf(el);
|
||||
// The deep link is only routed once the objects are known, so every
|
||||
// objects fetch AFTER the quick_complete is the reload it triggered.
|
||||
const atQuick = sent.findIndex((m) => m.type === "maintenance_supporter/task/quick_complete");
|
||||
expect(atQuick, "quick_complete fired").to.be.greaterThan(-1);
|
||||
const reloaded = sent
|
||||
.slice(atQuick + 1)
|
||||
.some((m) => m.type === "maintenance_supporter/objects");
|
||||
expect(reloaded, "objects refetched after the silent complete").to.equal(true);
|
||||
});
|
||||
|
||||
it("an unknown entry_id lands safely on the overview", async () => {
|
||||
setDeepLink("entry_id=does-not-exist&task_id=t1&action=complete");
|
||||
const { el } = await mountPanel([obj("e1", [task({ name: "Real" })])]);
|
||||
|
||||
+13
@@ -215,6 +215,19 @@ describe("task-detail renderer", () => {
|
||||
expect(host2.querySelector(".kpi-bar")).to.be.null;
|
||||
});
|
||||
|
||||
it("history tab offers 'add past completion' routing to openComplete (#142)", () => {
|
||||
let completed: MaintenanceTask | null = null;
|
||||
const t2 = task();
|
||||
const host = mount(t2, ctx({
|
||||
activeTab: "history",
|
||||
openComplete: (tk) => { completed = tk; },
|
||||
}));
|
||||
const btn = host.querySelector(".history-add-past-btn") as HTMLElement;
|
||||
expect(btn, "add-past button").to.exist;
|
||||
btn.click();
|
||||
expect(completed).to.equal(t2);
|
||||
});
|
||||
|
||||
it("object-manual row falls back to an attached manual when the URL is empty", () => {
|
||||
let opened: unknown = null;
|
||||
const host = mount(task(), ctx({
|
||||
|
||||
@@ -43,6 +43,15 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
/** #139: "2/4 · Flip blades" — which phase of the cycle this completion
|
||||
* records. Purely informative; the backend advances the cursor itself. */
|
||||
@property() public phaseLabel = "";
|
||||
/** Proof of presence: the task completes only via NFC/QR scan — the
|
||||
* dialog says so up front instead of relaying the server's refusal. */
|
||||
@property({ type: Boolean }) public requireTagScan = false;
|
||||
/** This open is the fallback of a QR/NFC scan (quick-complete refused for
|
||||
* missing defaults/details): the completion carries `via_tag_scan` so the
|
||||
* tag-scan gate counts the scan that got the user here, and the "scan
|
||||
* required" note stays hidden — they just did. Set by open(); never
|
||||
* survives into the next open. */
|
||||
@property({ type: Boolean }) public viaTagScan = false;
|
||||
@state() private _open = false;
|
||||
@state() private _notes = "";
|
||||
@state() private _cost = "";
|
||||
@@ -66,9 +75,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
* prefill the dialog so nobody re-ticks what is already done. */
|
||||
@property({ attribute: false }) public checklistPrefill: Record<string, boolean> = {};
|
||||
|
||||
public open(): void {
|
||||
public open(opts: { viaTagScan?: boolean } = {}): void {
|
||||
if (this._open) return;
|
||||
this._open = true;
|
||||
// Reset on every open — a plain open after a scan fallback must not
|
||||
// keep claiming proof of presence.
|
||||
this.viaTagScan = !!opts.viaTagScan;
|
||||
this._notes = "";
|
||||
this._cost = "";
|
||||
this._duration = "";
|
||||
@@ -173,6 +185,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
if (this._photoDocId) {
|
||||
data.photo_doc_id = this._photoDocId;
|
||||
}
|
||||
// Scan fallback: the backend accepts via_tag_scan on task/complete so a
|
||||
// require_tag_scan task can still be finished from the dialog the scan
|
||||
// opened. Only ever sent when a scan actually happened.
|
||||
if (this.viaTagScan) {
|
||||
data.via_tag_scan = true;
|
||||
}
|
||||
if (this._completedAt) {
|
||||
// Client-side guard mirrors the backend rule — a picked future moment
|
||||
// fails fast with a localized message instead of a WS roundtrip.
|
||||
@@ -290,6 +308,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
|
||||
${this.phaseLabel ? html`<div class="phase-line">${t("phase_current", L)}: ${this.phaseLabel}</div>` : nothing}
|
||||
${this.requireTagScan && !this.viaTagScan ? html`<div class="scan-required-note">${t("require_tag_scan_hint", L)}</div>` : nothing}
|
||||
<div class="content">
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
${this.checklist.length > 0 ? html`
|
||||
@@ -477,6 +496,14 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.scan-required-note {
|
||||
margin: -4px 0 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.phase-line {
|
||||
margin-top: -8px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
@@ -39,6 +39,10 @@ export class MsTextfield extends LitElement {
|
||||
@property() public max?: string;
|
||||
@property() public pattern?: string;
|
||||
@property() public helper?: string;
|
||||
/** Render a <textarea> instead of an <input> (free-text notes). The
|
||||
* `input` event contract is identical — target.value works for both. */
|
||||
@property({ type: Boolean }) public multiline = false;
|
||||
@property({ type: Number }) public rows = 3;
|
||||
|
||||
/** Forwards the native input's value into our `value` property and
|
||||
* re-fires as a bubbling `input` event so consumers reading
|
||||
@@ -56,6 +60,16 @@ export class MsTextfield extends LitElement {
|
||||
return html`
|
||||
<label class="field">
|
||||
${this.label ? html`<span class="label">${this.label}${this.required ? html`<span class="req">*</span>` : nothing}</span>` : nothing}
|
||||
${this.multiline ? html`
|
||||
<textarea
|
||||
.value=${this.value ?? ""}
|
||||
rows=${this.rows}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
></textarea>` : html`
|
||||
<input
|
||||
.value=${this.value ?? ""}
|
||||
.type=${this.type}
|
||||
@@ -68,7 +82,7 @@ export class MsTextfield extends LitElement {
|
||||
pattern=${this.pattern ?? nothing}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
/>
|
||||
/>`}
|
||||
${this.helper ? html`<span class="helper">${this.helper}</span>` : nothing}
|
||||
</label>
|
||||
`;
|
||||
@@ -103,6 +117,21 @@ export class MsTextfield extends LitElement {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
textarea {
|
||||
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;
|
||||
resize: vertical;
|
||||
}
|
||||
textarea:focus { border-color: var(--primary-color); }
|
||||
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -217,6 +217,7 @@ export class MaintenanceObjectDialog extends LitElement {
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
<span class="md-hint">${t("notes_markdown_hint", L)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
@@ -271,6 +272,9 @@ export class MaintenanceObjectDialog extends LitElement {
|
||||
.textarea-field textarea:focus {
|
||||
outline: none; border-color: var(--primary-color);
|
||||
}
|
||||
.md-hint {
|
||||
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
|
||||
}
|
||||
.parent-select {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
|
||||
+3
-1
@@ -10,6 +10,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, STATUS_COLORS, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
@@ -186,7 +187,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
? html`
|
||||
<div class="notes-section">
|
||||
<strong>${t("object_notes_label", L)}</strong>
|
||||
<div class="notes-body">${obj.notes}</div>
|
||||
<div class="notes-body">${renderNotesMarkdown(obj.notes)}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -294,6 +295,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
.task-name { flex: 1; font-size: 14px; }
|
||||
.task-status { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; }
|
||||
.notes-body { white-space: pre-wrap; font-size: 13px; padding: 8px; background: var(--secondary-background-color); border-radius: 6px; }
|
||||
.notes-body ha-markdown { white-space: normal; }
|
||||
.actions { display: flex; gap: 8px; padding-top: 8px; border-top: 1px solid var(--divider-color); }
|
||||
.actions .btn { flex: 1; }
|
||||
.btn {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { live } from "lit/directives/live.js";
|
||||
import type { HomeAssistant, AdvancedFeatures, BudgetStatus, HAUser } from "../types";
|
||||
import { t, langOf } from "../styles";
|
||||
import { signApiPath } from "../helpers/document-url";
|
||||
@@ -36,6 +37,8 @@ interface SettingsResponse {
|
||||
// so the picker matches the options-flow dropdown exactly. Optional for
|
||||
// backward-compat with an older backend that didn't send it.
|
||||
notify_targets?: string[];
|
||||
/** v2.67: todo.* entity the buy-task shopping sync mirrors into ("" = off). */
|
||||
shopping_list_entity?: string;
|
||||
panel_enabled: boolean;
|
||||
panel_title: string;
|
||||
/** Opt-in copy of the shipped Assist sentences into the config dir. */
|
||||
@@ -249,6 +252,11 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
this.dispatchEvent(new CustomEvent("settings-changed"));
|
||||
} catch {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
// The control already shows the rejected value (the browser changed it
|
||||
// before we asked the server). `_settings` is unchanged, so a plain
|
||||
// re-render would not touch the DOM — the selects bind their value
|
||||
// through `live()` precisely so this re-render snaps them back.
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,15 +587,16 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<h3>${t("settings_general", L)}</h3>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_default_warning", L)}</span>
|
||||
<input type="number" min="1" max="365" .value=${String(g.default_warning_days)}
|
||||
<input type="number" min="0" max="365" .value=${String(g.default_warning_days)}
|
||||
@change=${(e: Event) => {
|
||||
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (v >= 1 && v <= 365) this._updateSetting("default_warning_days", v);
|
||||
// 0 = no warning window (due soon only on the due date) — #145.
|
||||
if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v);
|
||||
}} />
|
||||
</label>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_currency", L)}</span>
|
||||
<select @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
|
||||
<select .value=${live(b.currency)} @change=${(e: Event) => this._updateSetting("budget_currency", (e.target as HTMLSelectElement).value)}>
|
||||
${CURRENCIES.map((c) => html`<option value=${c} ?selected=${b.currency === c}>${c}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
@@ -652,10 +661,29 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</div>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
|
||||
<label class="setting-row">
|
||||
<span class="setting-label" title=${t("settings_shopping_list_help", L)}>${t("settings_shopping_list", L)}</span>
|
||||
<select .value=${live(g.shopping_list_entity || "")}
|
||||
@change=${(e: Event) => this._updateSetting("shopping_list_entity", (e.target as HTMLSelectElement).value)}>
|
||||
<option value="" ?selected=${!g.shopping_list_entity}>${t("shopping_list_none", L)}</option>
|
||||
${this._todoEntities(g.shopping_list_entity || "").map((id) => html`
|
||||
<option value=${id} ?selected=${g.shopping_list_entity === id}>${id}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/** All todo.* entities, plus the saved value even when its integration
|
||||
* hasn't loaded (so an offline pick isn't silently dropped from the UI). */
|
||||
private _todoEntities(current: string): string[] {
|
||||
const ids = Object.keys(this.hass?.states || {}).filter((id) => id.startsWith("todo.")).sort();
|
||||
if (current && !ids.includes(current)) ids.unshift(current);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// --- Section: Notifications ---
|
||||
|
||||
private _renderNotifications(L: string) {
|
||||
@@ -762,7 +790,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_notify_scope", L)}</span>
|
||||
<select
|
||||
.value=${n.scope_view_id || ""}
|
||||
.value=${live(n.scope_view_id || "")}
|
||||
@change=${(e: Event) => this._updateSetting("notify_scope_view_id", (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="" ?selected=${!n.scope_view_id}>${t("settings_notify_scope_all", L)}</option>
|
||||
|
||||
@@ -244,6 +244,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// NFC
|
||||
@state() private _lastPerformed = "";
|
||||
@state() private _nfcTagId = "";
|
||||
@state() private _requireTagScan = false;
|
||||
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
|
||||
@state() private _readingUnit = "";
|
||||
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
|
||||
@@ -263,12 +264,16 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
// Task phases (#139): editable defs + the cycle order. The v1 editor keeps
|
||||
// each phase to name + checklist + at most ONE own-object part (the mower
|
||||
// "14 new blades" case); richer per-phase config exists at the WS level.
|
||||
// `carry` holds def fields the editor has no form for (notes,
|
||||
// required_completion_fields, future keys) and `extraParts` any links
|
||||
// beyond the first — both re-merged on save so a no-op edit never wipes
|
||||
// them (#106 pattern; same passthrough idea as trigger_config's carry).
|
||||
// `carry` holds def fields the editor has no form for (notes, future
|
||||
// keys) and `extraParts` any links beyond the first — both re-merged on
|
||||
// save so a no-op edit never wipes them (#106 pattern; same passthrough
|
||||
// idea as trigger_config's carry). Required completion fields are a real
|
||||
// form now (#139 follow-up): `reqOverride` distinguishes "inherit the
|
||||
// task's list" (off) from an explicit per-phase list — including the
|
||||
// meaningful EMPTY override "this phase demands nothing".
|
||||
@state() private _phaseDefs: Array<{
|
||||
id: string; name: string; checklistText: string; partId: string; partQty: string;
|
||||
reqOverride: boolean; reqFields: string[];
|
||||
extraParts: TaskPartLink[]; carry: Record<string, unknown>;
|
||||
}> = [];
|
||||
@state() private _phaseSeq: string[] = [];
|
||||
@@ -348,6 +353,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._entryId = entryId;
|
||||
this._taskId = task.id;
|
||||
this._error = "";
|
||||
// The object picker belongs to CREATE only. openCreate("", objects) left
|
||||
// the choices behind, so Create → Cancel → Edit showed the dropdown in
|
||||
// edit mode — and picking another object re-pointed _entryId while
|
||||
// _taskId kept the old task (audit 2026-08-29).
|
||||
this._objectChoices = [];
|
||||
this._name = task.name;
|
||||
this._type = task.type;
|
||||
this._scheduleType = task.schedule_type;
|
||||
@@ -389,6 +399,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._enabled = task.enabled !== false;
|
||||
this._lastPerformed = task.last_performed || "";
|
||||
this._nfcTagId = task.nfc_tag_id || "";
|
||||
this._requireTagScan = !!task.require_tag_scan;
|
||||
this._readingUnit = task.reading_unit || "";
|
||||
// Whole link, entry_id included — hydrating only part_id would turn every
|
||||
// shared-pool link into an own-part link on the next save (#111).
|
||||
@@ -401,14 +412,28 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
this._checklistText = (task.checklist || []).join("\n");
|
||||
this._phaseDefs = Object.entries(task.phases || {}).map(([id, def]) => {
|
||||
const { name: _n, checklist: _c, consumes_parts: _p, ...carry } = def as unknown as Record<string, unknown>;
|
||||
const {
|
||||
name: _n, checklist: _c, consumes_parts: _p, required_completion_fields: _r,
|
||||
...carry
|
||||
} = def as unknown as Record<string, unknown>;
|
||||
// The editor's single part field is the first OWN link (no entry_id —
|
||||
// the picker only lists the object's own parts). Every other link,
|
||||
// shared pools (#111) first among them, rides along whole in
|
||||
// `extraParts` and is re-emitted on save: hydrating index 0 blindly
|
||||
// turned a phase whose first link is foreign into an own-part link
|
||||
// (or lost it) on the next save (audit 2026-08-29).
|
||||
const links = def.consumes_parts || [];
|
||||
const ownIdx = links.findIndex((l) => !l.entry_id);
|
||||
const own = ownIdx >= 0 ? links[ownIdx] : undefined;
|
||||
return {
|
||||
id,
|
||||
name: def.name || id,
|
||||
checklistText: (def.checklist || []).join("\n"),
|
||||
partId: def.consumes_parts?.[0]?.part_id || "",
|
||||
partQty: def.consumes_parts?.[0]?.quantity != null ? String(def.consumes_parts[0].quantity) : "",
|
||||
extraParts: (def.consumes_parts || []).slice(1),
|
||||
partId: own?.part_id || "",
|
||||
partQty: own?.quantity != null ? String(own.quantity) : "",
|
||||
reqOverride: def.required_completion_fields !== undefined,
|
||||
reqFields: [...(def.required_completion_fields || [])],
|
||||
extraParts: links.filter((_, i) => i !== ownIdx).map((l) => ({ ...l })),
|
||||
carry,
|
||||
};
|
||||
});
|
||||
@@ -525,6 +550,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._enabled = true;
|
||||
this._lastPerformed = "";
|
||||
this._nfcTagId = "";
|
||||
this._requireTagScan = false;
|
||||
this._readingUnit = "";
|
||||
this._consumesParts = {};
|
||||
this._responsibleUserId = null;
|
||||
@@ -1032,7 +1058,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
private _addPhaseDef(): void {
|
||||
const id = this._phaseSlug(`phase-${this._phaseDefs.length + 1}`);
|
||||
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", extraParts: [], carry: {} }];
|
||||
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", reqOverride: false, reqFields: [], extraParts: [], carry: {} }];
|
||||
}
|
||||
|
||||
private _removePhaseDef(id: string): void {
|
||||
@@ -1040,7 +1066,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = this._phaseSeq.filter((s) => s !== id);
|
||||
}
|
||||
|
||||
private _patchPhaseDef(id: string, patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string }>): void {
|
||||
private _patchPhaseDef(
|
||||
id: string,
|
||||
patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string; reqOverride: boolean; reqFields: string[] }>,
|
||||
): void {
|
||||
this._phaseDefs = this._phaseDefs.map((d) => (d.id === id ? { ...d, ...patch } : d));
|
||||
}
|
||||
|
||||
@@ -1084,6 +1113,33 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => this._patchPhaseDef(d.id, { checklistText: (e.target as HTMLTextAreaElement).value })}
|
||||
></textarea>
|
||||
` : nothing}
|
||||
<label class="req-option phase-req-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${d.reqOverride}
|
||||
@change=${(e: Event) => this._patchPhaseDef(d.id, { reqOverride: (e.target as HTMLInputElement).checked })}
|
||||
/>
|
||||
<span>${t("phase_require_override", L)}</span>
|
||||
</label>
|
||||
${d.reqOverride ? html`
|
||||
<div class="required-completion phase-req-fields">
|
||||
${REQUIRED_COMPLETION_KEYS.map((field) => html`
|
||||
<label class="req-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${d.reqFields.includes(field)}
|
||||
@change=${(e: Event) => {
|
||||
const on = (e.target as HTMLInputElement).checked;
|
||||
const next = new Set(d.reqFields);
|
||||
if (on) next.add(field); else next.delete(field);
|
||||
this._patchPhaseDef(d.id, { reqFields: [...next] });
|
||||
}}
|
||||
/>
|
||||
<span>${t(REQUIRED_COMPLETION_LABELS[field], L)}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
`)}
|
||||
<ha-button appearance="plain" @click=${this._addPhaseDef}>
|
||||
@@ -1196,6 +1252,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.enabled = this._enabled;
|
||||
data.last_performed = this._lastPerformed || null;
|
||||
data.nfc_tag_id = this._nfcTagId || null;
|
||||
data.require_tag_scan = this._requireTagScan;
|
||||
data.reading_unit = this._readingUnit.trim() || null;
|
||||
// Task phases (#139): always sent — null clears a removed cycle.
|
||||
{
|
||||
@@ -1210,8 +1267,19 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
const qty = parseFloat(d.partQty);
|
||||
links.push({ part_id: d.partId, quantity: Number.isFinite(qty) && qty > 0 ? qty : 1 });
|
||||
}
|
||||
links.push(...d.extraParts);
|
||||
// entry_id travels only when present (a shared pool, #111) — an
|
||||
// own-part link stays byte-identical to what shipped before.
|
||||
for (const l of d.extraParts) {
|
||||
links.push(
|
||||
l.entry_id
|
||||
? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
|
||||
: { part_id: l.part_id, quantity: l.quantity },
|
||||
);
|
||||
}
|
||||
if (links.length) def.consumes_parts = links;
|
||||
// Override on → send the list, EMPTY included ("demands nothing");
|
||||
// off → omit the key so the task-level list falls through.
|
||||
if (d.reqOverride) def.required_completion_fields = [...d.reqFields];
|
||||
defs[d.id] = def;
|
||||
}
|
||||
const seq = this._phaseSeq.filter((id) => id in defs);
|
||||
@@ -2432,7 +2500,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<div class="dialog-title">${title}</div>
|
||||
<div class="content">
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
${this._objectChoices.length > 0 ? html`
|
||||
${this._taskId === null && this._objectChoices.length > 0 ? html`
|
||||
<div class="select-row">
|
||||
<label>${t("object", L)}</label>
|
||||
<select
|
||||
@@ -2703,6 +2771,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
${this._renderAdaptiveSection(L)}
|
||||
<ms-textfield
|
||||
label="${t("notes_optional", L)}"
|
||||
multiline
|
||||
.rows=${3}
|
||||
.helper=${t("notes_markdown_hint", L)}
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
@@ -2751,6 +2822,15 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<label class="req-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${this._requireTagScan}
|
||||
@change=${(e: Event) => (this._requireTagScan = (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span>${t("require_tag_scan", L)}</span>
|
||||
</label>
|
||||
${this._requireTagScan ? html`<div class="field-help">${t("require_tag_scan_help", L)}</div>` : nothing}
|
||||
<label class="toggle-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2923,6 +3003,15 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
min-height: 56px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.phase-req-toggle {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.phase-req-fields {
|
||||
margin: 2px 0 0 22px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.phase-remove {
|
||||
--mdc-icon-button-size: 36px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
+34
-30
@@ -17,8 +17,8 @@ import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { partsForCompletion } from "../helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "../helpers/phases";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||
@@ -63,6 +63,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
};
|
||||
@state() private _toast = "";
|
||||
private _featuresLoaded = false;
|
||||
/** Currency symbol for the complete dialog's cost suggestion — read off
|
||||
* the same settings response as the feature flags. */
|
||||
private _currencySymbol = "";
|
||||
|
||||
private get _lang(): string {
|
||||
return langOf(this.hass);
|
||||
@@ -93,10 +96,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
features?: Partial<AdvancedFeatures>;
|
||||
budget?: { currency_symbol?: string };
|
||||
}>({ type: "maintenance_supporter/settings" });
|
||||
if (r?.features) {
|
||||
this._features = { ...this._features, ...r.features };
|
||||
}
|
||||
this._currencySymbol = r?.budget?.currency_symbol || "";
|
||||
this._featuresLoaded = true;
|
||||
} catch {
|
||||
// Settings endpoint unavailable — leave defaults (all false), the
|
||||
@@ -158,35 +163,34 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
// (bug audit 2026-08-22).
|
||||
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
|
||||
const task = this._task!;
|
||||
const isBuy = !!(task as { part_ref?: string }).part_ref;
|
||||
// #139: a phased task completes the phase currently due, so the phase's
|
||||
// checklist/parts/required fields replace the task-level ones here.
|
||||
const phase = effectivePhase(task);
|
||||
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
|
||||
if (!isBuy) {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
objects: MaintenanceObjectResponse[];
|
||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||
parts = partsForCompletion({ consumes_parts: links }, this._entryId!, r.objects || [], this._lang);
|
||||
} catch {
|
||||
// Parts stay empty — the dialog still completes without them.
|
||||
}
|
||||
// The object list resolves the parts picker (own inventory + shared
|
||||
// pools, #99/#111), the "consumes" hint lines and a buy task's restock
|
||||
// default — fetched for every task kind, since a buy task needs its
|
||||
// part's restock quantity + unit cost too.
|
||||
let objects: MaintenanceObjectResponse[] = [];
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
objects: MaintenanceObjectResponse[];
|
||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||
objects = r.objects || [];
|
||||
} catch {
|
||||
// Parts stay empty — the dialog still completes without them.
|
||||
}
|
||||
const ok = openCompleteDialog({
|
||||
entry_id: this._entryId!,
|
||||
task_id: this._taskId!,
|
||||
task_name: task.name,
|
||||
checklist: phase ? phase.checklist : (task.checklist || []),
|
||||
adaptive_enabled: !!task.adaptive_config?.enabled,
|
||||
required_completion_fields: phase ? phase.requiredFields : (task.required_completion_fields || []),
|
||||
task_type: task.type || "",
|
||||
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
|
||||
parts,
|
||||
consumes_parts: isBuy ? [] : links,
|
||||
phase_label: phaseLabel(task),
|
||||
});
|
||||
// Same derivation as the panel and the card (phase override, tag-scan
|
||||
// gate, checklist ticks …) — see helpers/complete-dialog-args.
|
||||
const ok = openCompleteDialog(
|
||||
buildCompleteDialogArgs({
|
||||
entryId: this._entryId!,
|
||||
taskId: this._taskId!,
|
||||
taskName: task.name,
|
||||
task,
|
||||
objects,
|
||||
lang: this._lang,
|
||||
checklist: task.checklist || [],
|
||||
adaptiveEnabled: !!task.adaptive_config?.enabled,
|
||||
currencySymbol: this._currencySymbol,
|
||||
}),
|
||||
);
|
||||
if (ok) {
|
||||
this._notifyChanged("complete");
|
||||
this.close();
|
||||
|
||||
@@ -45,6 +45,7 @@ import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-
|
||||
import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog";
|
||||
import type { HomeAssistant, MaintenanceObject } from "./types";
|
||||
import { ensureLocale, isLocaleLoaded, langOf } from "./styles";
|
||||
import { fillAndOpenCompleteDialog, type CompleteDialogArgs } from "./helpers/complete-dialog-args";
|
||||
|
||||
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
|
||||
const TASK_DIALOG_TAG = "maintenance-task-dialog";
|
||||
@@ -274,41 +275,16 @@ export function openHistoryEditDialog(draft: HistoryEntryDraft): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** v2.3.0: open the rich complete-dialog from any Lovelace context. */
|
||||
export function openCompleteDialog(args: {
|
||||
entry_id: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
checklist?: string[];
|
||||
adaptive_enabled?: boolean;
|
||||
/** Details the task demands before it counts as done (v2.44). */
|
||||
required_completion_fields?: string[];
|
||||
/** Reading tasks need type+unit or the value field never renders. */
|
||||
task_type?: string;
|
||||
reading_unit?: string;
|
||||
/** #99/#111: per-completion parts selection incl. shared pools. */
|
||||
parts?: MaintenanceCompleteDialog["parts"];
|
||||
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
|
||||
/** #139: "2/4 · Flip blades" — the phase this completion records. */
|
||||
phase_label?: string;
|
||||
}): boolean {
|
||||
/** v2.3.0: open the rich complete-dialog from any Lovelace context.
|
||||
*
|
||||
* Takes the full argument bag (`buildCompleteDialogArgs` derives it from a
|
||||
* task + object list). Every dialog field is assigned — the dialog is a
|
||||
* singleton, so an omitted field must reset, never leak the previous
|
||||
* task's value (tag-scan gate, restock default, checklist ticks included). */
|
||||
export function openCompleteDialog(args: CompleteDialogArgs): boolean {
|
||||
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
||||
if (!syncHass(dlg)) return false;
|
||||
dlg.entryId = args.entry_id;
|
||||
dlg.taskId = args.task_id;
|
||||
dlg.taskName = args.task_name;
|
||||
dlg.checklist = args.checklist ?? [];
|
||||
dlg.adaptiveEnabled = !!args.adaptive_enabled;
|
||||
dlg.requiredFields = args.required_completion_fields ?? [];
|
||||
// Always assign — the dialog is a singleton, so an omitted field must not
|
||||
// leak the previous task's value.
|
||||
dlg.taskType = args.task_type ?? "";
|
||||
dlg.readingUnit = args.reading_unit ?? "";
|
||||
dlg.parts = args.parts ?? [];
|
||||
dlg.consumesParts = args.consumes_parts ?? [];
|
||||
dlg.phaseLabel = args.phase_label ?? "";
|
||||
dlg.lang = (getHass()?.language) || "en";
|
||||
dlg.open();
|
||||
fillAndOpenCompleteDialog(dlg, args, (getHass()?.language) || "en");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
|
||||
"at_time": "v",
|
||||
"notes_optional": "Poznámky (volitelné)",
|
||||
"notes_markdown_hint": "Markdown je podporován — **tučně**, seznamy, [odkazy](…)",
|
||||
"cost_optional": "Náklady (volitelné)",
|
||||
"duration_minutes": "Doba trvání v minutách (volitelné)",
|
||||
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Název panelu",
|
||||
"settings_notifications": "Oznámení",
|
||||
"settings_notify_service": "Služba oznámení",
|
||||
"settings_shopping_list": "Nákupní seznam (nákupní úkoly)",
|
||||
"settings_shopping_list_help": "Upozornění na nákup docházejících dílů se objeví v tomto seznamu; odškrtnutí doplní zásobu.",
|
||||
"shopping_list_none": "Vypnuto — žádný nákupní seznam",
|
||||
"settings_install_assist_sentences": "Nainstalovat věty pro Assist",
|
||||
"settings_install_assist_sentences_hint": "Zkopíruje hlasové věty do vaší konfigurace, aby je klasický agent Assist rozpoznal. Soubor, který jste upravili, nebude nikdy přepsán.",
|
||||
"test_notification": "Testovací oznámení",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Přidat krok",
|
||||
"phase_current": "Aktuální fáze",
|
||||
"phase_set": "Nastavit jako aktuální",
|
||||
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
|
||||
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)",
|
||||
"chart_history_alarm": "Pohled spouštěče z historie stavů: 1 = stav výstrahy držen po dobu přidržení, 0 = v pořádku (obvykle posledních ~10 dní)",
|
||||
"chart_history_count": "Počet změn sestaven z historie stavů od posledního servisu (obvykle posledních ~10 dní)",
|
||||
"prediction_cycles": "Naučeno z cyklů",
|
||||
"phase_require_override": "Přepsat „Vyžadovat při dokončení“ pro tuto fázi",
|
||||
"history_add_past": "Přidat dřívější dokončení",
|
||||
"require_tag_scan": "Dokončit pouze naskenováním štítku",
|
||||
"require_tag_scan_help": "Důkaz přítomnosti: „Hotovo“ je všude blokováno, dokud nenaskenujete NFC štítek nebo QR kód na zařízení. Automatizace mohou službě complete předat 'via_tag_scan'.",
|
||||
"require_tag_scan_hint": "Tento úkol lze dokončit pouze naskenováním jeho NFC štítku nebo QR kódu na zařízení — uložení zde bude odmítnuto."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Noter (valgfrit)",
|
||||
"notes_markdown_hint": "Markdown understøttes — **fed**, lister, [links](…)",
|
||||
"cost_optional": "Omkostning (valgfrit)",
|
||||
"duration_minutes": "Varighed i minutter (valgfrit)",
|
||||
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sidepanelets titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjeneste",
|
||||
"settings_shopping_list": "Indkøbsliste (købsopgaver)",
|
||||
"settings_shopping_list_help": "Købspåmindelser for dele med lav beholdning vises på denne liste; afkrydsning genopfylder delen.",
|
||||
"shopping_list_none": "Fra — ingen indkøbsliste",
|
||||
"settings_install_assist_sentences": "Installer Assist-sætninger",
|
||||
"settings_install_assist_sentences_hint": "Kopierer stemmesætningerne til din konfiguration, så den klassiske Assist-agent genkender dem. En fil, du selv har redigeret, overskrives aldrig.",
|
||||
"test_notification": "Testnotifikation",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Tilføj trin",
|
||||
"phase_current": "Aktuel fase",
|
||||
"phase_set": "Sæt som aktuel",
|
||||
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
|
||||
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)",
|
||||
"chart_history_alarm": "Udløservisning fra tilstandshistorikken: 1 = alarmtilstand fastholdt i holdetiden, 0 = ok (typisk de seneste ~10 dage)",
|
||||
"chart_history_count": "Antal skift genopbygget fra tilstandshistorikken siden sidste service (typisk de seneste ~10 dage)",
|
||||
"prediction_cycles": "Lært af cyklusser",
|
||||
"phase_require_override": "Tilsidesæt „Kræv ved udførelse“ for denne fase",
|
||||
"history_add_past": "Tilføj tidligere udførelse",
|
||||
"require_tag_scan": "Kan kun udføres ved tag-scanning",
|
||||
"require_tag_scan_help": "Tilstedeværelsesbevis: „Udført“ er blokeret overalt, indtil NFC-tagget eller QR-koden på selve tingen er scannet. Automatiseringer kan give 'via_tag_scan' til complete-tjenesten.",
|
||||
"require_tag_scan_hint": "Denne opgave udføres kun ved at scanne dens NFC-tag eller QR-kode på selve tingen — at gemme her vil blive afvist."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
|
||||
"at_time": "um",
|
||||
"notes_optional": "Notizen (optional)",
|
||||
"notes_markdown_hint": "Markdown wird unterstützt — **fett**, Listen, [Links](…)",
|
||||
"cost_optional": "Kosten (optional)",
|
||||
"duration_minutes": "Dauer in Minuten (optional)",
|
||||
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Panel-Titel",
|
||||
"settings_notifications": "Benachrichtigungen",
|
||||
"settings_notify_service": "Benachrichtigungsdienst",
|
||||
"settings_shopping_list": "Einkaufsliste (Kauf-Aufgaben)",
|
||||
"settings_shopping_list_help": "Kauf-Erinnerungen für knappe Teile erscheinen in dieser To-do-Liste; Abhaken füllt den Bestand auf.",
|
||||
"shopping_list_none": "Aus — keine Einkaufsliste",
|
||||
"settings_install_assist_sentences": "Assist-Sätze installieren",
|
||||
"settings_install_assist_sentences_hint": "Kopiert die Sprachbefehle in deine Konfiguration, damit der klassische Assist-Agent sie erkennt. Eine selbst bearbeitete Datei wird nie überschrieben.",
|
||||
"test_notification": "Test-Benachrichtigung",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Schritt anhängen",
|
||||
"phase_current": "Aktuelle Phase",
|
||||
"phase_set": "Als aktuell setzen",
|
||||
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
|
||||
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)",
|
||||
"chart_history_alarm": "Trigger-Sicht aus dem Recorder-Zustandsverlauf: 1 = Alarmzustand über die Haltezeit gehalten, 0 = in Ordnung (typisch die letzten ~10 Tage)",
|
||||
"chart_history_count": "Wechselzähler aus dem Recorder-Zustandsverlauf seit dem letzten Service (typisch die letzten ~10 Tage)",
|
||||
"prediction_cycles": "Gelernt aus Zyklen",
|
||||
"phase_require_override": "„Bei Erledigung verlangen“ für diese Phase überschreiben",
|
||||
"history_add_past": "Vergangene Erledigung nachtragen",
|
||||
"require_tag_scan": "Nur per Tag-Scan abschließbar",
|
||||
"require_tag_scan_help": "Anwesenheitsnachweis: „Erledigt“ ist überall blockiert, bis das NFC-Tag oder der QR-Code am Gerät gescannt wurde. Automationen können 'via_tag_scan' an den complete-Dienst übergeben.",
|
||||
"require_tag_scan_hint": "Diese Aufgabe kann nur per Scan des NFC-Tags oder QR-Codes am Gerät erledigt werden — Speichern hier wird abgelehnt."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Empty = midnight (default). HA timezone.",
|
||||
"at_time": "at",
|
||||
"notes_optional": "Notes (optional)",
|
||||
"notes_markdown_hint": "Markdown is supported — **bold**, lists, [links](…)",
|
||||
"cost_optional": "Cost (optional)",
|
||||
"duration_minutes": "Duration in minutes (optional)",
|
||||
"completed_at_optional": "Completed at (optional, empty = now)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sidebar panel title",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Notification service",
|
||||
"settings_shopping_list": "Shopping list (buy tasks)",
|
||||
"settings_shopping_list_help": "Low-part buy reminders appear in this to-do list; checking one off restocks the part.",
|
||||
"shopping_list_none": "Off — no shopping list",
|
||||
"settings_install_assist_sentences": "Install Assist sentences",
|
||||
"settings_install_assist_sentences_hint": "Copies the voice sentences into your configuration so the classic Assist agent recognises them. A file you edited yourself is never overwritten.",
|
||||
"test_notification": "Test notification",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Add step",
|
||||
"phase_current": "Current phase",
|
||||
"phase_set": "Set as current",
|
||||
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
|
||||
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)",
|
||||
"chart_history_alarm": "Trigger view from recorder state history: 1 = alert state held for the hold time, 0 = fine (typically the last ~10 days)",
|
||||
"chart_history_count": "Change count rebuilt from recorder state history since the last service (typically the last ~10 days)",
|
||||
"prediction_cycles": "Learned from cycles",
|
||||
"phase_require_override": "Override “Require on completion” for this phase",
|
||||
"history_add_past": "Add past completion",
|
||||
"require_tag_scan": "Only complete by scanning the tag",
|
||||
"require_tag_scan_help": "Proof of presence: Done is blocked on every surface until the NFC tag or QR code on the thing itself is scanned. Automations can pass 'via_tag_scan' to the complete service.",
|
||||
"require_tag_scan_hint": "This task completes only by scanning its NFC tag or QR code on the thing itself — saving here will be refused."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
|
||||
"at_time": "a las",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
"notes_markdown_hint": "Se admite Markdown — **negrita**, listas, [enlaces](…)",
|
||||
"cost_optional": "Coste (opcional)",
|
||||
"duration_minutes": "Duración en minutos (opcional)",
|
||||
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Título del panel lateral",
|
||||
"settings_notifications": "Notificaciones",
|
||||
"settings_notify_service": "Servicio de notificación",
|
||||
"settings_shopping_list": "Lista de la compra (tareas de compra)",
|
||||
"settings_shopping_list_help": "Los recordatorios de compra de piezas escasas aparecen en esta lista; marcar uno repone la pieza.",
|
||||
"shopping_list_none": "Desactivado — sin lista de la compra",
|
||||
"settings_install_assist_sentences": "Instalar frases de Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia las frases de voz en tu configuración para que el agente clásico de Assist las reconozca. Un archivo que hayas editado nunca se sobrescribe.",
|
||||
"test_notification": "Notificación de prueba",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Añadir paso",
|
||||
"phase_current": "Fase actual",
|
||||
"phase_set": "Establecer como actual",
|
||||
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
|
||||
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)",
|
||||
"chart_history_alarm": "Vista del disparador desde el historial de estados: 1 = estado de alerta mantenido durante el tiempo de retención, 0 = correcto (normalmente los últimos ~10 días)",
|
||||
"chart_history_count": "Recuento de cambios reconstruido desde el historial de estados desde el último servicio (normalmente los últimos ~10 días)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Sobrescribir «Requerir al completar» para esta fase",
|
||||
"history_add_past": "Añadir una finalización pasada",
|
||||
"require_tag_scan": "Completar solo escaneando la etiqueta",
|
||||
"require_tag_scan_help": "Prueba de presencia: «Hecho» queda bloqueado en todas partes hasta escanear la etiqueta NFC o el código QR del propio objeto. Las automatizaciones pueden pasar 'via_tag_scan' al servicio complete.",
|
||||
"require_tag_scan_hint": "Esta tarea solo se completa escaneando su etiqueta NFC o código QR en el propio objeto; guardar aquí será rechazado."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
|
||||
"at_time": "klo",
|
||||
"notes_optional": "Muistiinpanot (valinnainen)",
|
||||
"notes_markdown_hint": "Markdown on tuettu — **lihavointi**, listat, [linkit](…)",
|
||||
"cost_optional": "Kustannus (valinnainen)",
|
||||
"duration_minutes": "Kesto minuutteina (valinnainen)",
|
||||
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Sivupalkin paneelin otsikko",
|
||||
"settings_notifications": "Ilmoitukset",
|
||||
"settings_notify_service": "Ilmoituspalvelu",
|
||||
"settings_shopping_list": "Ostoslista (ostotehtävät)",
|
||||
"settings_shopping_list_help": "Vähissä olevien osien ostomuistutukset näkyvät tässä luettelossa; kuittaus täydentää varaston.",
|
||||
"shopping_list_none": "Pois — ei ostoslistaa",
|
||||
"settings_install_assist_sentences": "Asenna Assist-lauseet",
|
||||
"settings_install_assist_sentences_hint": "Kopioi äänikomennot asetuksiisi, jotta perinteinen Assist-agentti tunnistaa ne. Itse muokkaamaasi tiedostoa ei koskaan korvata.",
|
||||
"test_notification": "Testi-ilmoitus",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lisää askel",
|
||||
"phase_current": "Nykyinen vaihe",
|
||||
"phase_set": "Aseta nykyiseksi",
|
||||
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
|
||||
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)",
|
||||
"chart_history_alarm": "Laukaisimen näkymä tilahistoriasta: 1 = hälytystila pysyi pitoajan, 0 = kunnossa (yleensä viimeiset ~10 päivää)",
|
||||
"chart_history_count": "Muutoslaskuri koottu tilahistoriasta viime huollon jälkeen (yleensä viimeiset ~10 päivää)",
|
||||
"prediction_cycles": "Opittu sykleistä",
|
||||
"phase_require_override": "Ohita ”Vaadi valmistuessa” tälle vaiheelle",
|
||||
"history_add_past": "Lisää aiempi suoritus",
|
||||
"require_tag_scan": "Suoritus vain tunnisteen skannauksella",
|
||||
"require_tag_scan_help": "Läsnäolotodistus: ”Valmis” on estetty kaikkialla, kunnes laitteen NFC-tunniste tai QR-koodi on skannattu. Automaatiot voivat välittää 'via_tag_scan' complete-palvelulle.",
|
||||
"require_tag_scan_hint": "Tämä tehtävä valmistuu vain skannaamalla sen NFC-tunniste tai QR-koodi itse laitteesta — tallennus tässä estetään."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
|
||||
"at_time": "à",
|
||||
"notes_optional": "Notes (optionnel)",
|
||||
"notes_markdown_hint": "Markdown est pris en charge — **gras**, listes, [liens](…)",
|
||||
"cost_optional": "Coût (optionnel)",
|
||||
"duration_minutes": "Durée en minutes (optionnel)",
|
||||
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titre du panneau latéral",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Service de notification",
|
||||
"settings_shopping_list": "Liste de courses (tâches d'achat)",
|
||||
"settings_shopping_list_help": "Les rappels d'achat pour pièces en rupture apparaissent dans cette liste ; cocher un élément réapprovisionne la pièce.",
|
||||
"shopping_list_none": "Désactivé — pas de liste de courses",
|
||||
"settings_install_assist_sentences": "Installer les phrases Assist",
|
||||
"settings_install_assist_sentences_hint": "Copie les phrases vocales dans votre configuration afin que l'agent Assist classique les reconnaisse. Un fichier que vous avez modifié n'est jamais écrasé.",
|
||||
"test_notification": "Notification de test",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Ajouter une étape",
|
||||
"phase_current": "Phase actuelle",
|
||||
"phase_set": "Définir comme actuelle",
|
||||
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
|
||||
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)",
|
||||
"chart_history_alarm": "Vue déclencheur issue de l'historique d'états : 1 = état d'alerte maintenu pendant le délai, 0 = normal (généralement les ~10 derniers jours)",
|
||||
"chart_history_count": "Nombre de changements reconstitué à partir de l'historique d'états depuis le dernier entretien (généralement les ~10 derniers jours)",
|
||||
"prediction_cycles": "Appris sur cycles",
|
||||
"phase_require_override": "Remplacer « Exiger à l’achèvement » pour cette phase",
|
||||
"history_add_past": "Ajouter une réalisation passée",
|
||||
"require_tag_scan": "Terminer uniquement en scannant le tag",
|
||||
"require_tag_scan_help": "Preuve de présence : « Terminé » est bloqué partout tant que le tag NFC ou le QR code sur l'objet n'a pas été scanné. Les automatisations peuvent passer 'via_tag_scan' au service complete.",
|
||||
"require_tag_scan_hint": "Cette tâche ne se termine qu'en scannant son tag NFC ou son QR code sur l'objet — l'enregistrement ici sera refusé."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
|
||||
"at_time": "बजे",
|
||||
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
|
||||
"notes_markdown_hint": "Markdown समर्थित है — **बोल्ड**, सूचियाँ, [लिंक](…)",
|
||||
"cost_optional": "लागत (वैकल्पिक)",
|
||||
"duration_minutes": "मिनटों में अवधि (वैकल्पिक)",
|
||||
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "साइडबार पैनल शीर्षक",
|
||||
"settings_notifications": "सूचनाएँ",
|
||||
"settings_notify_service": "सूचना सेवा",
|
||||
"settings_shopping_list": "शॉपिंग सूची (खरीद कार्य)",
|
||||
"settings_shopping_list_help": "कम स्टॉक वाले पुर्जों के खरीद अनुस्मारक इस सूची में दिखते हैं; टिक करने पर पुर्जा री-स्टॉक होता है।",
|
||||
"shopping_list_none": "बंद — कोई शॉपिंग सूची नहीं",
|
||||
"settings_install_assist_sentences": "Assist वाक्य इंस्टॉल करें",
|
||||
"settings_install_assist_sentences_hint": "वॉइस वाक्यों को आपके कॉन्फ़िगरेशन में कॉपी करता है ताकि क्लासिक Assist एजेंट उन्हें पहचान सके। आपके द्वारा संपादित फ़ाइल कभी अधिलेखित नहीं होती।",
|
||||
"test_notification": "परीक्षण सूचना",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "कदम जोड़ें",
|
||||
"phase_current": "वर्तमान चरण",
|
||||
"phase_set": "वर्तमान बनाएँ",
|
||||
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
|
||||
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)",
|
||||
"chart_history_alarm": "स्थिति इतिहास से ट्रिगर दृश्य: 1 = चेतावनी स्थिति होल्ड समय तक बनी रही, 0 = सामान्य (आमतौर पर पिछले ~10 दिन)",
|
||||
"chart_history_count": "पिछली सेवा के बाद से स्थिति इतिहास से पुनर्निर्मित परिवर्तन गणना (आमतौर पर पिछले ~10 दिन)",
|
||||
"prediction_cycles": "चक्रों से सीखा",
|
||||
"phase_require_override": "इस चरण के लिए “पूर्ण करते समय आवश्यक” को ओवरराइड करें",
|
||||
"history_add_past": "पिछली पूर्णता जोड़ें",
|
||||
"require_tag_scan": "केवल टैग स्कैन से पूर्ण करें",
|
||||
"require_tag_scan_help": "उपस्थिति प्रमाण: जब तक वस्तु पर लगा NFC टैग या QR कोड स्कैन न हो, हर जगह “पूर्ण” अवरुद्ध रहता है। ऑटोमेशन complete सेवा को 'via_tag_scan' दे सकते हैं।",
|
||||
"require_tag_scan_hint": "यह कार्य केवल वस्तु पर लगे NFC टैग या QR कोड को स्कैन करके ही पूर्ण होता है — यहाँ सहेजना अस्वीकार होगा।"
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
|
||||
"at_time": "ekkor:",
|
||||
"notes_optional": "Megjegyzések (opcionális)",
|
||||
"notes_markdown_hint": "A Markdown támogatott — **félkövér**, listák, [hivatkozások](…)",
|
||||
"cost_optional": "Költség (opcionális)",
|
||||
"duration_minutes": "Időtartam percben (opcionális)",
|
||||
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Oldalsáv-panel címe",
|
||||
"settings_notifications": "Értesítések",
|
||||
"settings_notify_service": "Értesítési szolgáltatás",
|
||||
"settings_shopping_list": "Bevásárlólista (vásárlási feladatok)",
|
||||
"settings_shopping_list_help": "A fogyóban lévő alkatrészek vásárlási emlékeztetői ezen a listán jelennek meg; a kipipálás feltölti a készletet.",
|
||||
"shopping_list_none": "Ki — nincs bevásárlólista",
|
||||
"settings_install_assist_sentences": "Assist mondatok telepítése",
|
||||
"settings_install_assist_sentences_hint": "Átmásolja a hangmondatokat a konfigurációdba, hogy a klasszikus Assist ügynök felismerje őket. Az általad szerkesztett fájlt soha nem írja felül.",
|
||||
"test_notification": "Tesztértesítés",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lépés hozzáadása",
|
||||
"phase_current": "Aktuális fázis",
|
||||
"phase_set": "Beállítás aktuálisként",
|
||||
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
|
||||
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)",
|
||||
"chart_history_alarm": "Trigger nézet az állapotelőzményekből: 1 = riasztási állapot a tartási idő alatt fennállt, 0 = rendben (általában az utolsó ~10 nap)",
|
||||
"chart_history_count": "Változásszámláló az állapotelőzményekből az utolsó szerviz óta (általában az utolsó ~10 nap)",
|
||||
"prediction_cycles": "Ciklusokból tanulva",
|
||||
"phase_require_override": "A „Megkövetelés elvégzéskor” felülbírálása ennél a fázisnál",
|
||||
"history_add_past": "Korábbi elvégzés rögzítése",
|
||||
"require_tag_scan": "Csak címke beolvasásával fejezhető be",
|
||||
"require_tag_scan_help": "Jelenlét-igazolás: a „Kész” mindenhol blokkolva van, amíg be nem olvasod az eszközön lévő NFC-címkét vagy QR-kódot. Az automatizálások 'via_tag_scan' értéket adhatnak át a complete szolgáltatásnak.",
|
||||
"require_tag_scan_hint": "Ez a feladat csak az eszközön lévő NFC-címke vagy QR-kód beolvasásával fejezhető be — az itteni mentés el lesz utasítva."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
|
||||
"at_time": "alle",
|
||||
"notes_optional": "Note (opzionale)",
|
||||
"notes_markdown_hint": "Markdown è supportato — **grassetto**, elenchi, [link](…)",
|
||||
"cost_optional": "Costo (opzionale)",
|
||||
"duration_minutes": "Durata in minuti (opzionale)",
|
||||
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titolo pannello laterale",
|
||||
"settings_notifications": "Notifiche",
|
||||
"settings_notify_service": "Servizio di notifica",
|
||||
"settings_shopping_list": "Lista della spesa (attività di acquisto)",
|
||||
"settings_shopping_list_help": "I promemoria di acquisto per ricambi scarsi appaiono in questo elenco; spuntarne uno rifornisce il ricambio.",
|
||||
"shopping_list_none": "Disattivato — nessuna lista della spesa",
|
||||
"settings_install_assist_sentences": "Installa le frasi di Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia le frasi vocali nella tua configurazione affinché l'agente Assist classico le riconosca. Un file modificato da te non viene mai sovrascritto.",
|
||||
"test_notification": "Notifica di test",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Aggiungi passo",
|
||||
"phase_current": "Fase attuale",
|
||||
"phase_set": "Imposta come attuale",
|
||||
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
|
||||
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)",
|
||||
"chart_history_alarm": "Vista del trigger dallo storico stati: 1 = stato di allarme mantenuto per il tempo di attesa, 0 = ok (in genere gli ultimi ~10 giorni)",
|
||||
"chart_history_count": "Conteggio dei cambi ricostruito dallo storico stati dall'ultimo intervento (in genere gli ultimi ~10 giorni)",
|
||||
"prediction_cycles": "Appreso da cicli",
|
||||
"phase_require_override": "Sostituisci «Richiedi al completamento» per questa fase",
|
||||
"history_add_past": "Aggiungi un completamento passato",
|
||||
"require_tag_scan": "Completa solo scansionando il tag",
|
||||
"require_tag_scan_help": "Prova di presenza: «Fatto» è bloccato ovunque finché non viene scansionato il tag NFC o il codice QR sull'oggetto. Le automazioni possono passare 'via_tag_scan' al servizio complete.",
|
||||
"require_tag_scan_hint": "Questa attività si completa solo scansionando il suo tag NFC o codice QR sull'oggetto — il salvataggio qui verrà rifiutato."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
|
||||
"at_time": "時刻",
|
||||
"notes_optional": "メモ(任意)",
|
||||
"notes_markdown_hint": "Markdownに対応 — **太字**、リスト、[リンク](…)",
|
||||
"cost_optional": "費用(任意)",
|
||||
"duration_minutes": "所要時間(分、任意)",
|
||||
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "サイドバーパネルのタイトル",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知サービス",
|
||||
"settings_shopping_list": "買い物リスト(購入タスク)",
|
||||
"settings_shopping_list_help": "在庫が少ない部品の購入リマインダーがこのリストに表示され、チェックすると部品が補充されます。",
|
||||
"shopping_list_none": "オフ — 買い物リストなし",
|
||||
"settings_install_assist_sentences": "Assist の文を導入する",
|
||||
"settings_install_assist_sentences_hint": "音声フレーズを設定に複製し、従来の Assist エージェントが認識できるようにします。自分で編集したファイルが上書きされることはありません。",
|
||||
"test_notification": "テスト通知",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "ステップを追加",
|
||||
"phase_current": "現在のフェーズ",
|
||||
"phase_set": "現在に設定",
|
||||
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
|
||||
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)",
|
||||
"chart_history_alarm": "状態履歴からのトリガー表示:1 = 保持時間の間アラート状態が続いた、0 = 正常(通常は直近約10日)",
|
||||
"chart_history_count": "前回のサービス以降の状態履歴から再構築した変化回数(通常は直近約10日)",
|
||||
"prediction_cycles": "学習したサイクル数",
|
||||
"phase_require_override": "このフェーズで「完了時に必須」を上書き",
|
||||
"history_add_past": "過去の完了を追加",
|
||||
"require_tag_scan": "タグのスキャンでのみ完了可能",
|
||||
"require_tag_scan_help": "在席証明:対象物のNFCタグまたはQRコードをスキャンするまで、どの画面でも「完了」はブロックされます。オートメーションはcompleteサービスに 'via_tag_scan' を渡せます。",
|
||||
"require_tag_scan_hint": "このタスクは対象物のNFCタグまたはQRコードをスキャンしないと完了できません。ここでの保存は拒否されます。"
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
|
||||
"at_time": "시각",
|
||||
"notes_optional": "메모 (선택)",
|
||||
"notes_markdown_hint": "Markdown 지원 — **굵게**, 목록, [링크](…)",
|
||||
"cost_optional": "비용 (선택)",
|
||||
"duration_minutes": "소요 시간(분, 선택)",
|
||||
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "사이드바 패널 제목",
|
||||
"settings_notifications": "알림",
|
||||
"settings_notify_service": "알림 서비스",
|
||||
"settings_shopping_list": "쇼핑 목록 (구매 작업)",
|
||||
"settings_shopping_list_help": "부족한 부품의 구매 알림이 이 목록에 표시되며, 체크하면 부품이 재입고됩니다.",
|
||||
"shopping_list_none": "끄기 — 쇼핑 목록 없음",
|
||||
"settings_install_assist_sentences": "Assist 문장 설치",
|
||||
"settings_install_assist_sentences_hint": "음성 문장을 설정에 복사하여 기존 Assist 에이전트가 인식하도록 합니다. 직접 수정한 파일은 절대 덮어쓰지 않습니다.",
|
||||
"test_notification": "테스트 알림",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "스텝 추가",
|
||||
"phase_current": "현재 단계",
|
||||
"phase_set": "현재로 설정",
|
||||
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
|
||||
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)",
|
||||
"chart_history_alarm": "상태 기록 기반 트리거 보기: 1 = 유지 시간 동안 경보 상태 유지, 0 = 정상 (보통 최근 약 10일)",
|
||||
"chart_history_count": "마지막 서비스 이후 상태 기록으로 재구성한 변경 횟수 (보통 최근 약 10일)",
|
||||
"prediction_cycles": "학습한 주기 수",
|
||||
"phase_require_override": "이 단계에서 ‘완료 시 필수’ 재정의",
|
||||
"history_add_past": "지난 완료 추가",
|
||||
"require_tag_scan": "태그 스캔으로만 완료 가능",
|
||||
"require_tag_scan_help": "현장 확인: 기기의 NFC 태그나 QR 코드를 스캔할 때까지 모든 화면에서 '완료'가 차단됩니다. 자동화는 complete 서비스에 'via_tag_scan'을 전달할 수 있습니다.",
|
||||
"require_tag_scan_hint": "이 작업은 기기의 NFC 태그나 QR 코드를 스캔해야만 완료됩니다. 여기서 저장하면 거부됩니다."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Notater (valgfritt)",
|
||||
"notes_markdown_hint": "Markdown støttes — **fet**, lister, [lenker](…)",
|
||||
"cost_optional": "Kostnad (valgfritt)",
|
||||
"duration_minutes": "Varighet i minutter (valgfritt)",
|
||||
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Tittel på sidepanel",
|
||||
"settings_notifications": "Varsler",
|
||||
"settings_notify_service": "Varslingstjeneste",
|
||||
"settings_shopping_list": "Handleliste (kjøpsoppgaver)",
|
||||
"settings_shopping_list_help": "Kjøpspåminnelser for deler med lav beholdning vises i denne listen; avkryssing fyller på beholdningen.",
|
||||
"shopping_list_none": "Av — ingen handleliste",
|
||||
"settings_install_assist_sentences": "Installer Assist-setninger",
|
||||
"settings_install_assist_sentences_hint": "Kopierer talesetningene til konfigurasjonen din slik at den klassiske Assist-agenten gjenkjenner dem. En fil du selv har endret, blir aldri overskrevet.",
|
||||
"test_notification": "Testvarsel",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Legg til trinn",
|
||||
"phase_current": "Gjeldende fase",
|
||||
"phase_set": "Sett som gjeldende",
|
||||
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
|
||||
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)",
|
||||
"chart_history_alarm": "Utløservisning fra tilstandshistorikken: 1 = alarmtilstand holdt gjennom holdetiden, 0 = ok (vanligvis de siste ~10 dagene)",
|
||||
"chart_history_count": "Antall endringer gjenoppbygd fra tilstandshistorikken siden siste service (vanligvis de siste ~10 dagene)",
|
||||
"prediction_cycles": "Lært av sykluser",
|
||||
"phase_require_override": "Overstyr «Krev ved fullføring» for denne fasen",
|
||||
"history_add_past": "Legg til tidligere fullføring",
|
||||
"require_tag_scan": "Kan bare fullføres ved tag-skanning",
|
||||
"require_tag_scan_help": "Tilstedeværelsesbevis: «Fullført» er blokkert overalt til NFC-taggen eller QR-koden på selve tingen er skannet. Automatiseringer kan sende 'via_tag_scan' til complete-tjenesten.",
|
||||
"require_tag_scan_hint": "Denne oppgaven fullføres bare ved å skanne NFC-taggen eller QR-koden på selve tingen — lagring her vil bli avvist."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
|
||||
"at_time": "om",
|
||||
"notes_optional": "Notities (optioneel)",
|
||||
"notes_markdown_hint": "Markdown wordt ondersteund — **vet**, lijsten, [links](…)",
|
||||
"cost_optional": "Kosten (optioneel)",
|
||||
"duration_minutes": "Duur in minuten (optioneel)",
|
||||
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Titel zijbalkpaneel",
|
||||
"settings_notifications": "Meldingen",
|
||||
"settings_notify_service": "Meldingsservice",
|
||||
"settings_shopping_list": "Boodschappenlijst (kooptaken)",
|
||||
"settings_shopping_list_help": "Koopherinneringen voor schaarse onderdelen verschijnen in deze lijst; afvinken vult de voorraad aan.",
|
||||
"shopping_list_none": "Uit — geen boodschappenlijst",
|
||||
"settings_install_assist_sentences": "Assist-zinnen installeren",
|
||||
"settings_install_assist_sentences_hint": "Kopieert de spraakzinnen naar je configuratie zodat de klassieke Assist-agent ze herkent. Een bestand dat je zelf hebt bewerkt wordt nooit overschreven.",
|
||||
"test_notification": "Testmelding",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Stap toevoegen",
|
||||
"phase_current": "Huidige fase",
|
||||
"phase_set": "Instellen als huidige",
|
||||
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
|
||||
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)",
|
||||
"chart_history_alarm": "Triggerweergave uit de statusgeschiedenis: 1 = alarmstatus gedurende de wachttijd aangehouden, 0 = in orde (meestal de laatste ~10 dagen)",
|
||||
"chart_history_count": "Aantal wisselingen opnieuw opgebouwd uit de statusgeschiedenis sinds de laatste service (meestal de laatste ~10 dagen)",
|
||||
"prediction_cycles": "Geleerd uit cycli",
|
||||
"phase_require_override": "„Vereisen bij afronden” voor deze fase overschrijven",
|
||||
"history_add_past": "Eerdere afronding toevoegen",
|
||||
"require_tag_scan": "Alleen af te ronden via tag-scan",
|
||||
"require_tag_scan_help": "Aanwezigheidsbewijs: „Klaar” is overal geblokkeerd totdat de NFC-tag of QR-code op het apparaat zelf is gescand. Automatiseringen kunnen 'via_tag_scan' aan de complete-service meegeven.",
|
||||
"require_tag_scan_hint": "Deze taak wordt alleen afgerond door de NFC-tag of QR-code op het apparaat te scannen — opslaan hier wordt geweigerd."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
|
||||
"at_time": "o",
|
||||
"notes_optional": "Notatki (opcjonalne)",
|
||||
"notes_markdown_hint": "Obsługiwany jest Markdown — **pogrubienie**, listy, [linki](…)",
|
||||
"cost_optional": "Koszt (opcjonalne)",
|
||||
"duration_minutes": "Czas trwania w minutach (opcjonalne)",
|
||||
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Tytuł panelu bocznego",
|
||||
"settings_notifications": "Powiadomienia",
|
||||
"settings_notify_service": "Usługa powiadomień",
|
||||
"settings_shopping_list": "Lista zakupów (zadania zakupowe)",
|
||||
"settings_shopping_list_help": "Przypomnienia o zakupie brakujących części pojawiają się na tej liście; odhaczenie uzupełnia zapas.",
|
||||
"shopping_list_none": "Wyłączone — brak listy zakupów",
|
||||
"settings_install_assist_sentences": "Zainstaluj zdania Assist",
|
||||
"settings_install_assist_sentences_hint": "Kopiuje zdania głosowe do Twojej konfiguracji, aby klasyczny agent Assist je rozpoznawał. Plik zmieniony przez Ciebie nigdy nie zostanie nadpisany.",
|
||||
"test_notification": "Powiadomienie testowe",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Dodaj krok",
|
||||
"phase_current": "Bieżąca faza",
|
||||
"phase_set": "Ustaw jako bieżącą",
|
||||
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
|
||||
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)",
|
||||
"chart_history_alarm": "Widok wyzwalacza z historii stanów: 1 = stan alarmu utrzymany przez czas podtrzymania, 0 = w porządku (zwykle ostatnie ~10 dni)",
|
||||
"chart_history_count": "Licznik zmian odtworzony z historii stanów od ostatniego serwisu (zwykle ostatnie ~10 dni)",
|
||||
"prediction_cycles": "Nauczone z cykli",
|
||||
"phase_require_override": "Nadpisz „Wymagaj przy ukończeniu” dla tej fazy",
|
||||
"history_add_past": "Dodaj wcześniejsze ukończenie",
|
||||
"require_tag_scan": "Ukończenie tylko przez skan tagu",
|
||||
"require_tag_scan_help": "Dowód obecności: „Gotowe” jest wszędzie zablokowane, dopóki nie zeskanujesz tagu NFC lub kodu QR na urządzeniu. Automatyzacje mogą przekazać 'via_tag_scan' do usługi complete.",
|
||||
"require_tag_scan_hint": "To zadanie można ukończyć tylko skanując jego tag NFC lub kod QR na urządzeniu — zapis tutaj zostanie odrzucony."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Observações (opcional)",
|
||||
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [links](…)",
|
||||
"cost_optional": "Custo (opcional)",
|
||||
"duration_minutes": "Duração em minutos (opcional)",
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Título do painel na barra lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"settings_shopping_list": "Lista de compras (tarefas de compra)",
|
||||
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
|
||||
"shopping_list_none": "Desligado — sem lista de compras",
|
||||
"settings_install_assist_sentences": "Instalar frases do Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um arquivo que você editou nunca é sobrescrito.",
|
||||
"test_notification": "Notificação de teste",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adicionar passo",
|
||||
"phase_current": "Fase atual",
|
||||
"phase_set": "Definir como atual",
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)",
|
||||
"chart_history_alarm": "Visão do gatilho a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
|
||||
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Substituir “Exigir na conclusão” para esta fase",
|
||||
"history_add_past": "Adicionar conclusão passada",
|
||||
"require_tag_scan": "Concluir apenas escaneando a etiqueta",
|
||||
"require_tag_scan_help": "Prova de presença: “Concluído” fica bloqueado em toda parte até escanear a etiqueta NFC ou o código QR no próprio objeto. Automações podem passar 'via_tag_scan' ao serviço complete.",
|
||||
"require_tag_scan_hint": "Esta tarefa só é concluída escaneando sua etiqueta NFC ou código QR no próprio objeto — salvar aqui será recusado."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
"notes_markdown_hint": "Markdown é suportado — **negrito**, listas, [ligações](…)",
|
||||
"cost_optional": "Custo (opcional)",
|
||||
"duration_minutes": "Duração em minutos (opcional)",
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Título do painel lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"settings_shopping_list": "Lista de compras (tarefas de compra)",
|
||||
"settings_shopping_list_help": "Os lembretes de compra de peças em falta aparecem nesta lista; marcar um repõe a peça.",
|
||||
"shopping_list_none": "Desligado — sem lista de compras",
|
||||
"settings_install_assist_sentences": "Instalar frases do Assist",
|
||||
"settings_install_assist_sentences_hint": "Copia as frases de voz para a sua configuração para que o agente Assist clássico as reconheça. Um ficheiro que editou nunca é substituído.",
|
||||
"test_notification": "Notificação de teste",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adicionar passo",
|
||||
"phase_current": "Fase atual",
|
||||
"phase_set": "Definir como atual",
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)",
|
||||
"chart_history_alarm": "Vista do disparador a partir do histórico de estados: 1 = estado de alerta mantido pelo tempo de espera, 0 = normal (normalmente os últimos ~10 dias)",
|
||||
"chart_history_count": "Contagem de mudanças reconstruída a partir do histórico de estados desde o último serviço (normalmente os últimos ~10 dias)",
|
||||
"prediction_cycles": "Aprendido de ciclos",
|
||||
"phase_require_override": "Substituir «Exigir na conclusão» para esta fase",
|
||||
"history_add_past": "Adicionar conclusão passada",
|
||||
"require_tag_scan": "Concluir apenas lendo a etiqueta",
|
||||
"require_tag_scan_help": "Prova de presença: «Concluído» fica bloqueado em todo o lado até ler a etiqueta NFC ou o código QR no próprio objeto. As automações podem passar 'via_tag_scan' ao serviço complete.",
|
||||
"require_tag_scan_hint": "Esta tarefa só é concluída lendo a sua etiqueta NFC ou código QR no próprio objeto — guardar aqui será recusado."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
|
||||
"at_time": "в",
|
||||
"notes_optional": "Примечания (опционально)",
|
||||
"notes_markdown_hint": "Поддерживается Markdown — **жирный**, списки, [ссылки](…)",
|
||||
"cost_optional": "Стоимость (опционально)",
|
||||
"duration_minutes": "Длительность в минутах (опционально)",
|
||||
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Заголовок панели",
|
||||
"settings_notifications": "Уведомления",
|
||||
"settings_notify_service": "Сервис уведомлений",
|
||||
"settings_shopping_list": "Список покупок (задачи покупки)",
|
||||
"settings_shopping_list_help": "Напоминания о покупке заканчивающихся деталей появляются в этом списке; отметка пополняет запас.",
|
||||
"shopping_list_none": "Выкл. — без списка покупок",
|
||||
"settings_install_assist_sentences": "Установить фразы Assist",
|
||||
"settings_install_assist_sentences_hint": "Копирует голосовые фразы в вашу конфигурацию, чтобы классический агент Assist их распознавал. Файл, который вы изменили, никогда не перезаписывается.",
|
||||
"test_notification": "Тестовое уведомление",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Добавить шаг",
|
||||
"phase_current": "Текущая фаза",
|
||||
"phase_set": "Сделать текущей",
|
||||
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
|
||||
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)",
|
||||
"chart_history_alarm": "Вид триггера из истории состояний: 1 = состояние тревоги удерживалось в течение времени удержания, 0 = норма (обычно последние ~10 дней)",
|
||||
"chart_history_count": "Счётчик изменений восстановлен из истории состояний с момента последнего обслуживания (обычно последние ~10 дней)",
|
||||
"prediction_cycles": "Изучено по циклам",
|
||||
"phase_require_override": "Переопределить «Требовать при выполнении» для этой фазы",
|
||||
"history_add_past": "Добавить прошлое выполнение",
|
||||
"require_tag_scan": "Завершение только по скану метки",
|
||||
"require_tag_scan_help": "Подтверждение присутствия: «Готово» заблокировано везде, пока не отсканирована NFC-метка или QR-код на самом устройстве. Автоматизации могут передавать 'via_tag_scan' в службу complete.",
|
||||
"require_tag_scan_hint": "Эта задача завершается только сканированием её NFC-метки или QR-кода на самом устройстве — сохранение здесь будет отклонено."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Anteckningar (valfritt)",
|
||||
"notes_markdown_hint": "Markdown stöds — **fetstil**, listor, [länkar](…)",
|
||||
"cost_optional": "Kostnad (valfritt)",
|
||||
"duration_minutes": "Varaktighet i minuter (valfritt)",
|
||||
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Sidopanelens titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjänst",
|
||||
"settings_shopping_list": "Inköpslista (köpuppgifter)",
|
||||
"settings_shopping_list_help": "Köppåminnelser för delar som tar slut visas i denna lista; att bocka av en fyller på lagret.",
|
||||
"shopping_list_none": "Av — ingen inköpslista",
|
||||
"settings_install_assist_sentences": "Installera Assist-meningar",
|
||||
"settings_install_assist_sentences_hint": "Kopierar rösmeningarna till din konfiguration så att den klassiska Assist-agenten känner igen dem. En fil du själv har ändrat skrivs aldrig över.",
|
||||
"test_notification": "Testnotifikation",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Lägg till steg",
|
||||
"phase_current": "Aktuell fas",
|
||||
"phase_set": "Ange som aktuell",
|
||||
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
|
||||
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)",
|
||||
"chart_history_alarm": "Utlösarvy från tillståndshistoriken: 1 = larmläge hållet under hålltiden, 0 = ok (vanligtvis de senaste ~10 dagarna)",
|
||||
"chart_history_count": "Antal ändringar återskapat från tillståndshistoriken sedan senaste service (vanligtvis de senaste ~10 dagarna)",
|
||||
"prediction_cycles": "Lärt av cykler",
|
||||
"phase_require_override": "Åsidosätt ”Kräv vid slutförande” för denna fas",
|
||||
"history_add_past": "Lägg till tidigare slutförande",
|
||||
"require_tag_scan": "Kan endast slutföras via tagg-skanning",
|
||||
"require_tag_scan_help": "Närvarobevis: ”Klar” är blockerad överallt tills NFC-taggen eller QR-koden på själva saken har skannats. Automationer kan skicka 'via_tag_scan' till complete-tjänsten.",
|
||||
"require_tag_scan_hint": "Den här uppgiften slutförs endast genom att skanna dess NFC-tagg eller QR-kod på själva saken — att spara här kommer att nekas."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
|
||||
"at_time": "saat",
|
||||
"notes_optional": "Notlar (isteğe bağlı)",
|
||||
"notes_markdown_hint": "Markdown desteklenir — **kalın**, listeler, [bağlantılar](…)",
|
||||
"cost_optional": "Maliyet (isteğe bağlı)",
|
||||
"duration_minutes": "Dakika cinsinden süre (isteğe bağlı)",
|
||||
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "Kenar çubuğu paneli başlığı",
|
||||
"settings_notifications": "Bildirimler",
|
||||
"settings_notify_service": "Bildirim servisi",
|
||||
"settings_shopping_list": "Alışveriş listesi (satın alma görevleri)",
|
||||
"settings_shopping_list_help": "Azalan parçalar için satın alma hatırlatıcıları bu listede görünür; işaretlemek parçayı yeniden stoklar.",
|
||||
"shopping_list_none": "Kapalı — alışveriş listesi yok",
|
||||
"settings_install_assist_sentences": "Assist cümlelerini yükle",
|
||||
"settings_install_assist_sentences_hint": "Sesli komut cümlelerini yapılandırmanıza kopyalar, böylece klasik Assist aracısı bunları tanır. Kendi düzenlediğiniz bir dosyanın üzerine asla yazılmaz.",
|
||||
"test_notification": "Test bildirimi",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Adım ekle",
|
||||
"phase_current": "Geçerli aşama",
|
||||
"phase_set": "Geçerli olarak ayarla",
|
||||
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
|
||||
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)",
|
||||
"chart_history_alarm": "Durum geçmişinden tetikleyici görünümü: 1 = uyarı durumu bekletme süresi boyunca sürdü, 0 = normal (genellikle son ~10 gün)",
|
||||
"chart_history_count": "Değişim sayısı son servisten bu yana durum geçmişinden yeniden oluşturuldu (genellikle son ~10 gün)",
|
||||
"prediction_cycles": "Döngülerden öğrenildi",
|
||||
"phase_require_override": "Bu aşama için “Tamamlarken zorunlu tut” ayarını geçersiz kıl",
|
||||
"history_add_past": "Geçmiş tamamlama ekle",
|
||||
"require_tag_scan": "Yalnızca etiket taranarak tamamlanır",
|
||||
"require_tag_scan_help": "Yerinde olma kanıtı: Cihazın üzerindeki NFC etiketi veya QR kodu taranana kadar 'Tamamlandı' her yerde engellenir. Otomasyonlar complete servisine 'via_tag_scan' iletebilir.",
|
||||
"require_tag_scan_hint": "Bu görev yalnızca cihazın üzerindeki NFC etiketi veya QR kodu taranarak tamamlanır — buradan kaydetme reddedilecektir."
|
||||
}
|
||||
@@ -126,6 +126,7 @@
|
||||
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
|
||||
"at_time": "о",
|
||||
"notes_optional": "Примітки (необов'язково)",
|
||||
"notes_markdown_hint": "Підтримується Markdown — **жирний**, списки, [посилання](…)",
|
||||
"cost_optional": "Вартість (необов'язково)",
|
||||
"duration_minutes": "Тривалість у хвилинах (необов'язково)",
|
||||
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
|
||||
@@ -451,6 +452,9 @@
|
||||
"settings_panel_title": "Заголовок панелі",
|
||||
"settings_notifications": "Сповіщення",
|
||||
"settings_notify_service": "Служба сповіщень",
|
||||
"settings_shopping_list": "Список покупок (завдання покупки)",
|
||||
"settings_shopping_list_help": "Нагадування про купівлю деталей, що закінчуються, з'являються в цьому списку; відмітка поповнює запас.",
|
||||
"shopping_list_none": "Вимк. — без списку покупок",
|
||||
"settings_install_assist_sentences": "Встановити фрази Assist",
|
||||
"settings_install_assist_sentences_hint": "Копіює голосові фрази до вашої конфігурації, щоб класичний агент Assist їх розпізнавав. Файл, який ви редагували, ніколи не перезаписується.",
|
||||
"test_notification": "Тестове сповіщення",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "Додати крок",
|
||||
"phase_current": "Поточна фаза",
|
||||
"phase_set": "Зробити поточною",
|
||||
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
|
||||
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)",
|
||||
"chart_history_alarm": "Вигляд тригера з історії станів: 1 = стан тривоги утримувався протягом часу утримання, 0 = норма (зазвичай останні ~10 днів)",
|
||||
"chart_history_count": "Лічильник змін відновлено з історії станів від останнього обслуговування (зазвичай останні ~10 днів)",
|
||||
"prediction_cycles": "Вивчено з циклів",
|
||||
"phase_require_override": "Перевизначити «Вимагати при виконанні» для цієї фази",
|
||||
"history_add_past": "Додати минуле виконання",
|
||||
"require_tag_scan": "Завершення лише скануванням мітки",
|
||||
"require_tag_scan_help": "Підтвердження присутності: «Готово» заблоковано всюди, доки не відскановано NFC-мітку або QR-код на самому пристрої. Автоматизації можуть передати 'via_tag_scan' службі complete.",
|
||||
"require_tag_scan_hint": "Це завдання завершується лише скануванням його NFC-мітки або QR-коду на самому пристрої — збереження тут буде відхилено."
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
|
||||
"at_time": "于",
|
||||
"notes_optional": "备注 (可选)",
|
||||
"notes_markdown_hint": "支持 Markdown — **加粗**、列表、[链接](…)",
|
||||
"cost_optional": "成本 (可选)",
|
||||
"duration_minutes": "耗时 (分钟, 可选)",
|
||||
"completed_at_optional": "完成时间(可选,留空 = 现在)",
|
||||
@@ -453,6 +454,9 @@
|
||||
"settings_panel_title": "侧边栏面板标题",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知服务",
|
||||
"settings_shopping_list": "购物清单(购买任务)",
|
||||
"settings_shopping_list_help": "库存不足部件的购买提醒会显示在此清单中;勾选即可补充库存。",
|
||||
"shopping_list_none": "关闭 — 无购物清单",
|
||||
"settings_install_assist_sentences": "安装 Assist 语句",
|
||||
"settings_install_assist_sentences_hint": "将语音语句复制到你的配置中,让传统 Assist 代理能够识别它们。你自己修改过的文件永远不会被覆盖。",
|
||||
"test_notification": "测试通知",
|
||||
@@ -898,5 +902,13 @@
|
||||
"phase_sequence_add_step": "添加步骤",
|
||||
"phase_current": "当前阶段",
|
||||
"phase_set": "设为当前",
|
||||
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
|
||||
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)",
|
||||
"chart_history_alarm": "来自状态历史的触发视图:1 = 警报状态保持了保持时间,0 = 正常(通常为最近约 10 天)",
|
||||
"chart_history_count": "根据上次维护以来的状态历史重建的变化次数(通常为最近约 10 天)",
|
||||
"prediction_cycles": "已学习的周期数",
|
||||
"phase_require_override": "为此阶段覆盖“完成时要求”",
|
||||
"history_add_past": "补录过去的完成记录",
|
||||
"require_tag_scan": "仅可通过扫描标签完成",
|
||||
"require_tag_scan_help": "在场证明:在扫描设备上的 NFC 标签或二维码之前,所有界面的“完成”均被阻止。自动化可向 complete 服务传递 'via_tag_scan'。",
|
||||
"require_tag_scan_hint": "此任务只能通过扫描设备上的 NFC 标签或二维码来完成——在此保存将被拒绝。"
|
||||
}
|
||||
@@ -18,10 +18,11 @@ import type {
|
||||
SavedViewFilters,
|
||||
} from "./types";
|
||||
import { UserService } from "./user-service";
|
||||
import { partsForCompletion } from "./helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||
import { phaseLabel } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import "./maintenance-card-editor";
|
||||
import "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
||||
// available on every dashboard without another extra_module_url entry.
|
||||
import "./components/battery-fleet-card";
|
||||
@@ -514,32 +515,26 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
@click=${(e: Event) => {
|
||||
// Stop the row's open-task handler from also firing
|
||||
e.stopPropagation();
|
||||
const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any;
|
||||
// #139: a phased task completes the phase
|
||||
// currently due — its fields override the
|
||||
// task-level ones (fallthrough included).
|
||||
const phase = effectivePhase(task);
|
||||
dlg.entryId = entry_id;
|
||||
dlg.taskId = task.id;
|
||||
dlg.taskName = task.name;
|
||||
dlg.checklist = phase ? phase.checklist : (task.checklist || []);
|
||||
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
|
||||
dlg.taskType = task.type || "";
|
||||
dlg.readingUnit = (task as any).reading_unit || "";
|
||||
dlg.requiredFields = phase ? phase.requiredFields : (task.required_completion_fields || []);
|
||||
dlg.phaseLabel = phaseLabel(task);
|
||||
dlg.lang = L;
|
||||
// #99: editable per-completion parts selection
|
||||
// (skip on buy tasks — those restock instead).
|
||||
// #111: the list also carries the shared pools
|
||||
// this task draws on, each named after its
|
||||
// owner — resolving against the object's own
|
||||
// parts alone left a foreign link invisible.
|
||||
const isBuy = !!(task as any).part_ref;
|
||||
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||
dlg.parts = isBuy ? [] : partsForCompletion({ consumes_parts: links }, entry_id, this._objects, L);
|
||||
dlg.consumesParts = isBuy ? [] : links;
|
||||
dlg.open();
|
||||
const dlg = this.shadowRoot!.querySelector<MaintenanceCompleteDialog>("maintenance-complete-dialog")!;
|
||||
// The SAME derivation the panel uses (phase
|
||||
// override, parts incl. shared pools, tag-scan
|
||||
// gate, restock default, checklist ticks) — the
|
||||
// card used to forward a subset.
|
||||
fillAndOpenCompleteDialog(
|
||||
dlg,
|
||||
buildCompleteDialogArgs({
|
||||
entryId: entry_id,
|
||||
taskId: task.id,
|
||||
taskName: task.name,
|
||||
task,
|
||||
objects: this._objects,
|
||||
lang: L,
|
||||
checklist: task.checklist || [],
|
||||
adaptiveEnabled: !!task.adaptive_config?.enabled,
|
||||
currencySymbol: this._stats?.budget?.currency_symbol || "",
|
||||
}),
|
||||
L,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
|
||||
@@ -15,9 +15,11 @@ import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
|
||||
import { warrantyStatus } from "./helpers/warranty";
|
||||
import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns";
|
||||
import { downloadTextFile } from "./helpers/download";
|
||||
import { renderNotesMarkdown } from "./helpers/notes-markdown";
|
||||
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
|
||||
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
|
||||
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||
import { describePartLink } from "./helpers/shared-parts";
|
||||
import { effectivePhase } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import { describeWsError } from "./ws-errors";
|
||||
import { panelStyles } from "./panel-styles";
|
||||
import type {
|
||||
@@ -694,6 +696,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
if ((msg as SubscriptionEvent<MaintenanceObjectResponse>).objects) {
|
||||
writeObjectsCache(next, this._stats ?? null);
|
||||
}
|
||||
// The KPI row (_stats) and the budget card come from their own
|
||||
// endpoints — a pushed change (completion from the card, a timer
|
||||
// wave flipping a task overdue) left them frozen at the last
|
||||
// _loadData until the next user action.
|
||||
void this._refreshKpis();
|
||||
// The open task's full history is a separate fetch too; the
|
||||
// truncated list payload never wins the "longer list" comparison
|
||||
// in _historyCtx, so a pushed completion stayed invisible there.
|
||||
if (
|
||||
this._view === "task" && this._selectedEntryId && this._selectedTaskId
|
||||
&& (!!ev.objects || (ev.delta || []).some((d) => d.entry_id === this._selectedEntryId))
|
||||
) {
|
||||
void this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
|
||||
}
|
||||
}
|
||||
},
|
||||
// deltas: only entries whose rebuilt response actually changed —
|
||||
@@ -713,6 +729,34 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Refetch the KPI statistics + budget status after a pushed object change.
|
||||
* Coalesced: a burst of deltas (import, timer wave) runs at most one
|
||||
* in-flight fetch plus one trailing catch-up instead of one per event. */
|
||||
private _kpiRefreshInFlight = false;
|
||||
private _kpiRefreshPending = false;
|
||||
|
||||
private async _refreshKpis(): Promise<void> {
|
||||
if (this._kpiRefreshInFlight) {
|
||||
this._kpiRefreshPending = true;
|
||||
return;
|
||||
}
|
||||
this._kpiRefreshInFlight = true;
|
||||
try {
|
||||
do {
|
||||
this._kpiRefreshPending = false;
|
||||
const [statsResult, budgetResult] = await Promise.all([
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null),
|
||||
this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null),
|
||||
]);
|
||||
if (!this.isConnected) return;
|
||||
if (statsResult) this._stats = statsResult as StatisticsResponse;
|
||||
if (budgetResult) this._budget = budgetResult as BudgetStatus;
|
||||
} while (this._kpiRefreshPending);
|
||||
} finally {
|
||||
this._kpiRefreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data accessors ---
|
||||
|
||||
private get _taskRows(): TaskRow[] {
|
||||
@@ -1734,17 +1778,27 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._showToast(t("quick_complete_success", this._lang));
|
||||
} catch (e: unknown) {
|
||||
const code = (e as { code?: string })?.code || "";
|
||||
if (code === "no_defaults") {
|
||||
// No defaults set — open the dialog so the user can fill them in.
|
||||
// Both refusals mean "the dialog has to collect something": no
|
||||
// quick-complete defaults configured, or the task demands completion
|
||||
// details a silent complete cannot supply. The dialog opens as the
|
||||
// scan's fallback — via_tag_scan keeps the proof of presence.
|
||||
if (code === "no_defaults" || code === "completion_details_required") {
|
||||
this._openCompleteDialog(
|
||||
entryId, taskId, task.name,
|
||||
this._features.checklists ? task.checklist : undefined,
|
||||
this._features.adaptive && !!task.adaptive_config?.enabled,
|
||||
{ viaTagScan: true },
|
||||
);
|
||||
} else {
|
||||
this._showToast(t("action_error", this._lang));
|
||||
// Relay the server's reason (too_early carries the earliest date)
|
||||
// instead of a generic "action failed".
|
||||
this._showToast(describeWsError(e, this._lang, t("action_error", this._lang)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Silent success: the list must reflect the completion right away — the
|
||||
// subscription delta may lag or be coalesced.
|
||||
try { await this._loadData(); } catch { /* subscription will sync */ }
|
||||
}
|
||||
|
||||
// v2.21: printable one-pager for a task — details, checklist tick boxes,
|
||||
@@ -1874,59 +1928,41 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
private _openCompleteDialog(
|
||||
entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean,
|
||||
opts?: { viaTagScan?: boolean },
|
||||
): void {
|
||||
void this._ui<MaintenanceCompleteDialog>("maintenance-complete-dialog")
|
||||
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled));
|
||||
.then((dlg) => dlg && this._fillAndOpenCompleteDialog(dlg, entryId, taskId, taskName, checklist, adaptiveEnabled, opts));
|
||||
}
|
||||
|
||||
private _fillAndOpenCompleteDialog(dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void {
|
||||
dlg.entryId = entryId;
|
||||
dlg.taskId = taskId;
|
||||
dlg.taskName = taskName;
|
||||
dlg.lang = this._lang;
|
||||
dlg.checklist = checklist || [];
|
||||
dlg.adaptiveEnabled = !!adaptiveEnabled;
|
||||
// v2.20 (#83): reading tasks get a value field — resolve type + unit here
|
||||
// so none of the many call sites need to thread them through.
|
||||
const tk = this._objects
|
||||
.find((o) => o.entry_id === entryId)
|
||||
?.tasks.find((tsk) => tsk.id === taskId);
|
||||
// #139: a phased task completes the phase currently due — its checklist,
|
||||
// parts and required fields override the task-level values (fallthrough
|
||||
// is already baked into effectivePhase). Resolved HERE so every call
|
||||
// site of _openCompleteDialog rotates for free.
|
||||
const phase = tk ? effectivePhase(tk) : null;
|
||||
dlg.phaseLabel = phase ? phaseLabel(tk) : "";
|
||||
dlg.taskType = tk?.type || "";
|
||||
dlg.readingUnit = tk?.reading_unit || "";
|
||||
// #73: ticks recorded during the cycle prefill the dialog's checklist.
|
||||
dlg.checklistPrefill = tk?.checklist_progress || {};
|
||||
dlg.requiredFields = phase ? phase.requiredFields : (tk?.required_completion_fields || []);
|
||||
if (phase) {
|
||||
dlg.checklist = this._features.checklists ? phase.checklist : [];
|
||||
}
|
||||
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
||||
// task shows what it will decrement (incl. the storage location).
|
||||
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._currencySymbol;
|
||||
// #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).
|
||||
const links = phase ? phase.consumesParts : (tk?.consumes_parts || []);
|
||||
dlg.consumesInfo = links.map((link) =>
|
||||
describePartLink(link, entryId, this._objects, this._lang),
|
||||
/** Every dialog field is derived in helpers/complete-dialog-args — the
|
||||
* ONE derivation the panel, the Lovelace card and the quick-actions
|
||||
* dialog share (phase override #139, reading type+unit #83, restock
|
||||
* default + unit cost #104, shared pools #111, checklist ticks #73,
|
||||
* tag-scan gate). Resolved HERE so none of the call sites need to
|
||||
* thread anything but the name through. */
|
||||
private _fillAndOpenCompleteDialog(
|
||||
dlg: MaintenanceCompleteDialog, entryId: string, taskId: string, taskName: string,
|
||||
checklist?: string[], adaptiveEnabled?: boolean, opts?: { viaTagScan?: boolean },
|
||||
): void {
|
||||
fillAndOpenCompleteDialog(
|
||||
dlg,
|
||||
buildCompleteDialogArgs({
|
||||
entryId,
|
||||
taskId,
|
||||
taskName,
|
||||
task: this._getTask(entryId, taskId),
|
||||
objects: this._objects,
|
||||
lang: this._lang,
|
||||
checklist,
|
||||
checklistsEnabled: this._features.checklists,
|
||||
adaptiveEnabled,
|
||||
currencySymbol: this._currencySymbol,
|
||||
viaTagScan: opts?.viaTagScan,
|
||||
}),
|
||||
this._lang,
|
||||
);
|
||||
// #99: editable per-completion parts selection (not on buy tasks — those
|
||||
// RESTOCK via the qty field instead of consuming). The list carries the
|
||||
// object's own parts plus the shared pools this task draws on, so a foreign
|
||||
// link is visible and untickable rather than silently absent.
|
||||
dlg.parts = tk?.part_ref ? [] : partsForCompletion({ consumes_parts: links }, entryId, this._objects, this._lang);
|
||||
dlg.consumesParts = tk?.part_ref ? [] : links;
|
||||
dlg.open();
|
||||
}
|
||||
|
||||
private _openQrForObject(entryId: string, objectName: string): void {
|
||||
@@ -3287,7 +3323,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${o.notes
|
||||
? html`<div class="object-notes">
|
||||
<div class="object-notes-label">${t("object_notes_label", L)}</div>
|
||||
<div class="object-notes-body">${o.notes}</div>
|
||||
<div class="object-notes-body">${renderNotesMarkdown(o.notes)}</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
|
||||
@@ -445,6 +445,12 @@ export const panelStyles = css`
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
}
|
||||
/* Markdown notes: <ha-markdown> parses the source itself — the container's
|
||||
pre-wrap must not leak into its rendered paragraphs. */
|
||||
.object-notes-body ha-markdown,
|
||||
.task-meta-notes ha-markdown {
|
||||
white-space: normal;
|
||||
}
|
||||
.empty { color: var(--secondary-text-color); font-style: italic; }
|
||||
.analysis-empty-state { text-align: center; padding: 24px 16px; }
|
||||
.analysis-empty-state .empty { font-size: 15px; margin-bottom: 8px; }
|
||||
@@ -1053,6 +1059,19 @@ export const panelStyles = css`
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* #142: "add a past completion" entry point in the history tab — the
|
||||
backdate field lives in the complete dialog, but people LOOK for it
|
||||
here. */
|
||||
.history-add-past {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.history-add-past-btn ha-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* #139: cycle-phase strip in the task overview */
|
||||
.phases-card {
|
||||
background: var(--card-background-color, #fff);
|
||||
|
||||
@@ -44,6 +44,7 @@ export function renderPredictionSection(task: MaintenanceTask, lang: string, fea
|
||||
<span class="prediction-value prediction-days${task.days_until_threshold === 0 ? " exceeded" : task.sensor_prediction_urgency ? " urgent" : ""}">${task.days_until_threshold === 0 ? t("threshold_exceeded", lang) : "~" + Math.round(task.days_until_threshold!) + " " + t("days", lang)}</span>
|
||||
${task.threshold_prediction_date ? html`<span class="prediction-date">${formatDate(task.threshold_prediction_date, lang)}</span>` : nothing}
|
||||
${task.threshold_prediction_confidence ? html`<span class="confidence-dot ${task.threshold_prediction_confidence}"></span>` : nothing}
|
||||
${(task.prediction_cycles ?? 0) > 0 ? html`<span class="prediction-cycles">${t("prediction_cycles", lang)}: ${task.prediction_cycles}</span>` : nothing}
|
||||
</div>
|
||||
` : nothing}
|
||||
${hasEnv && features.environmental ? html`
|
||||
|
||||
@@ -39,6 +39,100 @@ export interface SparklineContext {
|
||||
* [Q1 − 1.5·IQR, Q3 + 1.5·IQR]. Robust to a few wild glitch readings (a
|
||||
* pressure sensor spiking to 100 while it normally sits at 1.5–3). No-ops on
|
||||
* short series (< 4 points) where quartiles aren't meaningful. */
|
||||
/** A binary HA state as the 0/1 level the history fallback plots, or null
|
||||
* for states the fallback cannot express (then no trigger view is built). */
|
||||
export function binaryLevel(state: string | null | undefined): 0 | 1 | null {
|
||||
const s = (state ?? "").trim().toLowerCase();
|
||||
if (s === "on" || s === "open" || s === "true") return 1;
|
||||
if (s === "off" || s === "closed" || s === "false") return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface StateChangeView {
|
||||
points: ChartPoint[];
|
||||
/** "alarm": single-shot latch (target 1 + a to-state) — 1 while the alert
|
||||
* state is held, 0 otherwise. "count": change counter since the last
|
||||
* service, stepped, honouring the hold filter. */
|
||||
mode: "alarm" | "count";
|
||||
}
|
||||
|
||||
/** #141 (round 2): the recorder fallback plots the RAW entity state, but a
|
||||
* state_change trigger is about transitions. A water-valve switch that is
|
||||
* "on" all week with a few second-long "off" blips drew a flat 1 with the
|
||||
* "Target changes: 1" line on the same axis — while the header said 0 / 1,
|
||||
* because those blips never held for the #136 hold time. Re-express the
|
||||
* step series in the trigger's own terms:
|
||||
*
|
||||
* - latch (target_changes == 1 with a to-state): 1 = alert state HELD for
|
||||
* the hold time, 0 = fine. Reads as "is the trigger active" — for a
|
||||
* problem sensor (to-state on) this equals the raw line, for a switch
|
||||
* watched for "off" it is the inverse the user expects.
|
||||
* - counter (everything else): the cumulative number of matching, held
|
||||
* transitions since the last service, anchored so the line ends at the
|
||||
* trigger's live count (transitions before the recorder window are
|
||||
* unknown but already in that count).
|
||||
*
|
||||
* Returns null when the from/to states cannot be expressed as 0/1. */
|
||||
export function stateChangeView(
|
||||
points: ChartPoint[],
|
||||
tc: { trigger_from_state?: string | null; trigger_to_state?: string | null; trigger_target_changes?: number; trigger_for_minutes?: number },
|
||||
opts: { since: number | null; current: number | null; now?: number },
|
||||
): StateChangeView | null {
|
||||
if (points.length < 2) return null;
|
||||
const now = opts.now ?? Date.now();
|
||||
const holdMs = Math.max(0, tc.trigger_for_minutes ?? 0) * 60000;
|
||||
const fromLevel = tc.trigger_from_state ? binaryLevel(tc.trigger_from_state) : null;
|
||||
const toLevel = tc.trigger_to_state ? binaryLevel(tc.trigger_to_state) : null;
|
||||
if ((tc.trigger_from_state && fromLevel === null) || (tc.trigger_to_state && toLevel === null)) return null;
|
||||
|
||||
// Collapse the doubled step points into held segments (start, level).
|
||||
const sorted = [...points].sort((a, b) => a.ts - b.ts);
|
||||
const segments: { start: number; level: number }[] = [];
|
||||
for (const p of sorted) {
|
||||
const last = segments[segments.length - 1];
|
||||
if (!last || last.level !== p.val) segments.push({ start: p.ts, level: p.val });
|
||||
}
|
||||
const held = (i: number) => {
|
||||
const end = i + 1 < segments.length ? segments[i + 1].start : now;
|
||||
return end - segments[i].start >= holdMs;
|
||||
};
|
||||
const step = (out: ChartPoint[], ts: number, val: number) => {
|
||||
const last = out[out.length - 1];
|
||||
if (last && last.val !== val) out.push({ ts, val: last.val });
|
||||
out.push({ ts, val });
|
||||
};
|
||||
|
||||
const latch = (tc.trigger_target_changes ?? 1) === 1 && toLevel !== null;
|
||||
if (latch) {
|
||||
const out: ChartPoint[] = [];
|
||||
segments.forEach((seg, i) => step(out, seg.start, seg.level === toLevel && held(i) ? 1 : 0));
|
||||
const lastVal = out[out.length - 1]?.val ?? 0;
|
||||
out.push({ ts: now, val: lastVal });
|
||||
return { points: out, mode: "alarm" };
|
||||
}
|
||||
|
||||
const since = opts.since ?? segments[0].start;
|
||||
let counted = 0;
|
||||
const stepsAt: number[] = [];
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const prev = segments[i - 1], seg = segments[i];
|
||||
if (fromLevel !== null && prev.level !== fromLevel) continue;
|
||||
if (toLevel !== null && seg.level !== toLevel) continue;
|
||||
if (!held(i) || seg.start < since) continue;
|
||||
counted += 1;
|
||||
stepsAt.push(seg.start);
|
||||
}
|
||||
const base = Math.max(0, (opts.current ?? counted) - counted);
|
||||
const out: ChartPoint[] = [{ ts: Math.max(since, segments[0].start), val: base }];
|
||||
let val = base;
|
||||
for (const ts of stepsAt) {
|
||||
val += 1;
|
||||
step(out, ts, val);
|
||||
}
|
||||
out.push({ ts: now, val });
|
||||
return { points: out, mode: "count" };
|
||||
}
|
||||
|
||||
export function filterOutliers(points: ChartPoint[]): ChartPoint[] {
|
||||
if (points.length < 4) return points;
|
||||
const vals = points.map((p) => p.val).sort((a, b) => a - b);
|
||||
@@ -276,6 +370,13 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
const entityId = tc.entity_id || "";
|
||||
|
||||
let points = rawStatsPoints(task, ctx);
|
||||
// #141 round 2: a state_change trigger on recorder history draws the
|
||||
// trigger's view (alert held / change count), not the raw entity state.
|
||||
let stateView: StateChangeView | null = null;
|
||||
if (triggerType === "state_change" && entityId && ctx.historyFallbackIds?.has(entityId)) {
|
||||
stateView = stateChangeView(points, tc, { since: lastServiceTs(task), current: task.trigger_current_value ?? null });
|
||||
if (stateView) points = stateView.points;
|
||||
}
|
||||
// Runtime accumulates hours with no stored intermediate snapshots: synthesize
|
||||
// the current cycle (0 at the last service → live value now) so the chart
|
||||
// resets each completion and always has a drawable 2-point line.
|
||||
@@ -326,8 +427,9 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
}
|
||||
targetValue = tc.trigger_target_value;
|
||||
forceZero = true;
|
||||
} else if (triggerType === "state_change" && tc.trigger_target_changes) {
|
||||
// Change counts / runtime hours already accumulate from zero.
|
||||
} else if (triggerType === "state_change" && tc.trigger_target_changes && stateView?.mode !== "alarm") {
|
||||
// Change counts / runtime hours already accumulate from zero. A latch
|
||||
// ("alarm" view) has no meaningful target line: its axis is alert/ok.
|
||||
targetValue = tc.trigger_target_changes;
|
||||
forceZero = true;
|
||||
} else if (triggerType === "runtime" && tc.trigger_runtime_hours) {
|
||||
@@ -341,15 +443,25 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
// exists — a slow 0.25 %/day decline classifies stable yet still crosses
|
||||
// the threshold in a foreseeable number of days.
|
||||
let projection: ChartPoint[] | null = null;
|
||||
// A projection pointing AWAY from the configured threshold is noise (the
|
||||
// ET-4800 finding: a refill-skewed rising rate on a below-threshold ink
|
||||
// sensor projected the ink level going UP) — draw it only when it heads
|
||||
// toward the bound, or when no bound is configured at all.
|
||||
const rate = task.degradation_rate;
|
||||
const pointsAway =
|
||||
rate != null &&
|
||||
((tc.trigger_below != null && tc.trigger_above == null && rate > 0) ||
|
||||
(tc.trigger_above != null && tc.trigger_below == null && rate < 0));
|
||||
if (
|
||||
targetValue == null &&
|
||||
task.degradation_rate != null &&
|
||||
rate != null &&
|
||||
!pointsAway &&
|
||||
(task.degradation_trend !== "stable" || task.days_until_threshold != null) &&
|
||||
task.degradation_trend !== "insufficient_data" &&
|
||||
points.length >= 2
|
||||
) {
|
||||
const lp = points[points.length - 1];
|
||||
projection = [lp, { ts: lp.ts + 30 * 86400000, val: lp.val + task.degradation_rate * 30 }];
|
||||
projection = [lp, { ts: lp.ts + 30 * 86400000, val: lp.val + rate * 30 }];
|
||||
}
|
||||
|
||||
const events: ChartEvent[] = task.history
|
||||
@@ -380,7 +492,7 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
if (entityId && ctx.historyFallbackIds?.has(entityId) && !statsFetchedEmpty) {
|
||||
return html`<div class="chart-note">
|
||||
<ha-icon icon="mdi:information-outline"></ha-icon>
|
||||
${t("chart_history_fallback", ctx.lang)}
|
||||
${t(stateView?.mode === "alarm" ? "chart_history_alarm" : stateView?.mode === "count" ? "chart_history_count" : "chart_history_fallback", ctx.lang)}
|
||||
</div>`;
|
||||
}
|
||||
if (statsFetchedEmpty) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
import { t, formatDate, formatDateTime, formatRecurrence } from "../styles";
|
||||
import type { AdvancedFeatures, HomeAssistant, MaintenanceTask, ManualDocRef } from "../types";
|
||||
import { renderTriggerSection, type SparklineContext } from "./sparkline";
|
||||
@@ -222,8 +223,8 @@ function renderPhasesCard(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
const last = lastByPhase.get(id);
|
||||
return html`
|
||||
<div class="phase-step ${i === cursor ? "current" : ""}"
|
||||
title=${i === cursor ? t("phase_current", L) : (ctx.isOperator ? t("phase_set", L) : "")}
|
||||
@click=${() => { if (ctx.isOperator && i !== cursor) ctx.setPhaseCursor(i); }}>
|
||||
title=${i === cursor ? t("phase_current", L) : t("phase_set", L)}
|
||||
@click=${() => { if (i !== cursor) ctx.setPhaseCursor(i); }}>
|
||||
<span class="phase-step-name">${i + 1}. ${name}</span>
|
||||
${last ? html`<span class="phase-step-last">${formatDate(last, L)}</span>` : nothing}
|
||||
</div>
|
||||
@@ -284,7 +285,7 @@ function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
${task.notes ? html`
|
||||
<div class="task-meta-row">
|
||||
<ha-icon icon="mdi:note-text-outline"></ha-icon>
|
||||
<span class="task-meta-notes">${task.notes}</span>
|
||||
<span class="task-meta-notes">${renderNotesMarkdown(task.notes)}</span>
|
||||
</div>
|
||||
` : nothing}
|
||||
${safeTaskUrl ? html`
|
||||
@@ -494,6 +495,12 @@ export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext)
|
||||
function renderHistoryTab(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
return html`
|
||||
<div class="tab-content history-tab">
|
||||
<div class="history-add-past">
|
||||
<ha-button appearance="plain" class="history-add-past-btn" @click=${() => ctx.openComplete(task)}>
|
||||
<ha-icon icon="mdi:calendar-plus"></ha-icon>
|
||||
${t("history_add_past", ctx.lang)}
|
||||
</ha-button>
|
||||
</div>
|
||||
${renderHistoryFilters(task, ctx.history)}
|
||||
${renderHistoryList(task, ctx.history)}
|
||||
</div>
|
||||
|
||||
@@ -99,10 +99,15 @@ export function t(key: string, lang?: string): string {
|
||||
* in English until then rather than as raw keys). Was copy-pasted across the
|
||||
* panel and both cards (drift audit 2026-08). */
|
||||
export function syncLocaleFromHass(
|
||||
host: { hass?: { language?: string; locale?: unknown }; requestUpdate: () => void },
|
||||
host: {
|
||||
hass?: { language?: string; locale?: unknown; config?: { country?: string | null } };
|
||||
requestUpdate: () => void;
|
||||
},
|
||||
changedProps: Map<string, unknown>,
|
||||
): void {
|
||||
if (changedProps.has("hass")) setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0]);
|
||||
if (changedProps.has("hass")) {
|
||||
setDateTimePrefs(host.hass?.locale as Parameters<typeof setDateTimePrefs>[0], host.hass?.config?.country);
|
||||
}
|
||||
const lang = host.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
ensureLocale(lang).then(() => host.requestUpdate());
|
||||
@@ -196,15 +201,41 @@ function langToLocale(lang?: string): string {
|
||||
interface DateTimePrefs {
|
||||
date?: string;
|
||||
time?: string;
|
||||
country?: string;
|
||||
}
|
||||
const _w = window as unknown as { __msDateTimePrefs?: DateTimePrefs };
|
||||
const DT_PREFS: DateTimePrefs = (_w.__msDateTimePrefs ??= {});
|
||||
|
||||
/** Feed HA's per-user date/time format into formatDate/formatDateTime. */
|
||||
export function setDateTimePrefs(locale?: { date_format?: string; time_format?: string }): void {
|
||||
/** Feed HA's per-user date/time format into formatDate/formatDateTime.
|
||||
* `country` is the SERVER's configured country (#140): with the profile
|
||||
* format left on "language" it regionalizes the language default —
|
||||
* "en" + AU renders DD/MM/YYYY instead of the hard en-US mapping. */
|
||||
export function setDateTimePrefs(
|
||||
locale?: { date_format?: string; time_format?: string },
|
||||
country?: string | null,
|
||||
): void {
|
||||
if (!locale) return;
|
||||
DT_PREFS.date = locale.date_format;
|
||||
DT_PREFS.time = locale.time_format;
|
||||
if (country !== undefined) DT_PREFS.country = country || undefined;
|
||||
}
|
||||
|
||||
/** BCP-47 locale for the "language" default: the UI language regionalized
|
||||
* by the server country when Intl knows the combination (#140). Explicit
|
||||
* profile formats (DMY/MDY/YMD/system) never reach this path. */
|
||||
function resolveLocale(lang?: string): string {
|
||||
const base = langToLocale(lang);
|
||||
const country = DT_PREFS.country;
|
||||
if (country && /^[A-Za-z]{2}$/.test(country)) {
|
||||
const candidate = `${normLang(lang).split("-")[0]}-${country.toUpperCase()}`;
|
||||
try {
|
||||
new Intl.DateTimeFormat(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// syntactically invalid tag — keep the language mapping
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function _formatDateObj(d: Date, lang?: string): string {
|
||||
@@ -217,17 +248,17 @@ function _formatDateObj(d: Date, lang?: string): string {
|
||||
case "YMD": return `${yyyy}-${mm}-${dd}`;
|
||||
case "system":
|
||||
return d.toLocaleDateString(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
default: // "language" or unset — derive from the UI language (pre-#97 behaviour)
|
||||
return d.toLocaleDateString(langToLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
default: // "language" or unset — UI language, regionalized by the server country (#140)
|
||||
return d.toLocaleDateString(resolveLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
}
|
||||
}
|
||||
|
||||
function _formatTimeObj(d: Date, lang?: string): string {
|
||||
switch (DT_PREFS.time) {
|
||||
case "12": return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
|
||||
case "24": return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
case "12": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
|
||||
case "24": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
case "system": return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
||||
default: return d.toLocaleTimeString(langToLocale(lang), { hour: "2-digit", minute: "2-digit" });
|
||||
default: return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1291,6 +1322,11 @@ export const sharedStyles = css`
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.prediction-cycles {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.prediction-entity {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -248,6 +248,9 @@ export interface MaintenanceTask {
|
||||
degradation_rate?: number | null;
|
||||
degradation_trend?: string | null; // "rising" | "falling" | "stable" | "insufficient_data"
|
||||
degradation_r_squared?: number | null;
|
||||
/** How many prior service cycles the degradation rate learned from
|
||||
* (sawtooth sensors); 0 = current window only. */
|
||||
prediction_cycles?: number;
|
||||
days_until_threshold?: number | null;
|
||||
threshold_prediction_date?: string | null;
|
||||
threshold_prediction_confidence?: string | null; // "low" | "medium" | "high"
|
||||
@@ -259,6 +262,8 @@ export interface MaintenanceTask {
|
||||
responsible_user_id?: string | null;
|
||||
custom_icon?: string | null;
|
||||
nfc_tag_id?: string | null;
|
||||
/** Proof of presence: completion only via NFC/QR scan on the thing. */
|
||||
require_tag_scan?: boolean;
|
||||
/** Number of documents linked to this task (paperclip badge on the row). */
|
||||
document_count?: number;
|
||||
/** v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). */
|
||||
@@ -560,6 +565,9 @@ export interface HomeAssistant {
|
||||
// "system" | "DMY" | "MDY" | "YMD" resp. "language" | "system" | "12" | "24")
|
||||
// — issue #97: dates must follow them, not just the UI language.
|
||||
locale?: { language: string; number_format?: string; date_format?: string; time_format?: string };
|
||||
/** Server config — country (#140) feeds the "language"-default date
|
||||
* format ("en" + AU → en-AU → DD/MM/YYYY). */
|
||||
config?: { country?: string | null };
|
||||
localize(key: string, ...args: unknown[]): string;
|
||||
user?: { id: string; name: string; is_admin: boolean; is_owner: boolean };
|
||||
/** Current access token — used for authenticated `fetch()` to our HTTP
|
||||
|
||||
Reference in new issue
Block a user