361 files

This commit is contained in:
Home Assistant Version Control
2026-10-06 17:11:43 +00:00
parent d2c5af43e3
commit bf886c9347
362 changed files with 22060 additions and 1285 deletions
@@ -14,6 +14,7 @@ from homeassistant.auth import EVENT_USER_REMOVED
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
CoreState,
Event,
HomeAssistant,
ServiceCall,
@@ -1502,6 +1503,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
await todo_mirror.async_setup()
hass.data[DOMAIN][TODO_MIRROR_KEY] = todo_mirror
# 2026-10 places: tell a person what is due where they just arrived —
# same lifecycle as the mirrors (global entry owns it).
from .helpers.places import PLACES_KEY, PlaceArrivals
if PLACES_KEY not in hass.data[DOMAIN]:
place_arrivals = PlaceArrivals(hass)
place_arrivals.async_setup()
hass.data[DOMAIN][PLACES_KEY] = place_arrivals
# One-time migration: auto-enable advanced feature flags for existing users
options = dict(entry.options or entry.data)
if CONF_ADVANCED_ADAPTIVE not in options:
@@ -1727,6 +1737,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
entry.runtime_data = MaintenanceSupporterData(coordinator=coordinator, store=store)
await coordinator.async_config_entry_first_refresh()
# A shopping-list row checked while this object was not loaded (a
# reload after a task edit, a setup retry) waits for it — the resync
# books it now. At startup the pass after "started" covers it.
if hass.state is CoreState.running:
from .shopping_sync import schedule_resync
schedule_resync(hass)
# #148: a battery type that becomes known only after fleet setup (a
# Battery Notes note added later, a typed low-only Matter binary)
# never got its spare part — reconcile once per start, after the
@@ -2118,6 +2136,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
todo_mirror = hass.data.get(DOMAIN, {}).pop(TODO_MIRROR_KEY, None)
if todo_mirror is not None:
todo_mirror.async_teardown()
from .helpers.places import PLACES_KEY
place_arrivals = hass.data.get(DOMAIN, {}).pop(PLACES_KEY, None)
if place_arrivals is not None:
place_arrivals.async_teardown()
# NOT the full-text index: it is shared runtime (built once per boot
# by _async_setup_shared, which a hub reload does not re-run) — a
# reload of the global entry (any settings save) closed it for the
@@ -2,6 +2,7 @@
from __future__ import annotations
import dataclasses
import logging
from datetime import date, datetime, time, timedelta
from typing import TYPE_CHECKING, Any
@@ -700,6 +701,12 @@ class MaintenanceCalendar(CalendarEntity):
else:
live_tasks = {}
# 2026-10 places: an object maintained away from home names its
# place as the events' location (phone calendars show it).
from .helpers.places import object_place, zone_name
place_name = zone_name(self._hass, object_place(entry.data.get(CONF_OBJECT)))
# Merge static (ConfigEntry) + dynamic (Store) task data
tasks_data = merged_tasks(entry)
schedule_time_enabled = self._is_schedule_time_feature_enabled()
@@ -731,6 +738,8 @@ class MaintenanceCalendar(CalendarEntity):
event = self._create_event_for_task(task, obj_name, start_d, end_d, live_status=live.get("_status"))
if event:
if place_name:
event = dataclasses.replace(event, location=place_name)
events.append(event)
return events
@@ -29,6 +29,7 @@ from .const import (
CONF_ADVANCED_COMPLETION_ACTIONS,
CONF_ADVANCED_ENVIRONMENTAL,
CONF_ADVANCED_GROUPS,
CONF_ADVANCED_PLACES,
CONF_ADVANCED_SCHEDULE_TIME,
CONF_ADVANCED_SEASONAL,
CONF_ARCHIVE_ONEOFF_DAYS,
@@ -771,6 +772,10 @@ class GlobalOptionsFlow(OptionsFlow):
CONF_ADVANCED_COMPLETION_ACTIONS,
default=self._opt(CONF_ADVANCED_COMPLETION_ACTIONS),
): selector.BooleanSelector(),
vol.Optional(
CONF_ADVANCED_PLACES,
default=self._opt(CONF_ADVANCED_PLACES),
): selector.BooleanSelector(),
}
),
)
@@ -197,17 +197,14 @@ class TriggerStepsMixin(TriggerConfigMixin):
def _clear_stale_trigger_runtime(self, old_tc: dict[str, Any], new_tc: dict[str, Any]) -> None:
"""Drop persisted trigger runtime when the trigger fundamentally changed.
Mirrors the WS update path (websocket/tasks_crud.py): the Store
runtime wins over config on restore (#102), so an options-flow edit
that changes type/entities/baseline must clear it or the old
The same rule as the WS update path (trigger_runtime_stale): the
Store runtime wins over config on restore (#102), so an options-flow
edit that changes type/entities/baseline must clear it or the old
counters/anchors silently survive the edit (bug audit 2026-08-22).
"""
if (
old_tc.get("type") != new_tc.get("type")
or old_tc.get("entity_id") != new_tc.get("entity_id")
or old_tc.get("entity_ids") != new_tc.get("entity_ids")
or old_tc.get("trigger_baseline_value") != new_tc.get("trigger_baseline_value")
):
from .entity.triggers import trigger_runtime_stale
if trigger_runtime_stale(old_tc, new_tc):
rd = getattr(self.config_entry, "runtime_data", None)
store = getattr(rd, "store", None) if rd else None
if store is not None:
@@ -162,6 +162,9 @@ CONF_ADVANCED_SCHEDULE_TIME = "advanced_schedule_time_visible"
# Print-QR generator. Default OFF — beginners aren't overwhelmed; data
# still persists if a user toggles the flag off again.
CONF_ADVANCED_COMPLETION_ACTIONS = "advanced_completion_actions_visible"
# 2026-10: shows the object's place (an HA zone) and "remind only on site" in
# the panel. Visibility only, like the others: a place set before keeps working.
CONF_ADVANCED_PLACES = "advanced_places_visible"
# Panel-access overrides: HA user IDs who get the full admin panel despite
# not being HA admins. Empty list means only admins see the full panel.
@@ -419,6 +422,10 @@ VENDOR_URL = "/maintenance_supporter_vendor"
CONF_OBJECT = "object"
CONF_OBJECT_NAME = "name"
CONF_OBJECT_AREA = "area_id"
# 2026-10 places: an HA zone the object is maintained at (None = home), and
# whether its reminders wait until somebody is there (helpers/places.py).
CONF_OBJECT_PLACE = "place"
CONF_OBJECT_REMIND_ON_SITE = "remind_on_site"
CONF_OBJECT_MANUFACTURER = "manufacturer"
CONF_OBJECT_MODEL = "model"
CONF_OBJECT_SERIAL_NUMBER = "serial_number"
@@ -838,6 +845,12 @@ SIGNAL_DOCUMENTS_UPDATED = f"{DOMAIN}_documents_updated"
# --- Trigger Completion Cooldown ---
TRIGGER_COMPLETION_COOLDOWN_SECONDS = 600 # 10 minutes
# Places (helpers/places.py): a person must stay this long in a zone before
# the arrival message goes out (a drive past a zone edge is no visit), and
# gets it at most once per this window for the same place.
PLACE_ARRIVAL_DWELL_SECONDS = 120
PLACE_ARRIVAL_COOLDOWN_SECONDS = 6 * 3600
# Household double-complete window: two people tapping Complete on the same
# task within this many seconds count as ONE real-world action (journey M1).
# Short on purpose — a deliberate complete → reset → complete-again correction
@@ -89,6 +89,10 @@ _TRIGGER_EPOCH_TYPES = frozenset(
}
)
# Trigger types whose refresh-time fallback reads the live entity states (the
# others read persisted counters): only a reading may end their episode.
_STATE_READING_TRIGGER_TYPES = frozenset({"threshold", "counter", "due_date"})
def _takes_day_suggestion(schedule: Schedule) -> bool:
"""True when a suggested interval IN DAYS can be applied to the schedule:
@@ -706,6 +710,14 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
task._trigger_current_value = result.current_value
if in_cooldown:
return # the value for display only — no re-activation yet
if result.active is False and trigger_type in _STATE_READING_TRIGGER_TYPES and not self._trigger_has_reading(entity_ids):
# An episode ends on a reading, never on sensors that are all
# offline. The evaluators already give no verdict then; this
# keeps the rule for any evaluator to come: a counter read as
# "not reached" while it was unavailable flipped the task to OK
# and back, and the next restart announced the same activation
# again (live installation 2026-10-04).
return
if result.active is not None:
if task._trigger_active and not result.active:
# A recovery only this sweep saw still ends the episode.
@@ -1959,6 +1971,15 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
task_result["_trigger_current_value"] = value
task_result["_status"] = compute_status_from_task_dict(task_result)
def _trigger_has_reading(self, entity_ids: list[str]) -> bool:
"""Whether at least one of a trigger's entities has a state at all
(exists, and is not unavailable / unknown)."""
for entity_id in entity_ids:
state = self.hass.states.get(entity_id)
if state is not None and state.state not in UNAVAILABLE_STATES:
return True
return False
def note_trigger_cleared(self, task_id: str) -> None:
"""The task's trigger episode ended (every entity back in range).
Persisted, so :meth:`trigger_already_announced` sees the episode
@@ -70,6 +70,28 @@ def primary_entity_id(trigger_config: Any) -> str | None:
return next((eid for eid in normalize_entity_ids(trigger_config) if isinstance(eid, str) and eid), None)
def trigger_runtime_stale(old: Any, new: Any) -> bool:
"""Whether a trigger edit invalidates the task's persisted trigger
runtime (counter baselines, runtime hours, change counts): another type,
other entities or an edited counting start value. The ONE rule for the
WebSocket update and the options flow.
The entities are compared normalized. An old task stores only the legacy
``entity_id``, while every validated edit carries ``entity_ids`` too;
comparing the raw keys read that as "other entities", so the first save
of such a task wiped its baseline or hours even when only the target
changed (live installation 2026-10-04). A compound keeps its runtime per
condition and, as before, only a type change resets it.
"""
old_tc = old if isinstance(old, dict) else {}
new_tc = new if isinstance(new, dict) else {}
if old_tc.get("type") != new_tc.get("type"):
return True
if new_tc.get("type") != TriggerType.COMPOUND and normalize_entity_ids(old_tc) != normalize_entity_ids(new_tc):
return True
return bool(old_tc.get("trigger_baseline_value") != new_tc.get("trigger_baseline_value"))
def _migrate_flat_to_per_entity(config: dict[str, Any], first_entity_id: str) -> dict[str, dict[str, Any]]:
"""Create ``_trigger_state`` from legacy flat keys on first load.
@@ -227,6 +227,13 @@ def _build_export_object(
# gallery) — only emitted when set, like the fleet flag below.
if obj_data.get("template_id"):
export_obj["template_id"] = obj_data["template_id"]
# 2026-10 places — only when set, so an object at home exports as before.
# A zone id is instance-specific: on another instance without that zone
# the object behaves as at home and the panel says the zone is missing.
if obj_data.get("place"):
export_obj["place"] = obj_data["place"]
if obj_data.get("remind_on_site") is True:
export_obj["remind_on_site"] = True
# Battery fleet identity — only emitted for the fleet object so a plain
# object's export stays byte-identical to earlier versions. The importer
# mirrors these (and keeps the deterministic ``batt_<type>`` part ids).
@@ -42,12 +42,13 @@ export interface MockFeatures {
checklists: boolean;
schedule_time: boolean;
completion_actions: boolean;
places: boolean;
}
export const DEFAULT_FEATURES: MockFeatures = {
adaptive: false, seasonal: false,
environmental: false, budget: false, groups: false,
checklists: false, schedule_time: false, completion_actions: false,
checklists: false, schedule_time: false, completion_actions: false, places: false,
};
/**
@@ -69,7 +69,7 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
features: {
adaptive: false, seasonal: false,
environmental: false, budget: false, groups: false,
checklists: false, schedule_time: false, completion_actions: false,
checklists: false, schedule_time: false, completion_actions: false, places: false,
},
currencySymbol: "€",
collapsedSections: new Set(),
@@ -297,7 +297,7 @@ describe("task-detail renderer", () => {
features: {
adaptive: false, seasonal: false,
environmental: false, budget: false, groups: false,
checklists: true, schedule_time: false, completion_actions: false,
checklists: true, schedule_time: false, completion_actions: false, places: false,
},
setChecklistItem: (item, done) => calls.push([item, done]),
}),
@@ -66,7 +66,7 @@ function ctx(overrides: Partial<TaskDetailContext> = {}): TaskDetailContext {
features: {
adaptive: false, seasonal: false,
environmental: false, budget: false, groups: false,
checklists: false, schedule_time: false, completion_actions: false,
checklists: false, schedule_time: false, completion_actions: false, places: false,
},
currencySymbol: "€",
collapsedSections: new Set(),
@@ -21,6 +21,8 @@ export class MaintenanceObjectDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
/** All objects — choices for the parent-object picker (2.19). */
@property({ attribute: false }) public objects: MaintenanceObjectResponse[] = [];
/** 2026-10 places: the advanced feature switch shows the place fields. */
@property({ type: Boolean }) public placesEnabled = false;
@state() private _open = false;
@state() private _loading = false;
@state() private _error = "";
@@ -39,6 +41,9 @@ export class MaintenanceObjectDialog extends LitElement {
// 2.19: attach to an existing HA device / nest under another object
@state() private _haDeviceId = "";
@state() private _parentEntryId = "";
// 2026-10 places: an HA zone ("" = home) and "remind only on site".
@state() private _place = "";
@state() private _remindOnSite = false;
@state() private _entryId: string | null = null; // null = create, string = update
// Replace (journey N1): the successor's device. The old unit's device is
// what the object links to now; a new unit usually is a new device.
@@ -63,6 +68,8 @@ export class MaintenanceObjectDialog extends LitElement {
this._notes = "";
this._haDeviceId = "";
this._parentEntryId = "";
this._place = "";
this._remindOnSite = false;
this._replacing = false;
this._error = "";
this._open = true;
@@ -96,6 +103,8 @@ export class MaintenanceObjectDialog extends LitElement {
this._notes = obj.notes || "";
this._haDeviceId = obj.ha_device_id || "";
this._parentEntryId = obj.parent_entry_id || "";
this._place = obj.place || "";
this._remindOnSite = !!obj.remind_on_site;
this._replacing = false;
this._error = "";
this._open = true;
@@ -194,6 +203,35 @@ export class MaintenanceObjectDialog extends LitElement {
`;
}
/** 2026-10 places: the zone the object is maintained at ("" = home) and
* whether its reminders wait until somebody is there. */
private _renderPlace(L: string) {
const missing = !!this._place && !this.hass?.states?.[this._place];
return html`
<ha-form
.hass=${this.hass}
.data=${{ place: this._place || undefined }}
.schema=${[{ name: "place", selector: { entity: { domain: "zone" } } }]}
.computeLabel=${() => t("place_optional", L)}
@value-changed=${(e: CustomEvent) => {
this._place = ((e.detail.value as { place?: string })?.place as string) || "";
if (!this._place) this._remindOnSite = false;
}}
></ha-form>
${missing ? html`<div class="place-missing">${t("place_missing", L)}</div>` : nothing}
<label class="place-on-site ${this._place ? "" : "disabled"}">
<input
type="checkbox"
.checked=${this._remindOnSite}
?disabled=${!this._place}
@change=${(e: Event) => (this._remindOnSite = (e.target as HTMLInputElement).checked)}
/>
<span>${t("remind_on_site", L)}</span>
</label>
${this._place ? html`<div class="place-hint">${t("remind_on_site_hint", L)}</div>` : nothing}
`;
}
private async _save(): Promise<void> {
if (this._loading) return; // synchronous re-entry guard (double-click)
if (!this._name.trim()) return;
@@ -211,6 +249,8 @@ export class MaintenanceObjectDialog extends LitElement {
notes: this._notes.trim() || null,
ha_device_id: this._haDeviceId || null,
parent_entry_id: this._parentEntryId || null,
// Only with the feature on: switched off, a stored place is kept as is.
...(this.placesEnabled ? { place: this._place || null, remind_on_site: !!this._place && this._remindOnSite } : {}),
};
const res = await runWs<{ device_swap?: DeviceSwap }>(
this,
@@ -278,6 +318,7 @@ export class MaintenanceObjectDialog extends LitElement {
@value-changed=${(e: CustomEvent) =>
(this._areaId = (e.detail.value as string) || "")}
></ha-area-picker>
${this.placesEnabled ? this._renderPlace(L) : nothing}
<ms-date-field
kind="date"
clearable
@@ -352,6 +393,25 @@ export class MaintenanceObjectDialog extends LitElement {
}
static styles = css`
.place-on-site {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--primary-text-color);
}
.place-on-site.disabled {
color: var(--disabled-text-color, var(--secondary-text-color));
}
.place-hint,
.place-missing {
font-size: 12px;
color: var(--secondary-text-color);
margin-top: -4px;
}
.place-missing {
color: var(--warning-color, #c77700);
}
.dialog-title {
font-size: 18px;
font-weight: 500;
@@ -703,6 +703,7 @@ export class MaintenanceSettingsView extends LitElement {
{ key: "checklists", settingKey: "advanced_checklists_visible", label: t("feat_checklists", L), desc: t("feat_checklists_desc", L) },
{ key: "schedule_time", settingKey: "advanced_schedule_time_visible", label: t("feat_schedule_time", L), desc: t("feat_schedule_time_desc", L) },
{ key: "completion_actions", settingKey: "advanced_completion_actions_visible", label: t("feat_completion_actions", L), desc: t("feat_completion_actions_desc", L) },
{ key: "places", settingKey: "advanced_places_visible", label: t("feat_places", L), desc: t("feat_places_desc", L) },
];
return html`
@@ -28,6 +28,7 @@ import type { TriggerType } from "../types";
import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
import "./ms-textfield";
import { defaultNotifyIcon } from "../helpers/notify-icons";
import { primaryTriggerEntity, triggerEntityIds } from "../helpers/trigger-entities";
import "./ms-date-field";
import "./ms-cost-input";
@@ -160,7 +161,7 @@ export function applyTypeFields(c: TriggerConfig, d: TriggerTypeFields): void {
/** Map a persisted compound condition (storage shape) to an editable draft. */
function conditionToDraft(c: TriggerConfig): CompoundConditionDraft {
const ids = c.entity_ids || (c.entity_id ? [c.entity_id] : []);
const ids = triggerEntityIds(c);
return {
entityIds: ids.join(", "),
...typeFieldsFromConfig(c),
@@ -617,8 +618,8 @@ export class MaintenanceTaskDialog extends LitElement {
// Fleet task) must still hydrate the singular field — the save path
// gates on _triggerEntityId and would otherwise NULL the whole trigger
// on an unrelated edit (issue #106).
this._triggerEntityId = tc.entity_id || (tc.entity_ids && tc.entity_ids[0]) || "";
this._triggerEntityIds = tc.entity_ids || (tc.entity_id ? [tc.entity_id] : []);
this._triggerEntityId = primaryTriggerEntity(tc) || "";
this._triggerEntityIds = triggerEntityIds(tc);
this._triggerEntityLogic = tc.entity_logic || "any";
this._setTypeFields(typeFieldsFromConfig(tc));
this._triggerCombinator = tc.trigger_combinator === "all" ? "all" : "any";
@@ -78,7 +78,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
@state() private _features: AdvancedFeatures = {
adaptive: false, seasonal: false, environmental: false,
budget: false, groups: false, checklists: false, schedule_time: false,
completion_actions: false,
completion_actions: false, places: false,
};
@state() private _toast = "";
private readonly _toastTimer = new ToastTimer();
@@ -129,9 +129,19 @@ export function openCreateObjectDialog(): boolean {
if (!syncHass(dlg)) return false;
// openCreate is defined on MaintenanceObjectDialog
dlg.openCreate();
void applyObjectDialogSettings(dlg);
return true;
}
/** 2026-10 places: the object dialog shows its place fields by the same
* feature switch as in the panel. Opened at once; the fields appear when
* the (page-wide cached) settings are in. */
async function applyObjectDialogSettings(dlg: MaintenanceObjectDialog): Promise<void> {
const hass = getHass();
if (!hass) return;
dlg.placesEnabled = (await fetchSettingsOnce(hass)).features.places;
}
export function openEditObjectDialog(
entryId: string,
obj: MaintenanceObject,
@@ -139,6 +149,7 @@ export function openEditObjectDialog(
const dlg = getOrCreate<MaintenanceObjectDialog>(OBJECT_DIALOG_TAG);
if (!syncHass(dlg)) return false;
dlg.openEdit(entryId, obj);
void applyObjectDialogSettings(dlg);
return true;
}
@@ -545,6 +545,11 @@
"no_objects": "Zatím žádné objekty.",
"action_error": "Akce se nezdařila. Zkuste to znovu.",
"area_id_optional": "Oblast (volitelná)",
"place_optional": "Místo (zóna, volitelné)",
"remind_on_site": "Připomínat jen, když tam někdo je",
"remind_on_site_hint": "Připomínky čekají, dokud někdo nebude v této zóně. Kdo tam přijede, dostane zprávu s tím, co je potřeba udělat.",
"place_missing": "Zóna nenalezena: připomínky jako doma",
"place_home": "Domov",
"installation_date_optional": "Datum instalace (volitelné)",
"warranty_expiry_optional": "Konec záruky (volitelné)",
"warranty": "Záruka",
@@ -781,6 +786,8 @@
"owner_label": "Vlastník",
"feat_completion_actions": "Akce při dokončení",
"feat_completion_actions_desc": "Akce HA na úlohu při dokončení + QR rychlého dokončení s předvolenými hodnotami.",
"feat_places": "Místa",
"feat_places_desc": "Objekty udržované na jiném místě (zóna Home Assistantu): připomínky jen když tam někdo je a zpráva při příjezdu.",
"on_complete_action_title": "Při dokončení: spustit akci HA (volitelné)",
"on_complete_action_desc": "Volá službu HA, když je úloha dokončena — např. resetuje čítač na zařízení.",
"on_complete_action_target": "Cílová entita",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Používá díl: {name}",
"label_filter": "Štítek",
"all_labels": "Všechny štítky",
"filter_place": "Místo",
"filter_place_all": "Všechna místa",
"settings_notify_scope": "Upozorňovat jen pro zobrazení",
"settings_notify_scope_all": "Všechny úkoly",
"settings_notify_scope_hint": "Připomenutí posílají jen úkoly odpovídající filtrům štítku/uživatele zvoleného zobrazení. Stav, řazení a seskupení zobrazení se zde ignorují.",
@@ -547,6 +547,11 @@
"no_objects": "Ingen objekter endnu.",
"action_error": "Handlingen mislykkedes. Prøv igen.",
"area_id_optional": "Område (valgfrit)",
"place_optional": "Sted (zone, valgfrit)",
"remind_on_site": "Påmind kun, når nogen er der",
"remind_on_site_hint": "Påmindelser venter, til nogen er i denne zone. Den, der ankommer, får en besked med det, der skal gøres.",
"place_missing": "Zone ikke fundet: påmindelser som hjemme",
"place_home": "Hjemme",
"installation_date_optional": "Installationsdato (valgfrit)",
"warranty_expiry_optional": "Garantiens udløb (valgfrit)",
"warranty": "Garanti",
@@ -806,6 +811,8 @@
"owner_label": "Ejer",
"feat_completion_actions": "Fuldførelseshandlinger",
"feat_completion_actions_desc": "HA-handling pr. opgave ved fuldførelse + hurtig-fuldfør-QR med forudindstillede værdier.",
"feat_places": "Steder",
"feat_places_desc": "Objekter, der vedligeholdes et andet sted (en Home Assistant-zone): påmindelser kun når nogen er der, og en besked ved ankomst.",
"on_complete_action_title": "Ved fuldførelse: udløs HA-handling (valgfrit)",
"on_complete_action_desc": "Kalder en HA-tjeneste, når opgaven fuldføres — f.eks. nulstil en tæller på enheden.",
"on_complete_action_target": "Målenhed",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Bruger reservedel: {name}",
"label_filter": "Etiket",
"all_labels": "Alle etiketter",
"filter_place": "Sted",
"filter_place_all": "Alle steder",
"settings_notify_scope": "Underret kun for visning",
"settings_notify_scope_all": "Alle opgaver",
"settings_notify_scope_hint": "Kun opgaver, der matcher den valgte visnings etiket-/brugerfiltre, sender påmindelser. Visningens status, sortering og gruppering ignoreres her.",
@@ -547,6 +547,11 @@
"no_objects": "Keine Objekte vorhanden.",
"action_error": "Aktion fehlgeschlagen. Bitte erneut versuchen.",
"area_id_optional": "Bereich (optional)",
"place_optional": "Ort (Zone, optional)",
"remind_on_site": "Nur erinnern, wenn jemand dort ist",
"remind_on_site_hint": "Erinnerungen warten, bis jemand in dieser Zone ist. Wer dort ankommt, bekommt eine Nachricht mit dem, was fällig ist.",
"place_missing": "Zone nicht gefunden: Erinnerungen wie zuhause",
"place_home": "Zuhause",
"installation_date_optional": "Installationsdatum (optional)",
"warranty_expiry_optional": "Garantie-Ablauf (optional)",
"warranty": "Garantie",
@@ -806,6 +811,8 @@
"owner_label": "Besitzer",
"feat_completion_actions": "Completion-Actions",
"feat_completion_actions_desc": "Pro Aufgabe HA-Action beim Abschluss konfigurieren + Quick-Complete-QR mit voreingestellten Werten.",
"feat_places": "Orte",
"feat_places_desc": "Objekte an einem anderen Ort (eine Home-Assistant-Zone): Erinnerungen nur, wenn jemand dort ist, und eine Nachricht bei Ankunft.",
"on_complete_action_title": "Beim Abschluss: HA-Action auslösen (optional)",
"on_complete_action_desc": "Ruft beim Erledigen der Aufgabe einen HA-Service auf — z. B. einen Zähler am Gerät zurücksetzen.",
"on_complete_action_target": "Ziel-Entität",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Nutzt Ersatzteil: {name}",
"label_filter": "Label",
"all_labels": "Alle Labels",
"filter_place": "Ort",
"filter_place_all": "Alle Orte",
"settings_notify_scope": "Nur benachrichtigen für Ansicht",
"settings_notify_scope_all": "Alle Aufgaben",
"settings_notify_scope_hint": "Nur Aufgaben, die zu den Label-/Nutzer-Filtern der gewählten Ansicht passen, senden Erinnerungen. Status, Sortierung und Gruppierung der Ansicht werden hier ignoriert.",
@@ -547,6 +547,11 @@
"no_objects": "No objects yet.",
"action_error": "Action failed. Please try again.",
"area_id_optional": "Area (optional)",
"place_optional": "Place (zone, optional)",
"remind_on_site": "Remind only while somebody is there",
"remind_on_site_hint": "Reminders wait until somebody is in this zone. Whoever arrives there gets a message with what is due.",
"place_missing": "Zone not found: reminders as at home",
"place_home": "Home",
"installation_date_optional": "Installation date (optional)",
"warranty_expiry_optional": "Warranty expiry (optional)",
"warranty": "Warranty",
@@ -813,6 +818,8 @@
"owner_label": "Owner",
"feat_completion_actions": "Completion actions",
"feat_completion_actions_desc": "Per-task HA action on complete + quick-complete QR with pre-set values.",
"feat_places": "Places",
"feat_places_desc": "Objects maintained at another place (a Home Assistant zone): reminders only while somebody is there, and a message on arrival.",
"on_complete_action_title": "On complete: trigger HA action (optional)",
"on_complete_action_desc": "Calls an HA service when the task is completed — e.g. reset a counter on the device.",
"on_complete_action_target": "Target entity",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Uses part: {name}",
"label_filter": "Label",
"all_labels": "All labels",
"filter_place": "Place",
"filter_place_all": "All places",
"settings_notify_scope": "Notify only for view",
"settings_notify_scope_all": "All tasks",
"settings_notify_scope_hint": "Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",
@@ -545,6 +545,11 @@
"no_objects": "Aún no hay objetos.",
"action_error": "Acción fallida. Inténtelo de nuevo.",
"area_id_optional": "Área (opcional)",
"place_optional": "Lugar (zona, opcional)",
"remind_on_site": "Recordar solo cuando alguien esté allí",
"remind_on_site_hint": "Los recordatorios esperan a que alguien esté en esta zona. Quien llega allí recibe un mensaje con lo pendiente.",
"place_missing": "Zona no encontrada: recordatorios como en casa",
"place_home": "Casa",
"installation_date_optional": "Fecha de instalación (opcional)",
"warranty_expiry_optional": "Vencimiento de garantía (opcional)",
"warranty": "Garantía",
@@ -781,6 +786,8 @@
"owner_label": "Propietario",
"feat_completion_actions": "Acciones de finalización",
"feat_completion_actions_desc": "Acción HA por tarea al completar + QR de finalización rápida con valores prefijados.",
"feat_places": "Lugares",
"feat_places_desc": "Objetos mantenidos en otro lugar (una zona de Home Assistant): recordatorios solo cuando alguien está allí y un mensaje al llegar.",
"on_complete_action_title": "Al completar: ejecutar acción HA (opcional)",
"on_complete_action_desc": "Llama un servicio HA cuando se completa la tarea — p. ej. reiniciar un contador del dispositivo.",
"on_complete_action_target": "Entidad objetivo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa la pieza: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas las etiquetas",
"filter_place": "Lugar",
"filter_place_all": "Todos los lugares",
"settings_notify_scope": "Notificar solo para la vista",
"settings_notify_scope_all": "Todas las tareas",
"settings_notify_scope_hint": "Solo las tareas que coinciden con los filtros de etiqueta/usuario de la vista seleccionada envían recordatorios. El estado, el orden y la agrupación de la vista se ignoran aquí.",
@@ -547,6 +547,11 @@
"no_objects": "Ei vielä kohteita.",
"action_error": "Toiminto epäonnistui. Yritä uudelleen.",
"area_id_optional": "Alue (valinnainen)",
"place_optional": "Paikka (vyöhyke, valinnainen)",
"remind_on_site": "Muistuta vain, kun joku on paikalla",
"remind_on_site_hint": "Muistutukset odottavat, kunnes joku on tällä vyöhykkeellä. Sinne saapuva saa viestin tehtävistä asioista.",
"place_missing": "Vyöhykettä ei löydy: muistutukset kuin kotona",
"place_home": "Koti",
"installation_date_optional": "Asennuspäivä (valinnainen)",
"warranty_expiry_optional": "Takuun päättyminen (valinnainen)",
"warranty": "Takuu",
@@ -806,6 +811,8 @@
"owner_label": "Omistaja",
"feat_completion_actions": "Suoritustoiminnot",
"feat_completion_actions_desc": "Tehtäväkohtainen HA-toiminto suorituksen yhteydessä + pikasuoritus-QR esiasetetuilla arvoilla.",
"feat_places": "Paikat",
"feat_places_desc": "Toisessa paikassa (Home Assistant -vyöhyke) huollettavat kohteet: muistutukset vain, kun joku on paikalla, ja viesti saavuttaessa.",
"on_complete_action_title": "Suorituksen yhteydessä: laukaise HA-toiminto (valinnainen)",
"on_complete_action_desc": "Kutsuu HA-palvelua, kun tehtävä suoritetaan — esim. nollaa laitteen laskuri.",
"on_complete_action_target": "Kohde-entiteetti",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Käyttää varaosaa: {name}",
"label_filter": "Tunniste",
"all_labels": "Kaikki tunnisteet",
"filter_place": "Paikka",
"filter_place_all": "Kaikki paikat",
"settings_notify_scope": "Ilmoita vain näkymälle",
"settings_notify_scope_all": "Kaikki tehtävät",
"settings_notify_scope_hint": "Vain valitun näkymän tunniste-/käyttäjäsuodattimiin osuvat tehtävät lähettävät muistutuksia. Näkymän tila, lajittelu ja ryhmittely ohitetaan tässä.",
@@ -545,6 +545,11 @@
"no_objects": "Aucun objet pour l'instant.",
"action_error": "Action échouée. Veuillez réessayer.",
"area_id_optional": "Zone (optionnel)",
"place_optional": "Lieu (zone, facultatif)",
"remind_on_site": "Rappeler seulement quand quelqu'un y est",
"remind_on_site_hint": "Les rappels attendent que quelqu'un soit dans cette zone. Qui y arrive reçoit un message avec ce qui est à faire.",
"place_missing": "Zone introuvable : rappels comme à la maison",
"place_home": "Domicile",
"installation_date_optional": "Date d'installation (optionnel)",
"warranty_expiry_optional": "Fin de garantie (optionnel)",
"warranty": "Garantie",
@@ -781,6 +786,8 @@
"owner_label": "Propriétaire",
"feat_completion_actions": "Actions de finalisation",
"feat_completion_actions_desc": "Action HA par tâche lors de la finalisation + QR de finalisation rapide avec valeurs pré-définies.",
"feat_places": "Lieux",
"feat_places_desc": "Objets entretenus à un autre endroit (une zone Home Assistant) : rappels seulement quand quelqu'un y est, et un message à l'arrivée.",
"on_complete_action_title": "À la finalisation : déclencher une action HA (optionnel)",
"on_complete_action_desc": "Appelle un service HA quand la tâche est terminée — p. ex. réinitialiser un compteur sur l'appareil.",
"on_complete_action_target": "Entité cible",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Utilise la pièce : {name}",
"label_filter": "Étiquette",
"all_labels": "Toutes les étiquettes",
"filter_place": "Lieu",
"filter_place_all": "Tous les lieux",
"settings_notify_scope": "Notifier uniquement pour la vue",
"settings_notify_scope_all": "Toutes les tâches",
"settings_notify_scope_hint": "Seules les tâches correspondant aux filtres étiquette/utilisateur de la vue choisie envoient des rappels. Le statut, le tri et le regroupement de la vue sont ignorés ici.",
@@ -547,6 +547,11 @@
"no_objects": "अभी कोई वस्तु नहीं।",
"action_error": "क्रिया विफल। कृपया पुनः प्रयास करें।",
"area_id_optional": "क्षेत्र (वैकल्पिक)",
"place_optional": "स्थान (ज़ोन, वैकल्पिक)",
"remind_on_site": "केवल तब याद दिलाएँ जब कोई वहाँ हो",
"remind_on_site_hint": "रिमाइंडर तब तक रुकते हैं जब तक कोई इस ज़ोन में न हो। वहाँ पहुँचने वाले को बाकी कामों का संदेश मिलता है।",
"place_missing": "ज़ोन नहीं मिला: रिमाइंडर घर जैसे",
"place_home": "घर",
"installation_date_optional": "स्थापना तिथि (वैकल्पिक)",
"warranty_expiry_optional": "वारंटी समाप्ति (वैकल्पिक)",
"warranty": "वारंटी",
@@ -806,6 +811,8 @@
"owner_label": "स्वामी",
"feat_completion_actions": "पूर्णता क्रियाएँ",
"feat_completion_actions_desc": "पूर्ण होने पर प्रति-कार्य HA क्रिया + पूर्व-निर्धारित मानों के साथ त्वरित-पूर्ण QR।",
"feat_places": "स्थान",
"feat_places_desc": "किसी दूसरी जगह (Home Assistant ज़ोन) पर रखरखाव वाले ऑब्जेक्ट: रिमाइंडर तभी जब कोई वहाँ हो, और पहुँचने पर एक संदेश।",
"on_complete_action_title": "पूर्ण होने पर: HA क्रिया ट्रिगर करें (वैकल्पिक)",
"on_complete_action_desc": "कार्य पूर्ण होने पर एक HA सेवा कॉल करता है — उदा. डिवाइस पर काउंटर रीसेट करना।",
"on_complete_action_target": "लक्ष्य एंटिटी",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "पुर्ज़ा उपयोग करता है: {name}",
"label_filter": "लेबल",
"all_labels": "सभी लेबल",
"filter_place": "स्थान",
"filter_place_all": "सभी स्थान",
"settings_notify_scope": "केवल इस व्यू के लिए सूचित करें",
"settings_notify_scope_all": "सभी कार्य",
"settings_notify_scope_hint": "केवल वे कार्य अनुस्मारक भेजते हैं जो चुने गए व्यू के लेबल/उपयोगकर्ता फ़िल्टर से मेल खाते हैं। व्यू की स्थिति, क्रम और समूहन यहाँ अनदेखे रहते हैं।",
@@ -547,6 +547,11 @@
"no_objects": "Még nincsenek objektumok.",
"action_error": "A művelet nem sikerült. Próbálja újra.",
"area_id_optional": "Terület (opcionális)",
"place_optional": "Helyszín (zóna, opcionális)",
"remind_on_site": "Csak akkor emlékeztessen, ha valaki ott van",
"remind_on_site_hint": "Az emlékeztetők megvárják, amíg valaki ebben a zónában lesz. Aki odaér, üzenetet kap az esedékes teendőkről.",
"place_missing": "A zóna nem található: emlékeztetők, mint otthon",
"place_home": "Otthon",
"installation_date_optional": "Telepítés dátuma (opcionális)",
"warranty_expiry_optional": "Garancia lejárta (opcionális)",
"warranty": "Garancia",
@@ -813,6 +818,8 @@
"owner_label": "Tulajdonos",
"feat_completion_actions": "Elvégzési műveletek",
"feat_completion_actions_desc": "Feladatonkénti HA művelet elvégzéskor + gyorselvégző QR előre beállított értékekkel.",
"feat_places": "Helyszínek",
"feat_places_desc": "Másik helyszínen (Home Assistant-zóna) karbantartott objektumok: emlékeztető csak akkor, ha valaki ott van, és üzenet érkezéskor.",
"on_complete_action_title": "Elvégzéskor: HA művelet indítása (opcionális)",
"on_complete_action_desc": "HA szolgáltatást hív meg a feladat elvégzésekor — pl. számláló nullázása az eszközön.",
"on_complete_action_target": "Cél entitás",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Alkatrészt használ: {name}",
"label_filter": "Címke",
"all_labels": "Minden címke",
"filter_place": "Helyszín",
"filter_place_all": "Minden helyszín",
"settings_notify_scope": "Értesítés csak erre a nézetre",
"settings_notify_scope_all": "Minden feladat",
"settings_notify_scope_hint": "Csak a kiválasztott mentett nézet címke-/felhasználószűrőinek megfelelő feladatok küldenek emlékeztetőt. A nézet állapot-, rendezési és csoportosítási beállításait itt figyelmen kívül hagyjuk.",
@@ -545,6 +545,11 @@
"no_objects": "Nessun oggetto ancora.",
"action_error": "Azione fallita. Riprova.",
"area_id_optional": "Area (opzionale)",
"place_optional": "Luogo (zona, facoltativo)",
"remind_on_site": "Ricorda solo quando qualcuno è lì",
"remind_on_site_hint": "I promemoria aspettano che qualcuno sia in questa zona. Chi arriva lì riceve un messaggio con ciò che è da fare.",
"place_missing": "Zona non trovata: promemoria come a casa",
"place_home": "Casa",
"installation_date_optional": "Data di installazione (opzionale)",
"warranty_expiry_optional": "Scadenza garanzia (opzionale)",
"warranty": "Garanzia",
@@ -781,6 +786,8 @@
"owner_label": "Proprietario",
"feat_completion_actions": "Azioni di completamento",
"feat_completion_actions_desc": "Azione HA per attività al completamento + QR completamento rapido con valori predefiniti.",
"feat_places": "Luoghi",
"feat_places_desc": "Oggetti curati in un altro luogo (una zona di Home Assistant): promemoria solo quando qualcuno è lì e un messaggio all'arrivo.",
"on_complete_action_title": "Al completamento: attiva azione HA (opzionale)",
"on_complete_action_desc": "Chiama un servizio HA quando l'attività viene completata — es. azzerare un contatore sul dispositivo.",
"on_complete_action_target": "Entità target",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa il ricambio: {name}",
"label_filter": "Etichetta",
"all_labels": "Tutte le etichette",
"filter_place": "Luogo",
"filter_place_all": "Tutti i luoghi",
"settings_notify_scope": "Notifica solo per la vista",
"settings_notify_scope_all": "Tutte le attività",
"settings_notify_scope_hint": "Solo le attività che corrispondono ai filtri etichetta/utente della vista scelta inviano promemoria. Stato, ordinamento e raggruppamento della vista qui vengono ignorati.",
@@ -547,6 +547,11 @@
"no_objects": "対象がまだありません。",
"action_error": "操作に失敗しました。もう一度お試しください。",
"area_id_optional": "エリア(任意)",
"place_optional": "場所(ゾーン、任意)",
"remind_on_site": "誰かがいるときだけリマインド",
"remind_on_site_hint": "リマインドは誰かがこのゾーンにいるまで待機します。到着した人には、やるべきことをまとめたメッセージが届きます。",
"place_missing": "ゾーンが見つかりません:自宅と同じようにリマインドします",
"place_home": "自宅",
"installation_date_optional": "設置日(任意)",
"warranty_expiry_optional": "保証期限(任意)",
"warranty": "保証",
@@ -806,6 +811,8 @@
"owner_label": "オーナー",
"feat_completion_actions": "完了時アクション",
"feat_completion_actions_desc": "タスクごとの完了時HAアクション+事前設定値付きのクイック完了QR。",
"feat_places": "場所",
"feat_places_desc": "別の場所(Home Assistant のゾーン)で管理するオブジェクト:誰かがその場にいるときだけリマインドし、到着時に通知します。",
"on_complete_action_title": "完了時: HAアクションを実行(任意)",
"on_complete_action_desc": "タスク完了時にHAサービスを呼び出します — 例: デバイスのカウンターをリセット。",
"on_complete_action_target": "対象エンティティ",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "使用する部品: {name}",
"label_filter": "ラベル",
"all_labels": "すべてのラベル",
"filter_place": "場所",
"filter_place_all": "すべての場所",
"settings_notify_scope": "このビューのみ通知",
"settings_notify_scope_all": "すべてのタスク",
"settings_notify_scope_hint": "選択した保存済みビューのラベル/ユーザーフィルターに一致するタスクのみがリマインダーを送信します。ビューのステータス・並び順・グループ化はここでは無視されます。",
@@ -547,6 +547,11 @@
"no_objects": "아직 객체가 없습니다.",
"action_error": "동작에 실패했습니다. 다시 시도해 주세요.",
"area_id_optional": "구역 (선택)",
"place_optional": "장소 (구역, 선택 사항)",
"remind_on_site": "누군가 있을 때만 알림",
"remind_on_site_hint": "누군가 이 구역에 있을 때까지 알림이 대기합니다. 그곳에 도착한 사람은 해야 할 일을 담은 메시지를 받습니다.",
"place_missing": "구역을 찾을 수 없음: 집과 같이 알림",
"place_home": "집",
"installation_date_optional": "설치일 (선택)",
"warranty_expiry_optional": "보증 만료일 (선택)",
"warranty": "보증",
@@ -813,6 +818,8 @@
"owner_label": "소유자",
"feat_completion_actions": "완료 동작",
"feat_completion_actions_desc": "완료 시 작업별 HA 동작 실행 + 사전 설정값이 있는 빠른 완료 QR.",
"feat_places": "장소",
"feat_places_desc": "다른 장소(Home Assistant 구역)에서 관리하는 개체: 누군가 그곳에 있을 때만 알림을 보내고, 도착하면 메시지를 보냅니다.",
"on_complete_action_title": "완료 시: HA 동작 실행 (선택)",
"on_complete_action_desc": "작업이 완료되면 HA 서비스를 호출합니다 — 예: 기기의 카운터 초기화.",
"on_complete_action_target": "대상 엔티티",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "사용 부품: {name}",
"label_filter": "라벨",
"all_labels": "모든 라벨",
"filter_place": "장소",
"filter_place_all": "모든 장소",
"settings_notify_scope": "특정 보기만 알림",
"settings_notify_scope_all": "모든 작업",
"settings_notify_scope_hint": "선택한 저장된 보기의 라벨/사용자 필터와 일치하는 작업만 알림을 보냅니다. 보기의 상태·정렬·그룹화는 여기서 무시됩니다.",
@@ -547,6 +547,11 @@
"no_objects": "Ingen objekter ennå.",
"action_error": "Handlingen mislyktes. Prøv igjen.",
"area_id_optional": "Område (valgfritt)",
"place_optional": "Sted (sone, valgfritt)",
"remind_on_site": "Påminn bare når noen er der",
"remind_on_site_hint": "Påminnelser venter til noen er i denne sonen. Den som kommer dit, får en melding med det som skal gjøres.",
"place_missing": "Fant ikke sonen: påminnelser som hjemme",
"place_home": "Hjemme",
"installation_date_optional": "Installasjonsdato (valgfritt)",
"warranty_expiry_optional": "Garanti utløper (valgfritt)",
"warranty": "Garanti",
@@ -806,6 +811,8 @@
"owner_label": "Eier",
"feat_completion_actions": "Fullføringshandlinger",
"feat_completion_actions_desc": "HA-handling per oppgave ved fullføring + hurtigfullføring med QR og forhåndsinnstilte verdier.",
"feat_places": "Steder",
"feat_places_desc": "Objekter som vedlikeholdes et annet sted (en Home Assistant-sone): påminnelser bare når noen er der, og en melding ved ankomst.",
"on_complete_action_title": "Ved fullføring: utløs HA-handling (valgfritt)",
"on_complete_action_desc": "Kaller en HA-tjeneste når oppgaven fullføres — f.eks. tilbakestille en teller på enheten.",
"on_complete_action_target": "Målentitet",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Bruker del: {name}",
"label_filter": "Etikett",
"all_labels": "Alle etiketter",
"filter_place": "Sted",
"filter_place_all": "Alle steder",
"settings_notify_scope": "Varsle kun for visning",
"settings_notify_scope_all": "Alle oppgaver",
"settings_notify_scope_hint": "Bare oppgaver som matcher etikett-/brukerfiltrene til valgt visning sender påminnelser. Visningens status, sortering og gruppering ignoreres her.",
@@ -545,6 +545,11 @@
"no_objects": "Nog geen objecten.",
"action_error": "Actie mislukt. Probeer het opnieuw.",
"area_id_optional": "Gebied (optioneel)",
"place_optional": "Locatie (zone, optioneel)",
"remind_on_site": "Alleen herinneren als er iemand is",
"remind_on_site_hint": "Herinneringen wachten tot iemand in deze zone is. Wie daar aankomt, krijgt een bericht met wat er te doen is.",
"place_missing": "Zone niet gevonden: herinneringen zoals thuis",
"place_home": "Thuis",
"installation_date_optional": "Installatiedatum (optioneel)",
"warranty_expiry_optional": "Garantie verloopt (optioneel)",
"warranty": "Garantie",
@@ -781,6 +786,8 @@
"owner_label": "Eigenaar",
"feat_completion_actions": "Voltooiings-acties",
"feat_completion_actions_desc": "Per taak HA-actie bij voltooien + snel-voltooien-QR met vooraf ingestelde waarden.",
"feat_places": "Locaties",
"feat_places_desc": "Objecten op een andere locatie (een Home Assistant-zone): herinneringen alleen als er iemand is, en een bericht bij aankomst.",
"on_complete_action_title": "Bij voltooien: HA-actie uitvoeren (optioneel)",
"on_complete_action_desc": "Roept een HA-service aan wanneer de taak is voltooid — bv. een teller op het apparaat resetten.",
"on_complete_action_target": "Doel-entiteit",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Gebruikt onderdeel: {name}",
"label_filter": "Label",
"all_labels": "Alle labels",
"filter_place": "Locatie",
"filter_place_all": "Alle locaties",
"settings_notify_scope": "Alleen melden voor weergave",
"settings_notify_scope_all": "Alle taken",
"settings_notify_scope_hint": "Alleen taken die overeenkomen met de label-/gebruikersfilters van de gekozen weergave sturen herinneringen. Status, sortering en groepering van de weergave worden hier genegeerd.",
@@ -545,6 +545,11 @@
"no_objects": "Brak obiektów.",
"action_error": "Akcja nie powiodła się. Spróbuj ponownie.",
"area_id_optional": "Obszar (opcjonalny)",
"place_optional": "Miejsce (strefa, opcjonalnie)",
"remind_on_site": "Przypominaj tylko, gdy ktoś tam jest",
"remind_on_site_hint": "Przypomnienia czekają, aż ktoś będzie w tej strefie. Kto tam przyjedzie, dostaje wiadomość z tym, co jest do zrobienia.",
"place_missing": "Nie znaleziono strefy: przypomnienia jak w domu",
"place_home": "Dom",
"installation_date_optional": "Data instalacji (opcjonalna)",
"warranty_expiry_optional": "Wygaśnięcie gwarancji (opcjonalnie)",
"warranty": "Gwarancja",
@@ -781,6 +786,8 @@
"owner_label": "Właściciel",
"feat_completion_actions": "Akcje po zakończeniu",
"feat_completion_actions_desc": "Akcja HA per zadanie po zakończeniu + QR szybkiego zakończenia z predefiniowanymi wartościami.",
"feat_places": "Miejsca",
"feat_places_desc": "Obiekty w innym miejscu (strefa Home Assistant): przypomnienia tylko wtedy, gdy ktoś tam jest, i wiadomość po przybyciu.",
"on_complete_action_title": "Po zakończeniu: wywołaj akcję HA (opcjonalnie)",
"on_complete_action_desc": "Wywołuje usługę HA po zakończeniu zadania — np. zresetuj licznik urządzenia.",
"on_complete_action_target": "Encja docelowa",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Używa części: {name}",
"label_filter": "Etykieta",
"all_labels": "Wszystkie etykiety",
"filter_place": "Miejsce",
"filter_place_all": "Wszystkie miejsca",
"settings_notify_scope": "Powiadamiaj tylko dla widoku",
"settings_notify_scope_all": "Wszystkie zadania",
"settings_notify_scope_hint": "Przypomnienia wysyłają tylko zadania pasujące do filtrów etykiety/użytkownika wybranego widoku. Status, sortowanie i grupowanie widoku są tu ignorowane.",
@@ -547,6 +547,11 @@
"no_objects": "Ainda não há objetos.",
"action_error": "A ação falhou. Tente novamente.",
"area_id_optional": "Área (opcional)",
"place_optional": "Local (zona, opcional)",
"remind_on_site": "Lembrar só quando alguém estiver lá",
"remind_on_site_hint": "Os lembretes esperam até alguém estar nesta zona. Quem chega lá recebe uma mensagem com o que está pendente.",
"place_missing": "Zona não encontrada: lembretes como em casa",
"place_home": "Casa",
"installation_date_optional": "Data de instalação (opcional)",
"warranty_expiry_optional": "Fim da garantia (opcional)",
"warranty": "Garantia",
@@ -813,6 +818,8 @@
"owner_label": "Proprietário",
"feat_completion_actions": "Ações de conclusão",
"feat_completion_actions_desc": "Ação do HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.",
"feat_places": "Locais",
"feat_places_desc": "Objetos mantidos em outro local (uma zona do Home Assistant): lembretes só quando alguém está lá e uma mensagem na chegada.",
"on_complete_action_title": "Ao concluir: acionar ação do HA (opcional)",
"on_complete_action_desc": "Chama um serviço do HA quando a tarefa é concluída — ex.: zerar um contador no dispositivo.",
"on_complete_action_target": "Entidade alvo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa a peça: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas as etiquetas",
"filter_place": "Local",
"filter_place_all": "Todos os locais",
"settings_notify_scope": "Notificar apenas para a visualização",
"settings_notify_scope_all": "Todas as tarefas",
"settings_notify_scope_hint": "Apenas tarefas que correspondem aos filtros de etiqueta/usuário da visualização salva selecionada enviam lembretes. Status, ordenação e agrupamento da visualização são ignorados aqui.",
@@ -545,6 +545,11 @@
"no_objects": "Ainda sem objetos.",
"action_error": "Ação falhada. Tente novamente.",
"area_id_optional": "Área (opcional)",
"place_optional": "Local (zona, opcional)",
"remind_on_site": "Lembrar só quando alguém lá estiver",
"remind_on_site_hint": "Os lembretes esperam até alguém estar nesta zona. Quem lá chega recebe uma mensagem com o que está pendente.",
"place_missing": "Zona não encontrada: lembretes como em casa",
"place_home": "Casa",
"installation_date_optional": "Data de instalação (opcional)",
"warranty_expiry_optional": "Validade da garantia (opcional)",
"warranty": "Garantia",
@@ -781,6 +786,8 @@
"owner_label": "Proprietário",
"feat_completion_actions": "Ações de conclusão",
"feat_completion_actions_desc": "Ação HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.",
"feat_places": "Locais",
"feat_places_desc": "Objetos mantidos noutro local (uma zona do Home Assistant): lembretes só quando alguém lá está e uma mensagem à chegada.",
"on_complete_action_title": "Na conclusão: acionar ação HA (opcional)",
"on_complete_action_desc": "Chama um serviço HA quando a tarefa é concluída — p. ex. reiniciar um contador no dispositivo.",
"on_complete_action_target": "Entidade alvo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa a peça: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas as etiquetas",
"filter_place": "Local",
"filter_place_all": "Todos os locais",
"settings_notify_scope": "Notificar apenas para a vista",
"settings_notify_scope_all": "Todas as tarefas",
"settings_notify_scope_hint": "Apenas as tarefas que correspondem aos filtros de etiqueta/utilizador da vista escolhida enviam lembretes. O estado, a ordenação e o agrupamento da vista são ignorados aqui.",
@@ -545,6 +545,11 @@
"no_objects": "Пока нет объектов.",
"action_error": "Не удалось выполнить действие. Попробуйте ещё раз.",
"area_id_optional": "Зона (опционально)",
"place_optional": "Место (зона, необязательно)",
"remind_on_site": "Напоминать, только когда там кто-то есть",
"remind_on_site_hint": "Напоминания ждут, пока кто-нибудь окажется в этой зоне. Кто туда приезжает, получает сообщение о том, что нужно сделать.",
"place_missing": "Зона не найдена: напоминания как дома",
"place_home": "Дом",
"installation_date_optional": "Дата установки (опционально)",
"warranty_expiry_optional": "Окончание гарантии (опционально)",
"warranty": "Гарантия",
@@ -781,6 +786,8 @@
"owner_label": "Владелец",
"feat_completion_actions": "Действия при завершении",
"feat_completion_actions_desc": "HA-действие по задаче при завершении + QR быстрого завершения с предустановленными значениями.",
"feat_places": "Места",
"feat_places_desc": "Объекты в другом месте (зона Home Assistant): напоминания только когда там кто-то есть, и сообщение по прибытии.",
"on_complete_action_title": "При завершении: запустить HA-действие (опционально)",
"on_complete_action_desc": "Вызывает HA-сервис при завершении задачи — напр., сбросить счётчик на устройстве.",
"on_complete_action_target": "Целевая сущность",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Использует деталь: {name}",
"label_filter": "Метка",
"all_labels": "Все метки",
"filter_place": "Место",
"filter_place_all": "Все места",
"settings_notify_scope": "Уведомлять только для представления",
"settings_notify_scope_all": "Все задачи",
"settings_notify_scope_hint": "Напоминания отправляют только задачи, совпадающие с фильтрами метки/пользователя выбранного представления. Статус, сортировка и группировка представления здесь игнорируются.",
@@ -545,6 +545,11 @@
"no_objects": "Inga objekt än.",
"action_error": "Åtgärden misslyckades. Försök igen.",
"area_id_optional": "Område (valfritt)",
"place_optional": "Plats (zon, valfritt)",
"remind_on_site": "Påminn bara när någon är där",
"remind_on_site_hint": "Påminnelser väntar tills någon är i den här zonen. Den som kommer dit får ett meddelande med det som ska göras.",
"place_missing": "Zonen hittades inte: påminnelser som hemma",
"place_home": "Hemma",
"installation_date_optional": "Installationsdatum (valfritt)",
"warranty_expiry_optional": "Garantins utgång (valfritt)",
"warranty": "Garanti",
@@ -781,6 +786,8 @@
"owner_label": "Ägare",
"feat_completion_actions": "Slutförande-åtgärder",
"feat_completion_actions_desc": "HA-åtgärd per uppgift vid slutförande + snabb-slutför-QR med förinställda värden.",
"feat_places": "Platser",
"feat_places_desc": "Objekt som sköts på en annan plats (en Home Assistant-zon): påminnelser bara när någon är där, och ett meddelande vid ankomst.",
"on_complete_action_title": "Vid slutförande: utlös HA-åtgärd (valfritt)",
"on_complete_action_desc": "Anropar en HA-tjänst när uppgiften slutförs — t.ex. återställ en räknare på enheten.",
"on_complete_action_target": "Målentitet",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Använder reservdel: {name}",
"label_filter": "Etikett",
"all_labels": "Alla etiketter",
"filter_place": "Plats",
"filter_place_all": "Alla platser",
"settings_notify_scope": "Avisera endast för vy",
"settings_notify_scope_all": "Alla uppgifter",
"settings_notify_scope_hint": "Endast uppgifter som matchar den valda vyns etikett-/användarfilter skickar påminnelser. Vyns status, sortering och gruppering ignoreras här.",
@@ -547,6 +547,11 @@
"no_objects": "Henüz nesne yok.",
"action_error": "Eylem başarısız oldu. Lütfen tekrar deneyin.",
"area_id_optional": "Alan (isteğe bağlı)",
"place_optional": "Yer (bölge, isteğe bağlı)",
"remind_on_site": "Yalnızca biri oradayken hatırlat",
"remind_on_site_hint": "Hatırlatmalar biri bu bölgede olana kadar bekler. Oraya varan kişi yapılacakların listesini içeren bir mesaj alır.",
"place_missing": "Bölge bulunamadı: hatırlatmalar evdeki gibi",
"place_home": "Ev",
"installation_date_optional": "Kurulum tarihi (isteğe bağlı)",
"warranty_expiry_optional": "Garanti bitişi (isteğe bağlı)",
"warranty": "Garanti",
@@ -813,6 +818,8 @@
"owner_label": "Sahip",
"feat_completion_actions": "Tamamlama eylemleri",
"feat_completion_actions_desc": "Görev başına tamamlamada HA eylemi + önceden ayarlı değerlerle hızlı tamamlama QR'ı.",
"feat_places": "Yerler",
"feat_places_desc": "Başka bir yerde (Home Assistant bölgesi) bakımı yapılan nesneler: hatırlatmalar yalnızca biri oradayken, varışta bir mesaj.",
"on_complete_action_title": "Tamamlandığında: HA eylemi tetikle (isteğe bağlı)",
"on_complete_action_desc": "Görev tamamlandığında bir HA servisi çağırır — örn. cihazdaki bir sayacı sıfırlar.",
"on_complete_action_target": "Hedef varlık",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Kullanılan parça: {name}",
"label_filter": "Etiket",
"all_labels": "Tüm etiketler",
"filter_place": "Yer",
"filter_place_all": "Tüm yerler",
"settings_notify_scope": "Yalnızca şu görünüm için bildir",
"settings_notify_scope_all": "Tüm görevler",
"settings_notify_scope_hint": "Yalnızca seçili kayıtlı görünümün etiket/kullanıcı filtreleriyle eşleşen görevler hatırlatma gönderir. Görünümün durum, sıralama ve gruplaması burada dikkate alınmaz.",
@@ -545,6 +545,11 @@
"no_objects": "Поки немає об'єктів.",
"action_error": "Дія не вдалась. Спробуйте ще раз.",
"area_id_optional": "Зона (необов'язково)",
"place_optional": "Місце (зона, необов'язково)",
"remind_on_site": "Нагадувати, лише коли там хтось є",
"remind_on_site_hint": "Нагадування чекають, поки хтось буде в цій зоні. Хто туди прибуває, отримує повідомлення про те, що потрібно зробити.",
"place_missing": "Зону не знайдено: нагадування як удома",
"place_home": "Дім",
"installation_date_optional": "Дата встановлення (необов'язково)",
"warranty_expiry_optional": "Закінчення гарантії (необов'язково)",
"warranty": "Гарантія",
@@ -781,6 +786,8 @@
"owner_label": "Власник",
"feat_completion_actions": "Дії при завершенні",
"feat_completion_actions_desc": "Дія HA по завданню при завершенні + QR швидкого завершення з попередньо встановленими значеннями.",
"feat_places": "Місця",
"feat_places_desc": "Об'єкти в іншому місці (зона Home Assistant): нагадування лише тоді, коли там хтось є, і повідомлення після прибуття.",
"on_complete_action_title": "При завершенні: викликати HA-дію (опційно)",
"on_complete_action_desc": "Викликає HA-сервіс, коли завдання завершено — напр., скинути лічильник на пристрої.",
"on_complete_action_target": "Цільова сутність",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Використовує деталь: {name}",
"label_filter": "Мітка",
"all_labels": "Усі мітки",
"filter_place": "Місце",
"filter_place_all": "Усі місця",
"settings_notify_scope": "Сповіщати лише для подання",
"settings_notify_scope_all": "Усі завдання",
"settings_notify_scope_hint": "Нагадування надсилають лише завдання, що відповідають фільтрам мітки/користувача обраного подання. Статус, сортування та групування подання тут ігноруються.",
@@ -547,6 +547,11 @@
"no_objects": "暂无维护项。",
"action_error": "操作失败。请重试。",
"area_id_optional": "区域 (可选)",
"place_optional": "地点(区域,可选)",
"remind_on_site": "仅当有人在场时提醒",
"remind_on_site_hint": "提醒会等到有人在此区域时再发送。到达那里的人会收到一条列出待办事项的消息。",
"place_missing": "未找到区域:按在家处理提醒",
"place_home": "家",
"installation_date_optional": "安装日期 (可选)",
"warranty_expiry_optional": "保修到期 (可选)",
"warranty": "保修",
@@ -806,6 +811,8 @@
"owner_label": "所有者",
"feat_completion_actions": "完成动作",
"feat_completion_actions_desc": "为每个任务配置完成时的 HA 动作 + 预设值的快速完成二维码。",
"feat_places": "地点",
"feat_places_desc": "在其他地点(Home Assistant 区域)维护的对象:仅当有人在场时提醒,并在到达时发送消息。",
"on_complete_action_title": "完成时:触发 HA 动作(可选)",
"on_complete_action_desc": "任务完成时调用 HA 服务 — 例如重置设备上的物理计数器。",
"on_complete_action_target": "目标实体",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "使用配件:{name}",
"label_filter": "标签",
"all_labels": "所有标签",
"filter_place": "地点",
"filter_place_all": "所有地点",
"settings_notify_scope": "仅为该视图发送通知",
"settings_notify_scope_all": "所有任务",
"settings_notify_scope_hint": "只有匹配所选视图的标签/用户筛选的任务才会发送提醒。视图的状态、排序和分组在此处被忽略。",
@@ -131,6 +131,7 @@ import type { PartsCostMode } from "./helpers/parts-cost";
import { canWrite } from "./helpers/permissions";
import { renderStatusBadge } from "./renderers/status";
import { TOAST_MS, ACTION_TOAST_MS } from "./helpers/toast";
import { primaryTriggerEntity } from "./helpers/trigger-entities";
import { buildHistoryEntryDraft } from "./helpers/history-draft";
import { readingSlotDelta } from "./helpers/reading-slots";
import { bulkResultMessage, codedFailures, runWs, runWsEach } from "./helpers/ws-run";
@@ -236,6 +237,8 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _filterStatus = "";
@state() private _filterUser: string | null = null;
@state() private _filterLabel: string | null = null;
// 2026-10 places: "" = all, "home" = objects without a place, else a zone id.
@state() private _filterPlace = "";
/** #134: only tasks of this priority (low|normal|high); "" = no filter. */
@state() private _filterPriority = "";
// v2.24: shared saved filter views + the id of the one currently applied ("" =
@@ -264,7 +267,7 @@ export class MaintenanceSupporterPanel extends LitElement {
@state() private _groups: Record<string, MaintenanceGroup> = {};
@state() private _detailStatsData: Map<string, StatisticsPoint[]> = new Map();
@state() private _miniStatsData: Map<string, StatisticsPoint[]> = new Map();
@state() private _features: AdvancedFeatures = { adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false };
@state() private _features: AdvancedFeatures = { adaptive: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false, places: false };
// HA user IDs (UUIDs) granted full panel access despite not being HA admins.
@state() private _adminPanelUserIds: string[] = [];
// v2.8.4: master switch — the allowlist only grants the full panel when this
@@ -1087,7 +1090,7 @@ export class MaintenanceSupporterPanel extends LitElement {
const task = this._selectedEntryId && this._selectedTaskId
? this._getTask(this._selectedEntryId, this._selectedTaskId)
: null;
const entityId = task?.trigger_config?.entity_id;
const entityId = primaryTriggerEntity(task?.trigger_config);
if (entityId) {
// Drop the stale-range series so the chart shows its loading state.
const updated = new Map(this._detailStatsData);
@@ -1111,7 +1114,8 @@ export class MaintenanceSupporterPanel extends LitElement {
const entities: Array<{ entityId: string; isCounter: boolean }> = [];
for (const obj of this._objects) {
for (const task of obj.tasks) {
const entityId = task.trigger_config?.entity_id;
// The battery fleet's row shows "N to replace", not a sparkline.
const entityId = task.battery_fleet_task ? undefined : primaryTriggerEntity(task.trigger_config);
if (!entityId) continue;
entities.push({ entityId, isCounter: this._isCounterEntity(task.trigger_config) });
}
@@ -1217,6 +1221,9 @@ export class MaintenanceSupporterPanel extends LitElement {
// Label filter (v2.26 — also captured by saved views)
if (this._filterLabel && !(task.labels || []).includes(this._filterLabel)) continue;
// Place filter (2026-10): the object's zone, "home" for none.
if (this._filterPlace && (obj.object.place || "home") !== this._filterPlace) continue;
// Priority filter (#134 — also captured by saved views). Tasks
// without an explicit priority are "normal", the model default.
if (this._filterPriority && (task.priority || "normal") !== this._filterPriority) continue;
@@ -1353,8 +1360,9 @@ export class MaintenanceSupporterPanel extends LitElement {
if (s.msp_view === "task" && s.msp_entry && s.msp_task) {
this._historyFilter = null;
const task = this._getTask(s.msp_entry, s.msp_task);
if (task?.trigger_config?.entity_id) {
this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config));
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
}
}
@@ -1454,6 +1462,16 @@ export class MaintenanceSupporterPanel extends LitElement {
return [...seen].sort((a, b) => a.localeCompare(b));
}
/** 2026-10 places: the zones objects are maintained at, for the filter. */
private get _allPlaces(): { id: string; name: string }[] {
const seen = new Map<string, string>();
for (const obj of this._objects) {
const o = obj.object;
if (o.place && !seen.has(o.place)) seen.set(o.place, o.place_name || o.place);
}
return [...seen].map(([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
}
/** The panel's current task-list filter state, in the shape a view stores. */
private get _currentFilters(): SavedViewFilters {
return {
@@ -1615,8 +1633,9 @@ export class MaintenanceSupporterPanel extends LitElement {
this._resetStickyPane();
this._fetchFullHistory(entryId, taskId);
const task = this._getTask(entryId, taskId);
if (task?.trigger_config?.entity_id) {
this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config));
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
return;
}
@@ -1637,10 +1656,9 @@ export class MaintenanceSupporterPanel extends LitElement {
// Lazy-load statistics for the task's trigger entity
const task = this._getTask(entryId, taskId);
if (task?.trigger_config?.entity_id) {
const entityId = task.trigger_config.entity_id;
const isCounter = this._isCounterEntity(task.trigger_config);
this._fetchDetailStats(entityId, isCounter);
const entityId = primaryTriggerEntity(task?.trigger_config);
if (task && entityId) {
this._fetchDetailStats(entityId, this._isCounterEntity(task.trigger_config));
}
}
@@ -3196,6 +3214,7 @@ export class MaintenanceSupporterPanel extends LitElement {
<maintenance-object-dialog
.hass=${this.hass}
.objects=${this._objects}
.placesEnabled=${this._features.places}
@object-saved=${this._onObjectSaved}
@object-replaced=${this._onObjectReplaced}
></maintenance-object-dialog>
@@ -3543,6 +3562,7 @@ export class MaintenanceSupporterPanel extends LitElement {
(this._filterStatus ? 1 : 0) +
(this._filterUser ? 1 : 0) +
(this._filterLabel ? 1 : 0) +
(this._filterPlace ? 1 : 0) +
(this._filterPriority ? 1 : 0) +
(this._activeViewId ? 1 : 0);
@@ -3626,6 +3646,24 @@ export class MaintenanceSupporterPanel extends LitElement {
</select>
</label>
` : nothing}
${this._features.places && this._allPlaces.length > 0 ? html`
<label class="filter-field">
<span class="filter-label">${t("filter_place", L)}</span>
<select
.value=${this._filterPlace}
@change=${(e: Event) => {
this._filterPlace = (e.target as HTMLSelectElement).value;
this._activeViewId = "";
}}
>
<option value="">${t("filter_place_all", L)}</option>
<option value="home" ?selected=${this._filterPlace === "home"}>${t("place_home", L)}</option>
${this._allPlaces.map(
(pl) => html`<option value=${pl.id} ?selected=${this._filterPlace === pl.id}>${pl.name}</option>`
)}
</select>
</label>
` : nothing}
<label class="filter-field">
<span class="filter-label">${t("priority", L)}</span>
<select
@@ -4743,6 +4781,13 @@ export class MaintenanceSupporterPanel extends LitElement {
<a href="#" class="object-area-link" @click=${(e: Event) => { e.preventDefault(); this._showArea(o.area_id!); }}
>${areaDisplayName(o.area_id, this.hass?.areas, t("no_area", L))}</a></p>`
: nothing}
${o.place
? html`<p class="meta place-meta">
<ha-icon icon="mdi:map-marker-outline"></ha-icon>
${t("filter_place", L)}: ${o.place_name || o.place}${o.remind_on_site ? html` · ${t("remind_on_site", L)}` : nothing}
${o.place_missing ? html`<span class="place-missing-badge">${t("place_missing", L)}</span>` : nothing}
</p>`
: nothing}
${isSafeHttpUrl(o.documentation_url)
? html`<p class="meta">${t("documentation_url_label", L)}:
<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer">${o.documentation_url}</a>
@@ -963,6 +963,14 @@ export const panelStyles = css`
color: var(--info-color, #2196f3); font-weight: 500;
}
.paused-meta ha-icon { --mdc-icon-size: 16px; }
/* 2026-10 places: the object's place on its detail page. */
.place-meta {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.place-meta ha-icon { --mdc-icon-size: 16px; color: var(--secondary-text-color); }
.place-missing-badge {
font-size: 12px; color: var(--warning-color, #c77700);
}
.objects-table .oc-notes {
max-width: 220px;
overflow: hidden;
@@ -16,6 +16,7 @@ import { t, formatDate, formatDueDays, formatNumber } from "../styles";
import { px } from "./chart-utils";
import { daysProgress } from "../helpers/interval";
import { stampMs } from "../helpers/ha-time";
import { primaryTriggerEntity } from "../helpers/trigger-entities";
import type { MaintenanceTask, TaskRow, StatisticsPoint } from "../types";
const MINI_SPARKLINE_W = 60;
@@ -36,7 +37,9 @@ export function computeTrend(
miniStatsData: Map<string, StatisticsPoint[]>,
): TrendState | null {
const tc = row.trigger_config;
if (!tc?.entity_id) return null;
const entityId = primaryTriggerEntity(tc);
// The battery fleet's row tells "N to replace", not one battery's curve.
if (!tc || !entityId || row.battery_fleet_task) return null;
let toward: 1 | -1;
const type = tc.type || "threshold";
if (type === "threshold") {
@@ -48,7 +51,7 @@ export function computeTrend(
} else {
return null; // compound: conditions may point both ways
}
const statsPoints = miniStatsData.get(tc.entity_id) || [];
const statsPoints = miniStatsData.get(entityId) || [];
// Runtime and state_change accumulate SINCE the last completion — the
// history's trigger_value is the count at the previous trigger, a point
// from before the reset. Falling back to it read a freshly reset task
@@ -235,8 +238,9 @@ export function renderMiniSparkline(
miniStatsData: Map<string, StatisticsPoint[]>,
lang: string,
) {
if (!row.trigger_config?.entity_id) return nothing;
const entityId = row.trigger_config.entity_id;
const entityId = primaryTriggerEntity(row.trigger_config);
// The battery fleet's row tells "N to replace", not one battery's curve.
if (!entityId || row.battery_fleet_task) return nothing;
// PRIMARY: HA recorder statistics (daily, last 14 days)
const statsPoints = miniStatsData.get(entityId) || [];
@@ -16,6 +16,7 @@ import { t, fireMoreInfo, formatDueDays } from "../styles";
import { fmtNum, fmtVal } from "./chart-utils";
import { dueDaysOf } from "./progress";
import { stampMs } from "../helpers/ha-time";
import { primaryTriggerEntity, triggerEntityIds } from "../helpers/trigger-entities";
import "../components/trigger-chart";
import type { ChartPoint, ChartEvent } from "../components/trigger-chart";
import type { MaintenanceTask, TriggerConfig, StatisticsPoint } from "../types";
@@ -156,9 +157,9 @@ export function renderTriggerSection(task: MaintenanceTask, ctx: SparklineContex
const L = ctx.lang;
const info = task.trigger_entity_info;
const infos = task.trigger_entity_infos;
const friendlyName = info?.friendly_name || tc.entity_id || "—";
const entityId = tc.entity_id || "";
const entityIds = tc.entity_ids || (entityId ? [entityId] : []);
const entityId = primaryTriggerEntity(tc) || "";
const friendlyName = info?.friendly_name || entityId || "—";
const entityIds = triggerEntityIds(tc);
const unit = info?.unit_of_measurement || "";
const currentVal = task.trigger_current_value;
const triggerType = tc.type || "threshold";
@@ -215,7 +216,7 @@ export function renderTriggerSection(task: MaintenanceTask, ctx: SparklineContex
${triggerType === "compound" ? html`
<span class="trigger-limit-item"><span class="dot warn" aria-hidden="true"></span> ${t("compound_logic", L)}: ${tc.compound_logic || (tc as any).operator || "AND"}</span>
${(tc.conditions || []).map((cond: any, i: number) => html`
<span class="trigger-limit-item"><span class="dot range" aria-hidden="true"></span> ${i + 1}. ${t(cond.type === "due_date" ? "trigger_type_due_date" : cond.type || "unknown", L)}: ${cond.entity_id ? html`<span class="entity-link" @click=${(ev: Event) => fireMoreInfo(ev, cond.entity_id)}>${cond.entity_id}</span>` : ""}</span>
<span class="trigger-limit-item"><span class="dot range" aria-hidden="true"></span> ${i + 1}. ${t(cond.type === "due_date" ? "trigger_type_due_date" : cond.type || "unknown", L)}: ${triggerEntityIds(cond).map((eid, j) => html`${j > 0 ? ", " : ""}<span class="entity-link" @click=${(ev: Event) => fireMoreInfo(ev, eid)}>${eid}</span>`)}</span>
`)}
` : nothing}
</div>
@@ -330,7 +331,7 @@ function rawStatsPoints(task: MaintenanceTask, ctx: SparklineContext): ChartPoin
const tc = task.trigger_config;
if (!tc) return [];
const triggerType = tc.type || "threshold";
const entityId = tc.entity_id || "";
const entityId = primaryTriggerEntity(tc) || "";
// Runtime accumulates hours DERIVED from the entity's on/off time — the
// entity's own long-term statistics (an on/off ratio, or an unrelated raw
// sensor value) are NOT that accumulation. Plot only the recorded per-cycle
@@ -372,7 +373,7 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
const tc = task.trigger_config;
if (!tc) return nothing;
const triggerType = tc.type || "threshold";
const entityId = tc.entity_id || "";
const entityId = primaryTriggerEntity(tc) || "";
// A due date only counts down — no curve to tell.
if (triggerType === "due_date") return nothing;
@@ -40,6 +40,13 @@ export interface MaintenanceObject {
// 2.19: attach to an existing HA device / nest under another object
ha_device_id?: string | null;
parent_entry_id?: string | null;
/** 2026-10 places: an HA zone the object is maintained at (null = home),
* its name, whether the zone is gone (then: as at home), and whether the
* object's reminders wait until somebody is there. */
place?: string | null;
place_name?: string | null;
place_missing?: boolean;
remind_on_site?: boolean;
/** v1.4.0 (#43): optional link to PDF manual / vendor page for the object */
documentation_url?: string | null;
/** v1.4.10 (#46): free-form notes — part numbers, procedures, etc. */
@@ -550,6 +557,8 @@ export interface AdvancedFeatures {
schedule_time: boolean;
/** v1.3.0: gates per-task on_complete_action + quick_complete_defaults UI. */
completion_actions: boolean;
/** 2026-10: an object's place (an HA zone) and "remind only on site". */
places: boolean;
}
/** A single point in a recorder statistics time series. */
@@ -545,6 +545,11 @@
"no_objects": "Zatím žádné objekty.",
"action_error": "Akce se nezdařila. Zkuste to znovu.",
"area_id_optional": "Oblast (volitelná)",
"place_optional": "Místo (zóna, volitelné)",
"remind_on_site": "Připomínat jen, když tam někdo je",
"remind_on_site_hint": "Připomínky čekají, dokud někdo nebude v této zóně. Kdo tam přijede, dostane zprávu s tím, co je potřeba udělat.",
"place_missing": "Zóna nenalezena: připomínky jako doma",
"place_home": "Domov",
"installation_date_optional": "Datum instalace (volitelné)",
"warranty_expiry_optional": "Konec záruky (volitelné)",
"warranty": "Záruka",
@@ -781,6 +786,8 @@
"owner_label": "Vlastník",
"feat_completion_actions": "Akce při dokončení",
"feat_completion_actions_desc": "Akce HA na úlohu při dokončení + QR rychlého dokončení s předvolenými hodnotami.",
"feat_places": "Místa",
"feat_places_desc": "Objekty udržované na jiném místě (zóna Home Assistantu): připomínky jen když tam někdo je a zpráva při příjezdu.",
"on_complete_action_title": "Při dokončení: spustit akci HA (volitelné)",
"on_complete_action_desc": "Volá službu HA, když je úloha dokončena — např. resetuje čítač na zařízení.",
"on_complete_action_target": "Cílová entita",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Používá díl: {name}",
"label_filter": "Štítek",
"all_labels": "Všechny štítky",
"filter_place": "Místo",
"filter_place_all": "Všechna místa",
"settings_notify_scope": "Upozorňovat jen pro zobrazení",
"settings_notify_scope_all": "Všechny úkoly",
"settings_notify_scope_hint": "Připomenutí posílají jen úkoly odpovídající filtrům štítku/uživatele zvoleného zobrazení. Stav, řazení a seskupení zobrazení se zde ignorují.",
@@ -547,6 +547,11 @@
"no_objects": "Ingen objekter endnu.",
"action_error": "Handlingen mislykkedes. Prøv igen.",
"area_id_optional": "Område (valgfrit)",
"place_optional": "Sted (zone, valgfrit)",
"remind_on_site": "Påmind kun, når nogen er der",
"remind_on_site_hint": "Påmindelser venter, til nogen er i denne zone. Den, der ankommer, får en besked med det, der skal gøres.",
"place_missing": "Zone ikke fundet: påmindelser som hjemme",
"place_home": "Hjemme",
"installation_date_optional": "Installationsdato (valgfrit)",
"warranty_expiry_optional": "Garantiens udløb (valgfrit)",
"warranty": "Garanti",
@@ -806,6 +811,8 @@
"owner_label": "Ejer",
"feat_completion_actions": "Fuldførelseshandlinger",
"feat_completion_actions_desc": "HA-handling pr. opgave ved fuldførelse + hurtig-fuldfør-QR med forudindstillede værdier.",
"feat_places": "Steder",
"feat_places_desc": "Objekter, der vedligeholdes et andet sted (en Home Assistant-zone): påmindelser kun når nogen er der, og en besked ved ankomst.",
"on_complete_action_title": "Ved fuldførelse: udløs HA-handling (valgfrit)",
"on_complete_action_desc": "Kalder en HA-tjeneste, når opgaven fuldføres — f.eks. nulstil en tæller på enheden.",
"on_complete_action_target": "Målenhed",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Bruger reservedel: {name}",
"label_filter": "Etiket",
"all_labels": "Alle etiketter",
"filter_place": "Sted",
"filter_place_all": "Alle steder",
"settings_notify_scope": "Underret kun for visning",
"settings_notify_scope_all": "Alle opgaver",
"settings_notify_scope_hint": "Kun opgaver, der matcher den valgte visnings etiket-/brugerfiltre, sender påmindelser. Visningens status, sortering og gruppering ignoreres her.",
@@ -547,6 +547,11 @@
"no_objects": "Keine Objekte vorhanden.",
"action_error": "Aktion fehlgeschlagen. Bitte erneut versuchen.",
"area_id_optional": "Bereich (optional)",
"place_optional": "Ort (Zone, optional)",
"remind_on_site": "Nur erinnern, wenn jemand dort ist",
"remind_on_site_hint": "Erinnerungen warten, bis jemand in dieser Zone ist. Wer dort ankommt, bekommt eine Nachricht mit dem, was fällig ist.",
"place_missing": "Zone nicht gefunden: Erinnerungen wie zuhause",
"place_home": "Zuhause",
"installation_date_optional": "Installationsdatum (optional)",
"warranty_expiry_optional": "Garantie-Ablauf (optional)",
"warranty": "Garantie",
@@ -806,6 +811,8 @@
"owner_label": "Besitzer",
"feat_completion_actions": "Completion-Actions",
"feat_completion_actions_desc": "Pro Aufgabe HA-Action beim Abschluss konfigurieren + Quick-Complete-QR mit voreingestellten Werten.",
"feat_places": "Orte",
"feat_places_desc": "Objekte an einem anderen Ort (eine Home-Assistant-Zone): Erinnerungen nur, wenn jemand dort ist, und eine Nachricht bei Ankunft.",
"on_complete_action_title": "Beim Abschluss: HA-Action auslösen (optional)",
"on_complete_action_desc": "Ruft beim Erledigen der Aufgabe einen HA-Service auf — z. B. einen Zähler am Gerät zurücksetzen.",
"on_complete_action_target": "Ziel-Entität",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Nutzt Ersatzteil: {name}",
"label_filter": "Label",
"all_labels": "Alle Labels",
"filter_place": "Ort",
"filter_place_all": "Alle Orte",
"settings_notify_scope": "Nur benachrichtigen für Ansicht",
"settings_notify_scope_all": "Alle Aufgaben",
"settings_notify_scope_hint": "Nur Aufgaben, die zu den Label-/Nutzer-Filtern der gewählten Ansicht passen, senden Erinnerungen. Status, Sortierung und Gruppierung der Ansicht werden hier ignoriert.",
@@ -547,6 +547,11 @@
"no_objects": "No objects yet.",
"action_error": "Action failed. Please try again.",
"area_id_optional": "Area (optional)",
"place_optional": "Place (zone, optional)",
"remind_on_site": "Remind only while somebody is there",
"remind_on_site_hint": "Reminders wait until somebody is in this zone. Whoever arrives there gets a message with what is due.",
"place_missing": "Zone not found: reminders as at home",
"place_home": "Home",
"installation_date_optional": "Installation date (optional)",
"warranty_expiry_optional": "Warranty expiry (optional)",
"warranty": "Warranty",
@@ -813,6 +818,8 @@
"owner_label": "Owner",
"feat_completion_actions": "Completion actions",
"feat_completion_actions_desc": "Per-task HA action on complete + quick-complete QR with pre-set values.",
"feat_places": "Places",
"feat_places_desc": "Objects maintained at another place (a Home Assistant zone): reminders only while somebody is there, and a message on arrival.",
"on_complete_action_title": "On complete: trigger HA action (optional)",
"on_complete_action_desc": "Calls an HA service when the task is completed — e.g. reset a counter on the device.",
"on_complete_action_target": "Target entity",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Uses part: {name}",
"label_filter": "Label",
"all_labels": "All labels",
"filter_place": "Place",
"filter_place_all": "All places",
"settings_notify_scope": "Notify only for view",
"settings_notify_scope_all": "All tasks",
"settings_notify_scope_hint": "Only tasks matching the selected saved view's label/user filters send reminders. Status, sorting and grouping of the view are ignored here.",
@@ -545,6 +545,11 @@
"no_objects": "Aún no hay objetos.",
"action_error": "Acción fallida. Inténtelo de nuevo.",
"area_id_optional": "Área (opcional)",
"place_optional": "Lugar (zona, opcional)",
"remind_on_site": "Recordar solo cuando alguien esté allí",
"remind_on_site_hint": "Los recordatorios esperan a que alguien esté en esta zona. Quien llega allí recibe un mensaje con lo pendiente.",
"place_missing": "Zona no encontrada: recordatorios como en casa",
"place_home": "Casa",
"installation_date_optional": "Fecha de instalación (opcional)",
"warranty_expiry_optional": "Vencimiento de garantía (opcional)",
"warranty": "Garantía",
@@ -781,6 +786,8 @@
"owner_label": "Propietario",
"feat_completion_actions": "Acciones de finalización",
"feat_completion_actions_desc": "Acción HA por tarea al completar + QR de finalización rápida con valores prefijados.",
"feat_places": "Lugares",
"feat_places_desc": "Objetos mantenidos en otro lugar (una zona de Home Assistant): recordatorios solo cuando alguien está allí y un mensaje al llegar.",
"on_complete_action_title": "Al completar: ejecutar acción HA (opcional)",
"on_complete_action_desc": "Llama un servicio HA cuando se completa la tarea — p. ej. reiniciar un contador del dispositivo.",
"on_complete_action_target": "Entidad objetivo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa la pieza: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas las etiquetas",
"filter_place": "Lugar",
"filter_place_all": "Todos los lugares",
"settings_notify_scope": "Notificar solo para la vista",
"settings_notify_scope_all": "Todas las tareas",
"settings_notify_scope_hint": "Solo las tareas que coinciden con los filtros de etiqueta/usuario de la vista seleccionada envían recordatorios. El estado, el orden y la agrupación de la vista se ignoran aquí.",
@@ -547,6 +547,11 @@
"no_objects": "Ei vielä kohteita.",
"action_error": "Toiminto epäonnistui. Yritä uudelleen.",
"area_id_optional": "Alue (valinnainen)",
"place_optional": "Paikka (vyöhyke, valinnainen)",
"remind_on_site": "Muistuta vain, kun joku on paikalla",
"remind_on_site_hint": "Muistutukset odottavat, kunnes joku on tällä vyöhykkeellä. Sinne saapuva saa viestin tehtävistä asioista.",
"place_missing": "Vyöhykettä ei löydy: muistutukset kuin kotona",
"place_home": "Koti",
"installation_date_optional": "Asennuspäivä (valinnainen)",
"warranty_expiry_optional": "Takuun päättyminen (valinnainen)",
"warranty": "Takuu",
@@ -806,6 +811,8 @@
"owner_label": "Omistaja",
"feat_completion_actions": "Suoritustoiminnot",
"feat_completion_actions_desc": "Tehtäväkohtainen HA-toiminto suorituksen yhteydessä + pikasuoritus-QR esiasetetuilla arvoilla.",
"feat_places": "Paikat",
"feat_places_desc": "Toisessa paikassa (Home Assistant -vyöhyke) huollettavat kohteet: muistutukset vain, kun joku on paikalla, ja viesti saavuttaessa.",
"on_complete_action_title": "Suorituksen yhteydessä: laukaise HA-toiminto (valinnainen)",
"on_complete_action_desc": "Kutsuu HA-palvelua, kun tehtävä suoritetaan — esim. nollaa laitteen laskuri.",
"on_complete_action_target": "Kohde-entiteetti",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Käyttää varaosaa: {name}",
"label_filter": "Tunniste",
"all_labels": "Kaikki tunnisteet",
"filter_place": "Paikka",
"filter_place_all": "Kaikki paikat",
"settings_notify_scope": "Ilmoita vain näkymälle",
"settings_notify_scope_all": "Kaikki tehtävät",
"settings_notify_scope_hint": "Vain valitun näkymän tunniste-/käyttäjäsuodattimiin osuvat tehtävät lähettävät muistutuksia. Näkymän tila, lajittelu ja ryhmittely ohitetaan tässä.",
@@ -545,6 +545,11 @@
"no_objects": "Aucun objet pour l'instant.",
"action_error": "Action échouée. Veuillez réessayer.",
"area_id_optional": "Zone (optionnel)",
"place_optional": "Lieu (zone, facultatif)",
"remind_on_site": "Rappeler seulement quand quelqu'un y est",
"remind_on_site_hint": "Les rappels attendent que quelqu'un soit dans cette zone. Qui y arrive reçoit un message avec ce qui est à faire.",
"place_missing": "Zone introuvable : rappels comme à la maison",
"place_home": "Domicile",
"installation_date_optional": "Date d'installation (optionnel)",
"warranty_expiry_optional": "Fin de garantie (optionnel)",
"warranty": "Garantie",
@@ -781,6 +786,8 @@
"owner_label": "Propriétaire",
"feat_completion_actions": "Actions de finalisation",
"feat_completion_actions_desc": "Action HA par tâche lors de la finalisation + QR de finalisation rapide avec valeurs pré-définies.",
"feat_places": "Lieux",
"feat_places_desc": "Objets entretenus à un autre endroit (une zone Home Assistant) : rappels seulement quand quelqu'un y est, et un message à l'arrivée.",
"on_complete_action_title": "À la finalisation : déclencher une action HA (optionnel)",
"on_complete_action_desc": "Appelle un service HA quand la tâche est terminée — p. ex. réinitialiser un compteur sur l'appareil.",
"on_complete_action_target": "Entité cible",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Utilise la pièce : {name}",
"label_filter": "Étiquette",
"all_labels": "Toutes les étiquettes",
"filter_place": "Lieu",
"filter_place_all": "Tous les lieux",
"settings_notify_scope": "Notifier uniquement pour la vue",
"settings_notify_scope_all": "Toutes les tâches",
"settings_notify_scope_hint": "Seules les tâches correspondant aux filtres étiquette/utilisateur de la vue choisie envoient des rappels. Le statut, le tri et le regroupement de la vue sont ignorés ici.",
@@ -547,6 +547,11 @@
"no_objects": "अभी कोई वस्तु नहीं।",
"action_error": "क्रिया विफल। कृपया पुनः प्रयास करें।",
"area_id_optional": "क्षेत्र (वैकल्पिक)",
"place_optional": "स्थान (ज़ोन, वैकल्पिक)",
"remind_on_site": "केवल तब याद दिलाएँ जब कोई वहाँ हो",
"remind_on_site_hint": "रिमाइंडर तब तक रुकते हैं जब तक कोई इस ज़ोन में न हो। वहाँ पहुँचने वाले को बाकी कामों का संदेश मिलता है।",
"place_missing": "ज़ोन नहीं मिला: रिमाइंडर घर जैसे",
"place_home": "घर",
"installation_date_optional": "स्थापना तिथि (वैकल्पिक)",
"warranty_expiry_optional": "वारंटी समाप्ति (वैकल्पिक)",
"warranty": "वारंटी",
@@ -806,6 +811,8 @@
"owner_label": "स्वामी",
"feat_completion_actions": "पूर्णता क्रियाएँ",
"feat_completion_actions_desc": "पूर्ण होने पर प्रति-कार्य HA क्रिया + पूर्व-निर्धारित मानों के साथ त्वरित-पूर्ण QR।",
"feat_places": "स्थान",
"feat_places_desc": "किसी दूसरी जगह (Home Assistant ज़ोन) पर रखरखाव वाले ऑब्जेक्ट: रिमाइंडर तभी जब कोई वहाँ हो, और पहुँचने पर एक संदेश।",
"on_complete_action_title": "पूर्ण होने पर: HA क्रिया ट्रिगर करें (वैकल्पिक)",
"on_complete_action_desc": "कार्य पूर्ण होने पर एक HA सेवा कॉल करता है — उदा. डिवाइस पर काउंटर रीसेट करना।",
"on_complete_action_target": "लक्ष्य एंटिटी",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "पुर्ज़ा उपयोग करता है: {name}",
"label_filter": "लेबल",
"all_labels": "सभी लेबल",
"filter_place": "स्थान",
"filter_place_all": "सभी स्थान",
"settings_notify_scope": "केवल इस व्यू के लिए सूचित करें",
"settings_notify_scope_all": "सभी कार्य",
"settings_notify_scope_hint": "केवल वे कार्य अनुस्मारक भेजते हैं जो चुने गए व्यू के लेबल/उपयोगकर्ता फ़िल्टर से मेल खाते हैं। व्यू की स्थिति, क्रम और समूहन यहाँ अनदेखे रहते हैं।",
@@ -547,6 +547,11 @@
"no_objects": "Még nincsenek objektumok.",
"action_error": "A művelet nem sikerült. Próbálja újra.",
"area_id_optional": "Terület (opcionális)",
"place_optional": "Helyszín (zóna, opcionális)",
"remind_on_site": "Csak akkor emlékeztessen, ha valaki ott van",
"remind_on_site_hint": "Az emlékeztetők megvárják, amíg valaki ebben a zónában lesz. Aki odaér, üzenetet kap az esedékes teendőkről.",
"place_missing": "A zóna nem található: emlékeztetők, mint otthon",
"place_home": "Otthon",
"installation_date_optional": "Telepítés dátuma (opcionális)",
"warranty_expiry_optional": "Garancia lejárta (opcionális)",
"warranty": "Garancia",
@@ -813,6 +818,8 @@
"owner_label": "Tulajdonos",
"feat_completion_actions": "Elvégzési műveletek",
"feat_completion_actions_desc": "Feladatonkénti HA művelet elvégzéskor + gyorselvégző QR előre beállított értékekkel.",
"feat_places": "Helyszínek",
"feat_places_desc": "Másik helyszínen (Home Assistant-zóna) karbantartott objektumok: emlékeztető csak akkor, ha valaki ott van, és üzenet érkezéskor.",
"on_complete_action_title": "Elvégzéskor: HA művelet indítása (opcionális)",
"on_complete_action_desc": "HA szolgáltatást hív meg a feladat elvégzésekor — pl. számláló nullázása az eszközön.",
"on_complete_action_target": "Cél entitás",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Alkatrészt használ: {name}",
"label_filter": "Címke",
"all_labels": "Minden címke",
"filter_place": "Helyszín",
"filter_place_all": "Minden helyszín",
"settings_notify_scope": "Értesítés csak erre a nézetre",
"settings_notify_scope_all": "Minden feladat",
"settings_notify_scope_hint": "Csak a kiválasztott mentett nézet címke-/felhasználószűrőinek megfelelő feladatok küldenek emlékeztetőt. A nézet állapot-, rendezési és csoportosítási beállításait itt figyelmen kívül hagyjuk.",
@@ -545,6 +545,11 @@
"no_objects": "Nessun oggetto ancora.",
"action_error": "Azione fallita. Riprova.",
"area_id_optional": "Area (opzionale)",
"place_optional": "Luogo (zona, facoltativo)",
"remind_on_site": "Ricorda solo quando qualcuno è lì",
"remind_on_site_hint": "I promemoria aspettano che qualcuno sia in questa zona. Chi arriva lì riceve un messaggio con ciò che è da fare.",
"place_missing": "Zona non trovata: promemoria come a casa",
"place_home": "Casa",
"installation_date_optional": "Data di installazione (opzionale)",
"warranty_expiry_optional": "Scadenza garanzia (opzionale)",
"warranty": "Garanzia",
@@ -781,6 +786,8 @@
"owner_label": "Proprietario",
"feat_completion_actions": "Azioni di completamento",
"feat_completion_actions_desc": "Azione HA per attività al completamento + QR completamento rapido con valori predefiniti.",
"feat_places": "Luoghi",
"feat_places_desc": "Oggetti curati in un altro luogo (una zona di Home Assistant): promemoria solo quando qualcuno è lì e un messaggio all'arrivo.",
"on_complete_action_title": "Al completamento: attiva azione HA (opzionale)",
"on_complete_action_desc": "Chiama un servizio HA quando l'attività viene completata — es. azzerare un contatore sul dispositivo.",
"on_complete_action_target": "Entità target",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa il ricambio: {name}",
"label_filter": "Etichetta",
"all_labels": "Tutte le etichette",
"filter_place": "Luogo",
"filter_place_all": "Tutti i luoghi",
"settings_notify_scope": "Notifica solo per la vista",
"settings_notify_scope_all": "Tutte le attività",
"settings_notify_scope_hint": "Solo le attività che corrispondono ai filtri etichetta/utente della vista scelta inviano promemoria. Stato, ordinamento e raggruppamento della vista qui vengono ignorati.",
@@ -547,6 +547,11 @@
"no_objects": "対象がまだありません。",
"action_error": "操作に失敗しました。もう一度お試しください。",
"area_id_optional": "エリア(任意)",
"place_optional": "場所(ゾーン、任意)",
"remind_on_site": "誰かがいるときだけリマインド",
"remind_on_site_hint": "リマインドは誰かがこのゾーンにいるまで待機します。到着した人には、やるべきことをまとめたメッセージが届きます。",
"place_missing": "ゾーンが見つかりません:自宅と同じようにリマインドします",
"place_home": "自宅",
"installation_date_optional": "設置日(任意)",
"warranty_expiry_optional": "保証期限(任意)",
"warranty": "保証",
@@ -806,6 +811,8 @@
"owner_label": "オーナー",
"feat_completion_actions": "完了時アクション",
"feat_completion_actions_desc": "タスクごとの完了時HAアクション+事前設定値付きのクイック完了QR。",
"feat_places": "場所",
"feat_places_desc": "別の場所(Home Assistant のゾーン)で管理するオブジェクト:誰かがその場にいるときだけリマインドし、到着時に通知します。",
"on_complete_action_title": "完了時: HAアクションを実行(任意)",
"on_complete_action_desc": "タスク完了時にHAサービスを呼び出します — 例: デバイスのカウンターをリセット。",
"on_complete_action_target": "対象エンティティ",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "使用する部品: {name}",
"label_filter": "ラベル",
"all_labels": "すべてのラベル",
"filter_place": "場所",
"filter_place_all": "すべての場所",
"settings_notify_scope": "このビューのみ通知",
"settings_notify_scope_all": "すべてのタスク",
"settings_notify_scope_hint": "選択した保存済みビューのラベル/ユーザーフィルターに一致するタスクのみがリマインダーを送信します。ビューのステータス・並び順・グループ化はここでは無視されます。",
@@ -547,6 +547,11 @@
"no_objects": "아직 객체가 없습니다.",
"action_error": "동작에 실패했습니다. 다시 시도해 주세요.",
"area_id_optional": "구역 (선택)",
"place_optional": "장소 (구역, 선택 사항)",
"remind_on_site": "누군가 있을 때만 알림",
"remind_on_site_hint": "누군가 이 구역에 있을 때까지 알림이 대기합니다. 그곳에 도착한 사람은 해야 할 일을 담은 메시지를 받습니다.",
"place_missing": "구역을 찾을 수 없음: 집과 같이 알림",
"place_home": "집",
"installation_date_optional": "설치일 (선택)",
"warranty_expiry_optional": "보증 만료일 (선택)",
"warranty": "보증",
@@ -813,6 +818,8 @@
"owner_label": "소유자",
"feat_completion_actions": "완료 동작",
"feat_completion_actions_desc": "완료 시 작업별 HA 동작 실행 + 사전 설정값이 있는 빠른 완료 QR.",
"feat_places": "장소",
"feat_places_desc": "다른 장소(Home Assistant 구역)에서 관리하는 개체: 누군가 그곳에 있을 때만 알림을 보내고, 도착하면 메시지를 보냅니다.",
"on_complete_action_title": "완료 시: HA 동작 실행 (선택)",
"on_complete_action_desc": "작업이 완료되면 HA 서비스를 호출합니다 — 예: 기기의 카운터 초기화.",
"on_complete_action_target": "대상 엔티티",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "사용 부품: {name}",
"label_filter": "라벨",
"all_labels": "모든 라벨",
"filter_place": "장소",
"filter_place_all": "모든 장소",
"settings_notify_scope": "특정 보기만 알림",
"settings_notify_scope_all": "모든 작업",
"settings_notify_scope_hint": "선택한 저장된 보기의 라벨/사용자 필터와 일치하는 작업만 알림을 보냅니다. 보기의 상태·정렬·그룹화는 여기서 무시됩니다.",
@@ -547,6 +547,11 @@
"no_objects": "Ingen objekter ennå.",
"action_error": "Handlingen mislyktes. Prøv igjen.",
"area_id_optional": "Område (valgfritt)",
"place_optional": "Sted (sone, valgfritt)",
"remind_on_site": "Påminn bare når noen er der",
"remind_on_site_hint": "Påminnelser venter til noen er i denne sonen. Den som kommer dit, får en melding med det som skal gjøres.",
"place_missing": "Fant ikke sonen: påminnelser som hjemme",
"place_home": "Hjemme",
"installation_date_optional": "Installasjonsdato (valgfritt)",
"warranty_expiry_optional": "Garanti utløper (valgfritt)",
"warranty": "Garanti",
@@ -806,6 +811,8 @@
"owner_label": "Eier",
"feat_completion_actions": "Fullføringshandlinger",
"feat_completion_actions_desc": "HA-handling per oppgave ved fullføring + hurtigfullføring med QR og forhåndsinnstilte verdier.",
"feat_places": "Steder",
"feat_places_desc": "Objekter som vedlikeholdes et annet sted (en Home Assistant-sone): påminnelser bare når noen er der, og en melding ved ankomst.",
"on_complete_action_title": "Ved fullføring: utløs HA-handling (valgfritt)",
"on_complete_action_desc": "Kaller en HA-tjeneste når oppgaven fullføres — f.eks. tilbakestille en teller på enheten.",
"on_complete_action_target": "Målentitet",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Bruker del: {name}",
"label_filter": "Etikett",
"all_labels": "Alle etiketter",
"filter_place": "Sted",
"filter_place_all": "Alle steder",
"settings_notify_scope": "Varsle kun for visning",
"settings_notify_scope_all": "Alle oppgaver",
"settings_notify_scope_hint": "Bare oppgaver som matcher etikett-/brukerfiltrene til valgt visning sender påminnelser. Visningens status, sortering og gruppering ignoreres her.",
@@ -545,6 +545,11 @@
"no_objects": "Nog geen objecten.",
"action_error": "Actie mislukt. Probeer het opnieuw.",
"area_id_optional": "Gebied (optioneel)",
"place_optional": "Locatie (zone, optioneel)",
"remind_on_site": "Alleen herinneren als er iemand is",
"remind_on_site_hint": "Herinneringen wachten tot iemand in deze zone is. Wie daar aankomt, krijgt een bericht met wat er te doen is.",
"place_missing": "Zone niet gevonden: herinneringen zoals thuis",
"place_home": "Thuis",
"installation_date_optional": "Installatiedatum (optioneel)",
"warranty_expiry_optional": "Garantie verloopt (optioneel)",
"warranty": "Garantie",
@@ -781,6 +786,8 @@
"owner_label": "Eigenaar",
"feat_completion_actions": "Voltooiings-acties",
"feat_completion_actions_desc": "Per taak HA-actie bij voltooien + snel-voltooien-QR met vooraf ingestelde waarden.",
"feat_places": "Locaties",
"feat_places_desc": "Objecten op een andere locatie (een Home Assistant-zone): herinneringen alleen als er iemand is, en een bericht bij aankomst.",
"on_complete_action_title": "Bij voltooien: HA-actie uitvoeren (optioneel)",
"on_complete_action_desc": "Roept een HA-service aan wanneer de taak is voltooid — bv. een teller op het apparaat resetten.",
"on_complete_action_target": "Doel-entiteit",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Gebruikt onderdeel: {name}",
"label_filter": "Label",
"all_labels": "Alle labels",
"filter_place": "Locatie",
"filter_place_all": "Alle locaties",
"settings_notify_scope": "Alleen melden voor weergave",
"settings_notify_scope_all": "Alle taken",
"settings_notify_scope_hint": "Alleen taken die overeenkomen met de label-/gebruikersfilters van de gekozen weergave sturen herinneringen. Status, sortering en groepering van de weergave worden hier genegeerd.",
@@ -545,6 +545,11 @@
"no_objects": "Brak obiektów.",
"action_error": "Akcja nie powiodła się. Spróbuj ponownie.",
"area_id_optional": "Obszar (opcjonalny)",
"place_optional": "Miejsce (strefa, opcjonalnie)",
"remind_on_site": "Przypominaj tylko, gdy ktoś tam jest",
"remind_on_site_hint": "Przypomnienia czekają, aż ktoś będzie w tej strefie. Kto tam przyjedzie, dostaje wiadomość z tym, co jest do zrobienia.",
"place_missing": "Nie znaleziono strefy: przypomnienia jak w domu",
"place_home": "Dom",
"installation_date_optional": "Data instalacji (opcjonalna)",
"warranty_expiry_optional": "Wygaśnięcie gwarancji (opcjonalnie)",
"warranty": "Gwarancja",
@@ -781,6 +786,8 @@
"owner_label": "Właściciel",
"feat_completion_actions": "Akcje po zakończeniu",
"feat_completion_actions_desc": "Akcja HA per zadanie po zakończeniu + QR szybkiego zakończenia z predefiniowanymi wartościami.",
"feat_places": "Miejsca",
"feat_places_desc": "Obiekty w innym miejscu (strefa Home Assistant): przypomnienia tylko wtedy, gdy ktoś tam jest, i wiadomość po przybyciu.",
"on_complete_action_title": "Po zakończeniu: wywołaj akcję HA (opcjonalnie)",
"on_complete_action_desc": "Wywołuje usługę HA po zakończeniu zadania — np. zresetuj licznik urządzenia.",
"on_complete_action_target": "Encja docelowa",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Używa części: {name}",
"label_filter": "Etykieta",
"all_labels": "Wszystkie etykiety",
"filter_place": "Miejsce",
"filter_place_all": "Wszystkie miejsca",
"settings_notify_scope": "Powiadamiaj tylko dla widoku",
"settings_notify_scope_all": "Wszystkie zadania",
"settings_notify_scope_hint": "Przypomnienia wysyłają tylko zadania pasujące do filtrów etykiety/użytkownika wybranego widoku. Status, sortowanie i grupowanie widoku są tu ignorowane.",
@@ -547,6 +547,11 @@
"no_objects": "Ainda não há objetos.",
"action_error": "A ação falhou. Tente novamente.",
"area_id_optional": "Área (opcional)",
"place_optional": "Local (zona, opcional)",
"remind_on_site": "Lembrar só quando alguém estiver lá",
"remind_on_site_hint": "Os lembretes esperam até alguém estar nesta zona. Quem chega lá recebe uma mensagem com o que está pendente.",
"place_missing": "Zona não encontrada: lembretes como em casa",
"place_home": "Casa",
"installation_date_optional": "Data de instalação (opcional)",
"warranty_expiry_optional": "Fim da garantia (opcional)",
"warranty": "Garantia",
@@ -813,6 +818,8 @@
"owner_label": "Proprietário",
"feat_completion_actions": "Ações de conclusão",
"feat_completion_actions_desc": "Ação do HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.",
"feat_places": "Locais",
"feat_places_desc": "Objetos mantidos em outro local (uma zona do Home Assistant): lembretes só quando alguém está lá e uma mensagem na chegada.",
"on_complete_action_title": "Ao concluir: acionar ação do HA (opcional)",
"on_complete_action_desc": "Chama um serviço do HA quando a tarefa é concluída — ex.: zerar um contador no dispositivo.",
"on_complete_action_target": "Entidade alvo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa a peça: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas as etiquetas",
"filter_place": "Local",
"filter_place_all": "Todos os locais",
"settings_notify_scope": "Notificar apenas para a visualização",
"settings_notify_scope_all": "Todas as tarefas",
"settings_notify_scope_hint": "Apenas tarefas que correspondem aos filtros de etiqueta/usuário da visualização salva selecionada enviam lembretes. Status, ordenação e agrupamento da visualização são ignorados aqui.",
@@ -545,6 +545,11 @@
"no_objects": "Ainda sem objetos.",
"action_error": "Ação falhada. Tente novamente.",
"area_id_optional": "Área (opcional)",
"place_optional": "Local (zona, opcional)",
"remind_on_site": "Lembrar só quando alguém lá estiver",
"remind_on_site_hint": "Os lembretes esperam até alguém estar nesta zona. Quem lá chega recebe uma mensagem com o que está pendente.",
"place_missing": "Zona não encontrada: lembretes como em casa",
"place_home": "Casa",
"installation_date_optional": "Data de instalação (opcional)",
"warranty_expiry_optional": "Validade da garantia (opcional)",
"warranty": "Garantia",
@@ -781,6 +786,8 @@
"owner_label": "Proprietário",
"feat_completion_actions": "Ações de conclusão",
"feat_completion_actions_desc": "Ação HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.",
"feat_places": "Locais",
"feat_places_desc": "Objetos mantidos noutro local (uma zona do Home Assistant): lembretes só quando alguém lá está e uma mensagem à chegada.",
"on_complete_action_title": "Na conclusão: acionar ação HA (opcional)",
"on_complete_action_desc": "Chama um serviço HA quando a tarefa é concluída — p. ex. reiniciar um contador no dispositivo.",
"on_complete_action_target": "Entidade alvo",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Usa a peça: {name}",
"label_filter": "Etiqueta",
"all_labels": "Todas as etiquetas",
"filter_place": "Local",
"filter_place_all": "Todos os locais",
"settings_notify_scope": "Notificar apenas para a vista",
"settings_notify_scope_all": "Todas as tarefas",
"settings_notify_scope_hint": "Apenas as tarefas que correspondem aos filtros de etiqueta/utilizador da vista escolhida enviam lembretes. O estado, a ordenação e o agrupamento da vista são ignorados aqui.",
@@ -545,6 +545,11 @@
"no_objects": "Пока нет объектов.",
"action_error": "Не удалось выполнить действие. Попробуйте ещё раз.",
"area_id_optional": "Зона (опционально)",
"place_optional": "Место (зона, необязательно)",
"remind_on_site": "Напоминать, только когда там кто-то есть",
"remind_on_site_hint": "Напоминания ждут, пока кто-нибудь окажется в этой зоне. Кто туда приезжает, получает сообщение о том, что нужно сделать.",
"place_missing": "Зона не найдена: напоминания как дома",
"place_home": "Дом",
"installation_date_optional": "Дата установки (опционально)",
"warranty_expiry_optional": "Окончание гарантии (опционально)",
"warranty": "Гарантия",
@@ -781,6 +786,8 @@
"owner_label": "Владелец",
"feat_completion_actions": "Действия при завершении",
"feat_completion_actions_desc": "HA-действие по задаче при завершении + QR быстрого завершения с предустановленными значениями.",
"feat_places": "Места",
"feat_places_desc": "Объекты в другом месте (зона Home Assistant): напоминания только когда там кто-то есть, и сообщение по прибытии.",
"on_complete_action_title": "При завершении: запустить HA-действие (опционально)",
"on_complete_action_desc": "Вызывает HA-сервис при завершении задачи — напр., сбросить счётчик на устройстве.",
"on_complete_action_target": "Целевая сущность",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Использует деталь: {name}",
"label_filter": "Метка",
"all_labels": "Все метки",
"filter_place": "Место",
"filter_place_all": "Все места",
"settings_notify_scope": "Уведомлять только для представления",
"settings_notify_scope_all": "Все задачи",
"settings_notify_scope_hint": "Напоминания отправляют только задачи, совпадающие с фильтрами метки/пользователя выбранного представления. Статус, сортировка и группировка представления здесь игнорируются.",
@@ -545,6 +545,11 @@
"no_objects": "Inga objekt än.",
"action_error": "Åtgärden misslyckades. Försök igen.",
"area_id_optional": "Område (valfritt)",
"place_optional": "Plats (zon, valfritt)",
"remind_on_site": "Påminn bara när någon är där",
"remind_on_site_hint": "Påminnelser väntar tills någon är i den här zonen. Den som kommer dit får ett meddelande med det som ska göras.",
"place_missing": "Zonen hittades inte: påminnelser som hemma",
"place_home": "Hemma",
"installation_date_optional": "Installationsdatum (valfritt)",
"warranty_expiry_optional": "Garantins utgång (valfritt)",
"warranty": "Garanti",
@@ -781,6 +786,8 @@
"owner_label": "Ägare",
"feat_completion_actions": "Slutförande-åtgärder",
"feat_completion_actions_desc": "HA-åtgärd per uppgift vid slutförande + snabb-slutför-QR med förinställda värden.",
"feat_places": "Platser",
"feat_places_desc": "Objekt som sköts på en annan plats (en Home Assistant-zon): påminnelser bara när någon är där, och ett meddelande vid ankomst.",
"on_complete_action_title": "Vid slutförande: utlös HA-åtgärd (valfritt)",
"on_complete_action_desc": "Anropar en HA-tjänst när uppgiften slutförs — t.ex. återställ en räknare på enheten.",
"on_complete_action_target": "Målentitet",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Använder reservdel: {name}",
"label_filter": "Etikett",
"all_labels": "Alla etiketter",
"filter_place": "Plats",
"filter_place_all": "Alla platser",
"settings_notify_scope": "Avisera endast för vy",
"settings_notify_scope_all": "Alla uppgifter",
"settings_notify_scope_hint": "Endast uppgifter som matchar den valda vyns etikett-/användarfilter skickar påminnelser. Vyns status, sortering och gruppering ignoreras här.",
@@ -547,6 +547,11 @@
"no_objects": "Henüz nesne yok.",
"action_error": "Eylem başarısız oldu. Lütfen tekrar deneyin.",
"area_id_optional": "Alan (isteğe bağlı)",
"place_optional": "Yer (bölge, isteğe bağlı)",
"remind_on_site": "Yalnızca biri oradayken hatırlat",
"remind_on_site_hint": "Hatırlatmalar biri bu bölgede olana kadar bekler. Oraya varan kişi yapılacakların listesini içeren bir mesaj alır.",
"place_missing": "Bölge bulunamadı: hatırlatmalar evdeki gibi",
"place_home": "Ev",
"installation_date_optional": "Kurulum tarihi (isteğe bağlı)",
"warranty_expiry_optional": "Garanti bitişi (isteğe bağlı)",
"warranty": "Garanti",
@@ -813,6 +818,8 @@
"owner_label": "Sahip",
"feat_completion_actions": "Tamamlama eylemleri",
"feat_completion_actions_desc": "Görev başına tamamlamada HA eylemi + önceden ayarlı değerlerle hızlı tamamlama QR'ı.",
"feat_places": "Yerler",
"feat_places_desc": "Başka bir yerde (Home Assistant bölgesi) bakımı yapılan nesneler: hatırlatmalar yalnızca biri oradayken, varışta bir mesaj.",
"on_complete_action_title": "Tamamlandığında: HA eylemi tetikle (isteğe bağlı)",
"on_complete_action_desc": "Görev tamamlandığında bir HA servisi çağırır — örn. cihazdaki bir sayacı sıfırlar.",
"on_complete_action_target": "Hedef varlık",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Kullanılan parça: {name}",
"label_filter": "Etiket",
"all_labels": "Tüm etiketler",
"filter_place": "Yer",
"filter_place_all": "Tüm yerler",
"settings_notify_scope": "Yalnızca şu görünüm için bildir",
"settings_notify_scope_all": "Tüm görevler",
"settings_notify_scope_hint": "Yalnızca seçili kayıtlı görünümün etiket/kullanıcı filtreleriyle eşleşen görevler hatırlatma gönderir. Görünümün durum, sıralama ve gruplaması burada dikkate alınmaz.",
@@ -545,6 +545,11 @@
"no_objects": "Поки немає об'єктів.",
"action_error": "Дія не вдалась. Спробуйте ще раз.",
"area_id_optional": "Зона (необов'язково)",
"place_optional": "Місце (зона, необов'язково)",
"remind_on_site": "Нагадувати, лише коли там хтось є",
"remind_on_site_hint": "Нагадування чекають, поки хтось буде в цій зоні. Хто туди прибуває, отримує повідомлення про те, що потрібно зробити.",
"place_missing": "Зону не знайдено: нагадування як удома",
"place_home": "Дім",
"installation_date_optional": "Дата встановлення (необов'язково)",
"warranty_expiry_optional": "Закінчення гарантії (необов'язково)",
"warranty": "Гарантія",
@@ -781,6 +786,8 @@
"owner_label": "Власник",
"feat_completion_actions": "Дії при завершенні",
"feat_completion_actions_desc": "Дія HA по завданню при завершенні + QR швидкого завершення з попередньо встановленими значеннями.",
"feat_places": "Місця",
"feat_places_desc": "Об'єкти в іншому місці (зона Home Assistant): нагадування лише тоді, коли там хтось є, і повідомлення після прибуття.",
"on_complete_action_title": "При завершенні: викликати HA-дію (опційно)",
"on_complete_action_desc": "Викликає HA-сервіс, коли завдання завершено — напр., скинути лічильник на пристрої.",
"on_complete_action_target": "Цільова сутність",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "Використовує деталь: {name}",
"label_filter": "Мітка",
"all_labels": "Усі мітки",
"filter_place": "Місце",
"filter_place_all": "Усі місця",
"settings_notify_scope": "Сповіщати лише для подання",
"settings_notify_scope_all": "Усі завдання",
"settings_notify_scope_hint": "Нагадування надсилають лише завдання, що відповідають фільтрам мітки/користувача обраного подання. Статус, сортування та групування подання тут ігноруються.",
@@ -547,6 +547,11 @@
"no_objects": "暂无维护项。",
"action_error": "操作失败。请重试。",
"area_id_optional": "区域 (可选)",
"place_optional": "地点(区域,可选)",
"remind_on_site": "仅当有人在场时提醒",
"remind_on_site_hint": "提醒会等到有人在此区域时再发送。到达那里的人会收到一条列出待办事项的消息。",
"place_missing": "未找到区域:按在家处理提醒",
"place_home": "家",
"installation_date_optional": "安装日期 (可选)",
"warranty_expiry_optional": "保修到期 (可选)",
"warranty": "保修",
@@ -806,6 +811,8 @@
"owner_label": "所有者",
"feat_completion_actions": "完成动作",
"feat_completion_actions_desc": "为每个任务配置完成时的 HA 动作 + 预设值的快速完成二维码。",
"feat_places": "地点",
"feat_places_desc": "在其他地点(Home Assistant 区域)维护的对象:仅当有人在场时提醒,并在到达时发送消息。",
"on_complete_action_title": "完成时:触发 HA 动作(可选)",
"on_complete_action_desc": "任务完成时调用 HA 服务 — 例如重置设备上的物理计数器。",
"on_complete_action_target": "目标实体",
@@ -1125,6 +1132,8 @@
"adopt_problem_part": "使用配件:{name}",
"label_filter": "标签",
"all_labels": "所有标签",
"filter_place": "地点",
"filter_place_all": "所有地点",
"settings_notify_scope": "仅为该视图发送通知",
"settings_notify_scope_all": "所有任务",
"settings_notify_scope_hint": "只有匹配所选视图的标签/用户筛选的任务才会发送提醒。视图的状态、排序和分组在此处被忽略。",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,301 @@
/*! maintenance_supporter frontend 2.99.0 */
import{e as y}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-PCR5FRSH.js";import{d as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-RHKFQ4Q2.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GM2CHGSG.js";import{H as b,a as o,b as u,c as n,f as l,h as v,l as g,m as d,s as r,u as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._objectNames={};this._objects=[];this._covered={};this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=t=>{let i=new Set(this._selected);i.has(t)?i.delete(t):i.add(t),this._selected=i};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(t=>t.entity_id))};this._adopt=async()=>{if(this._selected.size===0||this._adopting)return;this._error="";let t=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)??void 0,object_name:this._effectiveName(e),device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),i=await h(this,{type:"maintenance_supporter/problem_sensors/adopt",selections:t},{busy:e=>{this._adopting=e},onError:e=>{this._error=e}});i!==void 0&&(this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:i})),this._open=!1)}}get _lang(){return b(this.hass)}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._objectNames={},this._responsible="",this._forMinutes="0",this._userService?this._userService.updateHass(this.hass):this._userService=new y(this.hass);let[t,i,e]=await Promise.all([h(this,{type:"maintenance_supporter/problem_sensors/discover"},{onError:s=>{this._error=s}}),this._userService.getUsers().catch(()=>[]),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}).catch(()=>({objects:[]}))]);this._loading=!1,t!==void 0&&(this._objects=(e?.objects||[]).filter(s=>!s.object.archived_at).map(s=>({entry_id:s.entry_id,name:s.object.name,tasks:(s.tasks||[]).length})),this._sensors=t?.sensors||[],this._covered=Object.fromEntries(this._sensors.map(s=>[s.entity_id,s.covered_by??null])),this._selected=new Set(this._sensors.filter(s=>!s.covered_by).map(s=>s.entity_id)),this._users=i)}_defaultName(t){return t.suggested_entry_id?t.suggested_object_name:t.candidate?.name??t.suggested_object_name}_effectiveName(t){return(this._objectNames[t.entity_id]??"").trim()||this._defaultName(t)}_existingEntryFor(t){let i=this._effectiveName(t),e=t.suggested_entry_id||t.target_entry_id||null;if(i===this._defaultName(t)&&e)return e;let s=this._objects.find(c=>c.name.trim().toLowerCase()===i.toLowerCase());return s?s.entry_id:null}_schedulePreview(){clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._preview()},350)}async _preview(){let t=this._sensors.map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)})),i=await h(this,{type:"maintenance_supporter/problem_sensors/preview",selections:t},{onError:()=>{}});i?.covered&&(this._covered={...this._covered,...i.covered})}_summary(t){let i=new Map;for(let e of this._sensors){if(!this._selected.has(e.entity_id))continue;let s=this._existingEntryFor(e),c=this._effectiveName(e),_=s??`new:${c.toLowerCase()}`,p=i.get(_)??{name:c,entry:s,adding:0};p.adding+=1,i.set(_,p)}return i.size===0?l:n`
<div class="summary">
<div class="summary-title">${r("adopt_summary_title",t)}</div>
${[...i.values()].map(e=>{if(!e.entry)return n`<div class="summary-line new">
${r("setups_new_object_count",t).replace("{name}",e.name).replace("{count}",String(e.adding))}
</div>`;let s=this._objects.find(c=>c.entry_id===e.entry)?.tasks??0;return n`<div class="summary-line">
${r("adopt_summary_existing",t).replace("{name}",e.name).replace("{before}",String(s)).replace("{after}",String(s+e.adding))}
</div>`})}
</div>
`}_close(){clearTimeout(this._previewTimer),this._open=!1}_targetLabel(t,i){if(t.candidate&&this._existingEntryFor(t)===t.candidate.entry_id){let e=t.candidate.reasons.map(s=>r(`setups_reason_${s}`,i)).join(", ");return r("setups_target_match",i).replace("{name}",t.candidate.name).replace("{reasons}",e)}return this._effectiveName(t)}render(){if(!this._open)return n``;let t=this._lang,i=this._sensors.length>0&&this._selected.size===this._sensors.length;return n`
<div class="overlay" @click=${this._close}>
<div class="card" @click=${e=>e.stopPropagation()}>
<div class="title">${r("adopt_problem_title",t)}</div>
<div class="hint">${r("adopt_problem_hint",t)}</div>
${this._error?n`<div class="error">${this._error}</div>`:l}
${this._loading?n`<div class="loading">…</div>`:this._sensors.length===0?n`<div class="empty">${r("adopt_problem_none",t)}</div>`:n`
<label class="select-all">
<input
type="checkbox"
.checked=${i}
@change=${this._toggleAll}
/>
<span>${r("selected",t)}: ${this._selected.size} / ${this._sensors.length}</span>
</label>
<div class="row-sub adopt-object-hint">${r("adopt_object_hint",t)}</div>
<datalist id="adopt-object-names">
${this._objects.map(e=>n`<option value=${e.name}></option>`)}
</datalist>
<div class="list">
${this._sensors.map(e=>{let s=this._selected.has(e.entity_id),c=e.state==="on",_=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return n`
<label class="row">
<input
type="checkbox"
.checked=${s}
@change=${()=>this._toggle(e.entity_id)}
/>
<div class="row-main">
<div class="row-top">
<span class="row-name">${e.name}</span>
<span class="chip ${c?"chip-active":"chip-ok"}">
${c?r("adopt_problem_active",t):r("adopt_problem_ok",t)}
</span>
</div>
${_?n`<div class="row-sub">${_}</div>`:l}
${s?n`<div class="row-object" @click=${p=>p.stopPropagation()}>
<input
class="adopt-object"
list="adopt-object-names"
aria-label=${r("object",t)}
placeholder=${this._defaultName(e)}
.value=${this._objectNames[e.entity_id]??this._defaultName(e)}
@input=${p=>{this._objectNames={...this._objectNames,[e.entity_id]:p.target.value},this._schedulePreview()}}
/>
</div>`:l}
<div class="row-target">
→ ${this._targetLabel(e,t)}${this._existingEntryFor(e)?l:n` <span class="new-tag">${r("adopt_problem_new_object",t)}</span>`}
</div>
${this._covered[e.entity_id]?n`<div class="maybe">
${r("setups_maybe_covered",t).replace("{name}",this._covered[e.entity_id].name)}
</div>`:l}
${e.suggested_part_name?n`<div class="row-part">
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
${r("adopt_problem_part",t).replace("{name}",e.suggested_part_name)}
</div>`:l}
</div>
</label>
`})}
</div>
`}
${!this._loading&&this._sensors.length>0?this._summary(t):l}
${!this._loading&&this._sensors.length>0?n`
<label class="responsible">
<span>${r("for_at_least_minutes",t)}</span>
<input
class="for-input"
type="number"
min=${m[0]}
max=${m[1]}
.value=${this._forMinutes}
@input=${e=>this._forMinutes=e.target.value}
/>
</label>
<div class="for-hint">${r("adopt_for_minutes_hint",t)}</div>
`:l}
${!this._loading&&this._sensors.length>0&&this._users.length>0?n`
<label class="responsible">
<span>${r("adopt_problem_responsible",t)}</span>
<select
.value=${this._responsible}
@change=${e=>{this._responsible=e.target.value}}
>
<option value="" ?selected=${!this._responsible}>${r("no_user_assigned",t)}</option>
${this._users.map(e=>n`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
</select>
</label>
`:l}
<div class="actions">
<ha-button appearance="plain" @click=${this._close}>
${r("cancel",t)}
</ha-button>
<ha-button
@click=${this._adopt}
.disabled=${this._selected.size===0||this._adopting}
>
${r("adopt_problem_adopt",t)}
</ha-button>
</div>
</div>
</div>
`}};a.styles=u`
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.card {
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
min-width: min(360px, calc(100vw - 24px));
max-width: 560px;
width: 90vw;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.title {
font-size: 18px;
font-weight: 500;
}
.hint {
color: var(--secondary-text-color);
font-size: 13px;
}
.error {
color: var(--error-color, #f44336);
font-size: 13px;
}
.loading,
.empty {
color: var(--secondary-text-color);
font-size: 14px;
padding: 12px 0;
}
.select-all {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--secondary-text-color);
cursor: pointer;
}
.select-all input {
cursor: pointer;
}
.list {
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
max-height: 50vh;
}
.row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px;
border: 1px solid var(--divider-color);
border-radius: 6px;
cursor: pointer;
}
.row input {
margin-top: 2px;
cursor: pointer;
}
.row-main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.row-top {
display: flex;
align-items: center;
gap: 8px;
}
.row-name {
font-weight: 500;
font-size: 13px;
}
.row-sub {
color: var(--secondary-text-color);
font-size: 12px;
}
.row-object { margin-top: 6px; }
.row-object input {
width: 100%; box-sizing: border-box; padding: 6px 8px; font: inherit;
border: 1px solid var(--divider-color); border-radius: 6px;
background: var(--card-background-color); color: var(--primary-text-color);
}
.adopt-object-hint { margin: 0 0 8px; }
.row-target {
color: var(--secondary-text-color);
font-size: 12px;
}
.row-part {
color: var(--secondary-text-color);
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.row-part ha-icon {
--mdc-icon-size: 14px;
}
.new-tag {
font-style: italic;
}
.maybe {
font-size: 11px;
color: var(--warning-color, #ff9800);
}
.summary {
font-size: 12px;
border-top: 1px dashed var(--divider-color);
padding-top: 6px;
}
.summary-title {
font-weight: 500;
margin-bottom: 2px;
}
.summary-line {
color: var(--primary-color);
}
.chip {
font-size: 11px;
padding: 1px 8px;
border-radius: 10px;
white-space: nowrap;
}
.chip-active {
background: var(--error-color, #f44336);
color: #fff;
}
.chip-ok {
background: var(--divider-color);
color: var(--secondary-text-color);
}
.responsible {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--secondary-text-color);
flex-wrap: wrap;
}
.for-input {
width: 70px;
background: var(--card-background-color);
color: var(--primary-text-color);
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 4px 6px;
}
.for-hint {
font-size: 11px;
color: var(--secondary-text-color);
margin: -4px 0 2px;
}
.responsible select {
flex: 1;
min-width: 140px;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--divider-color);
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
font-size: 13px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 8px;
}
`,o([g({attribute:!1})],a.prototype,"hass",2),o([d()],a.prototype,"_open",2),o([d()],a.prototype,"_loading",2),o([d()],a.prototype,"_adopting",2),o([d()],a.prototype,"_error",2),o([d()],a.prototype,"_sensors",2),o([d()],a.prototype,"_selected",2),o([d()],a.prototype,"_objectNames",2),o([d()],a.prototype,"_objects",2),o([d()],a.prototype,"_covered",2),o([d()],a.prototype,"_users",2),o([d()],a.prototype,"_responsible",2),o([d()],a.prototype,"_forMinutes",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",a);export{a as MaintenanceAdoptProblemSensorsDialog};
@@ -0,0 +1,279 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as X}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MQRW7LAF.js";import{a as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-5MYBEQC2.js";import{f as E,g as M,h as z,i as Q,j as U,k as Y,l as q,m as G,n as R,o as A,p as W}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MGSZZ3YF.js";import{a as B}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-EKN6HBJN.js";import{e as P,f as K}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-DRT62KZF.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SYPCMUXO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-55ND3GKE.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RUYEMIDM.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JQXHX4U4.js";import{B as S,H as F,K as g,L as v,O as N,P as I,T as w,V as T,a as $,c as p,f as y,h as H,l as k,m as j,n as C,q as D,s as a,u as L,y as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";function J(b,i,t,s){let l=b.entries.filter(e=>e.type==="completed").sort((e,n)=>e.ts-n.ts),d=`${b.from?t.date(b.from):"\u2026"} \u2013 ${b.to?t.date(b.to):"\u2026"}`,r=b.totals,h=[[i.completions,t.number(r.completions)],[i.totalCost,t.cost(r.totalCost)],[i.avgCost,r.avgCost!=null?t.cost(r.avgCost):i.none],[i.totalTime,r.totalDuration?t.duration(r.totalDuration):i.none]].map(([e,n])=>`<div class="kpi"><div class="k">${o(e)}</div><div class="v">${o(n)}</div></div>`).join(""),u=b.byObject.map(e=>`<tr>
<td>${o(e.objectName)}</td>
<td class="num">${o(t.number(e.completions))}</td>
<td class="num">${o(t.cost(e.cost))}</td>
<td class="num">${o(t.share(e.share))}</td>
</tr>`).join(""),f=b.buckets.buckets.reduce((e,n)=>Math.max(e,n.cost),0),c=b.buckets.buckets.map(e=>{let n=f>0?Math.round(e.cost/f*100):0;return`<tr>
<td class="nowrap">${o(t.bucket(e))}</td>
<td class="num">${o(t.number(e.completions))}</td>
<td class="bar-cell">${n>0?`<span class="bar" style="width:${n}%"></span>`:""}</td>
<td class="num">${o(t.cost(e.cost))}</td>
</tr>`}).join(""),m=l.map(e=>{let n=[e.notes,e.completedBy?`${i.completedBy}: ${e.completedBy}`:null].filter(Boolean).join(" \xB7 "),x=e.phaseName?`${e.taskName} \xB7 ${e.phaseName}`:e.taskName;return`<tr>
<td class="nowrap">${o(t.date(e.timestamp))}</td>
<td>${o(e.objectName)}</td>
<td>${o(x)}</td>
<td class="num">${e.cost!=null?o(t.cost(e.cost)):o(i.none)}</td>
<td class="num">${e.duration!=null?o(t.duration(e.duration)):o(i.none)}</td>
<td class="notes">${o(n)||o(i.none)}</td>
</tr>`}).join(`
`);return`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${o(i.title)} \u2014 ${o(b.areaName)}</title>
<style>
/* Printable sheet: it opens as a blob in whatever viewer the OS supplies
(Companion = WebView, dark phones paint a dark default canvas), so the
document states its own light scheme and paints its background. */
:root { color-scheme: light; }
* { box-sizing: border-box; }
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; }
h2 { font-size: 15px; margin: 24px 0 8px; }
.sub { color: #666; margin: 0 0 16px; }
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 0 0 8px; }
.kpi { border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; }
.kpi .k { color: #777; font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; }
.kpi .v { font-size: 16px; font-weight: 600; }
table { border-collapse: collapse; width: 100%; }
th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: #666; border-bottom: 2px solid #ccc; padding: 6px 8px; }
td { border-bottom: 1px solid #e5e5e5; padding: 6px 8px; vertical-align: top; }
td.num, th.num { text-align: right; white-space: nowrap; }
td.nowrap { white-space: nowrap; }
td.notes { color: #444; white-space: pre-wrap; }
td.bar-cell { width: 40%; vertical-align: middle; }
.bar { display: block; height: 10px; background: #9aa7b8; border-radius: 0 3px 3px 0;
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
tfoot td { border-bottom: none; border-top: 2px solid #ccc; font-weight: 600; }
tr { break-inside: avoid; }
.cap-note { margin-top: 14px; color: #888; font-size: 11px; }
@media print { body { margin: 12mm; } }
</style>
</head>
<body>
<h1>${o(i.title)} \u2014 ${o(b.areaName)}</h1>
<p class="sub">${o(i.period)}: ${o(d)} \xB7 ${o(i.generated)} ${o(t.date(s))}</p>
<div class="kpis">${h}</div>
<h2>${o(i.costPerObject)}</h2>
<table>
<thead><tr>
<th>${o(i.colObject)}</th><th class="num">${o(i.completions)}</th>
<th class="num">${o(i.colCost)}</th><th class="num">${o(i.share)}</th>
</tr></thead>
<tbody>${u||`<tr><td colspan="4">${o(i.none)}</td></tr>`}</tbody>
<tfoot><tr>
<td>${o(i.totalCost)}</td><td class="num">${o(t.number(r.completions))}</td>
<td class="num">${o(t.cost(r.totalCost))}</td><td></td>
</tr></tfoot>
</table>
<h2>${o(i.costPerBucket)}</h2>
<table>
<thead><tr>
<th>${o(i.period)}</th><th class="num">${o(i.completions)}</th>
<th></th><th class="num">${o(i.colCost)}</th>
</tr></thead>
<tbody>${c||`<tr><td colspan="4">${o(i.none)}</td></tr>`}</tbody>
</table>
<h2>${o(i.historyHeading)} (${o(t.number(l.length))})</h2>
<table>
<thead><tr>
<th>${o(i.colDate)}</th><th>${o(i.colObject)}</th><th>${o(i.colTask)}</th>
<th class="num">${o(i.colCost)}</th><th class="num">${o(i.colDuration)}</th><th>${o(i.colNotes)}</th>
</tr></thead>
<tbody>
${m||`<tr><td colspan="6">${o(i.none)}</td></tr>`}
</tbody>
<tfoot><tr>
<td colspan="3">${o(i.totalCost)}</td>
<td class="num">${o(t.cost(r.totalCost))}</td>
<td class="num">${r.totalDuration?o(t.duration(r.totalDuration)):""}</td><td></td>
</tr></tfoot>
</table>
${b.capped?`<p class="cap-note">${o(i.capNote)}</p>`:""}
</body>
</html>`}var Z=25,V=5,tt={completed:C.ok,missed:C.overdue,reset:C.paused,skipped:"var(--secondary-text-color)"},_=class extends H{constructor(){super(...arguments);this.areaKey="";this.objects=[];this.showArchived=!1;this.currencySymbol="";this.userName=()=>null;this._from="";this._to="";this._entryFilter="";this._taskQuery="";this._expanded=!1;this._printing=!1;this._histories=new X(this);this._localeReady=!1;this._filtersFor=null;this._memo=null}createRenderRoot(){return this}get _lang(){return F(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("areaKey")&&this._filtersFor!==this.areaKey){this._filtersFor=this.areaKey;let s=R();this._from=s.from,this._to=s.to,this._entryFilter="",this._taskQuery="",this._expanded=!1}if(t.has("objects")||t.has("areaKey")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let s=this._areaObjects().flatMap(l=>l.tasks.map(d=>({entryId:l.entry_id,task:d})));this._histories.sync(this.hass,s)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,L(this._lang).then(()=>this.requestUpdate()))}_areaObjects(){return z(this.objects,this.areaKey,this.showArchived).sort((t,s)=>t.object.name.localeCompare(s.object.name))}_entries(t){let s=[this.objects,this.areaKey,this.showArchived,this._histories.version];if(this._memo&&this._memo.deps.every((d,r)=>d===s[r]))return this._memo.entries;let l=Q(t,(d,r)=>this._histories.historyOf(d,r));return this._memo={deps:s,entries:l},l}_figures(){let t=this._areaObjects(),s=this._entries(t),l={from:this._from||null,to:this._to||null},d=U(s,{...l,entryId:this._entryFilter||null,taskQuery:this._taskQuery}),r=this._entryFilter?t.filter(h=>h.entry_id===this._entryFilter):t;return{objects:t,entries:s,filtered:d,totals:Y(d),buckets:G(d,l),byObject:q(d,r)}}_setRange(t){this._from=t.from,this._to=t.to,this._expanded=!1}_openObject(t){this.dispatchEvent(new CustomEvent("open-object",{detail:{entryId:t},bubbles:!0,composed:!0}))}_openTask(t,s){this.dispatchEvent(new CustomEvent("open-task",{detail:{entryId:t,taskId:s},bubbles:!0,composed:!0}))}_bucketLabel(t,s){return t.month==null?String(t.year):`${T(t.month,this._lang,s)} ${t.year}`}_countText(t,s,l){let d=this._lang;return l===1?a(s,d):a(t,d).replace("{n}",g(l,d))}async _print(){if(this._printing)return;let t=K();this._printing=!0;try{await this._histories.settled()}finally{this._printing=!1}let s=this._lang,l=this.currencySymbol,{filtered:d,totals:r,buckets:h,byObject:u}=this._figures(),f={title:a("area_report_title",s),generated:a("report_generated",s),period:a("area_report_period",s),completions:a("area_kpi_completions",s),totalCost:a("total_cost",s),avgCost:a("avg_cost",s),totalTime:a("area_kpi_total_time",s),costPerObject:a("area_cost_per_object",s),costPerBucket:h.unit==="month"?a("area_cost_per_month",s):a("area_cost_per_year",s),share:a("area_cost_share",s),historyHeading:a("area_history_section",s),colDate:a("date",s),colObject:a("object",s),colTask:a("task_name",s),colCost:a("cost",s),colDuration:a("duration",s),colNotes:a("notes_label",s),completedBy:a("completed_by",s),capNote:a("object_history_cap_note",s),none:"\u2014"},c=d.filter(n=>n.type==="completed"),m=d.map(n=>({...n,completedBy:n.completedBy?this.userName(n.completedBy):null,notes:n.notes?B(n.notes,s):null})),e=J({areaName:E(this.areaKey,this.hass?.areas,a("no_area",s)),from:this._from||S(c[c.length-1]?.timestamp)||null,to:this._to||O(),totals:r,byObject:u,buckets:h,entries:m,capped:this._histories.capped},f,{date:n=>N(n,s),cost:n=>v(n,l,s),duration:n=>w(n,s),bucket:n=>this._bucketLabel(n,"long"),share:n=>`${g(n*100,s,0)} %`,number:n=>g(n,s)},new Date().toISOString());P(e,t)}_renderRangeChips(t){let s=this._lang,l=O(),d=R(l),r=u=>this._from===u.from&&this._to===u.to,h=W(t,Number(l.slice(0,4))).slice(0,V);return p`
<div class="filter-chips area-range-chips">
<button class="filter-chip ${r(d)?"active":""}" @click=${()=>this._setRange(R())}>
${a("area_range_12m",s)}
</button>
${h.map(u=>p`
<button class="filter-chip ${r(A(u))?"active":""}" @click=${()=>this._setRange(A(u))}>${u}</button>
`)}
<button class="filter-chip ${!this._from&&!this._to?"active":""}" @click=${()=>this._setRange({from:"",to:""})}>
${a("area_range_all",s)}
</button>
</div>
`}_renderChart(t){let s=this._lang,l=this.currencySymbol,d=t.reduce((c,m)=>Math.max(c,m.cost),0),r=t.reduce((c,m)=>Math.min(c,m.cost),0);if(d<=0&&r>=0)return p`<p class="area-empty">${a("area_no_costs",s)}</p>`;let h=c=>Math.round(c/(d-r)*1e3)/10,u=Math.max(1,Math.ceil(t.length/12)),f=t.map((c,m)=>({b:c,i:m})).filter(({i:c})=>c%u===0);return p`
<div class="area-chart">
<div class="area-chart-max">${v(d,l,s)}</div>
<div class="area-chart-bars">
${r<0?p`<div class="area-chart-zero" style="bottom: ${h(-r)}%"></div>`:y}
${t.map(c=>{let m=`${this._bucketLabel(c,"long")}: ${v(c.cost,l,s)} \xB7 ${a("area_kpi_completions",s)}: ${g(c.completions,s)}`,e=c.cost<0?h(c.cost-r):h(-r);return p`<div class="area-bar" title=${m} aria-label=${m}>
${c.cost!==0?p`<div class="area-bar-fill ${c.cost<0?"credit":""}" style="height: ${h(Math.abs(c.cost))}%${e?`; bottom: ${e}%`:""}"></div>`:y}
</div>`})}
</div>
${r<0?p`<div class="area-chart-min">${v(r,l,s)}</div>`:y}
<div class="area-chart-axis ${u>1?"grouped":""}">
${f.map(({b:c,i:m})=>p`<span class="area-bar-label" style="flex-grow: ${Math.min(u,t.length-m)}">${c.month==null?String(c.year):p`${T(c.month,s,"short")}${c.month===0||m===0?p`<br />${c.year}`:y}`}</span>`)}
</div>
</div>
`}render(){if(!this.hass||!this.areaKey)return y;let t=this._lang,s=this.currencySymbol,{objects:l,entries:d,filtered:r,totals:h,buckets:u,byObject:f}=this._figures(),c=l.reduce((e,n)=>e+n.tasks.filter(x=>this.showArchived||!x.archived).length,0),m=this._expanded?r:r.slice(0,Z);return p`
<div class="detail-section area-view">
<div class="detail-header">
<h2 class="area-title">
<ha-icon .icon=${M(this.areaKey,this.hass.areas)}></ha-icon>
${E(this.areaKey,this.hass.areas,a("no_area",t))}
</h2>
<div class="action-buttons">
<ha-button appearance="plain" class="area-print" .disabled=${this._printing} @click=${()=>this._print()}>
<ha-icon icon="mdi:printer-outline"></ha-icon>
${this._printing?a("loading",t):a("area_report_print",t)}
</ha-button>
</div>
</div>
<p class="meta">
${this._countText("area_object_count","area_object_count_one",l.length)} ·
${this._countText("templates_task_count","templates_task_count_one",c)}
${this._histories.loading?p` · <span class="area-loading">${a("loading",t)}</span>`:y}
</p>
${this._renderRangeChips(d)}
<div class="filter-bar area-filters">
<ms-date-field
kind="date"
clearable
.hass=${this.hass}
.lang=${t}
.label=${a("date_from",t)}
.value=${this._from}
@value-changed=${e=>{this._from=e.detail.value}}
></ms-date-field>
<ms-date-field
kind="date"
clearable
.hass=${this.hass}
.lang=${t}
.label=${a("date_to",t)}
.value=${this._to}
@value-changed=${e=>{this._to=e.detail.value}}
></ms-date-field>
<label class="filter-field">
<span class="filter-label">${a("object",t)}</span>
<select class="area-object-filter" .value=${this._entryFilter}
@change=${e=>{this._entryFilter=e.target.value}}>
<option value="" ?selected=${!this._entryFilter}>${a("all_objects",t)}</option>
${l.map(e=>p`<option value=${e.entry_id} ?selected=${e.entry_id===this._entryFilter}>${e.object.name}</option>`)}
</select>
</label>
<input
type="search"
class="search-input area-task-filter"
aria-label=${a("area_task_filter",t)}
placeholder=${a("area_task_filter",t)}
.value=${this._taskQuery}
@input=${e=>{this._taskQuery=e.target.value}}
/>
</div>
<div class="kpi-bar area-kpis">
<div class="kpi-card">
<div class="kpi-label">${a("area_kpi_completions",t)}</div>
<div class="kpi-value-large area-kpi-completions">${g(h.completions,t)}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">${a("total_cost",t)}</div>
<div class="kpi-value-large area-kpi-cost">${v(h.totalCost,s,t)}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">${a("avg_cost",t)}</div>
<div class="kpi-value-large">${h.avgCost!=null?v(h.avgCost,s,t):"\u2014"}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">${a("area_kpi_total_time",t)}</div>
<div class="kpi-value-large">${h.totalDuration?w(h.totalDuration,t):"\u2014"}</div>
</div>
</div>
<h3>${u.unit==="month"?a("area_cost_per_month",t):a("area_cost_per_year",t)}</h3>
${this._renderChart(u.buckets)}
<h3>${a("area_cost_per_object",t)}</h3>
<div class="objects-table-wrap">
<table class="objects-table area-object-table">
<thead>
<tr>
<th>${a("object",t)}</th>
<th class="num">${a("area_kpi_completions",t)}</th>
<th class="num">${a("cost",t)}</th>
<th class="num">${a("area_cost_share",t)}</th>
</tr>
</thead>
<tbody>
${f.map(e=>p`
<tr class="objects-table-row" @click=${()=>this._openObject(e.entryId)}>
<td><span class="objects-table-name">${e.objectName}</span></td>
<td class="num">${g(e.completions,t)}</td>
<td class="num">${v(e.cost,s,t)}</td>
<td class="num">
<span class="area-share"><span class="area-share-fill" style="width: ${Math.round(e.share*100)}%"></span></span>
${g(e.share*100,t,0)} %
</td>
</tr>
`)}
</tbody>
</table>
</div>
<h3>
${a("area_history_section",t)}
<span class="area-count">${g(r.length,t)}</span>
</h3>
${r.length===0?p`<p class="area-empty">${a("object_history_empty",t)}</p>`:p`
<div class="objects-table-wrap">
<table class="objects-table area-history-table">
<thead>
<tr>
<th>${a("date",t)}</th>
<th></th>
<th>${a("object",t)}</th>
<th>${a("task_name",t)}</th>
<th class="num">${a("cost",t)}</th>
<th class="num">${a("duration",t)}</th>
<th>${a("notes_label",t)}</th>
</tr>
</thead>
<tbody>
${m.map(e=>{let n=e.notes?B(e.notes,t):"";return p`
<tr class="objects-table-row area-history-row" @click=${()=>this._openTask(e.entryId,e.taskId)}>
<td title=${I(e.timestamp,t)}>${N(e.timestamp,t)}</td>
<td>
<span class="area-entry-type" style="color: ${tt[e.type]??"inherit"}">
<ha-icon .icon=${D[e.type]||"mdi:circle"}></ha-icon>${a(e.type,t)}
</span>
</td>
<td>
<button class="area-object-link" @click=${x=>{x.stopPropagation(),this._openObject(e.entryId)}}>
${e.objectName}
</button>
</td>
<td><button class="area-task-link">${e.taskName}${e.phaseName?` \xB7 ${e.phaseName}`:""}</button></td>
<td class="num">${e.cost!=null?v(e.cost,s,t):"\u2014"}</td>
<td class="num">${e.duration!=null?w(e.duration,t):"\u2014"}</td>
<td class="oc-notes" title=${n}>${n||"\u2014"}</td>
</tr>
`})}
</tbody>
</table>
</div>
${r.length>m.length?p`<ha-button appearance="plain" class="area-more" @click=${()=>{this._expanded=!0}}>
${a("show_all",t)} (${g(r.length,t)})
</ha-button>`:y}
`}
${this._histories.capped?p`<p class="area-cap-note">${a("object_history_cap_note",t)}</p>`:y}
</div>
`}};$([k({attribute:!1})],_.prototype,"hass",2),$([k()],_.prototype,"areaKey",2),$([k({attribute:!1})],_.prototype,"objects",2),$([k({type:Boolean})],_.prototype,"showArchived",2),$([k()],_.prototype,"currencySymbol",2),$([k({attribute:!1})],_.prototype,"userName",2),$([j()],_.prototype,"_from",2),$([j()],_.prototype,"_to",2),$([j()],_.prototype,"_entryFilter",2),$([j()],_.prototype,"_taskQuery",2),$([j()],_.prototype,"_expanded",2),$([j()],_.prototype,"_printing",2);customElements.get("maintenance-area-view")||customElements.define("maintenance-area-view",_);export{_ as MaintenanceAreaView};
@@ -0,0 +1,54 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MQRW7LAF.js";import{q as j,r as E,s as L,t as R,u as C}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MGSZZ3YF.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SYPCMUXO.js";import{a as _,b as S,c as A}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-HFZZVTW7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-55ND3GKE.js";import{H as g,K as k,L as v,O as w,a as l,c as a,f as m,h as y,l as p,m as b,s as r,u as $,y as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var q=[{key:"name",label:"area",num:!1},{key:"objects",label:"objects",num:!0},{key:"tasks",label:"tasks",num:!0},{key:"overdue",label:"overdue",num:!0},{key:"due_soon",label:"due_soon",num:!0},{key:"cost_year",label:"area_cost_year",num:!0},{key:"cost_total",label:"total_cost",num:!0},{key:"last",label:"last_performed",num:!1}],i=class extends y{constructor(){super(...arguments);this.objects=[];this.showArchived=!1;this.currencySymbol="";this._query="";this._sort=E(S(_.areaSort));this._histories=new O(this);this._localeReady=!1;this._memo=null}createRenderRoot(){return this}get _lang(){return g(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("objects")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let o=this.objects.filter(s=>this.showArchived||!s.object.archived).flatMap(s=>s.tasks.map(n=>({entryId:s.entry_id,task:n})));this._histories.sync(this.hass,o)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,$(this._lang).then(()=>this.requestUpdate()))}_rows(t){let o=this._lang,s=[this.objects,this.showArchived,this._histories.version,this.hass?.areas,o,t];if(this._memo&&this._memo.deps.every((c,h)=>c===s[h]))return this._memo.rows;let n=j(this.objects,{showArchived:this.showArchived,areas:this.hass?.areas,noAreaLabel:r("no_area",o),year:t,historyOf:(c,h)=>this._histories.historyOf(c,h)});return this._memo={deps:s,rows:n},n}_setSort(t){this._sort=L(this._sort,t),A(_.areaSort,`${this._sort.key}:${this._sort.dir}`)}_open(t){this.dispatchEvent(new CustomEvent("open-area",{detail:{areaKey:t},bubbles:!0,composed:!0}))}_toggleArchived(){this.dispatchEvent(new CustomEvent("archived-toggle",{bubbles:!0,composed:!0}))}_headerLabel(t,o,s){return t==="area_cost_year"?r("area_cost_year",s).replace("{year}",String(o)):r(t,s)}render(){if(!this.hass)return m;let t=this._lang,o=Number(f().slice(0,4)),s=this.currencySymbol,n=this._rows(o),c=R(C(n,this._query),this._sort),h=this.objects.filter(e=>e.object.archived).length,d=e=>k(e,t);return a`
<div class="filter-bar area-filter-bar">
<input
type="search"
class="search-input"
aria-label=${r("areas_filter",t)}
placeholder=${r("areas_filter",t)}
.value=${this._query}
@input=${e=>{this._query=e.target.value}}
/>
${this._histories.loading?a`<span class="area-loading">${r("loading",t)}</span>`:m}
${h>0?a`<ha-button class="archived-toggle ${this.showArchived?"active":""}" @click=${()=>this._toggleArchived()}>
<ha-icon icon="mdi:archive-outline"></ha-icon>
${this.showArchived?r("hide_archived",t):`${r("show_archived",t)} (${h})`}
</ha-button>`:m}
</div>
${n.length===0?a`<div class="empty-state">${r("no_objects",t)}</div>`:c.length===0?a`<div class="empty-state">${r("areas_filter_none",t)}</div>`:a`
<div class="objects-table-wrap">
<table class="objects-table areas-table">
<thead>
<tr>
${q.map(e=>{let u=this._sort.key===e.key;return a`<th class=${e.num?"num":""}
aria-sort=${u?this._sort.dir==="asc"?"ascending":"descending":"none"}>
<button class="area-sort ${u?"active":""}" title=${r("sort_label",t)} @click=${()=>this._setSort(e.key)}>
${this._headerLabel(e.label,o,t)}
${u?a`<ha-icon icon=${this._sort.dir==="asc"?"mdi:arrow-up":"mdi:arrow-down"}></ha-icon>`:m}
</button>
</th>`})}
</tr>
</thead>
<tbody>
${c.map(e=>a`
<tr class="objects-table-row" tabindex="0"
@click=${()=>this._open(e.key)}
@keydown=${u=>{u.key==="Enter"&&this._open(e.key)}}>
<td>
<ha-icon class="area-row-icon" .icon=${e.icon}></ha-icon>
<span class="objects-table-name">${e.name}</span>
</td>
<td class="num">${d(e.objects)}</td>
<td class="num">${d(e.tasks)}</td>
<td class="num">${e.overdue?a`<span class="area-count overdue">${d(e.overdue)}</span>`:d(0)}</td>
<td class="num">${e.dueSoon?a`<span class="area-count due-soon">${d(e.dueSoon)}</span>`:d(0)}</td>
<td class="num">${v(e.costYear,s,t)}</td>
<td class="num">${v(e.costTotal,s,t)}</td>
<td>${e.lastCompletion?w(e.lastCompletion,t):"\u2014"}</td>
</tr>
`)}
</tbody>
</table>
</div>
`}
`}};l([p({attribute:!1})],i.prototype,"hass",2),l([p({attribute:!1})],i.prototype,"objects",2),l([p({type:Boolean})],i.prototype,"showArchived",2),l([p()],i.prototype,"currencySymbol",2),l([b()],i.prototype,"_query",2),l([b()],i.prototype,"_sort",2);customElements.get("maintenance-areas-view")||customElements.define("maintenance-areas-view",i);export{i as MaintenanceAreasView};
@@ -0,0 +1,113 @@
/*! maintenance_supporter frontend 2.99.0 */
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TX2GMEMJ.js";import{H as u,a as n,b as _,c as o,f as p,h as c,l as g,m as a,s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var d=["round_robin","least_completed","random"],v=["low","normal","high"],m=365,i=class extends c{constructor(){super(...arguments);this._open=!1;this._mode="assign";this._count=0;this._users=[];this._present=[];this._assignKind="one";this._person="";this._pool=new Set;this._rotation=d[0];this._addText="";this._remove=new Set;this._setWarning=!1;this._warning=7;this._setPriority=!1;this._priority="normal";this._setMute=!1;this._mute=!1;this._resolve=null}get _lang(){return u(this.hass)}open(t,e,r){return this._resolve?.(null),this._mode=t,this._count=e.length,this._users=[...r].sort((l,h)=>l.name.localeCompare(h.name)),this._present=[...new Set(e.flatMap(l=>l.labels||[]))].sort((l,h)=>l.localeCompare(h)),this._assignKind="one",this._person="",this._pool=new Set,this._rotation=d[0],this._addText="",this._remove=new Set,this._setWarning=!1,this._warning=7,this._setPriority=!1,this._priority="normal",this._setMute=!1,this._mute=!1,this._open=!0,new Promise(l=>{this._resolve=l})}_finish(t){this._open=!1;let e=this._resolve;this._resolve=null,e?.(t)}changes(){if(this._mode==="assign"){if(this._assignKind==="one")return{responsible_user_id:this._person||null,assignee_pool:null,rotation_strategy:null};let e=this._users.map(r=>r.id).filter(r=>this._pool.has(r));return e.length<2?null:{assignee_pool:e,rotation_strategy:this._rotation,responsible_user_id:null}}if(this._mode==="labels"){let e=[...new Set(this._addText.split(",").map(l=>l.trim()).filter(Boolean))],r=[...this._remove];return e.length||r.length?{labels_add:e,labels_remove:r}:null}let t={};return this._setWarning&&(t.warning_days=this._warning),this._setPriority&&(t.priority=this._priority),this._setMute&&(t.notify_enabled=!this._mute),Object.keys(t).length?t:null}_toggle(t,e){let r=new Set(t);return r.has(e)?r.delete(e):r.add(e),r}_renderAssign(t){return o`
<div class="hint">${s("bulk_assign_hint",t)}</div>
<label class="choice">
<input type="radio" name="kind" .checked=${this._assignKind==="one"} @change=${()=>this._assignKind="one"} />
${s("bulk_assign_one",t)}
</label>
${this._assignKind==="one"?o`<select class="person" .value=${this._person} @change=${e=>this._person=e.target.value}>
<option value="" ?selected=${!this._person}>${s("unassigned",t)}</option>
${this._users.map(e=>o`<option value=${e.id} ?selected=${this._person===e.id}>${e.name}</option>`)}
</select>`:p}
<label class="choice">
<input type="radio" name="kind" .checked=${this._assignKind==="rotate"} @change=${()=>this._assignKind="rotate"} />
${s("bulk_assign_rotate",t)}
</label>
${this._assignKind==="rotate"?o`<div class="pool">
${this._users.map(e=>o`<label class="row">
<input type="checkbox" .checked=${this._pool.has(e.id)} @change=${()=>this._pool=this._toggle(this._pool,e.id)} />
${e.name}
</label>`)}
</div>
<label class="field">
<span>${s("rotation_strategy",t)}</span>
<select class="rotation" .value=${this._rotation} @change=${e=>this._rotation=e.target.value}>
${d.map(e=>o`<option value=${e} ?selected=${this._rotation===e}>${s(`rotation_${e}`,t)}</option>`)}
</select>
</label>
${this._pool.size<2?o`<div class="hint">${s("bulk_rotation_min_two",t)}</div>`:p}`:p}
`}_renderLabels(t){return o`
<ms-textfield
class="labels-add"
label=${s("bulk_labels_add",t)}
.helper=${s("labels_help",t)}
.value=${this._addText}
@input=${e=>this._addText=e.target.value}
></ms-textfield>
${this._present.length?o`<div class="section-title">${s("bulk_labels_remove",t)}</div>
<div class="pool">
${this._present.map(e=>o`<label class="row">
<input type="checkbox" .checked=${this._remove.has(e)} @change=${()=>this._remove=this._toggle(this._remove,e)} />
${e}
</label>`)}
</div>`:p}
`}_renderEdit(t){return o`
<div class="hint">${s("bulk_edit_hint",t)}</div>
<div class="row edit-row">
<input type="checkbox" class="set-warning" .checked=${this._setWarning} @change=${e=>this._setWarning=e.target.checked} />
<span class="grow">${s("warning_days",t)}</span>
<input type="number" class="warning" min="0" max=${m} .value=${String(this._warning)} ?disabled=${!this._setWarning}
@change=${e=>{let r=parseInt(e.target.value,10);this._warning=Number.isInteger(r)?Math.min(Math.max(r,0),m):this._warning}} />
</div>
<div class="row edit-row">
<input type="checkbox" class="set-priority" .checked=${this._setPriority} @change=${e=>this._setPriority=e.target.checked} />
<span class="grow">${s("priority",t)}</span>
<select class="priority" .value=${this._priority} ?disabled=${!this._setPriority} @change=${e=>this._priority=e.target.value}>
${v.map(e=>o`<option value=${e} ?selected=${this._priority===e}>${s(`priority_${e}`,t)}</option>`)}
</select>
</div>
<div class="row edit-row">
<input type="checkbox" class="set-mute" .checked=${this._setMute} @change=${e=>this._setMute=e.target.checked} />
<span class="grow">${s("bulk_no_notifications",t)}</span>
<input type="checkbox" class="mute" .checked=${this._mute} ?disabled=${!this._setMute} @change=${e=>this._mute=e.target.checked} />
</div>
`}render(){if(!this._open)return o``;let t=this._lang,e=this._mode==="assign"?s("bulk_assign",t):this._mode==="labels"?s("labels",t):s("edit",t),r=this.changes()!==null;return o`
<ha-dialog open @closed=${()=>this._finish(null)}>
<div class="dialog-title">${e}</div>
<div class="content">
<div class="count">${s("bulk_n_selected",t).replace("{n}",String(this._count))}</div>
${this._mode==="assign"?this._renderAssign(t):this._mode==="labels"?this._renderLabels(t):this._renderEdit(t)}
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${()=>this._finish(null)}>${s("cancel",t)}</ha-button>
<ha-button class="apply" .disabled=${!r} @click=${()=>this._finish(this.changes())}>${s("save",t)}</ha-button>
</div>
</ha-dialog>
`}};i.styles=_`
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
.content {
display: flex;
flex-direction: column;
gap: 10px;
min-width: min(360px, calc(100vw - 24px));
max-width: 520px;
max-height: 60vh;
overflow-y: auto;
}
@media (max-width: 600px) {
.content { min-width: 0; max-width: none; max-height: none; }
}
.count, .hint { color: var(--secondary-text-color); font-size: 13px; }
.section-title {
font-size: 14px;
font-weight: 500;
padding-bottom: 4px;
border-bottom: 1px solid var(--divider-color);
}
.choice, .row { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }
.pool { display: flex; flex-direction: column; gap: 4px; padding-left: 26px; }
.field { display: flex; align-items: center; gap: 8px; justify-content: space-between; font-size: 14px; }
.grow { flex: 1; min-width: 0; }
select, input[type="number"] {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--divider-color);
border-radius: 4px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
max-width: 100%;
}
input[type="number"] { width: 72px; }
select.person { margin-left: 26px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 16px; }
`,n([g({attribute:!1})],i.prototype,"hass",2),n([a()],i.prototype,"_open",2),n([a()],i.prototype,"_mode",2),n([a()],i.prototype,"_count",2),n([a()],i.prototype,"_users",2),n([a()],i.prototype,"_present",2),n([a()],i.prototype,"_assignKind",2),n([a()],i.prototype,"_person",2),n([a()],i.prototype,"_pool",2),n([a()],i.prototype,"_rotation",2),n([a()],i.prototype,"_addText",2),n([a()],i.prototype,"_remove",2),n([a()],i.prototype,"_setWarning",2),n([a()],i.prototype,"_warning",2),n([a()],i.prototype,"_setPriority",2),n([a()],i.prototype,"_priority",2),n([a()],i.prototype,"_setMute",2),n([a()],i.prototype,"_mute",2);customElements.get("maintenance-bulk-edit-dialog")||customElements.define("maintenance-bulk-edit-dialog",i);export{i as MaintenanceBulkEditDialog};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{s as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var _=[{key:"name",labelKey:"name",required:!0},{key:"ref_no",labelKey:"ref_number"},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=_.map(e=>e.key),i=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function g(e){if(!Array.isArray(e))return[...i];let n=new Set,r=[];for(let t of e)typeof t=="string"&&u.includes(t)&&!n.has(t)&&(n.add(t),r.push(t));return r.length?(r.includes("name")||r.unshift("name"),r):[...i]}function f(e,n){return a(`home_dwelling_${e==="house"||e==="apartment"?e:"unknown"}`,n)}function m(e,n){if(!e)return"";try{return new Intl.DisplayNames([n],{type:"region"}).of(e)||e}catch{return e}}var l={floors:"home_reason_floors",basement_floor:"home_reason_basement",area_basement:"home_reason_basement",area_garden:"home_reason_garden",area_shed:"home_reason_shed",area_driveway:"home_reason_driveway",area_garage:"home_reason_garage",entity_garage:"home_reason_garage",area_attic:"home_reason_attic",entity_lawn_mower:"home_reason_lawn_mower",entity_gate:"home_reason_gate",area_balcony:"home_reason_balcony",small_home:"home_reason_small_home"},c={ups:"home_reason_ups"};function d(e,n){let r=[];for(let t of e){let o=l[t];if(!o)continue;let s=a(o,n);r.includes(s)||r.push(s)}return r.join(", ")}function b(e,n,r){if(e==="starter")return a("home_reason_starter",n);if(e==="country")return a("home_reason_country",n).replace("{country}",m(r,n));if(e.startsWith("feature_")){let t=e.slice(8),o=l[`area_${t}`]??c[t];return a("home_reason_feature",n).replace("{feature}",o?a(o,n):e)}return a(`home_trait_${e}`,n)}export{_ as a,i as b,g as c,f as d,m as e,d as f,b as g};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
function c(n){return n==="use"?"use":"purchase"}var s=n=>typeof n=="number"&&Number.isFinite(n)?n:null;function a(n){let t=s(n?.cost);if(t===null)return null;let o=s(n?.package_size);return o&&o>0?t/o:t}function p(n,t){let o=s(t)&&t>0?t:s(n.restock_quantity)||1,e=s(n.package_size);return Math.round(o*(e&&e>0?e:1)*100)/100}function l(n){let t=s(n.cost);if(n.cost_basis!=="use")return t;let o=s(n.parts_cost),e=n.purchase?null:t;return e===null&&o===null?null:(e??0)+(o??0)}function _(n){if(!n)return[];let t=[],o=n.photo_doc_id;typeof o=="string"&&t.push(o);let e=n.photo_doc_ids;Array.isArray(e)&&t.push(...e);let r=[];for(let u of t){if(typeof u!="string")continue;let i=u.trim();if(!(!i||r.includes(i))&&(r.push(i),r.length>=10))break}return r}export{_ as a,c as b,a as c,p as d,l as e};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
function r(t){return String(t??"").replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}export{r as a};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{b as s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JQXHX4U4.js";async function a(o,n,t=300){return(await o.connection.sendMessagePromise({type:"auth/sign_path",path:n,expires:t})).path}async function r(o,n,t=300){return a(o,`/api/maintenance_supporter/document/${n}`,t)}async function p(o,n,t=""){let e=window.open("about:blank","_blank");try{let i=await r(o,n);e&&(e.location.href=new URL(i+t,window.location.origin).href)}catch(i){throw e&&e.close(),i}}async function l(o,n,t){s(await r(o,n,30),t)}function m(o,n=null){let t=URL.createObjectURL(new Blob([o],{type:"text/html"}));n&&!n.closed?n.location.href=t:window.open(t,"_blank"),setTimeout(()=>URL.revokeObjectURL(t),6e4)}function u(){return window.open("about:blank","_blank")}export{a,r as b,p as c,l as d,m as e,u as f};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{O as n,s as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var l={"Completed from dashboard button":"hist_note_button","Completed from the To-do list":"hist_note_todo","Completed by voice":"hist_note_voice","Completed from the notification":"hist_note_notification","Completed via NFC tag":"hist_note_nfc","Completed from the shopping list":"hist_note_shopping_list","Completed from a mirrored to-do list":"hist_note_todo_mirror","Skipped from notification":"hist_note_skipped_notification","Skipped from dashboard button":"hist_note_skipped_button","Sensor trigger activated":"hist_note_sensor_triggered","Initial value set during task creation":"hist_note_initial"},m=[[/^Auto-completed: sensor recovered \((.+)\)$/,"hist_note_auto_recovered",t=>({value:t[1]})],[/^Reset to (\d{4}-\d{2}-\d{2})$/,"hist_note_reset",(t,e)=>({date:n(t[1],e)})],[/^Trigger entity replaced: (.+) → (.+)$/,"hist_note_trigger_replaced",t=>({old:t[1],new:t[2]})],[/^Sensor trigger removed \(entity was: (.+)\)\. Schedule converted to (\w+)\.$/,"hist_note_trigger_removed",(t,e)=>({entity:t[1],schedule:o(t[2],e)})],[/^Entity (.+) removed from compound trigger; (\d+) conditions remain\.$/,"hist_note_compound_removed",t=>({entity:t[1],count:t[2]})]];function h(t,e){if(!t)return"";let i=l[t.trim()];if(i)return o(i,e);for(let[s,d,_]of m){let r=t.trim().match(s);if(r){let c=_(r,e);return o(d,e).replace(/\{(\w+)\}/g,(a,g)=>c[g]??a)}}return t}export{h as a};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-55ND3GKE.js";import{a as l}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-RHKFQ4Q2.js";var a={operatorWriteEnabled:!1,operatorIds:[]};function m(e,t=a){return e?e.is_admin?!0:t.operatorWriteEnabled&&t.operatorIds.includes(e.id):!1}var _=["buttons_compact","buttons","icons"];function p(e){let[t,n]=l.default_warning_days,s=e?.general?.default_warning_days,r=e?.general?.row_action_style;return{features:{...o.features,...e?.features??{}},defaultWarningDays:typeof s=="number"&&Number.isFinite(s)&&s>=t&&s<=n?s:o.defaultWarningDays,rowActionStyle:_.includes(r??"")?r:o.rowActionStyle,access:{operatorWriteEnabled:e?.operator_write_enabled===!0,operatorIds:Array.isArray(e?.admin_panel_user_ids)?e.admin_panel_user_ids.filter(d=>typeof d=="string"):[]},budget:e?.budget??null,rowActionNoticePending:e?.general?.row_action_notice_pending===!0,refsInLists:e?.general?.ref_numbers_in_lists===!0,objectsTableColumns:Array.isArray(e?.objects_table_columns)?e.objects_table_columns:void 0,partsCostMode:i(e?.budget?.parts_cost_mode)}}var o={features:{adaptive:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1,places:!1},defaultWarningDays:7,rowActionStyle:"buttons_compact",access:a,budget:null,rowActionNoticePending:!1,refsInLists:!1,objectsTableColumns:void 0,partsCostMode:"purchase"};function c(){let e=window;return e.__msSettingsCache??={promise:null}}function S(e){let t=c();if(t.promise)return t.promise;let n=e.connection.sendMessagePromise({type:"maintenance_supporter/settings"}).then(s=>p(s)).catch(()=>(t.promise===n&&(t.promise=null),o));return t.promise=n,n}function y(){c().promise=null}var u=class{constructor(){this._handle=null}schedule(t,n=4e3){this.clear(),this._handle=setTimeout(()=>{this._handle=null,t()},n)}clear(){this._handle!==null&&(clearTimeout(this._handle),this._handle=null)}get pending(){return this._handle!==null}};export{a,m as b,_ as c,p as d,S as e,y as f,u as g};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
var r={objectSections:"msp-object-sections",printOptions:"msp-print-options",batteryRosterOpen:"msp-bf-roster-open",docSort:"msp-doc-sort",cameraLabel:"msp-camera-label",overviewTab:"msp-overview-tab",collapsedSections:"msp-collapsed-sections",chartRange:"msp-chart-range",chartHideOutliers:"msp-chart-hide-outliers",taskSort:"maintenance_supporter_sort",objectSort:"maintenance_supporter_object_sort",groupBy:"maintenance_supporter_groupby",objectView:"maintenance_supporter_object_view",objectsCache:"msp-objects-cache",gettingStartedDismissed:"msp-gs-dismissed",batteryRosterSort:"ms_bf_roster_sort",areaSort:"msp-area-sort"};function s(t){try{return localStorage.getItem(t)}catch{return null}}function o(t,e){try{localStorage.setItem(t,e)}catch{}}export{r as a,s as b,o as c};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
function c(o,t,e){let l=new Blob([o],{type:e}),d=URL.createObjectURL(l),n=document.createElement("a");n.href=d,n.download=t,n.target="_blank",n.rel="noopener",n.style.display="none",document.body.appendChild(n),n.dispatchEvent(new MouseEvent("click")),document.body.removeChild(n),setTimeout(()=>URL.revokeObjectURL(d),6e4)}function r(o,t){let e=document.createElement("a");e.href=o,e.download=t,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}export{c as a,r as b};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
/*! maintenance_supporter frontend 2.99.0 */
import{$ as w,A as k,K as R,M as x,a as m,b as h,c as g,f as v,h as b,l as y,m as _,s as l}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";function E(i){return`${i.entry_id??""}\0${i.part_id}`}function S(i,t,e,o){let n=!!i.entry_id&&i.entry_id!==t,r=n?i.entry_id:t,s=e.find(p=>p.entry_id===r),a=(s?.parts||[]).find(p=>p.id===i.part_id)||null,c=n&&s?.object?.name||"",u=a?.name||l("shared_part_unknown",o);return{part:a,foreign:n,ownerName:c,label:c?`${u} (${c})`:u}}function N(i,t,e,o){let{part:n,label:r}=S(i,t,e,o),s=n&&n.stock!==null&&n.stock!==void 0?` (${x(n.stock,n.unit,o)})`:"",a=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${R(i.quantity,o)}\xD7 ${r}${s}${a}`}function V(i,t,e,o){let r=(e.find(a=>a.entry_id===t)?.parts||[]).map(a=>({...a})),s=new Set(r.map(a=>E({part_id:a.id})));for(let a of i?.consumes_parts||[]){if(!a.entry_id||a.entry_id===t)continue;let c=E(a);if(s.has(c))continue;s.add(c);let{part:u,ownerName:p}=S(a,t,e,o);r.push({id:a.part_id,name:u?.name||l("shared_part_unknown",o),unit:u?.unit,stock:u?.stock??null,storage_location:u?.storage_location,entry_id:a.entry_id,owner_name:p})}return r}var L=20;function T(){let i=new Uint8Array(4);return(globalThis.crypto??{getRandomValues:t=>t.map(()=>Math.floor(Math.random()*256))}).getRandomValues(i),Array.from(i,t=>t.toString(16).padStart(2,"0")).join("")}function f(i){let t=i?.reading_values;if(!Array.isArray(t))return[];let e=[],o=new Set;for(let n of t){if(!n||typeof n!="object")continue;let r=n;typeof r.id!="string"||!r.id||o.has(r.id)||typeof r.value!="number"||!Number.isFinite(r.value)||(o.add(r.id),e.push({id:r.id,name:typeof r.name=="string"?r.name:r.id,unit:typeof r.unit=="string"?r.unit:null,value:r.value}))}return e}function P(i){return i.filter(t=>t.type==="completed"&&(t.reading_value!=null||f(t).length>0)).sort((t,e)=>t.timestamp.localeCompare(e.timestamp))}function M(i){return P(i??[]).map(t=>({timestamp:t.timestamp,values:f(t)})).filter(t=>t.values.length>0)}function A(i,t,e){let o;for(let n of i){if(e!==void 0){let s=k(n.timestamp);if(!isNaN(s)&&s>=e)break}let r=n.values.find(s=>s.id===t);r&&(o=r)}return o}function C(i,t,e){let o=f(t).find(r=>r.id===e);if(!o)return null;let n=null;for(let r of P(i)){if(r.timestamp>=t.timestamp)break;let s=f(r).find(a=>a.id===e);s&&(n=s.value)}return n==null?null:o.value-n}function D(i){let t=new Set,e=new Set;for(let o of i){let n=(o.name||"").trim().toLowerCase();n&&(t.has(n)?e.add(o.id):t.add(n))}return e}function F(i){let t=[],e=new Set,o=new Set;for(let n of i){let r=(n.name||"").trim();if(!(!r||e.has(n.id)||o.has(r.toLowerCase()))&&(e.add(n.id),o.add(r.toLowerCase()),t.push({id:n.id,name:r,unit:(n.unit||"").trim()||null}),t.length>=L))break}return t}function j(i){let t=(i??"").trim();if(t==="")return null;let e=Number(t.replace(",","."));return!Number.isFinite(e)||e<0?null:Math.round(e)}var B=["notes","cost","duration","photo","user"],I={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};var d=class extends b{constructor(){super(...arguments);this.value="";this.lang="en";this._credit=!1;this._amount=""}willUpdate(e){if(!e.has("value"))return;let o=(this.value??"").trim();if(o===this._signed())return;let n=parseFloat(o);if(o===""||Number.isNaN(n)){this._amount="",this._credit=!1;return}this._credit=n<0,this._amount=String(Math.abs(n))}_signed(){let e=this._amount.trim();return e===""?"":this._credit?`-${e}`:e}_emit(){this.value=this._signed(),this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:this.value},bubbles:!0,composed:!0}))}_setCredit(e){this._credit!==e&&(this._credit=e,this._emit())}_onInput(e){let o=e.target.value;o.startsWith("-")&&(this._credit=!0),this._amount=o.replace(/^-+/,""),this._emit()}render(){let e=this.lang;return g`
<div class="wrap">
<div class="sign" role="group">
<button type="button" class="kind cost ${this._credit?"":"on"}" aria-pressed=${String(!this._credit)}
@click=${()=>this._setCredit(!1)}>${l("cost",e)}</button>
<button type="button" class="kind credit ${this._credit?"on":""}" aria-pressed=${String(this._credit)}
@click=${()=>this._setCredit(!0)}>${l("cost_kind_credit",e)}</button>
</div>
<input type="number" step="0.01" min="0" inputmode="decimal" class="field-input amount"
.value=${this._amount} @input=${this._onInput} />
</div>
${this._credit?g`<div class="hint">${l("cost_credit_hint",e)}</div>`:v}
`}};d.styles=[w,h`
:host { display: block; }
.wrap { display: flex; gap: 8px; align-items: stretch; }
.sign {
display: inline-flex;
flex: none;
border: 1px solid var(--divider-color);
border-radius: 6px;
overflow: hidden;
}
.kind {
font: inherit;
font-size: 13px;
padding: 0 10px;
border: none;
background: transparent;
color: var(--secondary-text-color);
cursor: pointer;
}
.kind.on { background: var(--primary-color); color: var(--text-primary-color, #fff); }
.kind.credit.on { background: var(--success-color, #43a047); }
.amount { flex: 1; min-width: 0; }
.hint { margin-top: 4px; font-size: 12px; color: var(--secondary-text-color); }
`],m([y()],d.prototype,"value",2),m([y()],d.prototype,"lang",2),m([_()],d.prototype,"_credit",2),m([_()],d.prototype,"_amount",2);customElements.get("ms-cost-input")||customElements.define("ms-cost-input",d);export{E as a,N as b,V as c,L as d,T as e,f,M as g,A as h,C as i,D as j,F as k,j as l,B as m,I as n};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as y}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MGSZZ3YF.js";var l=6;async function g(e,i,n){let r=new Array(e.length),a=0,t=async()=>{for(;a<e.length;){let s=a++;r[s]=await n(e[s],s)}},o=Math.min(Math.max(1,i),e.length);return await Promise.all(Array.from({length:o},t)),r}function u(e){let i=e.history?.length??0;return typeof e.history_count!="number"||e.history_count>i}function d(e){return JSON.stringify([e.history_count??null,e.history??[]])}function h(e,i){return`${e}:${i}`}var c=class{constructor(i){this._host=i;this._cache=new Map;this._inFlight=new Map;this._running=0;this._pending=Promise.resolve();this.version=0;i.addController(this)}hostConnected(){}get loading(){return this._running>0}get capped(){for(let i of this._cache.values())if(i.entries.length>=y)return!0;return!1}historyOf(i,n){return this._cache.get(h(i,n.id))?.entries??n.history??[]}sync(i,n){if(!i?.connection)return this._pending;let r=n.filter(t=>u(t.task)).map(t=>({...t,key:h(t.entryId,t.task.id),sig:d(t.task)})).filter(t=>this._cache.get(t.key)?.sig!==t.sig&&this._inFlight.get(t.key)!==t.sig);if(!r.length)return this._pending;for(let t of r)this._inFlight.set(t.key,t.sig);this._running++,this._host.requestUpdate();let a=g(r,l,async t=>{let o;try{let s=await i.connection.sendMessagePromise({type:"maintenance_supporter/task/history",entry_id:t.entryId,task_id:t.task.id});o=Array.isArray(s?.history)?s.history:t.task.history??[]}catch{o=t.task.history??[]}this._inFlight.get(t.key)===t.sig&&(this._inFlight.delete(t.key),this._cache.set(t.key,{sig:t.sig,entries:o}))}).then(()=>{this._running--,this.version++,this._host.requestUpdate()});return this._pending=Promise.all([this._pending,a]).then(()=>{}),this._pending}settled(){return this._pending}};export{c as a};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{c as r,f as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var a=["#c62828","#ad1457","#6a1b9a","#4527a0","#283593","#1565c0","#00838f","#2e7d32","#558b2f","#ef6c00","#6d4c41","#546e7a"];function u(e){let s=(e||"").split(/\s+/).filter(Boolean);return s.length===0?"?":s.length===1?s[0][0].toUpperCase():(s[0][0]+s[s.length-1][0]).toUpperCase()}function c(e){let s=0;for(let n of e)s=s*31+n.charCodeAt(0)>>>0;return a[s%a.length]}function o(e){return e?{id:e.id,name:e.name,initials:e.initials||u(e.name),color:e.color||c(e.id)}:null}function h(e){return e?r`<span class="person-avatar" style="--person-color: ${e.color}" title=${e.name}>${e.initials}</span>`:i}function m(e,s=""){return e?r`<span class="person-chip ${s}" title=${e.name}>${h(e)}<span class="person-name">${e.name}</span></span>`:i}var l=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let n=Date.now();if(!s&&this.usersCache&&n-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=n,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,n,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:n,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getPerson(s){return o(this.getUser(s))}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(n=>n.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{a,o as b,h as c,m as d,l as e};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
var n={default_warning_days:[0,365],default_consumable_threshold:[1,90],battery_low_percent:[1,90],battery_recovered_percent:[20,100],archive_oneoff_days:[0,3650],delete_archived_oneoff_days:[0,3650],notify_due_soon_interval_hours:[0,720],notify_overdue_interval_hours:[0,720],notify_triggered_interval_hours:[0,720],max_notifications_per_day:[0,1e3],notification_bundle_threshold:[2,20],snooze_duration_hours:[1,168],warranty_reminder_days:[1,365],budget_alert_threshold:[10,100],currency_decimals:[0,3]};function o(e){let r=n[e];if(!r)throw new Error(`not an int-ranged setting: ${e}`);return r}var t=[0,14],_=[0,1440],u=[1,1e4],a=[0,365],d=15,s=[.1,.9],b=[0,365],A=[.01,1e4],E=[.01,1e5],i=[.1,5];export{n as a,o as b,t as c,_ as d,u as e,a as f,d as g,s as h,b as i,A as j,E as k,i as l};
@@ -0,0 +1,88 @@
/*! maintenance_supporter frontend 2.99.0 */
import{O as m,a,b as y,c as s,f as l,h as g,l as o,m as f,s as d}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";function $(n){let t=m("2001-02-03",n).match(/\d+/g)||[],e=v=>t.findIndex(_=>Number(_)===v),h=e(2001),c=e(2),u=e(3);return h===0?"YMD":c>=0&&u>=0&&c<u?"MDY":"DMY"}function p(n,i,t){if(n<1e3||n>9999||i<1||i>12||t<1||t>31)return null;let e=new Date(n,i-1,t);return e.getFullYear()!==n||e.getMonth()!==i-1||e.getDate()!==t?null:`${n}-${String(i).padStart(2,"0")}-${String(t).padStart(2,"0")}`}function b(n,i){let t=n.trim(),e;if(e=t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/))return p(+e[1],+e[2],+e[3]);if(e=t.match(/^(\d{4})$/))return p(+e[1],1,1);if(e=t.match(/^(\d{1,2})[./](\d{4})$/))return p(+e[2],+e[1],1);if(e=t.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/))return p(+e[3],+e[2],+e[1]);if(e=t.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/)){let h=+e[1],c=+e[2],u=+e[3];return $(i)==="MDY"?p(u,h,c):p(u,c,h)}return null}function x(n,i){if(i)switch(n){case"date":return i.split("T")[0];case"time":return i.length===5?`${i}:00`:i;case"datetime":{let[t,e="00:00:00"]=i.split("T");return`${t} ${e.length===5?`${e}:00`:e}`}}}function k(n,i){if(typeof i!="string"||!i)return"";switch(n){case"date":return i.slice(0,10);case"time":return i.slice(0,5);case"datetime":{let[t,e="00:00:00"]=i.split(" ");return`${t}T${e.length===5?`${e}:00`:e}`}}}var r=class extends g{constructor(){super(...arguments);this.kind="date";this.label="";this.value="";this.clearable=!1;this.disabled=!1;this.required=!1;this.lang="en";this._typing=!1;this._typed="";this._typedInvalid=!1}_startTyping(){this._typed=this.value,this._typedInvalid=!1,this._typing=!0,this.updateComplete.then(()=>this.shadowRoot?.querySelector("input.typed")?.focus())}_commitTyped(){if(!this._typed.trim()){this._typing=!1;return}let t=b(this._typed,this.lang);if(!t){this._typedInvalid=!0;return}this._typing=!1,this._typedInvalid=!1,this._emit(t)}_onTypedKey(t){t.key==="Enter"?(t.preventDefault(),this._commitTyped()):t.key==="Escape"&&(t.preventDefault(),this._typing=!1,this._typedInvalid=!1)}_selector(){switch(this.kind){case"date":return{date:{}};case"time":return{time:{no_second:!0}};case"datetime":return{datetime:{}}}}_onSelectorChange(t){t.stopPropagation(),this._emit(k(this.kind,t.detail?.value))}_clear(){this._emit("")}_emit(t){t!==this.value&&(this.value=t,this.dispatchEvent(new CustomEvent("value-changed",{bubbles:!0,composed:!0,detail:{value:t}})))}render(){return s`
<div class="field">
${this.label?s`<span class="label">${this.label}${this.required?s`<span class="req">*</span>`:l}</span>`:l}
<div class="row">
${this._typing?s`<input class="typed" type="text" inputmode="numeric" autocomplete="off"
.value=${this._typed}
placeholder=${m("1978-03-15",this.lang)}
aria-invalid=${this._typedInvalid?"true":"false"}
@input=${t=>{this._typed=t.target.value,this._typedInvalid=!1}}
@keydown=${this._onTypedKey}
@blur=${this._commitTyped} />`:s`<ha-selector
.hass=${this.hass}
.selector=${this._selector()}
.value=${x(this.kind,this.value)}
.required=${this.required}
.disabled=${this.disabled}
@value-changed=${this._onSelectorChange}
></ha-selector>`}
${this.kind==="date"&&!this.disabled&&!this._typing?s`<button type="button" class="type-toggle" title=${d("date_type_toggle",this.lang)} aria-label=${d("date_type_toggle",this.lang)} @click=${this._startTyping}>
<ha-icon icon="mdi:keyboard-outline"></ha-icon>
</button>`:l}
${this.clearable&&this.value&&!this.disabled&&!this._typing?s`<button type="button" class="clear" title=${d("clear",this.lang)} aria-label=${d("clear",this.lang)} @click=${this._clear}>
<ha-icon icon="mdi:close"></ha-icon>
</button>`:l}
</div>
${this._typedInvalid?s`<span class="helper invalid">${d("date_type_invalid",this.lang)}</span>`:this.helper?s`<span class="helper">${this.helper}</span>`:l}
</div>
`}};r.styles=y`
:host { display: block; }
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
font-size: 12px;
color: var(--secondary-text-color, #888);
font-weight: 500;
}
.req { color: var(--error-color, #f44336); margin-left: 2px; }
.row {
display: flex;
align-items: center;
gap: 4px;
}
ha-selector {
flex: 1;
min-width: 0;
}
.clear, .type-toggle {
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--secondary-text-color);
cursor: pointer;
--mdc-icon-size: 20px;
}
.clear:hover, .type-toggle:hover { background: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.08); }
.clear:focus-visible, .type-toggle:focus-visible { outline: 2px solid var(--primary-color); }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
font-style: italic;
}
.helper.invalid { color: var(--error-color, #f44336); font-style: normal; }
input.typed {
flex: 1;
min-width: 0;
height: 40px;
box-sizing: border-box;
padding: 0 12px;
border: 1px solid var(--divider-color, #ccc);
border-radius: 4px;
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
font: inherit;
}
input.typed[aria-invalid="true"] { border-color: var(--error-color, #f44336); }
input.typed:focus { outline: 2px solid var(--primary-color); outline-offset: -1px; }
`,a([o({attribute:!1})],r.prototype,"hass",2),a([o()],r.prototype,"kind",2),a([o()],r.prototype,"label",2),a([o()],r.prototype,"value",2),a([o()],r.prototype,"helper",2),a([o({type:Boolean})],r.prototype,"clearable",2),a([o({type:Boolean})],r.prototype,"disabled",2),a([o({type:Boolean})],r.prototype,"required",2),a([o()],r.prototype,"lang",2),a([f()],r.prototype,"_typing",2),a([f()],r.prototype,"_typed",2),a([f()],r.prototype,"_typedInvalid",2);customElements.get("ms-date-field")||customElements.define("ms-date-field",r);
@@ -0,0 +1,234 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as q}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JQXHX4U4.js";import{H as k,a,b as f,c as n,f as u,h as x,l as h,m as c,s as e}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";function d(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function _(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":d(r)}function y(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var l=class extends x{constructor(){super(...arguments);this.lang="";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._quickResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}get _dialogLang(){return this.lang||k(this.hass)}openForObject(t,i){this._entryId=t,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}openForTask(t,i,s,o){this._entryId=t,this._taskId=i,this._objectName=s,this._taskName=o,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}async _generate(){let t=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let s=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&(s.push(this.hass.connection.sendMessagePromise({...i,action:"complete"})),await this._hasQuickCompleteDefaults()&&s.push(this.hass.connection.sendMessagePromise({...i,action:"quick_complete"})));let o=await Promise.all(s);if(t!==this._generateSeq)return;this._viewResult=o[0],o.length>1&&(this._completeResult=o[1]),o.length>2&&(this._quickResult=o[2])}catch(i){if(t!==this._generateSeq)return;let s=i?.code,o=i?.message;this._error=s==="no_url"||typeof o=="string"&&o.includes("No Home Assistant URL")?e("qr_error_no_url",this._dialogLang):e("qr_error",this._dialogLang)}finally{t===this._generateSeq&&(this._loading=!1)}}async _hasQuickCompleteDefaults(){try{let i=((await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId})).tasks||[]).find(s=>s.id===this._taskId);return!!i?.quick_complete_defaults&&Object.keys(i.quick_complete_defaults).length>0}catch{return!1}}_setUrlMode(t){this._urlMode!==t&&(this._urlMode=t,this._generate())}_print(){if(!this._viewResult)return;let t=this._viewResult,i=t.label.task_name?`${t.label.object_name} \u2014 ${t.label.task_name}`:t.label.object_name,s=[t.label.manufacturer,t.label.model].filter(Boolean).join(" "),o=window.open("","_blank","width=600,height=500");if(!o)return;let p=this._dialogLang,m=d(i),g=d(s),v=!!this._completeResult,b=!!this._quickResult,w=d(e("qr_action_view",p)),$=d(e("qr_action_complete",p)),R=d(e("qr_action_quick_complete",p));o.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${m}</title>
<style>
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
carry their own white quiet zone and stay scannable either way, but the
labels below are explicit dark greys and would vanish on a WebView's dark
canvas. Same reasoning as helpers/report.ts. */
:root{color-scheme:light}
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
h2{margin:0 0 4px}
.sub{color:#666;font-size:14px;margin-bottom:16px}
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
.qr-col img{width:${b?"170px":v?"200px":"280px"}}
.qr-label{font-size:13px;font-weight:500;color:#333}
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
</style></head><body>
<h2>${m}</h2>
${g?`<div class="sub">${g}</div>`:""}
<div class="qr-row">
<div class="qr-col">
<img src="${_(this._viewResult.svg_data_uri)}" alt="QR Info" />
<div class="qr-label">${w}</div>
</div>
${v?`<div class="qr-col">
<img src="${_(this._completeResult.svg_data_uri)}" alt="QR Complete" />
<div class="qr-label">${$}</div>
</div>`:""}
${b?`<div class="qr-col">
<img src="${_(this._quickResult.svg_data_uri)}" alt="QR Quick-complete" />
<div class="qr-label">${R}</div>
</div>`:""}
</div>
<div class="url">${d(this._viewResult.url)}</div>
<script>setTimeout(()=>window.print(),300)<\/script>
</body></html>`),o.document.close()}_downloadSvg(t,i){let s=decodeURIComponent(t.svg_data_uri.replace("data:image/svg+xml,","")),o=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;q(s,`qr-${y(o)}-${i}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._quickResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let t=this._dialogLang,i=this._taskName?`${e("qr_code",t)}: ${this._objectName} \u2014 ${this._taskName}`:`${e("qr_code",t)}: ${this._objectName}`,s=!!this._viewResult;return n`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${i}</div>
<div class="content">
${this._loading?n`<div class="loading">${e("qr_generating",t)}</div>`:this._error?n`<div class="error">${this._error}</div>`:s?n`
<div class="qr-pair">
<div class="qr-item">
<img
class="qr-image ${this._completeResult?"small":""}"
src="${this._viewResult.svg_data_uri}"
alt="QR Info"
/>
<div class="qr-item-label">${e("qr_action_view",t)}</div>
<button class="dl-btn"
@click=${()=>this._downloadSvg(this._viewResult,"info")}>
<ha-icon icon="mdi:download"></ha-icon>
${e("qr_download",t)}
</button>
</div>
${this._completeResult?n`
<div class="qr-item">
<img
class="qr-image small"
src="${this._completeResult.svg_data_uri}"
alt="QR Complete"
/>
<div class="qr-item-label">${e("qr_action_complete",t)}</div>
<button class="dl-btn"
@click=${()=>this._downloadSvg(this._completeResult,"complete")}>
<ha-icon icon="mdi:download"></ha-icon>
${e("qr_download",t)}
</button>
</div>
`:u}
${this._quickResult?n`
<div class="qr-item quick">
<img
class="qr-image small"
src="${this._quickResult.svg_data_uri}"
alt="QR Quick-complete"
/>
<div class="qr-item-label">${e("qr_action_quick_complete",t)}</div>
<button class="dl-btn"
@click=${()=>this._downloadSvg(this._quickResult,"quick-complete")}>
<ha-icon icon="mdi:download"></ha-icon>
${e("qr_download",t)}
</button>
</div>
`:u}
</div>
<div class="url-display">${this._viewResult.url}</div>
`:u}
<div class="action-row">
<label>${e("qr_url_mode",t)}</label>
<div class="action-toggle">
<button class="toggle-btn ${this._urlMode==="companion"?"active":""}"
@click=${()=>this._setUrlMode("companion")}>${e("qr_mode_companion",t)}</button>
<button class="toggle-btn ${this._urlMode==="local"?"active":""}"
@click=${()=>this._setUrlMode("local")}>${e("qr_mode_local",t)}</button>
<button class="toggle-btn ${this._urlMode==="server"?"active":""}"
@click=${()=>this._setUrlMode("server")}>${e("qr_mode_server",t)}</button>
</div>
</div>
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${e("cancel",t)}
</ha-button>
<ha-button
@click=${this._print}
.disabled=${!s}
>
${e("qr_print",t)}
</ha-button>
</div>
</ha-dialog>
`}};l.styles=f`
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
min-width: 300px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
.qr-pair {
display: flex;
flex-wrap: wrap; /* three codes (quick-complete, #192) wrap on a phone */
gap: 20px;
justify-content: center;
width: 100%;
}
.qr-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.qr-image {
width: 240px;
height: 240px;
image-rendering: pixelated;
}
.qr-image.small {
width: 180px;
height: 180px;
}
.qr-item-label {
max-width: 180px;
font-size: 12px;
font-weight: 500;
color: var(--secondary-text-color);
text-align: center;
}
.dl-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: none;
border: 1px solid var(--divider-color, #e0e0e0);
cursor: pointer;
font-size: 13px;
color: var(--primary-text-color);
padding: 6px 14px;
border-radius: 18px;
transition: background 0.2s, border-color 0.2s;
}
.dl-btn:hover {
background: var(--secondary-background-color, #f5f5f5);
border-color: var(--primary-color);
}
.dl-btn ha-icon {
--mdc-icon-size: 18px;
}
.url-display {
font-size: 11px;
color: var(--secondary-text-color);
word-break: break-all;
text-align: center;
max-width: 400px;
}
.loading {
padding: 40px 0;
color: var(--secondary-text-color);
}
.error {
padding: 20px 0;
color: var(--error-color, #f44336);
}
.action-row {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.action-row label {
font-size: 13px;
color: var(--secondary-text-color);
}
.action-toggle {
display: flex;
gap: 4px;
background: var(--divider-color, #e0e0e0);
border-radius: 6px;
padding: 3px;
}
.toggle-btn {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--primary-text-color);
cursor: pointer;
border-radius: 4px;
font-size: 13px;
transition: all 0.2s;
line-height: 1.3;
}
.toggle-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
.toggle-btn.active {
background: var(--primary-color);
color: var(--text-primary-color, #fff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
`,a([h({attribute:!1})],l.prototype,"hass",2),a([h()],l.prototype,"lang",2),a([c()],l.prototype,"_open",2),a([c()],l.prototype,"_loading",2),a([c()],l.prototype,"_error",2),a([c()],l.prototype,"_viewResult",2),a([c()],l.prototype,"_completeResult",2),a([c()],l.prototype,"_quickResult",2),a([c()],l.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",l);export{l as a};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
var E={\u00DF:"s","\u1E9E":"s",\u00E6:"a",\u0153:"o",\u00F8:"o",\u0111:"d",\u00F0:"d",\u00FE:"t",\u0142:"l",\u0131:"i"},d=10,R=7,g=4,h=2,S=5,x=3,_=/[a-z0-9]+/g;function u(r){let e="";for(let t of r.toLowerCase()){if(t.charCodeAt(0)<128&&t.length===1){e+=t;continue}let n=E[t];if(n){e+=n;continue}let o=t.normalize("NFKD")[0]||t;e+=o.charCodeAt(0)<128?o:t}return e}function m(r){return u(r).match(_)||[]}function C(r){return u(r).replace(/[^a-z0-9]+/g,"")}function b(r){return r.replace(/ue/g,"u").replace(/oe/g,"o").replace(/ae/g,"a").replace(/ss/g,"s")}function F(r){let e=[];for(let t of m(r)){let n=[t],o=b(t);o&&o!==t&&n.push(o),e.push(n)}return e}function p(r,e){if(r===e)return!0;let t=r.length,n=e.length;if(Math.abs(t-n)>1)return!1;if(t===n){let l=[];for(let i=0;i<t;i++)r[i]!==e[i]&&l.push(i);if(l.length===1)return!0;if(l.length===2&&l[1]===l[0]+1){let i=l[0];return r[i]===e[i+1]&&r[i+1]===e[i]}return!1}let[o,s]=t>n?[r,e]:[e,r],c=0,f=0,a=!1;for(;c<o.length&&f<s.length;)if(o[c]===s[f])c++,f++;else{if(a)return!1;a=!0,c++}return!0}function N(r,e){let t=0;for(let n of r)if(n){if(e===n)return d;if(e.startsWith(n)){t=Math.max(t,R);continue}if(n.length>=x&&e.includes(n)){t=Math.max(t,g);continue}n.length>=S&&t<h&&(p(n,e)||e.length>n.length&&p(n,e.slice(0,n.length)))&&(t=h)}return t}function I(r,e){let t=0;for(let n of m(e))if(t=Math.max(t,N(r,n)),t===d)return t;if(t<g){let n=C(e);for(let o of r)if(o.length>=x&&n.includes(o))return g}return t}function O(r,e){if(!r)return!1;let t=F(e);if(!t.length){let n=u(e.trim());return n.length>0&&u(r).includes(n)}return M(t,[{text:r,weight:1}])>0}function M(r,e){if(!r.length)return 0;let t=e.filter(o=>o.text),n=0;for(let o of r){let s=0;for(let c of t){let f=I(o,c.text)*c.weight;f>s&&(s=f)}if(s===0)return 0;n+=s}return n}export{F as a,O as b,M as c};
@@ -0,0 +1,79 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as r,b as l,c as i,f as o,h as n,l as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var e=class extends n{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1;this.multiline=!1;this.rows=3}_onInput(s){let a=s.target.value;this.value=a,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:a}}))}render(){return i`
<label class="field">
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:o}</span>`:o}
${this.multiline?i`
<textarea
.value=${this.value??""}
rows=${this.rows}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
@input=${this._onInput}
@change=${this._onInput}
></textarea>`:i`
<input
.value=${this.value??""}
.type=${this.type}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
step=${this.step??o}
min=${this.min??o}
max=${this.max??o}
pattern=${this.pattern??o}
@input=${this._onInput}
@change=${this._onInput}
/>`}
${this.helper?i`<span class="helper">${this.helper}</span>`:o}
</label>
`}};e.styles=l`
:host { display: block; }
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
font-size: 12px;
color: var(--secondary-text-color, #888);
font-weight: 500;
}
.req { color: var(--error-color, #f44336); margin-left: 2px; }
input {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
}
input:focus {
border-color: var(--primary-color);
}
input:disabled { opacity: 0.5; cursor: not-allowed; }
textarea {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
resize: vertical;
}
textarea:focus { border-color: var(--primary-color); }
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
font-style: italic;
}
`,r([t()],e.prototype,"label",2),r([t()],e.prototype,"value",2),r([t()],e.prototype,"placeholder",2),r([t()],e.prototype,"type",2),r([t({type:Boolean})],e.prototype,"required",2),r([t({type:Boolean})],e.prototype,"disabled",2),r([t()],e.prototype,"step",2),r([t()],e.prototype,"min",2),r([t()],e.prototype,"max",2),r([t()],e.prototype,"pattern",2),r([t()],e.prototype,"helper",2),r([t({type:Boolean})],e.prototype,"multiline",2),r([t({type:Number})],e.prototype,"rows",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e);
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
async function i(o,n,e){let t=o.auth;if(t?.expired&&t.refreshAccessToken)try{await t.refreshAccessToken()}catch{}return fetch(n,{...e,headers:{...e.headers??{},Authorization:`Bearer ${t?.data?.access_token??""}`}})}var u=500*1024*1024;async function d(o,n,e,t="doc_too_large",s=[]){let r=await i(o,n,{method:"POST",body:e});if(r.status===413)throw new Error(t);if(!r.ok){let a=await r.json().catch(()=>null);throw typeof a?.code=="string"&&s.includes(a.code)?new Error(a.code):new Error("doc_upload_failed")}return await r.json()}async function c(o,n,e,t){let s=new FormData;s.append("entry_id",n);for(let a of t)s.append("tags",a);s.append("file",e,e.name);let r=await d(o,"/api/maintenance_supporter/document/upload",s);if(!r.id)throw new Error("doc_upload_failed");return{id:r.id,deduped:!!r.deduped,duplicate_in_object:r.duplicate_in_object??null}}async function l(o,n,e){return(await c(o,n,e,["photo"])).id}async function m(o,n,e){await Promise.all(e.map(t=>o.connection.sendMessagePromise({type:"maintenance_supporter/documents/discard_upload",entry_id:n,doc_id:t}).catch(()=>{})))}export{u as a,d as b,c,l as d,m as e};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,256 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GM2CHGSG.js";import{H as v,a as r,b as _,c as l,f as o,h,l as c,m as n,s as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";var s=class extends h{constructor(){super(...arguments);this.objects=[];this.placesEnabled=!1;this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._place="";this._remindOnSite=!1;this._entryId=null;this._replacing=!1;this._oldDeviceId="";this._replaceDevice="keep"}get _lang(){return v(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._place="",this._remindOnSite=!1,this._replacing=!1,this._error="",this._open=!0}openReplace(e,a){this._entryId=e,this._replacing=!0,this._name=a.name||"",this._oldDeviceId=a.ha_device_id||"",this._haDeviceId="",this._replaceDevice="keep",this._error="",this._open=!0}openEdit(e,a){this._entryId=e,this._name=a.name||"",this._manufacturer=a.manufacturer||"",this._model=a.model||"",this._serialNumber=a.serial_number||"",this._areaId=a.area_id||"",this._installationDate=a.installation_date||"",this._warrantyExpiry=a.warranty_expiry||"",this._documentationUrl=a.documentation_url||"",this._notes=a.notes||"",this._haDeviceId=a.ha_device_id||"",this._parentEntryId=a.parent_entry_id||"",this._place=a.place||"",this._remindOnSite=!!a.remind_on_site,this._replacing=!1,this._error="",this._open=!0}async _replace(){if(this._loading||!this._entryId)return;let e={type:"maintenance_supporter/object/replace",entry_id:this._entryId,name:this._name.trim()||null};this._oldDeviceId?(this._replaceDevice==="other"&&(e.ha_device_id=this._haDeviceId),this._replaceDevice==="none"&&(e.ha_device_id=null)):this._haDeviceId&&(e.ha_device_id=this._haDeviceId);let a=await p(this,e,{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-replaced",{detail:{entry_id:a?.entry_id,device_swap:a?.device_swap}})))}_deviceName(e){let t=this.hass?.devices?.[e];return t?.name_by_user||t?.name||e}_deviceSelector(e){return l`<ha-form
.hass=${this.hass}
.data=${{device:this._haDeviceId||void 0}}
.schema=${[{name:"device",selector:{device:{}}}]}
.computeLabel=${()=>e}
@value-changed=${a=>this._haDeviceId=a.detail.value?.device||""}
></ha-form>`}_renderReplace(){let e=this._lang,a=(d,u)=>l`<label class="radio-row">
<input
type="radio"
name="replace-device"
.checked=${this._replaceDevice===d}
@change=${()=>{this._replaceDevice=d}}
/>
<span>${u}</span>
</label>`,t=this._replaceDevice==="other"&&!this._haDeviceId;return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${i("replace_object",e)}</div>
<div class="content">
${this._error?l`<div class="error">${this._error}</div>`:o}
<div class="hint">${i("replace_object_prompt",e)}</div>
<ms-textfield
label="${i("replace_name_label",e)}"
.value=${this._name}
@input=${d=>this._name=d.target.value}
></ms-textfield>
${this._oldDeviceId?l`<div class="radio-group" role="radiogroup" aria-label=${i("replace_device_heading",e)}>
<div class="textarea-label">${i("replace_device_heading",e)}</div>
${a("keep",i("replace_device_keep",e).replace("{device}",this._deviceName(this._oldDeviceId)))}
${this._replaceDevice==="keep"?l`<div class="hint">${i("replace_device_keep_hint",e)}</div>`:o}
${a("other",i("replace_device_other",e))}
${this._replaceDevice==="other"?l`${this._deviceSelector(i("replace_device_pick",e))}
<div class="hint">${i("replace_device_other_hint",e)}</div>`:o}
${a("none",i("replace_device_none",e))}
</div>`:this._deviceSelector(i("link_device_optional",e))}
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${i("cancel",e)}
</ha-button>
<ha-button @click=${this._replace} .disabled=${this._loading||t}>
${this._loading?i("saving",e):i("replace_object",e)}
</ha-button>
</div>
</ha-dialog>
`}_renderPlace(e){let a=!!this._place&&!this.hass?.states?.[this._place];return l`
<ha-form
.hass=${this.hass}
.data=${{place:this._place||void 0}}
.schema=${[{name:"place",selector:{entity:{domain:"zone"}}}]}
.computeLabel=${()=>i("place_optional",e)}
@value-changed=${t=>{this._place=t.detail.value?.place||"",this._place||(this._remindOnSite=!1)}}
></ha-form>
${a?l`<div class="place-missing">${i("place_missing",e)}</div>`:o}
<label class="place-on-site ${this._place?"":"disabled"}">
<input
type="checkbox"
.checked=${this._remindOnSite}
?disabled=${!this._place}
@change=${t=>this._remindOnSite=t.target.checked}
/>
<span>${i("remind_on_site",e)}</span>
</label>
${this._place?l`<div class="place-hint">${i("remind_on_site_hint",e)}</div>`:o}
`}async _save(){if(this._loading||!this._name.trim())return;this._error="";let e={name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null,...this.placesEnabled?{place:this._place||null,remind_on_site:!!this._place&&this._remindOnSite}:{}},a=await p(this,this._entryId?{type:"maintenance_supporter/object/update",entry_id:this._entryId,...e}:{type:"maintenance_supporter/object/create",...e},{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-saved",{detail:{device_swap:a?.device_swap}})))}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;if(this._replacing)return this._renderReplace();let e=this._lang,a=this._entryId?i("edit_object",e):i("new_object",e);return l`
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${a}</div>
<div class="content">
${this._error?l`<div class="error">${this._error}</div>`:o}
<ms-textfield
label="${i("name",e)}"
required
.value=${this._name}
@input=${t=>this._name=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("manufacturer_optional",e)}"
.value=${this._manufacturer}
@input=${t=>this._manufacturer=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("model_optional",e)}"
.value=${this._model}
@input=${t=>this._model=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("serial_number_optional",e)}"
.value=${this._serialNumber}
@input=${t=>this._serialNumber=t.target.value}
></ms-textfield>
<ms-textfield
label="${i("documentation_url_optional",e)}"
type="url"
.value=${this._documentationUrl}
@input=${t=>this._documentationUrl=t.target.value}
></ms-textfield>
<ha-area-picker
.hass=${this.hass}
label="${i("area_id_optional",e)}"
.value=${this._areaId}
@value-changed=${t=>this._areaId=t.detail.value||""}
></ha-area-picker>
${this.placesEnabled?this._renderPlace(e):o}
<ms-date-field
kind="date"
clearable
.hass=${this.hass}
.lang=${e}
label="${i("installation_date_optional",e)}"
.value=${this._installationDate}
@value-changed=${t=>this._installationDate=t.detail.value}
></ms-date-field>
<ms-date-field
kind="date"
clearable
.hass=${this.hass}
.lang=${e}
label="${i("warranty_expiry_optional",e)}"
.value=${this._warrantyExpiry}
@value-changed=${t=>this._warrantyExpiry=t.detail.value}
></ms-date-field>
<ha-form
.hass=${this.hass}
.data=${{device:this._haDeviceId||void 0}}
.schema=${[{name:"device",selector:{device:{}}}]}
.computeLabel=${()=>i("link_device_optional",e)}
@value-changed=${t=>this._haDeviceId=t.detail.value?.device||""}
></ha-form>
${this._parentChoices().length?l`<label class="textarea-field">
<span class="textarea-label">${i("parent_object_optional",e)}</span>
<select
class="parent-select"
.value=${this._parentEntryId}
@change=${t=>this._parentEntryId=t.target.value}
>
<option value="" ?selected=${!this._parentEntryId}>
${i("parent_none",e)}
</option>
${this._parentChoices().map(t=>l`<option
value=${t.entry_id}
?selected=${this._parentEntryId===t.entry_id}
>${t.object.name}</option>`)}
</select>
</label>`:o}
<label class="textarea-field">
<span class="textarea-label">${i("object_notes_optional",e)}</span>
<textarea
rows="3"
.value=${this._notes}
@input=${t=>this._notes=t.target.value}
></textarea>
<span class="md-hint">${i("notes_markdown_hint",e)}</span>
</label>
</div>
<div class="dialog-actions">
<ha-button appearance="plain" @click=${this._close}>
${i("cancel",this._lang)}
</ha-button>
<ha-button
@click=${this._save}
.disabled=${this._loading||!this._name.trim()}
>
${this._loading?i("saving",this._lang):i("save",this._lang)}
</ha-button>
</div>
</ha-dialog>
`}};s.styles=_`
.place-on-site {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--primary-text-color);
}
.place-on-site.disabled {
color: var(--disabled-text-color, var(--secondary-text-color));
}
.place-hint,
.place-missing {
font-size: 12px;
color: var(--secondary-text-color);
margin-top: -4px;
}
.place-missing {
color: var(--warning-color, #c77700);
}
.dialog-title {
font-size: 18px;
font-weight: 500;
padding-bottom: 12px;
}
.content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 300px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
}
ms-textfield {
display: block;
}
.textarea-field {
display: flex; flex-direction: column; gap: 4px;
}
.textarea-label {
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
}
.textarea-field textarea {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
resize: vertical;
}
.textarea-field textarea:focus {
outline: none; border-color: var(--primary-color);
}
.md-hint {
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
}
.parent-select {
padding: 8px 10px; font-size: 14px; font-family: inherit;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color); border-radius: 6px;
}
.error {
color: var(--error-color, #f44336);
font-size: 13px;
}
.hint {
font-size: 13px;
color: var(--secondary-text-color);
line-height: 1.4;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
cursor: pointer;
}
.radio-row input {
margin: 0;
}
`,r([c({attribute:!1})],s.prototype,"hass",2),r([c({attribute:!1})],s.prototype,"objects",2),r([c({type:Boolean})],s.prototype,"placesEnabled",2),r([n()],s.prototype,"_open",2),r([n()],s.prototype,"_loading",2),r([n()],s.prototype,"_error",2),r([n()],s.prototype,"_name",2),r([n()],s.prototype,"_manufacturer",2),r([n()],s.prototype,"_model",2),r([n()],s.prototype,"_serialNumber",2),r([n()],s.prototype,"_areaId",2),r([n()],s.prototype,"_installationDate",2),r([n()],s.prototype,"_warrantyExpiry",2),r([n()],s.prototype,"_documentationUrl",2),r([n()],s.prototype,"_notes",2),r([n()],s.prototype,"_haDeviceId",2),r([n()],s.prototype,"_parentEntryId",2),r([n()],s.prototype,"_place",2),r([n()],s.prototype,"_remindOnSite",2),r([n()],s.prototype,"_entryId",2),r([n()],s.prototype,"_replacing",2),r([n()],s.prototype,"_oldDeviceId",2),r([n()],s.prototype,"_replaceDevice",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",s);export{s as a};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{d as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-6BKNHM6I.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-WACUKJSI.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-HFZZVTW7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-55ND3GKE.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-MNPRUCF5.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RUYEMIDM.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RHKFQ4Q2.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GM2CHGSG.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";export{a as MaintenanceCompleteDialog};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{G as a,H as b,I as c,J as d,K as e,L as f,M as g,N as h,O as i,P as j,Q as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-XTTCEGMX.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GWAP5R2S.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EKN6HBJN.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-DRT62KZF.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SYPCMUXO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZHMFEWT2.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-PL7JXZOW.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TX2GMEMJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-6BKNHM6I.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-WACUKJSI.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-HFZZVTW7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-55ND3GKE.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-MNPRUCF5.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RUYEMIDM.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-SUOGXDNI.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-JQXHX4U4.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-PCR5FRSH.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RHKFQ4Q2.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GM2CHGSG.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";export{b as __resetSettingsCacheForTests,a as getRowActionStyle,h as openCompleteDialog,c as openCreateObjectDialog,e as openCreateTaskDialog,d as openEditObjectDialog,f as openEditTaskDialog,g as openHistoryEditDialog,k as openObjectQuickActions,i as openQrDialog,j as openTaskQuickActions};
@@ -0,0 +1,2 @@
/*! maintenance_supporter frontend 2.99.0 */
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZHMFEWT2.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TX2GMEMJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-RUYEMIDM.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GM2CHGSG.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-3FGYBPXY.js";export{a as MaintenanceObjectDialog};

Some files were not shown because too many files have changed in this diff Show More