217 files
This commit is contained in:
+48
-32
@@ -11,18 +11,14 @@ import { property, state } from "lit/decorators.js";
|
||||
import { t, ensureLocale, DEFAULT_CURRENCY_SYMBOL } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { sectionCardSharedStyles } from "./section-card-shared-styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface BudgetStatus {
|
||||
monthly_budget?: number;
|
||||
monthly_spent?: number;
|
||||
yearly_budget?: number;
|
||||
yearly_spent?: number;
|
||||
currency_symbol?: string;
|
||||
}
|
||||
import type { BudgetStatus, HomeAssistant } from "../types";
|
||||
|
||||
interface CardConfig { type: string; title?: string; }
|
||||
|
||||
/** Backend default for budget_alert_threshold (const.py / settings_registry).
|
||||
* Only reached if an older core omits the field from budget_status. */
|
||||
const DEFAULT_ALERT_THRESHOLD_PCT = 80;
|
||||
|
||||
export class MaintenanceBudgetSectionCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() private _config: CardConfig = { type: "" };
|
||||
@@ -109,10 +105,21 @@ export class MaintenanceBudgetSectionCard extends LitElement {
|
||||
return html`<ha-card><div class="loading">${t("loading", L) || "Loading…"}</div></ha-card>`;
|
||||
}
|
||||
const sym = s.currency_symbol || DEFAULT_CURRENCY_SYMBOL;
|
||||
const mPct = s.monthly_budget ? Math.min(100, ((s.monthly_spent || 0) / s.monthly_budget) * 100) : 0;
|
||||
const yPct = s.yearly_budget ? Math.min(100, ((s.yearly_spent || 0) / s.yearly_budget) * 100) : 0;
|
||||
const mWarn = mPct >= 100 ? "danger" : mPct >= 80 ? "warning" : "ok";
|
||||
const yWarn = yPct >= 100 ? "danger" : yPct >= 80 ? "warning" : "ok";
|
||||
// The amber step is the CONFIGURED budget_alert_threshold, not a literal 80
|
||||
// — same rule the panel's budget bar uses (maintenance-panel._renderBudgetBar).
|
||||
const threshold = s.alert_threshold_pct ?? DEFAULT_ALERT_THRESHOLD_PCT;
|
||||
const tracks = [
|
||||
{
|
||||
label: t("budget_monthly", L) || "Monthly",
|
||||
spent: s.monthly_spent || 0,
|
||||
budget: s.monthly_budget || 0,
|
||||
},
|
||||
{
|
||||
label: t("budget_yearly", L) || "Yearly",
|
||||
spent: s.yearly_spent || 0,
|
||||
budget: s.yearly_budget || 0,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -127,25 +134,34 @@ export class MaintenanceBudgetSectionCard extends LitElement {
|
||||
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
|
||||
<div class="track">
|
||||
<div class="track-label-row">
|
||||
<label>${t("budget_monthly", L) || "Monthly"}</label>
|
||||
<span class="track-numbers ${mWarn}">
|
||||
${(s.monthly_spent || 0).toFixed(0)} / ${(s.monthly_budget || 0).toFixed(0)} ${sym}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar"><div class="bar-fill ${mWarn}" style="width:${mPct}%"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="track">
|
||||
<div class="track-label-row">
|
||||
<label>${t("budget_yearly", L) || "Yearly"}</label>
|
||||
<span class="track-numbers ${yWarn}">
|
||||
${(s.yearly_spent || 0).toFixed(0)} / ${(s.yearly_budget || 0).toFixed(0)} ${sym}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar"><div class="bar-fill ${yWarn}" style="width:${yPct}%"></div></div>
|
||||
</div>
|
||||
${tracks.map((track) => {
|
||||
// #104: budget tracking without a maximum — a bar needs a
|
||||
// denominator, so show the plain spent total instead of "9 / 0 €"
|
||||
// over an always-empty bar. Mirrors the panel's spent-only lines.
|
||||
if (!(track.budget > 0)) {
|
||||
return html`
|
||||
<div class="track spent-only">
|
||||
<div class="track-label-row">
|
||||
<label>${track.label}</label>
|
||||
<span class="track-numbers ok">${track.spent.toFixed(0)} ${sym}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const pct = Math.min(100, Math.max(0, (track.spent / track.budget) * 100));
|
||||
const warn = pct >= 100 ? "danger" : pct >= threshold ? "warning" : "ok";
|
||||
return html`
|
||||
<div class="track">
|
||||
<div class="track-label-row">
|
||||
<label>${track.label}</label>
|
||||
<span class="track-numbers ${warn}">
|
||||
${track.spent.toFixed(0)} / ${track.budget.toFixed(0)} ${sym}
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar"><div class="bar-fill ${warn}" style="width:${pct}%"></div></div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
|
||||
${this._isAdmin
|
||||
? html`
|
||||
|
||||
+86
-23
@@ -2,9 +2,11 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, TaskPartLink } from "../types";
|
||||
import { t, nativeFieldStyles } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { partLinkKey, type LinkedPart } from "../helpers/shared-parts";
|
||||
import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
|
||||
|
||||
export class MaintenanceCompleteDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -19,12 +21,20 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
@property() public readingUnit = "";
|
||||
/** Buy task (part_ref): default restock quantity — shows an editable qty field. */
|
||||
@property({ attribute: false }) public restockDefault: number | null = null;
|
||||
/** #99: the object's parts — enables the editable "parts used" section. */
|
||||
@property({ attribute: false }) public parts: Array<{ id: string; name: string; unit?: string | null; stock?: number | null }> = [];
|
||||
/** #99: the task's fixed consumes_parts links (prefill for the section). */
|
||||
@property({ attribute: false }) public consumesParts: Array<{ part_id: string; quantity: number }> = [];
|
||||
/** #99: the parts offered on completion — enables the editable "parts used"
|
||||
* section. Built by `partsForCompletion`: the object's own inventory plus
|
||||
* every shared pool this task links to (#111), each tagged with its owner. */
|
||||
@property({ attribute: false }) public parts: LinkedPart[] = [];
|
||||
/** #99: the task's fixed consumes_parts links (prefill for the section).
|
||||
* A link may carry an `entry_id` (#111) and MUST keep it through the edit —
|
||||
* without it the completion would decrement the wrong inventory, or none. */
|
||||
@property({ attribute: false }) public consumesParts: TaskPartLink[] = [];
|
||||
/** "Consumes: 1× HEPA-Filter (Shelf B)" hint lines for consuming tasks. */
|
||||
@property({ type: Array }) public consumesInfo: string[] = [];
|
||||
/** Details this task demands before it counts as done (v2.44). The backend
|
||||
* enforces the same list at every completion surface; blocking Save here
|
||||
* just means the user never has to meet that rejection. */
|
||||
@property({ type: Array }) public requiredFields: string[] = [];
|
||||
@state() private _open = false;
|
||||
@state() private _notes = "";
|
||||
@state() private _cost = "";
|
||||
@@ -38,7 +48,9 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
@state() private _photoUploading = false;
|
||||
@state() private _readingValue = "";
|
||||
@state() private _restockQty = "";
|
||||
@state() private _usedParts: Record<string, number> = {};
|
||||
/** Keyed by `partLinkKey` — the (entry_id, part_id) pair — because two
|
||||
* objects can carry the same part id, so part_id alone would merge pools. */
|
||||
@state() private _usedParts: Record<string, TaskPartLink> = {};
|
||||
|
||||
public open(): void {
|
||||
if (this._open) return;
|
||||
@@ -55,8 +67,9 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
this._readingValue = "";
|
||||
this._restockQty = this.restockDefault !== null ? String(this.restockDefault) : "";
|
||||
// #99: prefill "parts used" with the task's fixed links — the user can
|
||||
// untick or adjust before completing.
|
||||
this._usedParts = Object.fromEntries(this.consumesParts.map((l) => [l.part_id, l.quantity]));
|
||||
// untick or adjust before completing. The whole link is kept, entry_id
|
||||
// included, so a shared pool survives the edit (#111).
|
||||
this._usedParts = Object.fromEntries(this.consumesParts.map((l) => [partLinkKey(l), { ...l }]));
|
||||
}
|
||||
|
||||
private _toggleCheck(idx: number): void {
|
||||
@@ -149,10 +162,16 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
// #99: with a parts section shown, send the explicit selection — it
|
||||
// replaces the automatic consumes_parts deduction (empty = none used).
|
||||
// entry_id travels only when the pool is somebody else's (#111), so an
|
||||
// own-part payload is byte-identical to what shipped before.
|
||||
if (this.parts.length > 0) {
|
||||
data.used_parts = Object.entries(this._usedParts)
|
||||
.filter(([, qty]) => Number.isFinite(qty) && qty > 0)
|
||||
.map(([part_id, quantity]) => ({ part_id, quantity }));
|
||||
data.used_parts = Object.values(this._usedParts)
|
||||
.filter((l) => Number.isFinite(l.quantity) && l.quantity > 0)
|
||||
.map((l) =>
|
||||
l.entry_id
|
||||
? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
|
||||
: { part_id: l.part_id, quantity: l.quantity },
|
||||
);
|
||||
}
|
||||
await this.hass.connection.sendMessagePromise(data);
|
||||
this._open = false;
|
||||
@@ -164,6 +183,28 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Required details the user has not supplied yet (drives Save + markers). */
|
||||
private get _missingRequired(): string[] {
|
||||
const filled: Record<string, boolean> = {
|
||||
notes: this._notes.trim() !== "",
|
||||
cost: this._cost.trim() !== "",
|
||||
duration: this._duration.trim() !== "",
|
||||
photo: this._photoDocId !== "",
|
||||
// "Who did it" is filled in server-side from the authenticated
|
||||
// connection (websocket/tasks_actions.py), so the dialog satisfies it
|
||||
// as long as we ARE a logged-in user. Claiming it is always satisfied
|
||||
// was how a task requiring "user" ended up unclosable: Save stayed
|
||||
// enabled and the backend rejected the completion every time.
|
||||
user: !!this.hass?.user,
|
||||
};
|
||||
return this.requiredFields.filter((f) => !filled[f]);
|
||||
}
|
||||
|
||||
/** Marker appended to a required field's label. */
|
||||
private _req(field: string) {
|
||||
return this.requiredFields.includes(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._open = false;
|
||||
}
|
||||
@@ -200,25 +241,36 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
? html`<div class="used-parts">
|
||||
<span class="field-label">${t("complete_parts_used", L)}</span>
|
||||
${this.parts.map((pt) => {
|
||||
const qty = this._usedParts[pt.id];
|
||||
const checked = qty !== undefined;
|
||||
const key = partLinkKey({ part_id: pt.id, entry_id: pt.entry_id });
|
||||
const link = this._usedParts[key];
|
||||
const checked = link !== undefined;
|
||||
const base: TaskPartLink = pt.entry_id
|
||||
? { part_id: pt.id, quantity: 1, entry_id: pt.entry_id }
|
||||
: { part_id: pt.id, quantity: 1 };
|
||||
return html`<div class="used-part-row">
|
||||
<label class="used-part-check">
|
||||
<input type="checkbox" .checked=${checked}
|
||||
@change=${(e: Event) => {
|
||||
const next = { ...this._usedParts };
|
||||
if ((e.target as HTMLInputElement).checked) next[pt.id] = next[pt.id] || 1;
|
||||
else delete next[pt.id];
|
||||
if ((e.target as HTMLInputElement).checked) next[key] = next[key] || base;
|
||||
else delete next[key];
|
||||
this._usedParts = next;
|
||||
}} />
|
||||
<span>${pt.name}${pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : ""}</span>
|
||||
<span
|
||||
>${pt.name}${pt.owner_name
|
||||
? html`<span class="used-part-owner"> (${pt.owner_name})</span>`
|
||||
: nothing}${pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : ""}</span
|
||||
>
|
||||
</label>
|
||||
${checked
|
||||
? html`<input class="used-part-qty" type="number" min="0.01" max="999" step="0.01"
|
||||
.value=${String(qty)}
|
||||
.value=${String(link.quantity)}
|
||||
@input=${(e: Event) => {
|
||||
const v = parseFloat((e.target as HTMLInputElement).value);
|
||||
this._usedParts = { ...this._usedParts, [pt.id]: Number.isFinite(v) && v >= 0.01 ? v : 1 };
|
||||
this._usedParts = {
|
||||
...this._usedParts,
|
||||
[key]: { ...base, quantity: Number.isFinite(v) && v >= 0.01 ? v : 1 },
|
||||
};
|
||||
}} />`
|
||||
: nothing}
|
||||
</div>`;
|
||||
@@ -245,25 +297,25 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
only sees the title + Cancel/Complete buttons — the original
|
||||
bug report. Native inputs always render. -->
|
||||
<label class="field">
|
||||
<span class="field-label">${t("notes_optional", L)}</span>
|
||||
<span class="field-label">${t("notes_optional", L)}${this._req("notes")}</span>
|
||||
<input type="text" class="field-input"
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${t("cost_optional", L)}</span>
|
||||
<span class="field-label">${t("cost_optional", L)}${this._req("cost")}</span>
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._cost}
|
||||
@input=${(e: Event) => (this._cost = (e.target as HTMLInputElement).value)} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${t("duration_minutes", L)}</span>
|
||||
<span class="field-label">${t("duration_minutes", L)}${this._req("duration")}</span>
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._duration}
|
||||
@input=${(e: Event) => (this._duration = (e.target as HTMLInputElement).value)} />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">${t("completion_photo_optional", L)}</span>
|
||||
<span class="field-label">${t("completion_photo_optional", L)}${this._req("photo")}</span>
|
||||
${this._photoPreview
|
||||
? html`
|
||||
<div class="photo-preview">
|
||||
@@ -306,7 +358,10 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._complete}
|
||||
.disabled=${this._loading}
|
||||
.disabled=${this._loading || this._missingRequired.length > 0}
|
||||
title=${this._missingRequired.length
|
||||
? this._missingRequired.map((f) => t("err_required", L).replace("{field}", t(REQUIRED_COMPLETION_LABELS[f] ?? f, L))).join(" · ")
|
||||
: ""}
|
||||
>
|
||||
${this._loading ? t("completing", L) : t("complete", L)}
|
||||
</ha-button>
|
||||
@@ -316,6 +371,11 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
|
||||
static styles = [nativeFieldStyles, css`
|
||||
.req-mark {
|
||||
color: var(--error-color, #f44336);
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
@@ -348,6 +408,9 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.used-part-check input { cursor: pointer; }
|
||||
/* #111: whose stock this row draws on. Muted but never omitted — an
|
||||
unlabelled foreign pool is indistinguishable from an own part. */
|
||||
.used-part-owner { color: var(--secondary-text-color); }
|
||||
.used-part-qty {
|
||||
width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px;
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -155,9 +155,15 @@ export class MaintenanceQrDialog extends LitElement {
|
||||
const completeLabel = escapeHtml(t("qr_action_complete", L));
|
||||
|
||||
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${safeTitle}</title>
|
||||
<style>
|
||||
body{font-family:sans-serif;text-align:center;padding:20px}
|
||||
/* Printable sheet — must not inherit the phone's dark theme. The QR images
|
||||
carry their own white quiet zone and stay scannable either way, but the
|
||||
labels below are explicit dark greys and would vanish on a WebView's dark
|
||||
canvas. Same reasoning as helpers/report.ts. */
|
||||
:root{color-scheme:light}
|
||||
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
|
||||
h2{margin:0 0 4px}
|
||||
.sub{color:#666;font-size:14px;margin-bottom:16px}
|
||||
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
|
||||
|
||||
@@ -9,6 +9,15 @@ import { UserService } from "../user-service";
|
||||
import { OBJECT_COLUMNS, sanitizeColumns } from "../helpers/object-columns";
|
||||
import { downloadTextFile } from "../helpers/download";
|
||||
|
||||
/** One household member and the notify services they actually resolve to.
|
||||
* Empty `services` means no Companion device is linked, so their reminders
|
||||
* fall back to the household notification service. */
|
||||
interface PersonNotifyTarget {
|
||||
user_id: string;
|
||||
name: string;
|
||||
services: string[];
|
||||
}
|
||||
|
||||
/* Settings response shape from WS maintenance_supporter/settings */
|
||||
interface SettingsResponse {
|
||||
features: AdvancedFeatures;
|
||||
@@ -27,6 +36,8 @@ interface SettingsResponse {
|
||||
notify_targets?: string[];
|
||||
panel_enabled: boolean;
|
||||
panel_title: string;
|
||||
/** Opt-in copy of the shipped Assist sentences into the config dir. */
|
||||
install_assist_sentences?: boolean;
|
||||
};
|
||||
notifications: {
|
||||
due_soon_enabled: boolean;
|
||||
@@ -108,6 +119,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
@state() private _includeHistory = true;
|
||||
@state() private _toast = "";
|
||||
@state() private _testingNotification = false;
|
||||
@state() private _personTargets: PersonNotifyTarget[] = [];
|
||||
@state() private _testingUser = "";
|
||||
@state() private _users: HAUser[] = [];
|
||||
@state() private _savedViews: Array<{ id: string; name: string }> = [];
|
||||
|
||||
@@ -169,6 +182,23 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
} catch {
|
||||
this._users = [];
|
||||
}
|
||||
this._loadNotifyTargets();
|
||||
}
|
||||
|
||||
/** Which notify services each household member resolves to.
|
||||
*
|
||||
* Resolved by the backend through the same helper the reminder path uses,
|
||||
* so the list shown here is the list that will actually be used.
|
||||
*/
|
||||
private async _loadNotifyTargets(): Promise<void> {
|
||||
try {
|
||||
const res = await this.hass.connection.sendMessagePromise<{ targets: PersonNotifyTarget[] }>({
|
||||
type: "maintenance_supporter/notify/user_targets",
|
||||
});
|
||||
this._personTargets = res.targets || [];
|
||||
} catch {
|
||||
this._personTargets = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSettings(): Promise<void> {
|
||||
@@ -220,11 +250,13 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _sendTestNotification = async (): Promise<void> => {
|
||||
this._testingNotification = true;
|
||||
private _sendTestNotification = async (userId?: string): Promise<void> => {
|
||||
if (userId) this._testingUser = userId;
|
||||
else this._testingNotification = true;
|
||||
try {
|
||||
const res = await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/global/test_notification",
|
||||
...(userId ? { user_id: userId } : {}),
|
||||
}) as { success: boolean; message?: string };
|
||||
const msg = res.message
|
||||
|| (res.success ? t("test_notification_success", this._lang) : t("test_notification_failed", this._lang));
|
||||
@@ -232,7 +264,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
} catch {
|
||||
this._showToast(t("test_notification_failed", this._lang));
|
||||
} finally {
|
||||
this._testingNotification = false;
|
||||
if (userId) this._testingUser = "";
|
||||
else this._testingNotification = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -570,6 +603,12 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
@change=${(e: Event) => this._updateSetting("panel_title", (e.target as HTMLInputElement).value.trim())} />
|
||||
</label>
|
||||
` : ""}
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_install_assist_sentences", L)}</span>
|
||||
<input type="checkbox" .checked=${g.install_assist_sentences ?? false}
|
||||
@change=${(e: Event) => this._updateSetting("install_assist_sentences", (e.target as HTMLInputElement).checked)} />
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_install_assist_sentences_hint", L)}</div>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_notifications", L)}</span>
|
||||
<input type="checkbox" .checked=${g.notifications_enabled}
|
||||
@@ -588,10 +627,28 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<span class="setting-label">${t("test_notification", L)}</span>
|
||||
<button class="ha-button secondary"
|
||||
?disabled=${!g.notify_service || this._testingNotification}
|
||||
@click=${this._sendTestNotification}>
|
||||
@click=${() => this._sendTestNotification()}>
|
||||
${this._testingNotification ? t("testing", L) : t("send_test", L)}
|
||||
</button>
|
||||
</div>
|
||||
${this._personTargets.length ? html`
|
||||
<div class="notify-per-person">
|
||||
<span class="setting-label">${t("notify_per_person", L)}</span>
|
||||
${this._personTargets.map((target) => html`
|
||||
<div class="notify-person-row">
|
||||
<span class="notify-person-name">${target.name}</span>
|
||||
<span class="notify-person-target ${target.services.length ? "" : "muted"}">
|
||||
${target.services.length ? target.services.join(", ") : t("notify_no_own_device", L)}
|
||||
</span>
|
||||
<button class="ha-button secondary"
|
||||
?disabled=${!target.services.length || this._testingUser === target.user_id}
|
||||
@click=${() => this._sendTestNotification(target.user_id)}>
|
||||
${this._testingUser === target.user_id ? t("testing", L) : t("send_test", L)}
|
||||
</button>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
</div>
|
||||
`;
|
||||
@@ -1606,6 +1663,31 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
cursor: pointer;
|
||||
gap: 12px;
|
||||
}
|
||||
.notify-per-person {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--divider-color, #e0e0e0);
|
||||
}
|
||||
.notify-person-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 0 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.notify-person-name {
|
||||
font-weight: 500;
|
||||
min-width: 120px;
|
||||
}
|
||||
.notify-person-target {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
font-size: 0.9em;
|
||||
word-break: break-word;
|
||||
color: var(--secondary-text-color, #727272);
|
||||
}
|
||||
.notify-person-target.muted {
|
||||
font-style: italic;
|
||||
}
|
||||
/* v2.27: template gallery clustered by category */
|
||||
.tpl-group { margin-top: 14px; }
|
||||
.tpl-group-head {
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TriggerConfig, HAUser } from "../types";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, t, weekdayName } from "../styles";
|
||||
import { UserService } from "../user-service";
|
||||
import { partLinkKey } from "../helpers/shared-parts";
|
||||
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
|
||||
import "./ms-textfield";
|
||||
|
||||
const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"];
|
||||
@@ -122,6 +124,14 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@property({ type: Number, attribute: "default-warning-days" }) public defaultWarningDays = 7;
|
||||
/** The object's spare parts — offered as "consumes parts" checkboxes. */
|
||||
@state() private parts: Array<{ id: string; name: string; unit?: string }> = [];
|
||||
/** #111: OTHER objects that own spare parts, so a task can draw on a shared
|
||||
* pool (three vacuums, one box of dust bags). Grouped by owner in the UI —
|
||||
* which pool a link means must never be a guess. */
|
||||
@state() private _foreignOwners: Array<{
|
||||
entry_id: string;
|
||||
name: string;
|
||||
parts: Array<{ id: string; name: string; unit?: string }>;
|
||||
}> = [];
|
||||
@state() private _open = false;
|
||||
@state() private _loading = false;
|
||||
@state() private _error = "";
|
||||
@@ -210,7 +220,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@state() private _nfcTagId = "";
|
||||
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
|
||||
@state() private _readingUnit = "";
|
||||
@state() private _consumesParts: Record<string, number> = {};
|
||||
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
|
||||
* since the same part id can exist on two objects (battery fleet). */
|
||||
@state() private _consumesParts: Record<string, TaskPartLink> = {};
|
||||
@state() private _partsLoadFailed = false;
|
||||
@state() private _availableTags: Array<{id: string; name: string}> = [];
|
||||
|
||||
@@ -222,6 +234,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
// Checklist (newline-separated steps, one per line)
|
||||
@state() private _checklistText = "";
|
||||
/** Details this task demands on completion — enforced by the backend on
|
||||
* every surface, so a button press or voice command cannot bypass it. */
|
||||
@state() private _requiredCompletion: string[] = [];
|
||||
|
||||
// Schedule time (HH:MM, advanced feature)
|
||||
@state() private _scheduleTime = "";
|
||||
@@ -271,7 +286,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._objectChoices = [];
|
||||
}
|
||||
this._resetFields();
|
||||
await Promise.all([this._loadUsers(), this._loadTags(), this._loadParts()]);
|
||||
await Promise.all([this._loadUsers(), this._loadTags(), this._loadParts(), this._loadForeignPools()]);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -321,12 +336,17 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._lastPerformed = task.last_performed || "";
|
||||
this._nfcTagId = task.nfc_tag_id || "";
|
||||
this._readingUnit = task.reading_unit || "";
|
||||
this._consumesParts = Object.fromEntries((task.consumes_parts || []).map((l) => [l.part_id, l.quantity]));
|
||||
// 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).
|
||||
this._consumesParts = Object.fromEntries(
|
||||
(task.consumes_parts || []).map((l) => [partLinkKey(l), { ...l }]),
|
||||
);
|
||||
this._responsibleUserId = task.responsible_user_id || null;
|
||||
this._assigneePool = [...(task.assignee_pool || [])];
|
||||
this._rotationStrategy = task.rotation_strategy || "";
|
||||
|
||||
this._checklistText = (task.checklist || []).join("\n");
|
||||
this._requiredCompletion = [...(task.required_completion_fields || [])];
|
||||
this._scheduleTime = task.schedule_time || "";
|
||||
|
||||
// v1.3.0: hydrate on_complete_action + quick_complete_defaults
|
||||
@@ -395,7 +415,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._fetchEntityAttributes(this._triggerEntityId);
|
||||
}
|
||||
|
||||
await Promise.all([this._loadUsers(), this._loadTags(), this._loadParts()]);
|
||||
await Promise.all([this._loadUsers(), this._loadTags(), this._loadParts(), this._loadForeignPools()]);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -434,6 +454,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._assigneePool = [];
|
||||
this._rotationStrategy = "";
|
||||
this._checklistText = "";
|
||||
this._requiredCompletion = [];
|
||||
this._scheduleTime = "";
|
||||
this._environmentalEntity = "";
|
||||
this._environmentalAttribute = "";
|
||||
@@ -736,6 +757,40 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** #111: the other objects' spare-part pools this task could draw on.
|
||||
*
|
||||
* A sibling of `_loadParts`, run in the SAME Promise.all rather than nested
|
||||
* inside it: chaining it after the own-parts fetch delays the dialog opening
|
||||
* by a further round trip for a list that is secondary to it.
|
||||
*
|
||||
* Failure is soft on purpose — the own-parts picker is the primary path and
|
||||
* must not disappear because this second call did not come back. */
|
||||
private async _loadForeignPools(): Promise<void> {
|
||||
this._foreignOwners = [];
|
||||
if (!this._entryId) return;
|
||||
try {
|
||||
const result = (await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/objects",
|
||||
})) as {
|
||||
objects?: Array<{
|
||||
entry_id: string;
|
||||
object?: { name?: string };
|
||||
parts?: Array<{ id: string; name: string; unit?: string }>;
|
||||
}>;
|
||||
};
|
||||
this._foreignOwners = (result.objects || [])
|
||||
.filter((o) => o.entry_id !== this._entryId && (o.parts || []).length > 0)
|
||||
.map((o) => ({
|
||||
entry_id: o.entry_id,
|
||||
name: o.object?.name || o.entry_id,
|
||||
parts: o.parts || [],
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} catch {
|
||||
this._foreignOwners = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadTags(): Promise<void> {
|
||||
try {
|
||||
const result = await this.hass.connection.sendMessagePromise({
|
||||
@@ -773,6 +828,69 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** A task already drawing on a shared pool opens that section expanded — a
|
||||
* collapsed disclosure would hide a link that is very much active. */
|
||||
private get _hasForeignPick(): boolean {
|
||||
return Object.values(this._consumesParts).some((l) => !!l.entry_id);
|
||||
}
|
||||
|
||||
/** One "consumes parts" checkbox + quantity.
|
||||
*
|
||||
* `ownerEntryId` is undefined for the object's own parts and set for a pool
|
||||
* owned by another object (#111) — that argument is the ONLY difference
|
||||
* between the two lists, which is why they share this renderer. */
|
||||
private _renderConsumesRow(
|
||||
part: { id: string; name: string; unit?: string },
|
||||
ownerEntryId?: string,
|
||||
) {
|
||||
const key = partLinkKey({ part_id: part.id, entry_id: ownerEntryId });
|
||||
const link = this._consumesParts[key];
|
||||
const base: TaskPartLink = ownerEntryId
|
||||
? { part_id: part.id, quantity: 1, entry_id: ownerEntryId }
|
||||
: { part_id: part.id, quantity: 1 };
|
||||
return html`
|
||||
<div class="consumes-row">
|
||||
<label class="consumes-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${link !== undefined}
|
||||
@change=${(e: Event) => {
|
||||
const next = { ...this._consumesParts };
|
||||
if ((e.target as HTMLInputElement).checked) next[key] = next[key] || base;
|
||||
else delete next[key];
|
||||
this._consumesParts = next;
|
||||
}}
|
||||
/>
|
||||
<span>${part.name}${part.unit ? ` (${part.unit})` : ""}</span>
|
||||
</label>
|
||||
${link !== undefined
|
||||
? html`<input
|
||||
class="consumes-qty"
|
||||
type="number"
|
||||
min="0.01"
|
||||
max="999"
|
||||
step="0.01"
|
||||
.value=${String(link.quantity)}
|
||||
@input=${(e: Event) => {
|
||||
const v = parseFloat((e.target as HTMLInputElement).value);
|
||||
this._consumesParts = {
|
||||
...this._consumesParts,
|
||||
[key]: { ...base, quantity: Number.isFinite(v) && v >= 0.01 ? v : 1 },
|
||||
};
|
||||
}}
|
||||
/>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleRequired(field: string, on: boolean): void {
|
||||
const next = new Set(this._requiredCompletion);
|
||||
if (on) next.add(field);
|
||||
else next.delete(field);
|
||||
this._requiredCompletion = [...next];
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (this._loading) return; // synchronous re-entry guard (double-click)
|
||||
if (!this._name.trim()) return;
|
||||
@@ -787,7 +905,16 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
name: this._name,
|
||||
task_type: this._type,
|
||||
schedule_type: this._scheduleType,
|
||||
warning_days: parseInt(this._warningDays, 10) || 7,
|
||||
// `0` is a legal, meaningful value — "no due-soon window, go straight
|
||||
// from ok to overdue" (backend range is 0–365). The old
|
||||
// `parseInt(...) || 7` treated it as falsy and silently rewrote a
|
||||
// stored 0 to 7 on EVERY save, even when the user never touched the
|
||||
// field. Same class as bug #42, but worse: it needed no user action.
|
||||
// Only a genuinely unparseable field falls back, and to the
|
||||
// configured default rather than a hardcoded 7.
|
||||
warning_days: Number.isNaN(parseInt(this._warningDays, 10))
|
||||
? this.defaultWarningDays
|
||||
: Math.max(0, parseInt(this._warningDays, 10)),
|
||||
};
|
||||
const ecd = this._earliestCompletionDays.trim();
|
||||
data.earliest_completion_days = ecd === "" ? null : Math.max(0, parseInt(ecd, 10) || 0);
|
||||
@@ -836,14 +963,20 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.last_performed = this._lastPerformed || null;
|
||||
data.nfc_tag_id = this._nfcTagId || null;
|
||||
data.reading_unit = this._readingUnit.trim() || null;
|
||||
if (this.parts.length) {
|
||||
data.consumes_parts = Object.entries(this._consumesParts).map(([part_id, quantity]) => ({
|
||||
part_id,
|
||||
quantity,
|
||||
}));
|
||||
// Only send when a picker was actually rendered. A failed parts load
|
||||
// leaves both lists empty, and sending [] then would silently wipe links
|
||||
// the user never saw. entry_id is written ONLY for a foreign pick, so an
|
||||
// own-parts task saves byte-identically to before (#111).
|
||||
if (this.parts.length || this._foreignOwners.length) {
|
||||
data.consumes_parts = Object.values(this._consumesParts).map((l) =>
|
||||
l.entry_id
|
||||
? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
|
||||
: { part_id: l.part_id, quantity: l.quantity },
|
||||
);
|
||||
}
|
||||
data.responsible_user_id = this._responsibleUserId;
|
||||
data.assignee_pool = this._assigneePool;
|
||||
data.required_completion_fields = this._requiredCompletion;
|
||||
data.rotation_strategy =
|
||||
this._assigneePool.length >= 2 && this._rotationStrategy
|
||||
? this._rotationStrategy
|
||||
@@ -1700,6 +1833,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._entryId = (e.target as HTMLSelectElement).value;
|
||||
this._consumesParts = {};
|
||||
this._loadParts();
|
||||
// The new owner drops out of the shared-pool list and the old
|
||||
// one joins it, so this has to be recomputed too (#111).
|
||||
this._loadForeignPools();
|
||||
}}
|
||||
>
|
||||
${this._objectChoices.map(
|
||||
@@ -1738,44 +1874,27 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
${this._partsLoadFailed
|
||||
? html`<div class="field-help parts-load-failed">${t("parts_load_failed", L)}</div>`
|
||||
: nothing}
|
||||
${this.parts.length
|
||||
${this.parts.length || this._foreignOwners.length
|
||||
? html`
|
||||
<div class="field">
|
||||
<label>${t("consumes_parts_label", L)}</label>
|
||||
${this.parts.map((part) => {
|
||||
const qty = this._consumesParts[part.id];
|
||||
return html`
|
||||
<div class="consumes-row">
|
||||
<label class="consumes-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${qty !== undefined}
|
||||
@change=${(e: Event) => {
|
||||
const next = { ...this._consumesParts };
|
||||
if ((e.target as HTMLInputElement).checked) next[part.id] = next[part.id] || 1;
|
||||
else delete next[part.id];
|
||||
this._consumesParts = next;
|
||||
}}
|
||||
/>
|
||||
<span>${part.name}${part.unit ? ` (${part.unit})` : ""}</span>
|
||||
</label>
|
||||
${qty !== undefined
|
||||
? html`<input
|
||||
class="consumes-qty"
|
||||
type="number"
|
||||
min="0.01"
|
||||
max="999"
|
||||
step="0.01"
|
||||
.value=${String(qty)}
|
||||
@input=${(e: Event) => {
|
||||
const v = parseFloat((e.target as HTMLInputElement).value);
|
||||
this._consumesParts = { ...this._consumesParts, [part.id]: Number.isFinite(v) && v >= 0.01 ? v : 1 };
|
||||
}}
|
||||
/>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${this.parts.map((part) => this._renderConsumesRow(part))}
|
||||
${this._foreignOwners.length
|
||||
? html`
|
||||
<details class="shared-pools" ?open=${this._hasForeignPick}>
|
||||
<summary>${t("shared_parts_other_objects", L)}</summary>
|
||||
<div class="field-help">${t("shared_parts_help", L)}</div>
|
||||
${this._foreignOwners.map(
|
||||
(owner) => html`
|
||||
<div class="shared-pool-owner">${owner.name}</div>
|
||||
${owner.parts.map((part) =>
|
||||
this._renderConsumesRow(part, owner.entry_id),
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
</details>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -1857,6 +1976,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<ms-textfield
|
||||
label="${t("warning_days", L)}"
|
||||
type="number"
|
||||
min="0"
|
||||
max="365"
|
||||
.value=${this._warningDays}
|
||||
@input=${(e: Event) => (this._warningDays = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
@@ -1879,6 +2000,19 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
></textarea>
|
||||
<div class="field-help">${t("checklist_help", L)}</div>
|
||||
` : nothing}
|
||||
<h3>${t("require_on_completion", L)}</h3>
|
||||
<div class="required-completion">
|
||||
${REQUIRED_COMPLETION_KEYS.map((field) => html`
|
||||
<label class="req-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${this._requiredCompletion.includes(field)}
|
||||
@change=${(e: Event) => this._toggleRequired(field, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span>${t(REQUIRED_COMPLETION_LABELS[field], L)}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
<ms-textfield
|
||||
label="${t("last_performed_optional", L)}"
|
||||
type="date"
|
||||
@@ -2127,6 +2261,24 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
background: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
/* #111: other objects' pools sit behind a disclosure so the object's OWN
|
||||
parts stay the primary list; each group is headed by the owning object's
|
||||
name, so which pool a checkbox means is never a guess. */
|
||||
.shared-pools {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.shared-pools > summary {
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.shared-pool-owner {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.field-help {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
+4
-2
@@ -14,7 +14,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatRecurrence } from "../styles";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
@@ -294,7 +294,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task_id: this._taskId,
|
||||
});
|
||||
this._toast = r.recommended_interval
|
||||
? `${t("reanalyze_result", this._lang) || "Recomputed"}: ${r.recommended_interval}d (${r.data_points} pts)`
|
||||
// The analyzer always works in DAYS (helpers/interval_analyzer.py), so
|
||||
// the unit is pinned here rather than taken from the task's own unit.
|
||||
? `${t("reanalyze_result", this._lang) || "Recomputed"}: ${formatInterval(r.recommended_interval, "days", this._lang)} (${r.data_points} pts)`
|
||||
: (t("reanalyze_insufficient_data", this._lang) || "Not enough data");
|
||||
await this._loadTask();
|
||||
setTimeout(() => { this._toast = ""; }, 3500);
|
||||
|
||||
@@ -252,6 +252,8 @@ export function openCompleteDialog(args: {
|
||||
task_name: string;
|
||||
checklist?: string[];
|
||||
adaptive_enabled?: boolean;
|
||||
/** Details the task demands before it counts as done (v2.44). */
|
||||
required_completion_fields?: string[];
|
||||
}): boolean {
|
||||
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
||||
if (!syncHass(dlg)) return false;
|
||||
@@ -260,6 +262,7 @@ export function openCompleteDialog(args: {
|
||||
dlg.taskName = args.task_name;
|
||||
dlg.checklist = args.checklist ?? [];
|
||||
dlg.adaptiveEnabled = !!args.adaptive_enabled;
|
||||
dlg.requiredFields = args.required_completion_fields ?? [];
|
||||
dlg.lang = (getHass()?.language) || "en";
|
||||
dlg.open();
|
||||
return true;
|
||||
|
||||
@@ -23,6 +23,14 @@ const common = {
|
||||
sourcemap: false,
|
||||
external: [],
|
||||
define: { __MS_BUNDLE_VERSION__: JSON.stringify(manifestVersion) },
|
||||
// A readable banner as well as the define. After the minifier is done the
|
||||
// stamped version survives only as `var xy="2.44.1"` with a generated name,
|
||||
// which nothing outside the bundle can reliably find — so 2.44.0 shipped a
|
||||
// bundle built before the version bump and every install showed a permanent
|
||||
// "reload the panel" banner (#112). This line is what
|
||||
// tests/test_frontend_bundle_version.py checks, and it also lets anyone read
|
||||
// the built version straight out of devtools.
|
||||
banner: { js: `/*! maintenance_supporter frontend ${manifestVersion} */` },
|
||||
};
|
||||
|
||||
// Panel
|
||||
|
||||
@@ -73,10 +73,19 @@ export function buildObjectReportHtml(
|
||||
const totalCost = tasks.reduce((n, t) => n + (t.total_cost ?? 0), 0);
|
||||
|
||||
return `<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${esc(labels.title)} — ${esc(obj.name)}</title>
|
||||
<style>
|
||||
/* This is a PRINTABLE sheet, not part of the app's theme: it opens as a
|
||||
blob in whatever viewer the OS supplies. In the Companion app that is a
|
||||
WebView, and a WebView on a dark-themed phone paints a DARK default
|
||||
canvas — against which the dark body text below disappeared completely,
|
||||
leaving only the pale row borders showing as stripes. Declaring the
|
||||
scheme AND painting the background keeps the sheet identical everywhere,
|
||||
and matches what comes out of a printer. */
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font: 13px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; margin: 32px; }
|
||||
body { font: 13px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #fff; margin: 32px; }
|
||||
h1 { font-size: 22px; margin: 0 0 2px; }
|
||||
.sub { color: #666; margin: 0 0 20px; }
|
||||
.meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 6px 24px; margin-bottom: 20px; }
|
||||
|
||||
@@ -83,11 +83,17 @@ export function buildTaskWorksheetHtml(
|
||||
: "";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>${esc(task.name)} — ${esc(L.title)}</title>
|
||||
<html><head><meta charset="utf-8"><meta name="color-scheme" content="light">
|
||||
<title>${esc(task.name)} — ${esc(L.title)}</title>
|
||||
<style>
|
||||
/* A work sheet is meant to be printed or read as a sheet, so it must not
|
||||
inherit the phone's dark theme: the Companion app opens it in a WebView
|
||||
that paints a dark canvas, and this dark text would vanish against it.
|
||||
See the same note in report.ts. */
|
||||
:root { color-scheme: light; }
|
||||
@page { size: A4; margin: 14mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font: 13px/1.45 -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; margin: 0; }
|
||||
body { font: 13px/1.45 -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; background: #fff; margin: 0; }
|
||||
header { display: flex; justify-content: space-between; align-items: flex-start;
|
||||
border-bottom: 3px solid #111; padding-bottom: 8px; margin-bottom: 12px; }
|
||||
h1 { font-size: 22px; margin: 0 0 2px; }
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Hodnota spouštěče",
|
||||
"complete_title": "Dokončit: ",
|
||||
"checklist": "Kontrolní seznam",
|
||||
"require_on_completion": "Vyžadovat při dokončení",
|
||||
"checklist_steps_optional": "Kroky kontrolního seznamu (volitelné)",
|
||||
"checklist_placeholder": "Vyčistit filtr\nVyměnit těsnění\nOtestovat tlak",
|
||||
"checklist_help": "Jeden krok na řádek. Max 100 položek.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Prázdné = zobrazit všechny stavy.",
|
||||
"card_filter_objects": "Filtrovat podle objektů",
|
||||
"card_filter_objects_help": "Prázdné = zobrazit všechny objekty.",
|
||||
"card_filter_areas": "Filtrovat podle oblastí",
|
||||
"card_filter_areas_help": "Prázdné = zobrazit všechny oblasti.",
|
||||
"card_filter_entities": "Filtrovat podle entit (entity_ids)",
|
||||
"card_filter_entities_help": "Vyberte entity sensor / binary_sensor z této integrace. Prázdné = všechny.",
|
||||
"card_loading_objects": "Načítání objektů…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Název panelu",
|
||||
"settings_notifications": "Oznámení",
|
||||
"settings_notify_service": "Služba oznámení",
|
||||
"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í",
|
||||
"send_test": "Odeslat test",
|
||||
"testing": "Odesílání…",
|
||||
"test_notification_success": "Testovací oznámení odesláno",
|
||||
"test_notification_failed": "Testovací oznámení se nezdařilo",
|
||||
"notify_per_person": "Doručování podle osoby",
|
||||
"notify_no_own_device": "Žádné vlastní zařízení — použije domácí službu",
|
||||
"settings_notify_due_soon": "Oznámit když brzy",
|
||||
"settings_notify_overdue": "Oznámit když po termínu",
|
||||
"settings_notify_triggered": "Oznámit když spuštěno",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Podle uživatele",
|
||||
"filter_label": "Filtr",
|
||||
"user_label": "Uživatel",
|
||||
"photo_label": "Fotografie",
|
||||
"sort_label": "Řazení",
|
||||
"group_by_label": "Seskupit podle",
|
||||
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Upravit zásobu",
|
||||
"restock_quantity_label": "Zakoupené množství",
|
||||
"consumes_parts_label": "Spotřebovává díly",
|
||||
"shared_parts_other_objects": "Díly z jiných objektů",
|
||||
"shared_parts_help": "Několik objektů může sdílet jeden sklad. Dokončení tohoto úkolu odečte zásobu z vlastnícího objektu.",
|
||||
"shared_part_unknown": "Neznámý díl",
|
||||
"parts_load_failed": "Nepodařilo se načíst díly tohoto objektu — možnosti spotřeby dílů nyní nejsou k dispozici.",
|
||||
"settings_export_selection": "Omezit na vybrané objekty (volitelné)",
|
||||
"settings_docs_archive": "Archiv dokumentů (se soubory)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Udløserværdi",
|
||||
"complete_title": "Fuldfør: ",
|
||||
"checklist": "Tjekliste",
|
||||
"require_on_completion": "Kræv ved fuldførelse",
|
||||
"checklist_steps_optional": "Tjeklistetrin (valgfrit)",
|
||||
"checklist_placeholder": "Rengør filter\nUdskift pakning\nTest tryk",
|
||||
"checklist_help": "Ét trin pr. linje. Maks. 100 elementer.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Tom = vis alle statusser.",
|
||||
"card_filter_objects": "Filtrer efter objekter",
|
||||
"card_filter_objects_help": "Tom = vis alle objekter.",
|
||||
"card_filter_areas": "Filtrer efter områder",
|
||||
"card_filter_areas_help": "Tom = vis alle områder.",
|
||||
"card_filter_entities": "Filtrer efter enheder (entity_ids)",
|
||||
"card_filter_entities_help": "Vælg sensor- / binary_sensor-enheder fra denne integration. Tom = alle.",
|
||||
"card_loading_objects": "Indlæser objekter…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Sidepanelets titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjeneste",
|
||||
"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",
|
||||
"send_test": "Send test",
|
||||
"testing": "Sender…",
|
||||
"test_notification_success": "Testnotifikation sendt",
|
||||
"test_notification_failed": "Testnotifikation mislykkedes",
|
||||
"notify_per_person": "Levering pr. person",
|
||||
"notify_no_own_device": "Ingen egen enhed — bruger husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Notificer ved snart forfalden",
|
||||
"settings_notify_overdue": "Notificer ved forfalden",
|
||||
"settings_notify_triggered": "Notificer ved udløst",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "Efter bruger",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "Bruger",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Sortering",
|
||||
"group_by_label": "Grupper efter",
|
||||
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Justér lager",
|
||||
"restock_quantity_label": "Købt mængde",
|
||||
"consumes_parts_label": "Forbruger dele",
|
||||
"shared_parts_other_objects": "Dele fra andre objekter",
|
||||
"shared_parts_help": "Flere objekter kan dele det samme lager. Når opgaven fuldføres, trækkes der fra det ejende objekt.",
|
||||
"shared_part_unknown": "Ukendt del",
|
||||
"parts_load_failed": "Kunne ikke indlæse objektets reservedele — forbrugsindstillingerne er ikke tilgængelige lige nu.",
|
||||
"settings_export_selection": "Begræns til valgte objekter (valgfrit)",
|
||||
"settings_docs_archive": "Dokumentarkiv (med filer)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Trigger-Wert",
|
||||
"complete_title": "Erledigt: ",
|
||||
"checklist": "Checkliste",
|
||||
"require_on_completion": "Beim Abschließen verlangen",
|
||||
"checklist_steps_optional": "Checkliste-Schritte (optional)",
|
||||
"checklist_placeholder": "Filter reinigen\nDichtung ersetzen\nDruck testen",
|
||||
"checklist_help": "Ein Schritt pro Zeile. Max. 100 Einträge.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Leer = alle Status zeigen.",
|
||||
"card_filter_objects": "Nach Objekten filtern",
|
||||
"card_filter_objects_help": "Leer = alle Objekte zeigen.",
|
||||
"card_filter_areas": "Nach Bereichen filtern",
|
||||
"card_filter_areas_help": "Leer = alle Bereiche zeigen.",
|
||||
"card_filter_entities": "Nach Entitäten filtern (entity_ids)",
|
||||
"card_filter_entities_help": "Wähle Sensor-/Binary-Sensor-Entitäten dieser Integration. Leer = alle.",
|
||||
"card_loading_objects": "Lade Objekte…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Panel-Titel",
|
||||
"settings_notifications": "Benachrichtigungen",
|
||||
"settings_notify_service": "Benachrichtigungsdienst",
|
||||
"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",
|
||||
"send_test": "Test senden",
|
||||
"testing": "Sende…",
|
||||
"test_notification_success": "Test-Benachrichtigung gesendet",
|
||||
"test_notification_failed": "Test-Benachrichtigung fehlgeschlagen",
|
||||
"notify_per_person": "Zustellung pro Person",
|
||||
"notify_no_own_device": "Kein eigenes Gerät — nutzt den Haushaltsdienst",
|
||||
"settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen",
|
||||
"settings_notify_overdue": "Bei Überfälligkeit benachrichtigen",
|
||||
"settings_notify_triggered": "Bei Auslösung benachrichtigen",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "Nach Verantwortlichem",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "Benutzer",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Sortierung",
|
||||
"group_by_label": "Gruppieren nach",
|
||||
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Bestand anpassen",
|
||||
"restock_quantity_label": "Gekaufte Menge",
|
||||
"consumes_parts_label": "Verbraucht Teile",
|
||||
"shared_parts_other_objects": "Teile anderer Objekte",
|
||||
"shared_parts_help": "Mehrere Objekte können sich einen Bestand teilen. Beim Abschließen wird vom besitzenden Objekt abgebucht.",
|
||||
"shared_part_unknown": "Unbekanntes Teil",
|
||||
"parts_load_failed": "Die Teile dieses Objekts konnten nicht geladen werden — die Teileverbrauch-Optionen sind gerade nicht verfügbar.",
|
||||
"settings_export_selection": "Auf ausgewählte Objekte beschränken (optional)",
|
||||
"settings_docs_archive": "Dokumentenarchiv (mit Dateien)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Trigger value",
|
||||
"complete_title": "Complete: ",
|
||||
"checklist": "Checklist",
|
||||
"require_on_completion": "Require on completion",
|
||||
"checklist_steps_optional": "Checklist steps (optional)",
|
||||
"checklist_placeholder": "Clean filter\nReplace seal\nTest pressure",
|
||||
"checklist_help": "One step per line. Max 100 items.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Empty = show all statuses.",
|
||||
"card_filter_objects": "Filter by objects",
|
||||
"card_filter_objects_help": "Empty = show all objects.",
|
||||
"card_filter_areas": "Filter by areas",
|
||||
"card_filter_areas_help": "Empty = show all areas.",
|
||||
"card_filter_entities": "Filter by entities (entity_ids)",
|
||||
"card_filter_entities_help": "Pick sensor / binary_sensor entities from this integration. Empty = all.",
|
||||
"card_loading_objects": "Loading objects…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Sidebar panel title",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Notification service",
|
||||
"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",
|
||||
"send_test": "Send test",
|
||||
"testing": "Sending…",
|
||||
"test_notification_success": "Test notification sent",
|
||||
"test_notification_failed": "Test notification failed",
|
||||
"notify_per_person": "Per-person delivery",
|
||||
"notify_no_own_device": "No own device — uses the household service",
|
||||
"settings_notify_due_soon": "Notify when due soon",
|
||||
"settings_notify_overdue": "Notify when overdue",
|
||||
"settings_notify_triggered": "Notify when triggered",
|
||||
@@ -494,6 +501,7 @@
|
||||
"groupby_user": "By user",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "User",
|
||||
"photo_label": "Photo",
|
||||
"sort_label": "Sort",
|
||||
"group_by_label": "Group by",
|
||||
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
|
||||
@@ -728,6 +736,9 @@
|
||||
"part_restock": "Adjust stock",
|
||||
"restock_quantity_label": "Quantity bought",
|
||||
"consumes_parts_label": "Consumes parts",
|
||||
"shared_parts_other_objects": "Parts from other objects",
|
||||
"shared_parts_help": "Several objects can share one stock. Completing this task takes from the owning object.",
|
||||
"shared_part_unknown": "Unknown part",
|
||||
"parts_load_failed": "Couldn't load this object's parts — the consumes-parts options are unavailable right now.",
|
||||
"adopt_problem_button": "Adopt problem sensors",
|
||||
"adopt_problem_title": "Adopt problem sensors",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Valor del disparador",
|
||||
"complete_title": "Completada: ",
|
||||
"checklist": "Lista de verificación",
|
||||
"require_on_completion": "Exigir al completar",
|
||||
"checklist_steps_optional": "Pasos de la lista de verificación (opcional)",
|
||||
"checklist_placeholder": "Limpiar filtro\nReemplazar junta\nProbar presión",
|
||||
"checklist_help": "Un paso por línea. Máx. 100 elementos.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Vacío = mostrar todos los estados.",
|
||||
"card_filter_objects": "Filtrar por objetos",
|
||||
"card_filter_objects_help": "Vacío = mostrar todos los objetos.",
|
||||
"card_filter_areas": "Filtrar por áreas",
|
||||
"card_filter_areas_help": "Vacío = mostrar todas las áreas.",
|
||||
"card_filter_entities": "Filtrar por entidades (entity_ids)",
|
||||
"card_filter_entities_help": "Selecciona entidades sensor / binary_sensor de esta integración. Vacío = todas.",
|
||||
"card_loading_objects": "Cargando objetos…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Título del panel lateral",
|
||||
"settings_notifications": "Notificaciones",
|
||||
"settings_notify_service": "Servicio de notificación",
|
||||
"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",
|
||||
"send_test": "Enviar prueba",
|
||||
"testing": "Enviando…",
|
||||
"test_notification_success": "Notificación de prueba enviada",
|
||||
"test_notification_failed": "La notificación de prueba falló",
|
||||
"notify_per_person": "Entrega por persona",
|
||||
"notify_no_own_device": "Sin dispositivo propio — usa el servicio del hogar",
|
||||
"settings_notify_due_soon": "Notificar cuando esté próxima",
|
||||
"settings_notify_overdue": "Notificar cuando esté vencida",
|
||||
"settings_notify_triggered": "Notificar cuando se active",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Por usuario",
|
||||
"filter_label": "Filtro",
|
||||
"user_label": "Usuario",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Ordenar",
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Ajustar existencias",
|
||||
"restock_quantity_label": "Cantidad comprada",
|
||||
"consumes_parts_label": "Consume piezas",
|
||||
"shared_parts_other_objects": "Piezas de otros objetos",
|
||||
"shared_parts_help": "Varios objetos pueden compartir un mismo stock. Al completar esta tarea se descuenta del objeto propietario.",
|
||||
"shared_part_unknown": "Pieza desconocida",
|
||||
"parts_load_failed": "No se pudieron cargar las piezas de este objeto — las opciones de consumo de piezas no están disponibles ahora.",
|
||||
"settings_export_selection": "Limitar a los objetos seleccionados (opcional)",
|
||||
"settings_docs_archive": "Archivo de documentos (con archivos)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Laukaisuarvo",
|
||||
"complete_title": "Suorita: ",
|
||||
"checklist": "Tarkistuslista",
|
||||
"require_on_completion": "Vaadi valmistuessa",
|
||||
"checklist_steps_optional": "Tarkistuslistan vaiheet (valinnainen)",
|
||||
"checklist_placeholder": "Puhdista suodatin\nVaihda tiiviste\nTarkista paine",
|
||||
"checklist_help": "Yksi vaihe riviä kohden. Enintään 100 kohdetta.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Tyhjä = näytä kaikki tilat.",
|
||||
"card_filter_objects": "Suodata kohteiden mukaan",
|
||||
"card_filter_objects_help": "Tyhjä = näytä kaikki kohteet.",
|
||||
"card_filter_areas": "Suodata alueiden mukaan",
|
||||
"card_filter_areas_help": "Tyhjä = näytä kaikki alueet.",
|
||||
"card_filter_entities": "Suodata entiteettien mukaan (entity_id)",
|
||||
"card_filter_entities_help": "Valitse sensor- / binary_sensor-entiteetit tästä integraatiosta. Tyhjä = kaikki.",
|
||||
"card_loading_objects": "Ladataan kohteita…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Sivupalkin paneelin otsikko",
|
||||
"settings_notifications": "Ilmoitukset",
|
||||
"settings_notify_service": "Ilmoituspalvelu",
|
||||
"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",
|
||||
"send_test": "Lähetä testi",
|
||||
"testing": "Lähetetään…",
|
||||
"test_notification_success": "Testi-ilmoitus lähetetty",
|
||||
"test_notification_failed": "Testi-ilmoitus epäonnistui",
|
||||
"notify_per_person": "Toimitus henkilöittäin",
|
||||
"notify_no_own_device": "Ei omaa laitetta — käyttää kotitalouden palvelua",
|
||||
"settings_notify_due_soon": "Ilmoita, kun erääntyy pian",
|
||||
"settings_notify_overdue": "Ilmoita, kun myöhässä",
|
||||
"settings_notify_triggered": "Ilmoita, kun laukaistu",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "Käyttäjän mukaan",
|
||||
"filter_label": "Suodata",
|
||||
"user_label": "Käyttäjä",
|
||||
"photo_label": "Valokuva",
|
||||
"sort_label": "Lajittele",
|
||||
"group_by_label": "Ryhmittele",
|
||||
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Muuta varastoa",
|
||||
"restock_quantity_label": "Ostettu määrä",
|
||||
"consumes_parts_label": "Kuluttaa osia",
|
||||
"shared_parts_other_objects": "Muiden kohteiden osat",
|
||||
"shared_parts_help": "Useat kohteet voivat jakaa saman varaston. Tämän tehtävän suorittaminen vähentää omistavan kohteen varastoa.",
|
||||
"shared_part_unknown": "Tuntematon osa",
|
||||
"parts_load_failed": "Kohteen osia ei voitu ladata — osien kulutusvalinnat eivät ole juuri nyt käytettävissä.",
|
||||
"settings_export_selection": "Rajaa valittuihin kohteisiin (valinnainen)",
|
||||
"settings_docs_archive": "Asiakirja-arkisto (tiedostoineen)",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Valeur du déclencheur",
|
||||
"complete_title": "Terminé : ",
|
||||
"checklist": "Checklist",
|
||||
"require_on_completion": "Exiger à la clôture",
|
||||
"checklist_steps_optional": "Étapes de la checklist (optionnel)",
|
||||
"checklist_placeholder": "Nettoyer le filtre\nRemplacer le joint\nTester la pression",
|
||||
"checklist_help": "Une étape par ligne. Max 100 éléments.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Vide = afficher tous les statuts.",
|
||||
"card_filter_objects": "Filtrer par objets",
|
||||
"card_filter_objects_help": "Vide = afficher tous les objets.",
|
||||
"card_filter_areas": "Filtrer par zones",
|
||||
"card_filter_areas_help": "Vide = afficher toutes les zones.",
|
||||
"card_filter_entities": "Filtrer par entités (entity_ids)",
|
||||
"card_filter_entities_help": "Choisissez des entités sensor / binary_sensor de cette intégration. Vide = toutes.",
|
||||
"card_loading_objects": "Chargement des objets…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Titre du panneau latéral",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notify_service": "Service de notification",
|
||||
"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",
|
||||
"send_test": "Envoyer le test",
|
||||
"testing": "Envoi en cours…",
|
||||
"test_notification_success": "Notification de test envoyée",
|
||||
"test_notification_failed": "Échec de la notification de test",
|
||||
"notify_per_person": "Distribution par personne",
|
||||
"notify_no_own_device": "Aucun appareil propre — utilise le service du foyer",
|
||||
"settings_notify_due_soon": "Notifier quand bientôt dû",
|
||||
"settings_notify_overdue": "Notifier quand en retard",
|
||||
"settings_notify_triggered": "Notifier quand déclenché",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Par utilisateur",
|
||||
"filter_label": "Filtre",
|
||||
"user_label": "Utilisateur",
|
||||
"photo_label": "Photo",
|
||||
"sort_label": "Tri",
|
||||
"group_by_label": "Grouper par",
|
||||
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Ajuster le stock",
|
||||
"restock_quantity_label": "Quantité achetée",
|
||||
"consumes_parts_label": "Consomme des pièces",
|
||||
"shared_parts_other_objects": "Pièces d'autres objets",
|
||||
"shared_parts_help": "Plusieurs objets peuvent partager un même stock. Terminer cette tâche décompte du stock de l'objet propriétaire.",
|
||||
"shared_part_unknown": "Pièce inconnue",
|
||||
"parts_load_failed": "Impossible de charger les pièces de cet objet — les options de consommation de pièces sont indisponibles pour le moment.",
|
||||
"settings_export_selection": "Limiter aux objets sélectionnés (facultatif)",
|
||||
"settings_docs_archive": "Archive de documents (avec fichiers)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "ट्रिगर मान",
|
||||
"complete_title": "पूर्ण करें: ",
|
||||
"checklist": "चेकलिस्ट",
|
||||
"require_on_completion": "पूर्ण करते समय आवश्यक",
|
||||
"checklist_steps_optional": "चेकलिस्ट चरण (वैकल्पिक)",
|
||||
"checklist_placeholder": "फ़िल्टर साफ़ करें\nसील बदलें\nदबाव जाँचें",
|
||||
"checklist_help": "प्रति पंक्ति एक चरण। अधिकतम 100 आइटम।",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "खाली = सभी स्थितियाँ दिखाएँ।",
|
||||
"card_filter_objects": "वस्तुओं के अनुसार फ़िल्टर करें",
|
||||
"card_filter_objects_help": "खाली = सभी वस्तुएँ दिखाएँ।",
|
||||
"card_filter_areas": "क्षेत्रों के अनुसार फ़िल्टर करें",
|
||||
"card_filter_areas_help": "खाली = सभी क्षेत्र दिखाएँ।",
|
||||
"card_filter_entities": "एंटिटी के अनुसार फ़िल्टर करें (entity_ids)",
|
||||
"card_filter_entities_help": "इस इंटीग्रेशन से sensor / binary_sensor एंटिटी चुनें। खाली = सभी।",
|
||||
"card_loading_objects": "वस्तुएँ लोड हो रही हैं…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "साइडबार पैनल शीर्षक",
|
||||
"settings_notifications": "सूचनाएँ",
|
||||
"settings_notify_service": "सूचना सेवा",
|
||||
"settings_install_assist_sentences": "Assist वाक्य इंस्टॉल करें",
|
||||
"settings_install_assist_sentences_hint": "वॉइस वाक्यों को आपके कॉन्फ़िगरेशन में कॉपी करता है ताकि क्लासिक Assist एजेंट उन्हें पहचान सके। आपके द्वारा संपादित फ़ाइल कभी अधिलेखित नहीं होती।",
|
||||
"test_notification": "परीक्षण सूचना",
|
||||
"send_test": "परीक्षण भेजें",
|
||||
"testing": "भेजा जा रहा है…",
|
||||
"test_notification_success": "परीक्षण सूचना भेजी गई",
|
||||
"test_notification_failed": "परीक्षण सूचना विफल",
|
||||
"notify_per_person": "प्रति व्यक्ति वितरण",
|
||||
"notify_no_own_device": "कोई निजी डिवाइस नहीं — घरेलू सेवा का उपयोग करता है",
|
||||
"settings_notify_due_soon": "जल्द देय होने पर सूचित करें",
|
||||
"settings_notify_overdue": "अतिदेय होने पर सूचित करें",
|
||||
"settings_notify_triggered": "ट्रिगर होने पर सूचित करें",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "उपयोगकर्ता के अनुसार",
|
||||
"filter_label": "फ़िल्टर",
|
||||
"user_label": "उपयोगकर्ता",
|
||||
"photo_label": "फ़ोटो",
|
||||
"sort_label": "क्रमबद्ध करें",
|
||||
"group_by_label": "इसके अनुसार समूहित करें",
|
||||
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "स्टॉक समायोजित करें",
|
||||
"restock_quantity_label": "खरीदी गई मात्रा",
|
||||
"consumes_parts_label": "पुर्ज़े खपत",
|
||||
"shared_parts_other_objects": "अन्य वस्तुओं के पुर्ज़े",
|
||||
"shared_parts_help": "कई वस्तुएँ एक ही स्टॉक साझा कर सकती हैं। यह कार्य पूरा करने पर स्वामी वस्तु के स्टॉक से घटाया जाता है।",
|
||||
"shared_part_unknown": "अज्ञात पुर्ज़ा",
|
||||
"parts_load_failed": "इस ऑब्जेक्ट के पुर्ज़े लोड नहीं हो सके — पुर्ज़ा-खपत विकल्प अभी उपलब्ध नहीं हैं।",
|
||||
"settings_export_selection": "चयनित ऑब्जेक्ट तक सीमित करें (वैकल्पिक)",
|
||||
"settings_docs_archive": "दस्तावेज़ संग्रह (फ़ाइलों सहित)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Kiváltó érték",
|
||||
"complete_title": "Elvégzés: ",
|
||||
"checklist": "Ellenőrzőlista",
|
||||
"require_on_completion": "Befejezéskor kötelező",
|
||||
"checklist_steps_optional": "Ellenőrzőlista lépései (opcionális)",
|
||||
"checklist_placeholder": "Szűrő tisztítása\nTömítés cseréje\nNyomáspróba",
|
||||
"checklist_help": "Soronként egy lépés. Legfeljebb 100 elem.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Üres = minden állapot megjelenik.",
|
||||
"card_filter_objects": "Szűrés objektumok szerint",
|
||||
"card_filter_objects_help": "Üres = minden objektum megjelenik.",
|
||||
"card_filter_areas": "Szűrés területek szerint",
|
||||
"card_filter_areas_help": "Üres = minden terület megjelenik.",
|
||||
"card_filter_entities": "Szűrés entitások szerint (entity_id-k)",
|
||||
"card_filter_entities_help": "Válasszon sensor / binary_sensor entitásokat ebből az integrációból. Üres = mind.",
|
||||
"card_loading_objects": "Objektumok betöltése…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Oldalsáv-panel címe",
|
||||
"settings_notifications": "Értesítések",
|
||||
"settings_notify_service": "Értesítési szolgáltatás",
|
||||
"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",
|
||||
"send_test": "Teszt küldése",
|
||||
"testing": "Küldés…",
|
||||
"test_notification_success": "Tesztértesítés elküldve",
|
||||
"test_notification_failed": "A tesztértesítés nem sikerült",
|
||||
"notify_per_person": "Kézbesítés személyenként",
|
||||
"notify_no_own_device": "Nincs saját eszköz — a háztartási szolgáltatást használja",
|
||||
"settings_notify_due_soon": "Értesítés, ha hamarosan esedékes",
|
||||
"settings_notify_overdue": "Értesítés lejáratkor",
|
||||
"settings_notify_triggered": "Értesítés kiváltáskor",
|
||||
@@ -494,6 +501,7 @@
|
||||
"groupby_user": "Felhasználó szerint",
|
||||
"filter_label": "Szűrő",
|
||||
"user_label": "Felhasználó",
|
||||
"photo_label": "Fénykép",
|
||||
"sort_label": "Rendezés",
|
||||
"group_by_label": "Csoportosítás",
|
||||
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
|
||||
@@ -728,6 +736,9 @@
|
||||
"part_restock": "Készlet módosítása",
|
||||
"restock_quantity_label": "Vásárolt mennyiség",
|
||||
"consumes_parts_label": "Felhasznált alkatrészek",
|
||||
"shared_parts_other_objects": "Más objektumok alkatrészei",
|
||||
"shared_parts_help": "Több objektum is használhatja ugyanazt a készletet. A feladat elvégzése a tulajdonos objektum készletéből von le.",
|
||||
"shared_part_unknown": "Ismeretlen alkatrész",
|
||||
"parts_load_failed": "Az objektum alkatrészei nem tölthetők be — a felhasznált alkatrészek beállításai most nem érhetők el.",
|
||||
"adopt_problem_button": "Problémaérzékelők átvétele",
|
||||
"adopt_problem_title": "Problémaérzékelők átvétele",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Valore trigger",
|
||||
"complete_title": "Completato: ",
|
||||
"checklist": "Checklist",
|
||||
"require_on_completion": "Richiedi al completamento",
|
||||
"checklist_steps_optional": "Passaggi della checklist (opzionale)",
|
||||
"checklist_placeholder": "Pulire il filtro\nSostituire la guarnizione\nTestare la pressione",
|
||||
"checklist_help": "Un passaggio per riga. Max 100 elementi.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Vuoto = mostra tutti gli stati.",
|
||||
"card_filter_objects": "Filtra per oggetti",
|
||||
"card_filter_objects_help": "Vuoto = mostra tutti gli oggetti.",
|
||||
"card_filter_areas": "Filtra per aree",
|
||||
"card_filter_areas_help": "Vuoto = mostra tutte le aree.",
|
||||
"card_filter_entities": "Filtra per entità (entity_ids)",
|
||||
"card_filter_entities_help": "Seleziona entità sensor / binary_sensor da questa integrazione. Vuoto = tutte.",
|
||||
"card_loading_objects": "Caricamento oggetti…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Titolo pannello laterale",
|
||||
"settings_notifications": "Notifiche",
|
||||
"settings_notify_service": "Servizio di notifica",
|
||||
"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",
|
||||
"send_test": "Invia test",
|
||||
"testing": "Invio in corso…",
|
||||
"test_notification_success": "Notifica di test inviata",
|
||||
"test_notification_failed": "Notifica di test non riuscita",
|
||||
"notify_per_person": "Consegna per persona",
|
||||
"notify_no_own_device": "Nessun dispositivo proprio — usa il servizio della casa",
|
||||
"settings_notify_due_soon": "Notifica quando in scadenza",
|
||||
"settings_notify_overdue": "Notifica quando scaduto",
|
||||
"settings_notify_triggered": "Notifica quando attivato",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Per utente",
|
||||
"filter_label": "Filtro",
|
||||
"user_label": "Utente",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Ordinamento",
|
||||
"group_by_label": "Raggruppa per",
|
||||
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Correggi scorta",
|
||||
"restock_quantity_label": "Quantità acquistata",
|
||||
"consumes_parts_label": "Consuma ricambi",
|
||||
"shared_parts_other_objects": "Ricambi di altri oggetti",
|
||||
"shared_parts_help": "Più oggetti possono condividere una sola scorta. Completando questa attività si preleva dall'oggetto proprietario.",
|
||||
"shared_part_unknown": "Ricambio sconosciuto",
|
||||
"parts_load_failed": "Impossibile caricare i ricambi di questo oggetto — le opzioni di consumo ricambi non sono al momento disponibili.",
|
||||
"settings_export_selection": "Limita agli oggetti selezionati (facoltativo)",
|
||||
"settings_docs_archive": "Archivio documenti (con file)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "トリガー値",
|
||||
"complete_title": "完了: ",
|
||||
"checklist": "チェックリスト",
|
||||
"require_on_completion": "完了時に必須",
|
||||
"checklist_steps_optional": "チェックリストの手順(任意)",
|
||||
"checklist_placeholder": "フィルター清掃\nシール交換\n圧力テスト",
|
||||
"checklist_help": "1行に1手順。最大100項目。",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "空欄=すべての状態を表示。",
|
||||
"card_filter_objects": "対象で絞り込み",
|
||||
"card_filter_objects_help": "空欄=すべての対象を表示。",
|
||||
"card_filter_areas": "エリアで絞り込み",
|
||||
"card_filter_areas_help": "空欄=すべてのエリアを表示。",
|
||||
"card_filter_entities": "エンティティで絞り込み(entity_ids)",
|
||||
"card_filter_entities_help": "この統合のsensor/binary_sensorエンティティを選択。空欄=すべて。",
|
||||
"card_loading_objects": "対象を読み込み中…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "サイドバーパネルのタイトル",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知サービス",
|
||||
"settings_install_assist_sentences": "Assist の文を導入する",
|
||||
"settings_install_assist_sentences_hint": "音声フレーズを設定に複製し、従来の Assist エージェントが認識できるようにします。自分で編集したファイルが上書きされることはありません。",
|
||||
"test_notification": "テスト通知",
|
||||
"send_test": "テスト送信",
|
||||
"testing": "送信中…",
|
||||
"test_notification_success": "テスト通知を送信しました",
|
||||
"test_notification_failed": "テスト通知に失敗しました",
|
||||
"notify_per_person": "メンバーごとの配信",
|
||||
"notify_no_own_device": "個人の端末なし — 家庭用サービスを使用",
|
||||
"settings_notify_due_soon": "期限間近で通知",
|
||||
"settings_notify_overdue": "期限超過で通知",
|
||||
"settings_notify_triggered": "トリガー発生で通知",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "ユーザー別",
|
||||
"filter_label": "絞り込み",
|
||||
"user_label": "ユーザー",
|
||||
"photo_label": "写真",
|
||||
"sort_label": "並べ替え",
|
||||
"group_by_label": "グループ化",
|
||||
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "在庫を調整",
|
||||
"restock_quantity_label": "購入数量",
|
||||
"consumes_parts_label": "部品を消費",
|
||||
"shared_parts_other_objects": "他の対象の部品",
|
||||
"shared_parts_help": "複数の対象で同じ在庫を共有できます。このタスクを完了すると、所有する対象の在庫から差し引かれます。",
|
||||
"shared_part_unknown": "不明な部品",
|
||||
"parts_load_failed": "このオブジェクトの部品を読み込めませんでした — 部品消費オプションは現在利用できません。",
|
||||
"settings_export_selection": "選択したオブジェクトに限定(任意)",
|
||||
"settings_docs_archive": "ドキュメントアーカイブ(ファイル付き)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "트리거 값",
|
||||
"complete_title": "완료: ",
|
||||
"checklist": "체크리스트",
|
||||
"require_on_completion": "완료 시 필수 입력",
|
||||
"checklist_steps_optional": "체크리스트 단계 (선택)",
|
||||
"checklist_placeholder": "필터 청소\n씰 교체\n압력 테스트",
|
||||
"checklist_help": "한 줄에 한 단계씩 입력하세요. 최대 100개.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "비워두면 모든 상태를 표시합니다.",
|
||||
"card_filter_objects": "객체로 필터",
|
||||
"card_filter_objects_help": "비워두면 모든 객체를 표시합니다.",
|
||||
"card_filter_areas": "구역으로 필터",
|
||||
"card_filter_areas_help": "비워두면 모든 구역을 표시합니다.",
|
||||
"card_filter_entities": "엔티티로 필터 (entity_ids)",
|
||||
"card_filter_entities_help": "이 통합구성요소의 sensor / binary_sensor 엔티티를 선택하세요. 비워두면 전체.",
|
||||
"card_loading_objects": "객체 불러오는 중…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "사이드바 패널 제목",
|
||||
"settings_notifications": "알림",
|
||||
"settings_notify_service": "알림 서비스",
|
||||
"settings_install_assist_sentences": "Assist 문장 설치",
|
||||
"settings_install_assist_sentences_hint": "음성 문장을 설정에 복사하여 기존 Assist 에이전트가 인식하도록 합니다. 직접 수정한 파일은 절대 덮어쓰지 않습니다.",
|
||||
"test_notification": "테스트 알림",
|
||||
"send_test": "테스트 보내기",
|
||||
"testing": "보내는 중…",
|
||||
"test_notification_success": "테스트 알림을 보냈습니다",
|
||||
"test_notification_failed": "테스트 알림 전송 실패",
|
||||
"notify_per_person": "사용자별 전송",
|
||||
"notify_no_own_device": "개인 기기 없음 — 가정 서비스 사용",
|
||||
"settings_notify_due_soon": "기한 임박 시 알림",
|
||||
"settings_notify_overdue": "기한 초과 시 알림",
|
||||
"settings_notify_triggered": "트리거 시 알림",
|
||||
@@ -494,6 +501,7 @@
|
||||
"groupby_user": "사용자별",
|
||||
"filter_label": "필터",
|
||||
"user_label": "사용자",
|
||||
"photo_label": "사진",
|
||||
"sort_label": "정렬",
|
||||
"group_by_label": "그룹화",
|
||||
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
|
||||
@@ -728,6 +736,9 @@
|
||||
"part_restock": "재고 조정",
|
||||
"restock_quantity_label": "구매 수량",
|
||||
"consumes_parts_label": "사용하는 부품",
|
||||
"shared_parts_other_objects": "다른 객체의 부품",
|
||||
"shared_parts_help": "여러 객체가 하나의 재고를 공유할 수 있습니다. 이 작업을 완료하면 소유 객체의 재고에서 차감됩니다.",
|
||||
"shared_part_unknown": "알 수 없는 부품",
|
||||
"parts_load_failed": "이 객체의 부품을 불러올 수 없습니다 — 부품 사용 옵션을 지금은 쓸 수 없습니다.",
|
||||
"adopt_problem_button": "문제 센서 가져오기",
|
||||
"adopt_problem_title": "문제 센서 가져오기",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Utløserverdi",
|
||||
"complete_title": "Fullfør: ",
|
||||
"checklist": "Sjekkliste",
|
||||
"require_on_completion": "Krev ved fullføring",
|
||||
"checklist_steps_optional": "Sjekklistetrinn (valgfritt)",
|
||||
"checklist_placeholder": "Rengjør filter\nBytt pakning\nTest trykk",
|
||||
"checklist_help": "Ett trinn per linje. Maks 100 elementer.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Tom = vis alle statuser.",
|
||||
"card_filter_objects": "Filtrer etter objekter",
|
||||
"card_filter_objects_help": "Tom = vis alle objekter.",
|
||||
"card_filter_areas": "Filtrer etter områder",
|
||||
"card_filter_areas_help": "Tom = vis alle områder.",
|
||||
"card_filter_entities": "Filtrer etter entiteter (entity_ids)",
|
||||
"card_filter_entities_help": "Velg sensor-/binary_sensor-entiteter fra denne integrasjonen. Tom = alle.",
|
||||
"card_loading_objects": "Laster objekter…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Tittel på sidepanel",
|
||||
"settings_notifications": "Varsler",
|
||||
"settings_notify_service": "Varslingstjeneste",
|
||||
"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",
|
||||
"send_test": "Send test",
|
||||
"testing": "Sender…",
|
||||
"test_notification_success": "Testvarsel sendt",
|
||||
"test_notification_failed": "Testvarsel mislyktes",
|
||||
"notify_per_person": "Levering per person",
|
||||
"notify_no_own_device": "Ingen egen enhet — bruker husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Varsle når noe forfaller snart",
|
||||
"settings_notify_overdue": "Varsle når noe er forfalt",
|
||||
"settings_notify_triggered": "Varsle når noe utløses",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "Etter bruker",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "Bruker",
|
||||
"photo_label": "Bilde",
|
||||
"sort_label": "Sorter",
|
||||
"group_by_label": "Grupper etter",
|
||||
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Juster lager",
|
||||
"restock_quantity_label": "Kjøpt mengde",
|
||||
"consumes_parts_label": "Forbruker deler",
|
||||
"shared_parts_other_objects": "Deler fra andre objekter",
|
||||
"shared_parts_help": "Flere objekter kan dele samme lager. Når oppgaven fullføres, trekkes det fra det eiende objektet.",
|
||||
"shared_part_unknown": "Ukjent del",
|
||||
"parts_load_failed": "Kunne ikke laste objektets deler — forbruksvalgene er ikke tilgjengelige nå.",
|
||||
"settings_export_selection": "Begrens til valgte objekter (valgfritt)",
|
||||
"settings_docs_archive": "Dokumentarkiv (med filer)",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Triggerwaarde",
|
||||
"complete_title": "Voltooid: ",
|
||||
"checklist": "Checklist",
|
||||
"require_on_completion": "Vereisen bij afronden",
|
||||
"checklist_steps_optional": "Checklist-stappen (optioneel)",
|
||||
"checklist_placeholder": "Filter schoonmaken\nPakking vervangen\nDruk testen",
|
||||
"checklist_help": "Eén stap per regel. Max. 100 items.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Leeg = alle statussen tonen.",
|
||||
"card_filter_objects": "Filteren op objecten",
|
||||
"card_filter_objects_help": "Leeg = alle objecten tonen.",
|
||||
"card_filter_areas": "Filteren op gebieden",
|
||||
"card_filter_areas_help": "Leeg = alle gebieden tonen.",
|
||||
"card_filter_entities": "Filteren op entiteiten (entity_ids)",
|
||||
"card_filter_entities_help": "Kies sensor/binary_sensor entiteiten van deze integratie. Leeg = alle.",
|
||||
"card_loading_objects": "Objecten laden…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Titel zijbalkpaneel",
|
||||
"settings_notifications": "Meldingen",
|
||||
"settings_notify_service": "Meldingsservice",
|
||||
"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",
|
||||
"send_test": "Test versturen",
|
||||
"testing": "Verzenden…",
|
||||
"test_notification_success": "Testmelding verzonden",
|
||||
"test_notification_failed": "Testmelding mislukt",
|
||||
"notify_per_person": "Bezorging per persoon",
|
||||
"notify_no_own_device": "Geen eigen apparaat — gebruikt de huishoudelijke dienst",
|
||||
"settings_notify_due_soon": "Melding bij bijna verlopen",
|
||||
"settings_notify_overdue": "Melding bij achterstallig",
|
||||
"settings_notify_triggered": "Melding bij geactiveerd",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Per gebruiker",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "Gebruiker",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Sorteren",
|
||||
"group_by_label": "Groeperen op",
|
||||
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Voorraad aanpassen",
|
||||
"restock_quantity_label": "Gekochte hoeveelheid",
|
||||
"consumes_parts_label": "Verbruikt onderdelen",
|
||||
"shared_parts_other_objects": "Onderdelen van andere objecten",
|
||||
"shared_parts_help": "Meerdere objecten kunnen één voorraad delen. Bij afronden wordt van het eigenaarsobject afgeboekt.",
|
||||
"shared_part_unknown": "Onbekend onderdeel",
|
||||
"parts_load_failed": "Kon de onderdelen van dit object niet laden — de verbruiksopties zijn nu niet beschikbaar.",
|
||||
"settings_export_selection": "Beperken tot geselecteerde objecten (optioneel)",
|
||||
"settings_docs_archive": "Documentenarchief (met bestanden)",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Wartość wyzwalacza",
|
||||
"complete_title": "Wykonaj: ",
|
||||
"checklist": "Lista kontrolna",
|
||||
"require_on_completion": "Wymagaj przy zakończeniu",
|
||||
"checklist_steps_optional": "Kroki listy kontrolnej (opcjonalne)",
|
||||
"checklist_placeholder": "Wyczyść filtr\nWymień uszczelkę\nSprawdź ciśnienie",
|
||||
"checklist_help": "Jeden krok na linię. Maks. 100 elementów.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Puste = pokaż wszystkie statusy.",
|
||||
"card_filter_objects": "Filtruj wg obiektów",
|
||||
"card_filter_objects_help": "Puste = pokaż wszystkie obiekty.",
|
||||
"card_filter_areas": "Filtruj wg obszarów",
|
||||
"card_filter_areas_help": "Puste = pokaż wszystkie obszary.",
|
||||
"card_filter_entities": "Filtruj wg encji (entity_ids)",
|
||||
"card_filter_entities_help": "Wybierz encje sensor / binary_sensor z tej integracji. Puste = wszystkie.",
|
||||
"card_loading_objects": "Ładowanie obiektów…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Tytuł panelu bocznego",
|
||||
"settings_notifications": "Powiadomienia",
|
||||
"settings_notify_service": "Usługa powiadomień",
|
||||
"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",
|
||||
"send_test": "Wyślij test",
|
||||
"testing": "Wysyłanie…",
|
||||
"test_notification_success": "Powiadomienie testowe wysłane",
|
||||
"test_notification_failed": "Powiadomienie testowe nie powiodło się",
|
||||
"notify_per_person": "Dostarczanie dla każdej osoby",
|
||||
"notify_no_own_device": "Brak własnego urządzenia — używa usługi domowej",
|
||||
"settings_notify_due_soon": "Powiadom gdy wkrótce",
|
||||
"settings_notify_overdue": "Powiadom gdy zaległe",
|
||||
"settings_notify_triggered": "Powiadom gdy wyzwolone",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Wg użytkownika",
|
||||
"filter_label": "Filtr",
|
||||
"user_label": "Użytkownik",
|
||||
"photo_label": "Zdjęcie",
|
||||
"sort_label": "Sortowanie",
|
||||
"group_by_label": "Grupuj wg",
|
||||
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Koryguj stan",
|
||||
"restock_quantity_label": "Kupiona ilość",
|
||||
"consumes_parts_label": "Zużywa części",
|
||||
"shared_parts_other_objects": "Części z innych obiektów",
|
||||
"shared_parts_help": "Kilka obiektów może korzystać z jednego zapasu. Wykonanie tego zadania odejmuje ze stanu obiektu będącego właścicielem.",
|
||||
"shared_part_unknown": "Nieznana część",
|
||||
"parts_load_failed": "Nie udało się wczytać części tego obiektu — opcje zużycia części są teraz niedostępne.",
|
||||
"settings_export_selection": "Ogranicz do wybranych obiektów (opcjonalnie)",
|
||||
"settings_docs_archive": "Archiwum dokumentów (z plikami)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Valor do gatilho",
|
||||
"complete_title": "Concluir: ",
|
||||
"checklist": "Checklist",
|
||||
"require_on_completion": "Exigir ao concluir",
|
||||
"checklist_steps_optional": "Etapas do checklist (opcional)",
|
||||
"checklist_placeholder": "Limpar o filtro\nTrocar a vedação\nTestar a pressão",
|
||||
"checklist_help": "Uma etapa por linha. Máx. 100 itens.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Vazio = mostrar todos os status.",
|
||||
"card_filter_objects": "Filtrar por objetos",
|
||||
"card_filter_objects_help": "Vazio = mostrar todos os objetos.",
|
||||
"card_filter_areas": "Filtrar por áreas",
|
||||
"card_filter_areas_help": "Vazio = mostrar todas as áreas.",
|
||||
"card_filter_entities": "Filtrar por entidades (entity_ids)",
|
||||
"card_filter_entities_help": "Escolha entidades sensor / binary_sensor desta integração. Vazio = todas.",
|
||||
"card_loading_objects": "Carregando objetos…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Título do painel na barra lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"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",
|
||||
"send_test": "Enviar teste",
|
||||
"testing": "Enviando…",
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando vencer em breve",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
"settings_notify_triggered": "Notificar quando acionada",
|
||||
@@ -494,6 +501,7 @@
|
||||
"groupby_user": "Por usuário",
|
||||
"filter_label": "Filtro",
|
||||
"user_label": "Usuário",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Ordenar",
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
|
||||
@@ -728,6 +736,9 @@
|
||||
"part_restock": "Ajustar estoque",
|
||||
"restock_quantity_label": "Quantidade comprada",
|
||||
"consumes_parts_label": "Consome peças",
|
||||
"shared_parts_other_objects": "Peças de outros objetos",
|
||||
"shared_parts_help": "Vários objetos podem compartilhar o mesmo estoque. Concluir esta tarefa desconta do objeto proprietário.",
|
||||
"shared_part_unknown": "Peça desconhecida",
|
||||
"parts_load_failed": "Não foi possível carregar as peças deste objeto — as opções de consumo de peças estão indisponíveis no momento.",
|
||||
"adopt_problem_button": "Adotar sensores de problema",
|
||||
"adopt_problem_title": "Adotar sensores de problema",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Valor do acionador",
|
||||
"complete_title": "Concluída: ",
|
||||
"checklist": "Lista de verificação",
|
||||
"require_on_completion": "Exigir ao concluir",
|
||||
"checklist_steps_optional": "Passos da lista de verificação (opcional)",
|
||||
"checklist_placeholder": "Limpar filtro\nSubstituir vedação\nTestar pressão",
|
||||
"checklist_help": "Um passo por linha. Máx. 100 itens.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Vazio = mostrar todos os estados.",
|
||||
"card_filter_objects": "Filtrar por objetos",
|
||||
"card_filter_objects_help": "Vazio = mostrar todos os objetos.",
|
||||
"card_filter_areas": "Filtrar por áreas",
|
||||
"card_filter_areas_help": "Vazio = mostrar todas as áreas.",
|
||||
"card_filter_entities": "Filtrar por entidades (entity_ids)",
|
||||
"card_filter_entities_help": "Selecione entidades sensor / binary_sensor desta integração. Vazio = todas.",
|
||||
"card_loading_objects": "A carregar objetos…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Título do painel lateral",
|
||||
"settings_notifications": "Notificações",
|
||||
"settings_notify_service": "Serviço de notificação",
|
||||
"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",
|
||||
"send_test": "Enviar teste",
|
||||
"testing": "A enviar…",
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando próxima",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
"settings_notify_triggered": "Notificar quando acionada",
|
||||
@@ -483,6 +490,7 @@
|
||||
"sort_group": "Grupo",
|
||||
"filter_label": "Filtro",
|
||||
"user_label": "Utilizador",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Ordenar",
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Ajustar estoque",
|
||||
"restock_quantity_label": "Quantidade comprada",
|
||||
"consumes_parts_label": "Consome peças",
|
||||
"shared_parts_other_objects": "Peças de outros objetos",
|
||||
"shared_parts_help": "Vários objetos podem partilhar o mesmo stock. Concluir esta tarefa desconta do objeto proprietário.",
|
||||
"shared_part_unknown": "Peça desconhecida",
|
||||
"parts_load_failed": "Não foi possível carregar as peças deste objeto — as opções de consumo de peças estão indisponíveis.",
|
||||
"settings_export_selection": "Limitar aos objetos selecionados (opcional)",
|
||||
"settings_docs_archive": "Arquivo de documentos (com ficheiros)",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Значение триггера",
|
||||
"complete_title": "Выполнить: ",
|
||||
"checklist": "Контрольный список",
|
||||
"require_on_completion": "Требовать при завершении",
|
||||
"checklist_steps_optional": "Шаги контрольного списка (необязательно)",
|
||||
"checklist_placeholder": "Очистить фильтр\nЗаменить уплотнитель\nПроверить давление",
|
||||
"checklist_help": "Один шаг на строку. Макс. 100 элементов.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Пусто = показать все статусы.",
|
||||
"card_filter_objects": "Фильтровать по объектам",
|
||||
"card_filter_objects_help": "Пусто = показать все объекты.",
|
||||
"card_filter_areas": "Фильтровать по зонам",
|
||||
"card_filter_areas_help": "Пусто = показать все зоны.",
|
||||
"card_filter_entities": "Фильтровать по сущностям (entity_ids)",
|
||||
"card_filter_entities_help": "Выберите сущности sensor / binary_sensor из этой интеграции. Пусто = все.",
|
||||
"card_loading_objects": "Загрузка объектов…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Заголовок панели",
|
||||
"settings_notifications": "Уведомления",
|
||||
"settings_notify_service": "Сервис уведомлений",
|
||||
"settings_install_assist_sentences": "Установить фразы Assist",
|
||||
"settings_install_assist_sentences_hint": "Копирует голосовые фразы в вашу конфигурацию, чтобы классический агент Assist их распознавал. Файл, который вы изменили, никогда не перезаписывается.",
|
||||
"test_notification": "Тестовое уведомление",
|
||||
"send_test": "Отправить тест",
|
||||
"testing": "Отправка…",
|
||||
"test_notification_success": "Тестовое уведомление отправлено",
|
||||
"test_notification_failed": "Не удалось отправить тестовое уведомление",
|
||||
"notify_per_person": "Доставка по пользователям",
|
||||
"notify_no_own_device": "Нет своего устройства — используется общий сервис",
|
||||
"settings_notify_due_soon": "Уведомлять, когда срок скоро истекает",
|
||||
"settings_notify_overdue": "Уведомлять при просрочке",
|
||||
"settings_notify_triggered": "Уведомлять при срабатывании",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "По пользователю",
|
||||
"filter_label": "Фильтр",
|
||||
"user_label": "Пользователь",
|
||||
"photo_label": "Фото",
|
||||
"sort_label": "Сортировка",
|
||||
"group_by_label": "Группировать по",
|
||||
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Изменить запас",
|
||||
"restock_quantity_label": "Куплено, шт.",
|
||||
"consumes_parts_label": "Расходует детали",
|
||||
"shared_parts_other_objects": "Детали других объектов",
|
||||
"shared_parts_help": "Несколько объектов могут использовать один запас. Выполнение этой задачи списывает детали у объекта-владельца.",
|
||||
"shared_part_unknown": "Неизвестная деталь",
|
||||
"parts_load_failed": "Не удалось загрузить запчасти этого объекта — параметры расхода запчастей сейчас недоступны.",
|
||||
"settings_export_selection": "Ограничить выбранными объектами (необязательно)",
|
||||
"settings_docs_archive": "Архив документов (с файлами)",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Utlösarvärde",
|
||||
"complete_title": "Slutför: ",
|
||||
"checklist": "Checklista",
|
||||
"require_on_completion": "Kräv vid slutförande",
|
||||
"checklist_steps_optional": "Checkliststeg (valfritt)",
|
||||
"checklist_placeholder": "Rengör filter\nByt tätning\nTesta tryck",
|
||||
"checklist_help": "Ett steg per rad. Max 100 objekt.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Tomt = visa alla statusar.",
|
||||
"card_filter_objects": "Filtrera efter objekt",
|
||||
"card_filter_objects_help": "Tomt = visa alla objekt.",
|
||||
"card_filter_areas": "Filtrera efter områden",
|
||||
"card_filter_areas_help": "Tomt = visa alla områden.",
|
||||
"card_filter_entities": "Filtrera efter entiteter (entity_ids)",
|
||||
"card_filter_entities_help": "Välj sensor- / binary_sensor-entiteter från denna integration. Tomt = alla.",
|
||||
"card_loading_objects": "Laddar objekt…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Sidopanelens titel",
|
||||
"settings_notifications": "Notifikationer",
|
||||
"settings_notify_service": "Notifikationstjänst",
|
||||
"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",
|
||||
"send_test": "Skicka test",
|
||||
"testing": "Skickar…",
|
||||
"test_notification_success": "Testnotifikation skickad",
|
||||
"test_notification_failed": "Testnotifikation misslyckades",
|
||||
"notify_per_person": "Leverans per person",
|
||||
"notify_no_own_device": "Ingen egen enhet — använder hushållets tjänst",
|
||||
"settings_notify_due_soon": "Notifiera när snart förfallande",
|
||||
"settings_notify_overdue": "Notifiera när försenad",
|
||||
"settings_notify_triggered": "Notifiera när utlöst",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "Per användare",
|
||||
"filter_label": "Filter",
|
||||
"user_label": "Användare",
|
||||
"photo_label": "Foto",
|
||||
"sort_label": "Sortering",
|
||||
"group_by_label": "Gruppera efter",
|
||||
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Justera lager",
|
||||
"restock_quantity_label": "Köpt antal",
|
||||
"consumes_parts_label": "Förbrukar delar",
|
||||
"shared_parts_other_objects": "Delar från andra objekt",
|
||||
"shared_parts_help": "Flera objekt kan dela samma lager. När uppgiften slutförs dras det från det ägande objektet.",
|
||||
"shared_part_unknown": "Okänd del",
|
||||
"parts_load_failed": "Kunde inte läsa in objektets delar — förbrukningsalternativen är inte tillgängliga just nu.",
|
||||
"settings_export_selection": "Begränsa till valda objekt (valfritt)",
|
||||
"settings_docs_archive": "Dokumentarkiv (med filer)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "Tetikleme değeri",
|
||||
"complete_title": "Tamamla: ",
|
||||
"checklist": "Kontrol listesi",
|
||||
"require_on_completion": "Tamamlarken zorunlu tut",
|
||||
"checklist_steps_optional": "Kontrol listesi adımları (isteğe bağlı)",
|
||||
"checklist_placeholder": "Filtreyi temizle\nContayı değiştir\nBasıncı test et",
|
||||
"checklist_help": "Her satıra bir adım. En fazla 100 madde.",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "Boş = tüm durumları göster.",
|
||||
"card_filter_objects": "Nesnelere göre filtrele",
|
||||
"card_filter_objects_help": "Boş = tüm nesneleri göster.",
|
||||
"card_filter_areas": "Alanlara göre filtrele",
|
||||
"card_filter_areas_help": "Boş = tüm alanları göster.",
|
||||
"card_filter_entities": "Varlıklara göre filtrele (entity_id)",
|
||||
"card_filter_entities_help": "Bu entegrasyonun sensor / binary_sensor varlıklarını seçin. Boş = tümü.",
|
||||
"card_loading_objects": "Nesneler yükleniyor…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "Kenar çubuğu paneli başlığı",
|
||||
"settings_notifications": "Bildirimler",
|
||||
"settings_notify_service": "Bildirim servisi",
|
||||
"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",
|
||||
"send_test": "Test gönder",
|
||||
"testing": "Gönderiliyor…",
|
||||
"test_notification_success": "Test bildirimi gönderildi",
|
||||
"test_notification_failed": "Test bildirimi başarısız oldu",
|
||||
"notify_per_person": "Kişi bazında iletim",
|
||||
"notify_no_own_device": "Kendi cihazı yok — ev servisini kullanır",
|
||||
"settings_notify_due_soon": "Vade yaklaştığında bildir",
|
||||
"settings_notify_overdue": "Geciktiğinde bildir",
|
||||
"settings_notify_triggered": "Tetiklendiğinde bildir",
|
||||
@@ -494,6 +501,7 @@
|
||||
"groupby_user": "Kullanıcıya göre",
|
||||
"filter_label": "Filtre",
|
||||
"user_label": "Kullanıcı",
|
||||
"photo_label": "Fotoğraf",
|
||||
"sort_label": "Sırala",
|
||||
"group_by_label": "Grupla",
|
||||
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
|
||||
@@ -728,6 +736,9 @@
|
||||
"part_restock": "Stoku ayarla",
|
||||
"restock_quantity_label": "Satın alınan miktar",
|
||||
"consumes_parts_label": "Parça tüketir",
|
||||
"shared_parts_other_objects": "Diğer nesnelerin parçaları",
|
||||
"shared_parts_help": "Birden fazla nesne aynı stoğu paylaşabilir. Bu görev tamamlandığında stok, sahibi olan nesneden düşülür.",
|
||||
"shared_part_unknown": "Bilinmeyen parça",
|
||||
"parts_load_failed": "Bu nesnenin parçaları yüklenemedi — parça tüketimi seçenekleri şu anda kullanılamıyor.",
|
||||
"adopt_problem_button": "Sorun sensörlerini devral",
|
||||
"adopt_problem_title": "Sorun sensörlerini devral",
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"trigger_val": "Значення тригера",
|
||||
"complete_title": "Виконати: ",
|
||||
"checklist": "Чекліст",
|
||||
"require_on_completion": "Вимагати при завершенні",
|
||||
"checklist_steps_optional": "Кроки чекліста (необов'язково)",
|
||||
"checklist_placeholder": "Очистити фільтр\nЗамінити ущільнювач\nПеревірити тиск",
|
||||
"checklist_help": "Один крок на рядок. Макс. 100 елементів.",
|
||||
@@ -362,6 +363,8 @@
|
||||
"card_filter_status_help": "Порожньо = показати всі статуси.",
|
||||
"card_filter_objects": "Фільтрувати за об'єктами",
|
||||
"card_filter_objects_help": "Порожньо = показати всі об'єкти.",
|
||||
"card_filter_areas": "Фільтрувати за зонами",
|
||||
"card_filter_areas_help": "Порожньо = показати всі зони.",
|
||||
"card_filter_entities": "Фільтрувати за сутностями (entity_ids)",
|
||||
"card_filter_entities_help": "Виберіть сутності sensor / binary_sensor з цієї інтеграції. Порожньо = всі.",
|
||||
"card_loading_objects": "Завантаження об'єктів…",
|
||||
@@ -431,11 +434,15 @@
|
||||
"settings_panel_title": "Заголовок панелі",
|
||||
"settings_notifications": "Сповіщення",
|
||||
"settings_notify_service": "Служба сповіщень",
|
||||
"settings_install_assist_sentences": "Встановити фрази Assist",
|
||||
"settings_install_assist_sentences_hint": "Копіює голосові фрази до вашої конфігурації, щоб класичний агент Assist їх розпізнавав. Файл, який ви редагували, ніколи не перезаписується.",
|
||||
"test_notification": "Тестове сповіщення",
|
||||
"send_test": "Надіслати тест",
|
||||
"testing": "Надсилання…",
|
||||
"test_notification_success": "Тестове сповіщення надіслано",
|
||||
"test_notification_failed": "Не вдалося надіслати тестове сповіщення",
|
||||
"notify_per_person": "Доставка за особами",
|
||||
"notify_no_own_device": "Немає власного пристрою — використовується загальна служба",
|
||||
"settings_notify_due_soon": "Сповіщати, коли термін наближається",
|
||||
"settings_notify_overdue": "Сповіщати про прострочення",
|
||||
"settings_notify_triggered": "Сповіщати про спрацювання",
|
||||
@@ -487,6 +494,7 @@
|
||||
"groupby_user": "За користувачем",
|
||||
"filter_label": "Фільтр",
|
||||
"user_label": "Користувач",
|
||||
"photo_label": "Фото",
|
||||
"sort_label": "Сортування",
|
||||
"group_by_label": "Групувати за",
|
||||
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "Змінити запас",
|
||||
"restock_quantity_label": "Куплена кількість",
|
||||
"consumes_parts_label": "Витрачає деталі",
|
||||
"shared_parts_other_objects": "Деталі інших об'єктів",
|
||||
"shared_parts_help": "Кілька об'єктів можуть використовувати один запас. Виконання цього завдання списує деталі з об'єкта-власника.",
|
||||
"shared_part_unknown": "Невідома деталь",
|
||||
"parts_load_failed": "Не вдалося завантажити запчастини цього об'єкта — параметри витрати запчастин зараз недоступні.",
|
||||
"settings_export_selection": "Обмежити вибраними об'єктами (необов'язково)",
|
||||
"settings_docs_archive": "Архів документів (з файлами)",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"trigger_val": "触发值",
|
||||
"complete_title": "完成: ",
|
||||
"checklist": "检查清单",
|
||||
"require_on_completion": "完成时必填",
|
||||
"checklist_steps_optional": "检查步骤 (可选)",
|
||||
"checklist_placeholder": "清理过滤器\n更换密封圈\n测试压力",
|
||||
"checklist_help": "每行一个步骤。最多 100 项。",
|
||||
@@ -363,6 +364,8 @@
|
||||
"card_filter_status_help": "留空则显示所有状态。",
|
||||
"card_filter_objects": "按维护项过滤",
|
||||
"card_filter_objects_help": "留空则显示所有维护项。",
|
||||
"card_filter_areas": "按区域过滤",
|
||||
"card_filter_areas_help": "留空则显示所有区域。",
|
||||
"card_filter_entities": "按实体过滤 (entity_ids)",
|
||||
"card_filter_entities_help": "选择该集成的传感器或二进制传感器实体。留空则显示全部。",
|
||||
"card_loading_objects": "正在加载维护项…",
|
||||
@@ -432,11 +435,15 @@
|
||||
"settings_panel_title": "侧边栏面板标题",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notify_service": "通知服务",
|
||||
"settings_install_assist_sentences": "安装 Assist 语句",
|
||||
"settings_install_assist_sentences_hint": "将语音语句复制到你的配置中,让传统 Assist 代理能够识别它们。你自己修改过的文件永远不会被覆盖。",
|
||||
"test_notification": "测试通知",
|
||||
"send_test": "发送测试",
|
||||
"testing": "正在发送…",
|
||||
"test_notification_success": "测试通知已发送",
|
||||
"test_notification_failed": "测试通知发送失败",
|
||||
"notify_per_person": "按成员分别投递",
|
||||
"notify_no_own_device": "无个人设备 — 使用家庭通知服务",
|
||||
"settings_notify_due_soon": "到期前通知提醒",
|
||||
"settings_notify_overdue": "超期后通知提醒",
|
||||
"settings_notify_triggered": "触发时通知提醒",
|
||||
@@ -488,6 +495,7 @@
|
||||
"groupby_user": "按负责人",
|
||||
"filter_label": "过滤器",
|
||||
"user_label": "用户",
|
||||
"photo_label": "照片",
|
||||
"sort_label": "排序",
|
||||
"group_by_label": "分组依据",
|
||||
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
|
||||
@@ -722,6 +730,9 @@
|
||||
"part_restock": "调整库存",
|
||||
"restock_quantity_label": "购买数量",
|
||||
"consumes_parts_label": "消耗配件",
|
||||
"shared_parts_other_objects": "其他设备的配件",
|
||||
"shared_parts_help": "多个设备可以共用同一批库存。完成此任务将从所属设备的库存中扣减。",
|
||||
"shared_part_unknown": "未知配件",
|
||||
"parts_load_failed": "无法加载此对象的配件 — 配件消耗选项暂时不可用。",
|
||||
"settings_export_selection": "仅限所选对象(可选)",
|
||||
"settings_docs_archive": "文档存档(含文件)",
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
type CalendarEvent,
|
||||
} from "./helpers/calendar-bucket";
|
||||
import { calendarStyles } from "./calendar-styles";
|
||||
import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs } from "./styles";
|
||||
import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays } from "./styles";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
MaintenanceObjectResponse,
|
||||
@@ -267,7 +267,7 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
const statusClass = `cal-status-${ev.status}`;
|
||||
const projClass = ev.projected ? "cal-event-projected" : "";
|
||||
const overdueLabel = ev.status === "overdue" && ev.days_until_due != null
|
||||
? ` (${Math.abs(ev.days_until_due)}d ${t("overdue", L).toLowerCase()})`
|
||||
? ` (${formatDueDays(ev.days_until_due, L)})`
|
||||
: "";
|
||||
const recurEvery = ev.projected && ev.interval_days
|
||||
? html`<span class="cal-event-recur">${
|
||||
|
||||
@@ -81,6 +81,18 @@ export class MaintenanceSupporterCardEditor extends LitElement {
|
||||
this._valueChanged("filter_objects", [...current]);
|
||||
}
|
||||
|
||||
private _toggleLabel(label: string, on: boolean): void {
|
||||
const current = new Set(this._config.filter_labels || []);
|
||||
if (on) current.add(label); else current.delete(label);
|
||||
this._valueChanged("filter_labels", [...current]);
|
||||
}
|
||||
|
||||
private _toggleArea(areaId: string, on: boolean): void {
|
||||
const current = new Set(this._config.filter_areas || []);
|
||||
if (on) current.add(areaId); else current.delete(areaId);
|
||||
this._valueChanged("filter_areas", [...current]);
|
||||
}
|
||||
|
||||
private _onEntitiesChanged = (e: CustomEvent<{ value: string[] }>): void => {
|
||||
this._valueChanged("entity_ids", e.detail.value || []);
|
||||
};
|
||||
@@ -92,6 +104,30 @@ export class MaintenanceSupporterCardEditor extends LitElement {
|
||||
const objectNames = [...this._objects]
|
||||
.map((o) => o.object.name)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
// Areas (C8): offer exactly the areas that actually hold an object — an
|
||||
// area with nothing in it could only ever empty the card. Names resolve
|
||||
// through hass.areas; an area HA no longer knows falls back to its raw id
|
||||
// so an existing config stays visible (and un-checkable) instead of
|
||||
// vanishing silently.
|
||||
const selectedAreas = new Set(this._config.filter_areas || []);
|
||||
const areaIds = [
|
||||
...new Set(
|
||||
this._objects
|
||||
.map((o) => o.object.area_id)
|
||||
.filter((a): a is string => !!a)
|
||||
.concat([...selectedAreas]),
|
||||
),
|
||||
];
|
||||
const areaName = (id: string): string => this.hass?.areas?.[id]?.name || id;
|
||||
const areas = areaIds
|
||||
.map((id) => ({ id, name: areaName(id) }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const selectedLabels = new Set(this._config.filter_labels || []);
|
||||
// Labels are free-form per task, so the picker offers exactly the ones in
|
||||
// use — an empty list simply hides the section.
|
||||
const labelNames = [
|
||||
...new Set(this._objects.flatMap((o) => o.tasks.flatMap((tk) => tk.labels || []))),
|
||||
].sort((a, b) => a.localeCompare(b));
|
||||
// Build the list of OUR sensor + binary_sensor entity_ids so the
|
||||
// ha-entities-picker only shows maintenance_supporter entities, not
|
||||
// every sensor in HA.
|
||||
@@ -152,6 +188,38 @@ export class MaintenanceSupporterCardEditor extends LitElement {
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<!-- Area filter (C8): selects whole objects by the room they sit in.
|
||||
Hidden while no object has an area — the section would be an
|
||||
empty box otherwise. -->
|
||||
${areas.length ? html`
|
||||
<div class="field">
|
||||
<div class="field-label">${t("card_filter_areas", L)}</div>
|
||||
<div class="object-list">
|
||||
${areas.map((a) => html`
|
||||
<label class="object-row">
|
||||
<input type="checkbox"
|
||||
.checked=${selectedAreas.has(a.id)}
|
||||
@change=${(e: Event) => this._toggleArea(a.id, (e.target as HTMLInputElement).checked)} />
|
||||
<span>${a.name}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
<div class="field-help">${t("card_filter_areas_help", L)}</div>
|
||||
</div>` : nothing}
|
||||
${labelNames.length ? html`
|
||||
<div class="field">
|
||||
<div class="field-label">${t("labels", L)}</div>
|
||||
<div class="object-list">
|
||||
${labelNames.map((name) => html`
|
||||
<label class="object-row">
|
||||
<input type="checkbox"
|
||||
.checked=${selectedLabels.has(name)}
|
||||
@change=${(e: Event) => this._toggleLabel(name, (e.target as HTMLInputElement).checked)} />
|
||||
<span>${name}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>` : nothing}
|
||||
|
||||
<!-- Entity-id filter (HA-native pattern). Limited to our integration's
|
||||
sensor + binary_sensor entities via includeEntities so the picker
|
||||
@@ -219,6 +287,14 @@ export class MaintenanceSupporterCardEditor extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-formfield label="${t("documents", L)}">
|
||||
<ha-switch
|
||||
.checked=${this._config.show_documents !== false}
|
||||
@change=${(e: Event) =>
|
||||
this._valueChanged("show_documents", (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-formfield label="${t("card_compact", L)}">
|
||||
<ha-switch
|
||||
.checked=${this._config.compact || false}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs } from "./styles";
|
||||
import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setDateTimePrefs, formatDueDays } from "./styles";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
MaintenanceObjectResponse,
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
SavedViewFilters,
|
||||
} from "./types";
|
||||
import { UserService } from "./user-service";
|
||||
import { partsForCompletion } from "./helpers/shared-parts";
|
||||
import "./maintenance-card-editor";
|
||||
import "./components/complete-dialog";
|
||||
import {
|
||||
@@ -21,6 +22,13 @@ import {
|
||||
openTaskQuickActions,
|
||||
} from "./dialog-mount";
|
||||
|
||||
interface CardDoc {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: string;
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
interface FlatTask {
|
||||
entry_id: string;
|
||||
object_name: string;
|
||||
@@ -39,6 +47,9 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
@state() private _userNames: Record<string, string> = {};
|
||||
private _userService: UserService | null = null;
|
||||
private _userNamesLoaded = false;
|
||||
/** entry_id → (task_id → documents) for the row chips. */
|
||||
@state() private _taskDocs: Record<string, Record<string, CardDoc[]>> = {};
|
||||
private _docsLoadedFor = new Set<string>();
|
||||
|
||||
private get _lang(): string {
|
||||
return this.hass?.language || "en";
|
||||
@@ -114,6 +125,15 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
) {
|
||||
this._loadUserNames();
|
||||
}
|
||||
// Documents: same shape of guard — fetch per object, once, and only for
|
||||
// objects that actually have a document attached to one of their tasks.
|
||||
if (this.hass && this._config.show_documents !== false) {
|
||||
for (const obj of this._objects) {
|
||||
if (this._docsLoadedFor.has(obj.entry_id)) continue;
|
||||
if (!obj.tasks.some((tk) => (tk.document_count ?? 0) > 0)) continue;
|
||||
this._loadDocuments(obj.entry_id);
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
if (!this._dataLoaded) {
|
||||
this._dataLoaded = true;
|
||||
@@ -146,12 +166,6 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
await this._loadViewFilters();
|
||||
}
|
||||
|
||||
/** Resolve display names for the assignee badge (best-effort).
|
||||
*
|
||||
* `users/list` is a READ-tier command, so the household members this card
|
||||
* is built for can call it without admin rights. A failure (or a task
|
||||
* whose user was deleted) leaves the name unresolved and the badge simply
|
||||
* does not render — never a raw user id. */
|
||||
/** Display name of the task's responsible user, or "" when the badge must
|
||||
* stay hidden (feature off, nobody assigned, or the name not resolved).
|
||||
* With a rotation this is whoever is up next — the pointer the engine
|
||||
@@ -163,6 +177,12 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
return this._userNames[id] || "";
|
||||
}
|
||||
|
||||
/** Resolve display names for the assignee badge (best-effort).
|
||||
*
|
||||
* `users/list` is a READ-tier command, so the household members this card
|
||||
* is built for can call it without admin rights. A failure (or a task
|
||||
* whose user was deleted) leaves the name unresolved and the badge simply
|
||||
* does not render — never a raw user id. */
|
||||
private async _loadUserNames(): Promise<void> {
|
||||
this._userNamesLoaded = true;
|
||||
if (!this._userService) this._userService = new UserService(this.hass);
|
||||
@@ -175,6 +195,62 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch one object's documents and index them by task.
|
||||
*
|
||||
* `documents/list` is READ tier, like `users/list` — the household members
|
||||
* this card is for may call it. A failure leaves the row without chips
|
||||
* rather than breaking the card. */
|
||||
private async _loadDocuments(entryId: string): Promise<void> {
|
||||
this._docsLoadedFor.add(entryId);
|
||||
try {
|
||||
const res = (await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/documents/list",
|
||||
entry_id: entryId,
|
||||
})) as { documents: Array<CardDoc & { task_ids?: string[] }> };
|
||||
const byTask: Record<string, CardDoc[]> = {};
|
||||
for (const doc of res.documents || []) {
|
||||
for (const taskId of doc.task_ids || []) {
|
||||
(byTask[taskId] ||= []).push({ id: doc.id, title: doc.title, kind: doc.kind, url: doc.url });
|
||||
}
|
||||
}
|
||||
this._taskDocs = { ...this._taskDocs, [entryId]: byTask };
|
||||
} catch {
|
||||
// no chips for this object; the card is unaffected otherwise
|
||||
}
|
||||
}
|
||||
|
||||
/** Chips to render on a row: linked documents plus the task's own manual
|
||||
* link, which is the same "the manual is one tap away" affordance. */
|
||||
private _docsFor(entryId: string, task: MaintenanceTask): CardDoc[] {
|
||||
if (this._config.show_documents === false) return [];
|
||||
const linked = this._taskDocs[entryId]?.[task.id] || [];
|
||||
const out = [...linked];
|
||||
if (task.documentation_url) {
|
||||
out.push({ id: `url:${task.id}`, title: t("documentation_label", this._lang), kind: "weblink", url: task.documentation_url });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Open a chip: a web link directly, a stored file through a signed path
|
||||
* (the same route the panel uses, so it works in the Companion app). */
|
||||
private async _openDoc(doc: CardDoc): Promise<void> {
|
||||
if (doc.kind === "weblink" && doc.url) {
|
||||
window.open(doc.url, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
const win = window.open("about:blank", "_blank");
|
||||
try {
|
||||
const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({
|
||||
type: "auth/sign_path",
|
||||
path: `/api/maintenance_supporter/document/${doc.id}`,
|
||||
expires: 300,
|
||||
});
|
||||
if (win) win.location.href = new URL(signed.path, window.location.origin).href;
|
||||
} catch {
|
||||
if (win) win.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the configured saved view's filters (best-effort). A missing or
|
||||
* deleted view degrades to "no view filter" — same fallback semantics as
|
||||
* the backend's notification routing, never an inexplicably empty card. */
|
||||
@@ -222,6 +298,8 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
entity_ids,
|
||||
filter_due_min_days,
|
||||
filter_due_max_days,
|
||||
filter_labels,
|
||||
filter_areas,
|
||||
max_items,
|
||||
} = this._config;
|
||||
const entityFilter = entity_ids?.length ? new Set(entity_ids) : null;
|
||||
@@ -238,6 +316,13 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
|
||||
for (const obj of this._objects) {
|
||||
if (filter_objects?.length && !filter_objects.includes(obj.object.name)) continue;
|
||||
// Areas (C8): object-level, so it selects whole objects like
|
||||
// filter_objects does — "the tasks for this room". An object with no
|
||||
// area_id can never satisfy a non-empty list.
|
||||
if (filter_areas?.length) {
|
||||
const areaId = obj.object.area_id;
|
||||
if (!areaId || !filter_areas.includes(areaId)) continue;
|
||||
}
|
||||
for (const task of obj.tasks) {
|
||||
// Completed one-time tasks ("done") are hidden from the active list.
|
||||
if (task.is_done) continue;
|
||||
@@ -245,6 +330,8 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
// never shown on the Lovelace card.
|
||||
if (task.archived || obj.object.archived) continue;
|
||||
if (filter_status?.length && !filter_status.includes(task.status)) continue;
|
||||
// Labels: a task passes when it carries at least one configured label.
|
||||
if (filter_labels?.length && !(task.labels || []).some((lb) => filter_labels.includes(lb))) continue;
|
||||
// entity_ids: HA-native filter — match the task's sensor or
|
||||
// binary_sensor entity_id. Both fields come pre-resolved from the
|
||||
// backend WS response (see _build_task_summary in websocket/__init__.py).
|
||||
@@ -384,13 +471,26 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._docsFor(entry_id, task).length
|
||||
? html`<div class="doc-chips">
|
||||
${this._docsFor(entry_id, task).map((doc) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="doc-chip"
|
||||
title="${doc.title}"
|
||||
@click=${(e: Event) => { e.stopPropagation(); void this._openDoc(doc); }}
|
||||
>
|
||||
<ha-icon icon=${doc.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline"}></ha-icon>
|
||||
<span>${doc.title}</span>
|
||||
</button>
|
||||
`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="task-due">
|
||||
${task.days_until_due !== null && task.days_until_due !== undefined
|
||||
? task.days_until_due < 0
|
||||
? html`<span class="overdue-text">${Math.abs(task.days_until_due)}${L.startsWith("de") ? "T" : "d"}</span>`
|
||||
: task.days_until_due === 0
|
||||
? t("today", L)
|
||||
: `${task.days_until_due}${L.startsWith("de") ? "T" : "d"}`
|
||||
? html`<span class="overdue-text">${formatDueDays(task.days_until_due, L)}</span>`
|
||||
: formatDueDays(task.days_until_due, L)
|
||||
: task.trigger_active
|
||||
? "⚡"
|
||||
: "—"}
|
||||
@@ -411,13 +511,17 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
|
||||
dlg.taskType = task.type || "";
|
||||
dlg.readingUnit = (task as any).reading_unit || "";
|
||||
dlg.requiredFields = task.required_completion_fields || [];
|
||||
dlg.lang = L;
|
||||
// #99: editable per-completion parts selection
|
||||
// (skip on buy tasks — those restock instead).
|
||||
const obj = this._objects.find((o) => o.entry_id === entry_id);
|
||||
// #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;
|
||||
dlg.parts = isBuy ? [] : (obj?.parts || []);
|
||||
dlg.consumesParts = isBuy ? [] : ((task as any).consumes_parts || []);
|
||||
dlg.parts = isBuy ? [] : partsForCompletion(task, entry_id, this._objects, L);
|
||||
dlg.consumesParts = isBuy ? [] : (task.consumes_parts || []);
|
||||
dlg.open();
|
||||
}}
|
||||
>
|
||||
@@ -534,7 +638,37 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.task-due { font-size: 13px; color: var(--secondary-text-color); min-width: 40px; text-align: right; }
|
||||
.doc-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-right: 6px;
|
||||
max-width: 45%;
|
||||
}
|
||||
.doc-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
max-width: 14ch;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--divider-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
background: none;
|
||||
color: var(--secondary-text-color);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.doc-chip:hover { color: var(--primary-color); border-color: var(--primary-color); }
|
||||
.doc-chip ha-icon { --mdc-icon-size: 12px; width: 12px; height: 12px; }
|
||||
/* nowrap: the due label is localized via formatDueDays ("5 d overdue",
|
||||
"5 T überfällig") — without it a narrow phone card wraps that onto a
|
||||
second line and the row grows taller. The name column ellipsizes
|
||||
instead, which it already does by design. */
|
||||
.task-due { font-size: 13px; color: var(--secondary-text-color); min-width: 40px; text-align: right; white-space: nowrap; }
|
||||
.overdue-text { color: var(--error-color); font-weight: 500; }
|
||||
|
||||
.complete-btn {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { warrantyStatus } from "./helpers/warranty";
|
||||
import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns";
|
||||
import { downloadTextFile } from "./helpers/download";
|
||||
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
|
||||
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
|
||||
import { describeWsError } from "./ws-errors";
|
||||
import { panelStyles } from "./panel-styles";
|
||||
import type {
|
||||
@@ -1759,17 +1760,13 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
statusLabel: (st: string) => t(st, L),
|
||||
parts: t("consumes_parts_label", L),
|
||||
};
|
||||
// Required parts as checkable lines: qty × name (stock unit) — location.
|
||||
const wsParts = obj.parts || [];
|
||||
const partsLines = (task.consumes_parts || [])
|
||||
.map((link) => {
|
||||
const pt = wsParts.find((x) => x.id === link.part_id);
|
||||
if (!pt) return "";
|
||||
const stock = pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : "";
|
||||
const loc = pt.storage_location ? ` — ${pt.storage_location}` : "";
|
||||
return `${link.quantity}× ${pt.name}${stock}${loc}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
// Required parts as checkable lines: qty × name (owner) (stock unit) —
|
||||
// location. A pool owned by another object (#111) names that object, and
|
||||
// a link that resolves to nothing prints "Unknown part" rather than the
|
||||
// blank line the old own-parts-only lookup produced.
|
||||
const partsLines = (task.consumes_parts || []).map((link) =>
|
||||
describePartLink(link, obj.entry_id, this._objects, L),
|
||||
);
|
||||
const html = buildTaskWorksheetHtml(
|
||||
task, obj.object.name, labels,
|
||||
(iso) => formatDate(iso, L),
|
||||
@@ -1804,24 +1801,22 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
?.tasks.find((tsk) => tsk.id === taskId);
|
||||
dlg.taskType = tk?.type || "";
|
||||
dlg.readingUnit = tk?.reading_unit || "";
|
||||
dlg.requiredFields = tk?.required_completion_fields || [];
|
||||
// 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 partById = new Map(objParts.map((pt) => [pt.id, pt]));
|
||||
const refPart = tk?.part_ref ? partById.get(tk.part_ref.part_id) : undefined;
|
||||
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;
|
||||
dlg.consumesInfo = (tk?.consumes_parts || [])
|
||||
.map((link) => {
|
||||
const pt = partById.get(link.part_id);
|
||||
if (!pt) return "";
|
||||
const loc = pt.storage_location ? ` — ${pt.storage_location}` : "";
|
||||
const stock = pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : "";
|
||||
return `${link.quantity}× ${pt.name}${stock}${loc}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
// #111: a link may point at another object's pool — name that object, and
|
||||
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
|
||||
dlg.consumesInfo = (tk?.consumes_parts || []).map((link) =>
|
||||
describePartLink(link, entryId, this._objects, this._lang),
|
||||
);
|
||||
// #99: editable per-completion parts selection (not on buy tasks — those
|
||||
// RESTOCK via the qty field instead of consuming).
|
||||
dlg.parts = tk?.part_ref ? [] : objParts;
|
||||
// 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(tk, entryId, this._objects, this._lang);
|
||||
dlg.consumesParts = tk?.part_ref ? [] : (tk?.consumes_parts || []);
|
||||
dlg.open();
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ export interface MaintenanceTask {
|
||||
labels?: string[];
|
||||
assignee_pool?: string[];
|
||||
rotation_strategy?: string | null;
|
||||
/** Details this task demands on completion (v2.44): notes/cost/duration/photo/user. */
|
||||
required_completion_fields?: string[];
|
||||
earliest_completion_days?: number | null;
|
||||
// v1.3.0: completion-action + quick-complete (gated by completion_actions feature)
|
||||
on_complete_action?: {
|
||||
@@ -236,11 +238,25 @@ export interface MaintenanceTask {
|
||||
sensor_entity_id?: string | null;
|
||||
binary_sensor_entity_id?: string | null;
|
||||
/** Spare parts consumed by completing this task. */
|
||||
consumes_parts?: Array<{ part_id: string; quantity: number }> | null;
|
||||
consumes_parts?: TaskPartLink[] | null;
|
||||
/** Present on an auto-created "buy" reminder: the owning part. */
|
||||
part_ref?: { part_id: string } | null;
|
||||
}
|
||||
|
||||
/** One entry of a task's `consumes_parts`.
|
||||
*
|
||||
* `entry_id` ABSENT = the part belongs to the task's own object. That is what
|
||||
* every link written before #111 looks like and what is still written for own
|
||||
* parts — nothing emits an entry_id for them. PRESENT = the task draws on a
|
||||
* stock pool owned by that other object (three vacuums, one box of dust bags),
|
||||
* and completing the task decrements the other object's stock.
|
||||
*/
|
||||
export interface TaskPartLink {
|
||||
part_id: string;
|
||||
quantity: number;
|
||||
entry_id?: string;
|
||||
}
|
||||
|
||||
/** A spare part / consumable on an object (full definition + derived state). */
|
||||
export interface MaintenancePart {
|
||||
id: string;
|
||||
@@ -328,6 +344,18 @@ export interface CardConfig {
|
||||
// Defaults to ON — with rotations the row is the only place a household
|
||||
// sees who is up next. Rows without an assignee simply render nothing.
|
||||
show_assignee?: boolean;
|
||||
// Labels to limit the card to (v2.44). A task passes when it carries at
|
||||
// least one of them — same OR semantics as filter_status / filter_objects.
|
||||
filter_labels?: string[];
|
||||
// HA area ids to limit the card to (C8). A task passes when its parent
|
||||
// OBJECT sits in one of these areas — same OR semantics as filter_objects,
|
||||
// and ANDed with every other filter. Objects without an area never match a
|
||||
// non-empty list. Empty / unset = no area filtering.
|
||||
filter_areas?: string[];
|
||||
// Show the task's linked documents (and its documentation link) as chips
|
||||
// on the row, so the manual is one tap away. Defaults to ON; rows without
|
||||
// a document render nothing extra.
|
||||
show_documents?: boolean;
|
||||
// Saved-view scope (v2.26): apply a saved view's task-selecting filters
|
||||
// (status / user / label) ON TOP of the card's own filters. The view's
|
||||
// sort/group dimensions are panel display state and are not applied here.
|
||||
|
||||
Reference in New Issue
Block a user