189 files
This commit is contained in:
@@ -60,6 +60,7 @@ from .const import (
|
||||
CONF_WEEKLY_DIGEST_ENABLED,
|
||||
DEFAULT_PANEL_ENABLED,
|
||||
DEFAULT_WARNING_DAYS,
|
||||
DOCUMENT_TEXT_INDEX_KEY,
|
||||
DOMAIN,
|
||||
EVENT_UNSUBS_KEY,
|
||||
GLOBAL_UNIQUE_ID,
|
||||
@@ -137,12 +138,15 @@ type MaintenanceSupporterConfigEntry = ConfigEntry[MaintenanceSupporterData]
|
||||
# --- Service Schemas ---
|
||||
SERVICE_COMPLETE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): vol.All(cv.entity_ids, vol.Length(min=1)),
|
||||
vol.Optional("notes"): vol.All(cv.string, vol.Length(max=2000)),
|
||||
vol.Optional("cost"): vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)),
|
||||
vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)),
|
||||
# Meter readings (v2.20, #83): recorded value for `reading` tasks.
|
||||
vol.Optional("reading_value"): vol.All(vol.Coerce(float), vol.Range(min=-1e12, max=1e12)),
|
||||
# #161 phase 2: {reading name: value} for a task with reading slots —
|
||||
# automations know meters by name; resolved case-insensitively.
|
||||
vol.Optional("reading_values"): {cv.string: vol.Any(vol.All(vol.Coerce(float), vol.Range(min=-1e12, max=1e12)), None)},
|
||||
# #128: who did it — a person ENTITY (validated picker, no free text);
|
||||
# resolved to the linked HA user id. Omitted -> the calling user.
|
||||
vol.Optional("completed_by"): cv.entity_id,
|
||||
@@ -155,14 +159,14 @@ SERVICE_COMPLETE_SCHEMA = vol.Schema(
|
||||
|
||||
SERVICE_RESET_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): vol.All(cv.entity_ids, vol.Length(min=1)),
|
||||
vol.Optional("date"): cv.date,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SKIP_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): vol.All(cv.entity_ids, vol.Length(min=1)),
|
||||
vol.Optional("reason"): vol.All(cv.string, vol.Length(max=2000)),
|
||||
}
|
||||
)
|
||||
@@ -455,6 +459,24 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
await doc_store.async_load()
|
||||
hass.data[DOMAIN][DOCUMENT_STORE_KEY] = doc_store
|
||||
|
||||
# Full-text index over the blobs (#171): extraction meta loads now, the
|
||||
# sidecars are read on the first search, new blobs are extracted as they
|
||||
# arrive and the existing library is backfilled after HA started.
|
||||
from .helpers.document_text import DocumentTextIndex
|
||||
|
||||
text_index = DocumentTextIndex(hass, doc_store)
|
||||
await text_index.async_load()
|
||||
doc_store.text_index = text_index
|
||||
hass.data[DOMAIN][DOCUMENT_TEXT_INDEX_KEY] = text_index
|
||||
|
||||
# Reference numbers (#170): the object-level counter lives in its own
|
||||
# small Store; objects are numbered when their entry is set up.
|
||||
from .helpers.reference_numbers import REFERENCE_NUMBERS_KEY, ReferenceNumbers
|
||||
|
||||
reference_numbers = ReferenceNumbers(hass)
|
||||
await reference_numbers.async_load()
|
||||
hass.data[DOMAIN][REFERENCE_NUMBERS_KEY] = reference_numbers
|
||||
|
||||
|
||||
# Authenticated upload + serve endpoints for document blobs (the blobs live
|
||||
# under /config, so they must never be exposed via an unauthenticated path).
|
||||
@@ -536,11 +558,15 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
# Per-task values must not fan out: a reading belongs to ONE
|
||||
# meter, cost/duration would multiply into the budget and the
|
||||
# averages, and one physical scan proves exactly one tag.
|
||||
for field in ("reading_value", "cost", "duration", "via_tag_scan"):
|
||||
for field in ("reading_value", "reading_values", "cost", "duration"):
|
||||
if call.data.get(field) is not None:
|
||||
raise ServiceValidationError(
|
||||
f"'{field}' can only be used with a single entity_id"
|
||||
)
|
||||
# An explicit ``via_tag_scan: false`` claims nothing — only a
|
||||
# TRUE flag is the one-tag-one-task proof that must not fan out.
|
||||
if call.data.get("via_tag_scan"):
|
||||
raise ServiceValidationError("'via_tag_scan' can only be used with a single entity_id")
|
||||
# #128: explicit person beats the call context; the context covers the
|
||||
# common case for free (a dashboard tap propagates the tapping user).
|
||||
if call.data.get("completed_by"):
|
||||
@@ -549,12 +575,29 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
completed_by = call.context.user_id if call.context else None
|
||||
|
||||
async def _one(coordinator: MaintenanceCoordinator, task_id: str) -> None:
|
||||
reading_values = None
|
||||
if call.data.get("reading_values"):
|
||||
from .helpers.reading_slots import resolve_reading_values_by_name
|
||||
|
||||
slot_task = coordinator._get_merged_tasks_data().get(task_id) or {}
|
||||
try:
|
||||
reading_values = (
|
||||
resolve_reading_values_by_name(
|
||||
slot_task.get("readings") or [],
|
||||
call.data["reading_values"],
|
||||
default_unit=slot_task.get("reading_unit"),
|
||||
)
|
||||
or None
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(str(err)) from err
|
||||
await coordinator.complete_maintenance(
|
||||
task_id=task_id,
|
||||
notes=call.data.get("notes"),
|
||||
cost=call.data.get("cost"),
|
||||
duration=call.data.get("duration"),
|
||||
reading_value=call.data.get("reading_value"),
|
||||
reading_values=reading_values,
|
||||
completed_by=completed_by,
|
||||
completed_at=call.data.get("completed_at"),
|
||||
# Proof of presence: an automation that reacted to the physical
|
||||
@@ -1409,6 +1452,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
# HA-started so the store has finished loading.
|
||||
entry.async_on_unload(async_at_started(hass, _check_document_storage_issues))
|
||||
|
||||
# Backfill the document search index (#171) a couple of minutes after
|
||||
# start — pypdf is CPU work, and boot is the wrong moment for it.
|
||||
entry.async_on_unload(async_at_started(hass, _schedule_document_text_backfill))
|
||||
|
||||
# A global entry now exists — clear the orphan repair issue immediately
|
||||
# (e.g. right after the repair flow recreated it, when HA is already
|
||||
# started and the async_at_started check above won't fire again).
|
||||
@@ -1544,6 +1591,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
if retranslate_seeded_texts(hass, entry, normalize_language(hass)):
|
||||
_LOGGER.info("Retranslated the battery fleet's seeded texts for %s", entry.title)
|
||||
|
||||
# #170: give this object (and any other still unnumbered one) its
|
||||
# reference number before the first refresh numbers the tasks.
|
||||
from .helpers.reference_numbers import REFERENCE_NUMBERS_KEY, ReferenceNumbers
|
||||
|
||||
reference_numbers = hass.data[DOMAIN].get(REFERENCE_NUMBERS_KEY)
|
||||
if isinstance(reference_numbers, ReferenceNumbers):
|
||||
await reference_numbers.async_assign_objects()
|
||||
|
||||
coordinator = MaintenanceCoordinator(hass, entry, store)
|
||||
entry.runtime_data = MaintenanceSupporterData(coordinator=coordinator, store=store)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -1573,10 +1628,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
result["pruned"],
|
||||
result["orphans_removed"],
|
||||
)
|
||||
if result.get("trigger_healed"):
|
||||
if result.get("trigger_healed") or result["added"]:
|
||||
# #156: the recovery flag only reaches the LIVE trigger
|
||||
# after a reload — do it once; the next start finds it set.
|
||||
_LOGGER.info("Battery-fleet trigger healed (auto_complete_on_recovery) — reloading %s", entry.title)
|
||||
# after a reload — and so does the stock SENSOR of a part
|
||||
# added here: the sensor platform was set up before this
|
||||
# ran, so the new part had no entity until the next
|
||||
# restart (bug review 2026-09-04). Reload once; the next
|
||||
# start finds both in place and adds nothing.
|
||||
_LOGGER.info(
|
||||
"Battery-fleet reconcile needs a reload of %s (trigger healed: %s, parts added: %s)",
|
||||
entry.title,
|
||||
bool(result.get("trigger_healed")),
|
||||
result["added"] or "none",
|
||||
)
|
||||
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
||||
|
||||
entry.async_on_unload(async_at_started(hass, _fleet_parts_at_start))
|
||||
@@ -1693,6 +1757,16 @@ def _verify_notify_service(hass: HomeAssistant) -> None:
|
||||
_DOC_STORAGE_ISSUE_ID = "document_storage_issues"
|
||||
|
||||
|
||||
@callback
|
||||
def _schedule_document_text_backfill(hass: HomeAssistant) -> None:
|
||||
"""Kick off the delayed full-text backfill (see helpers/document_text)."""
|
||||
from .helpers.document_text import DocumentTextIndex
|
||||
|
||||
index = hass.data.get(DOMAIN, {}).get(DOCUMENT_TEXT_INDEX_KEY)
|
||||
if isinstance(index, DocumentTextIndex):
|
||||
index.schedule_backfill()
|
||||
|
||||
|
||||
async def _check_document_storage_issues(hass: HomeAssistant) -> None:
|
||||
"""Sync the document-storage repair issue with the current on-disk reality.
|
||||
|
||||
@@ -1912,6 +1986,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
|
||||
shopping_sync = hass.data.get(DOMAIN, {}).pop(SHOPPING_SYNC_KEY, None)
|
||||
if shopping_sync is not None:
|
||||
shopping_sync.async_teardown()
|
||||
text_index = hass.data.get(DOMAIN, {}).get(DOCUMENT_TEXT_INDEX_KEY)
|
||||
if text_index is not None:
|
||||
text_index.cancel()
|
||||
|
||||
# Flush a pending debounced store save BEFORE tearing down — belt and
|
||||
# suspenders next to the store cache: disk is current the moment the entry
|
||||
@@ -2020,11 +2097,15 @@ def _get_coordinator_for_entity(hass: HomeAssistant, entity_id: str) -> Maintena
|
||||
return None
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||
if config_entry is None:
|
||||
# Only OUR entries: a foreign integration's entry may carry its own
|
||||
# runtime_data, and reading ``.coordinator`` off it raised a bare
|
||||
# AttributeError that escaped the per-task error collection and aborted
|
||||
# the rest of a multi-entity call (bug review 2026-09-04).
|
||||
if config_entry is None or config_entry.domain != DOMAIN:
|
||||
return None
|
||||
|
||||
runtime_data: MaintenanceSupporterData | None = getattr(config_entry, "runtime_data", None)
|
||||
if runtime_data is None:
|
||||
runtime_data = getattr(config_entry, "runtime_data", None)
|
||||
if not isinstance(runtime_data, MaintenanceSupporterData):
|
||||
return None
|
||||
|
||||
return runtime_data.coordinator
|
||||
|
||||
@@ -32,6 +32,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Hardcoded English labels for the weekday/occurrence sub-options keep the
|
||||
# config-flow i18n surface small; the kind names are translated via strings.json.
|
||||
CALENDAR_KIND_VALUES = (KIND_WEEKDAYS, KIND_NTH_WEEKDAY, KIND_DAY_OF_MONTH)
|
||||
# #168: every kind that produces a due DATE takes a time of day — the
|
||||
# interval, the three calendar kinds and a one-off. Sensor-based and
|
||||
# manual tasks have no due date to refine. Mirrors SCHEDULE_TIME_KINDS in
|
||||
# the panel's task-dialog.ts.
|
||||
SCHEDULE_TIME_KINDS = ("time_based", "one_time", *CALENDAR_KIND_VALUES)
|
||||
_WEEKDAY_LABELS = (
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
|
||||
@@ -369,7 +369,7 @@ async def _send_test_to(
|
||||
) -> str:
|
||||
"""Send the test payload to every resolved service; "success" if any went."""
|
||||
try:
|
||||
from .helpers.notification_manager import async_dispatch_notify
|
||||
from .helpers.notify_hooks import KIND_TEST, async_emit_and_dispatch, notification_context
|
||||
|
||||
push_msg = _get_test_result_text(hass, "push_message")
|
||||
service_data: dict[str, Any] = {
|
||||
@@ -389,9 +389,12 @@ async def _send_test_to(
|
||||
test_actions.append({"action": "MS_TEST_SNOOZE", "title": "\U0001f4a4 Snooze"})
|
||||
service_data["data"] = {"actions": test_actions}
|
||||
# Dual-path: legacy notify service OR notify entity (send_message).
|
||||
# #165: the test walks the same hook as a real notification, so the
|
||||
# event and the extra-data template can be verified from Settings.
|
||||
context = notification_context(hass, KIND_TEST)
|
||||
sent_any = False
|
||||
for service in services:
|
||||
if await async_dispatch_notify(hass, service, service_data, blocking=True):
|
||||
if await async_emit_and_dispatch(hass, service, service_data, context, blocking=True):
|
||||
sent_any = True
|
||||
return "success" if sent_any else "failed"
|
||||
except Exception: # noqa: BLE001 - any failure mode reports "failed" to the UI
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.helpers import selector
|
||||
|
||||
from .config_flow_helpers import (
|
||||
CALENDAR_KIND_VALUES,
|
||||
SCHEDULE_TIME_KINDS,
|
||||
apply_season_ends,
|
||||
calendar_current,
|
||||
calendar_schema,
|
||||
@@ -49,6 +50,7 @@ from .const import (
|
||||
ScheduleType,
|
||||
)
|
||||
from .helpers.global_options import get_default_warning_days
|
||||
from .helpers.reading_slots import parse_reading_slots_text, reading_slots_text
|
||||
from .helpers.schedule import (
|
||||
read_legacy_fields,
|
||||
)
|
||||
@@ -273,6 +275,14 @@ class TaskCrudMixin:
|
||||
updated_task[CONF_TASK_READING_UNIT] = ru
|
||||
else:
|
||||
updated_task.pop(CONF_TASK_READING_UNIT, None)
|
||||
# #161 phase 2: reading slots, one "Name | Unit" per line. Ids
|
||||
# are kept for lines whose name already exists (delta chain).
|
||||
if "readings_text" in user_input:
|
||||
slots = parse_reading_slots_text(user_input.get("readings_text") or "", task.get("readings"))
|
||||
if slots:
|
||||
updated_task["readings"] = slots
|
||||
else:
|
||||
updated_task.pop("readings", None)
|
||||
|
||||
from .helpers.sanitize import cap_task_fields
|
||||
|
||||
@@ -389,16 +399,6 @@ class TaskCrudMixin:
|
||||
CONF_TASK_INTERVAL_ANCHOR,
|
||||
default=sched["interval_anchor"],
|
||||
): interval_anchor_selector(),
|
||||
**(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TASK_SCHEDULE_TIME,
|
||||
default=task.get("schedule_time", ""),
|
||||
): _OptionalTimeSelector(),
|
||||
}
|
||||
if self._get_global_options().get(CONF_ADVANCED_SCHEDULE_TIME, False)
|
||||
else dict[Any, Any]()
|
||||
),
|
||||
}
|
||||
if sched["schedule_type"] == ScheduleType.TIME_BASED
|
||||
else dict[Any, Any]()
|
||||
@@ -421,6 +421,20 @@ class TaskCrudMixin:
|
||||
if sched["schedule_type"] == ScheduleType.TIME_BASED or sched["schedule_type"] in CALENDAR_KIND_VALUES
|
||||
else dict[Any, Any]()
|
||||
),
|
||||
# Time of day (#168): every date-driven kind — "each Sunday at
|
||||
# 21:00" needs it as much as a 7-day interval does. Only present
|
||||
# while the global advanced flag is on; clear by submitting "".
|
||||
**(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TASK_SCHEDULE_TIME,
|
||||
default=task.get("schedule_time", ""),
|
||||
): _OptionalTimeSelector(),
|
||||
}
|
||||
if self._get_global_options().get(CONF_ADVANCED_SCHEDULE_TIME, False)
|
||||
and sched["schedule_type"] in SCHEDULE_TIME_KINDS
|
||||
else dict[Any, Any]()
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_TASK_WARNING_DAYS,
|
||||
default=task.get("warning_days", get_default_warning_days(self.hass)),
|
||||
@@ -487,6 +501,9 @@ class TaskCrudMixin:
|
||||
nfc_tag_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
|
||||
vol.Optional("require_tag_scan", default=bool(task.get("require_tag_scan"))): selector.BooleanSelector(),
|
||||
reading_unit_key: selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)),
|
||||
vol.Optional("readings_text", default=reading_slots_text(task.get("readings"))): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT, multiline=True)
|
||||
),
|
||||
vol.Optional("go_back", default=False): selector.BooleanSelector(),
|
||||
}
|
||||
),
|
||||
|
||||
@@ -179,7 +179,15 @@ class TriggerStepsMixin(TriggerConfigMixin):
|
||||
|
||||
async def async_step_edit_trigger_proceed(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""Proceed with editing the trigger after reviewing the summary."""
|
||||
self._current_task = {}
|
||||
# Seed the stored trigger so the shared steps see it as ``prev_tc``:
|
||||
# the attribute step rebuilds trigger_config from the form and carries
|
||||
# over only the panel-managed keys the flow has no field for
|
||||
# (#53 recovery flag, #102 baseline, #149 session cap) — with an
|
||||
# empty seed those were silently dropped on every options-flow edit
|
||||
# (bug review 2026-09-04). The type steps also default their
|
||||
# recovery/baseline/combinator fields from it.
|
||||
task = self.config_entry.data.get(CONF_TASKS, {}).get(self._selected_task_id or "", {})
|
||||
self._current_task = {"trigger_config": dict(task.get("trigger_config") or {})}
|
||||
self._trigger_on_complete = self._save_edited_trigger
|
||||
self._on_cancel = self._show_task_action_menu
|
||||
return await self.async_step_opt_sensor_select()
|
||||
|
||||
@@ -16,6 +16,7 @@ DOMAIN = "maintenance_supporter"
|
||||
# at all (the #86 panel-flag desync came from exactly this literal-key class).
|
||||
NOTIFICATION_MANAGER_KEY = "_notification_manager"
|
||||
DOCUMENT_STORE_KEY = "_document_store"
|
||||
DOCUMENT_TEXT_INDEX_KEY = "_document_text_index"
|
||||
BUDGET_CACHE_KEY = "_budget_cache"
|
||||
EVENT_UNSUBS_KEY = "_event_unsubs"
|
||||
# TOP-LEVEL hass.data key (survives the DOMAIN pop on last-entry unload):
|
||||
@@ -173,6 +174,9 @@ CONF_OPERATOR_WRITE_ENABLED = "operator_write_enabled"
|
||||
# show. Selectable from KNOWN_OBJECT_TABLE_COLUMNS only — known object fields,
|
||||
# never arbitrary state attributes.
|
||||
CONF_OBJECTS_TABLE_COLUMNS = "objects_table_columns"
|
||||
# #169 follow-up: per-member avatar overrides {user_id: {initials, color}}
|
||||
# — shapes/palette in helpers/member_display.py.
|
||||
CONF_MEMBER_DISPLAY = "member_display"
|
||||
|
||||
# v2.21: template-gallery curation — ids of built-in templates the admin has
|
||||
# hidden from the "From template" pickers (panel gallery + config flow). The
|
||||
@@ -200,6 +204,7 @@ MAX_ADOPTED_NOTES = 100
|
||||
# these; the WS update handler drops anything outside this set.
|
||||
KNOWN_OBJECT_TABLE_COLUMNS = [
|
||||
"name",
|
||||
"ref_no", # #170 reference number — opt-in column
|
||||
"manufacturer",
|
||||
"model",
|
||||
"serial_number",
|
||||
@@ -273,6 +278,13 @@ CONF_NOTIFICATION_TITLE_STYLE = "notification_title_style"
|
||||
# MATCHING that view's task-selecting filters (label + responsible user) send
|
||||
# reminders. "" = no scope (all tasks). See helpers/saved_views.view_matches_task.
|
||||
CONF_NOTIFY_SCOPE_VIEW_ID = "notify_scope_view_id"
|
||||
# #165: your own notification rule — fire the event and send nothing
|
||||
# yourself (an automation routes), and/or merge template-rendered extra
|
||||
# keys into every notify payload's `data` (Ticker category, Pushover
|
||||
# priority, Telegram parse_mode …).
|
||||
CONF_NOTIFY_EVENT_ONLY = "notify_event_only"
|
||||
CONF_NOTIFY_EXTRA_DATA = "notify_extra_data"
|
||||
MAX_NOTIFY_EXTRA_DATA_LENGTH = 2000
|
||||
NOTIFICATION_TITLE_STYLES = ("default", "object_name", "task_name")
|
||||
|
||||
# --- Config Keys: Notification Actions ---
|
||||
@@ -373,6 +385,18 @@ BATTERY_FLEET_INCLUDED = "battery_fleet_included"
|
||||
# vacuums, smart rings — the #107 filter) in the roster as rechargeables, so
|
||||
# "needs a recharge" can notify like "needs a swap". Default off: #107 stays.
|
||||
BATTERY_FLEET_TRACK_SELF_CHARGING = "battery_fleet_track_self_charging"
|
||||
# D#162: a Battery Notes note WITHOUT a level sensor (only the type sensor,
|
||||
# the last-replaced timestamp and the "replaced" button exist) has no
|
||||
# reading that could ever fire the fleet task — the forecast is all there
|
||||
# is. Default ON: a passed predicted date on such a note counts as due
|
||||
# (low → task trigger). Stored as False to switch off; absent means on.
|
||||
BATTERY_FLEET_DUE_WITHOUT_SENSOR = "battery_fleet_due_without_sensor"
|
||||
# Bug review 2026-09-04: ``batt_<type>`` part ids the user DELETED. The
|
||||
# start-up reconcile re-minted them on every boot as long as the type was
|
||||
# still in the fleet, so a deleted type-part never stayed deleted. Written
|
||||
# by part/delete, honoured by the reconcile, cleared by an explicit
|
||||
# Battery-Fleet setup (the user asked for the full set again).
|
||||
BATTERY_FLEET_REMOVED_PARTS = "battery_fleet_removed_parts"
|
||||
# Task-side link: task["consumes_parts"] = [{"part_id", "quantity"}] — a
|
||||
# completion decrements each linked part's stock.
|
||||
CONF_TASK_CONSUMES_PARTS = "consumes_parts"
|
||||
@@ -402,6 +426,9 @@ CONF_TASK_NFC_TAG = "nfc_tag_id"
|
||||
# v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...).
|
||||
CONF_TASK_READING_UNIT = "reading_unit"
|
||||
MAX_READING_UNIT_LENGTH = 32
|
||||
# #161 phase 2: named reading slots ([{id, name, unit}]) — several values per
|
||||
# completion; shapes/caps live in helpers/reading_slots.py.
|
||||
CONF_TASK_READINGS = "readings"
|
||||
MAX_NFC_TAG_LENGTH = 256
|
||||
CONF_TASK_INTERVAL_ANCHOR = "interval_anchor"
|
||||
CONF_TASK_SCHEDULE_TIME = "schedule_time"
|
||||
@@ -569,6 +596,9 @@ EVENT_TRIGGER_DEACTIVATED = f"{DOMAIN}_trigger_deactivated"
|
||||
# state mutation has persisted. Power users wire HA automations on these;
|
||||
# the integration's own action_listener also subscribes to the COMPLETED
|
||||
# event to dispatch the per-task on_complete_action service-call.
|
||||
# #165: one event per notification the integration is about to send —
|
||||
# the hook for user-written routing (see helpers/notify_hooks.py).
|
||||
EVENT_NOTIFICATION = f"{DOMAIN}_notification"
|
||||
EVENT_TASK_COMPLETED = f"{DOMAIN}_task_completed"
|
||||
EVENT_TASK_SKIPPED = f"{DOMAIN}_task_skipped"
|
||||
EVENT_TASK_RESET = f"{DOMAIN}_task_reset"
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.util import dt as dt_util
|
||||
if TYPE_CHECKING:
|
||||
from .calendar import MaintenanceCalendar
|
||||
from .todo import MaintenanceTodoList
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -47,6 +48,8 @@ from .const import (
|
||||
MaintenanceStatus,
|
||||
ScheduleType,
|
||||
TriggerEntityState,
|
||||
slugify_object_name,
|
||||
task_unique_id,
|
||||
)
|
||||
from .helpers.budget import compute_spend
|
||||
from .helpers.entry_tasks import write_task
|
||||
@@ -160,6 +163,20 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
obj_data.get("name"),
|
||||
)
|
||||
|
||||
def _assign_task_refs(self) -> None:
|
||||
"""#170: number tasks that have none yet (every creation path ends in
|
||||
a refresh, so this is the one place), and completions recorded before
|
||||
the numbering existed — oldest first, so a first start after the
|
||||
update yields the numbering a user would have written down by hand.
|
||||
No-op when everything is numbered."""
|
||||
from .helpers.reference_numbers import assign_history_refs, assign_task_refs
|
||||
|
||||
new_data = assign_task_refs(dict(self.entry.data))
|
||||
if new_data is not None:
|
||||
self.hass.config_entries.async_update_entry(self.entry, data=new_data)
|
||||
if assign_history_refs(self._store, self.entry.data.get(CONF_TASKS, {})):
|
||||
self._store.async_delay_save()
|
||||
|
||||
@property
|
||||
def maintenance_object(self) -> MaintenanceObject:
|
||||
"""Return the maintenance object from config entry data."""
|
||||
@@ -176,6 +193,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# Seasonal pause (N3): auto-resume on the first refresh on/after
|
||||
# paused_until, then continue this refresh un-paused.
|
||||
await self._async_maybe_auto_resume()
|
||||
self._assign_task_refs()
|
||||
|
||||
obj = self.maintenance_object
|
||||
tasks = self.tasks
|
||||
@@ -946,8 +964,9 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
checklist_state: dict[str, bool] | None = None,
|
||||
feedback: str | None = None,
|
||||
completed_by: str | None = None,
|
||||
photo_doc_id: str | None = None,
|
||||
photo_doc_ids: list[str] | None = None,
|
||||
reading_value: float | None = None,
|
||||
reading_values: list[dict[str, Any]] | None = None,
|
||||
restock_quantity: float | None = None,
|
||||
used_parts: list[dict[str, Any]] | None = None,
|
||||
auto: bool = False,
|
||||
@@ -984,6 +1003,16 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
translation_key="completed_at_in_future",
|
||||
)
|
||||
|
||||
# #161 phase 2: a task with reading slots records the per-slot
|
||||
# snapshot, never the single scalar — refused HERE so an old cached
|
||||
# bundle or an automation cannot mix the two shapes on one entry.
|
||||
if reading_value is not None and merged[task_id].get("readings"):
|
||||
raise ServiceValidationError(
|
||||
"This task records named readings — use reading_values instead of reading_value",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="reading_slots_required",
|
||||
)
|
||||
|
||||
# Required completion details. Checked HERE — the one point every
|
||||
# surface funnels through — so a task demanding a note cannot be
|
||||
# closed out from a button, the to-do list, an NFC tag, a
|
||||
@@ -1013,7 +1042,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
notes=notes,
|
||||
cost=cost,
|
||||
duration=duration,
|
||||
photo_doc_id=photo_doc_id,
|
||||
photo_doc_ids=photo_doc_ids,
|
||||
completed_by=completed_by,
|
||||
)
|
||||
if missing:
|
||||
@@ -1183,22 +1212,30 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
checklist_state=checklist_state,
|
||||
feedback=feedback,
|
||||
completed_by=completed_by,
|
||||
photo_doc_id=photo_doc_id,
|
||||
photo_doc_ids=photo_doc_ids,
|
||||
reading_value=reading_value,
|
||||
reading_values=reading_values,
|
||||
used_parts=enriched_used,
|
||||
auto=auto,
|
||||
completed_at=completed_at,
|
||||
)
|
||||
# #170: the completion's reference number ("8.3-2") — handed out here,
|
||||
# in the one place every completion path passes, from the task's
|
||||
# high-water counter, so a deleted or backdated entry never renumbers.
|
||||
if task.history:
|
||||
from .helpers.reference_numbers import next_history_ref
|
||||
|
||||
task.history[-1]["ref_no"] = next_history_ref(self._store, task_id, task.history)
|
||||
# #73: a completed cycle retires its in-cycle checklist ticks — the
|
||||
# snapshot that matters is in the history entry above. A pure backfill
|
||||
# closed no current cycle, so the live ticks stay.
|
||||
if is_latest:
|
||||
self._store.clear_checklist_progress(task_id)
|
||||
|
||||
# Link the completion photo to this task so it also surfaces under the
|
||||
# object's documents and is deref'd correctly on cleanup. Best-effort:
|
||||
# a bad/removed doc_id must never block the completion itself.
|
||||
if photo_doc_id:
|
||||
# Link the completion photos to this task so they also surface under
|
||||
# the object's documents and are deref'd correctly on cleanup.
|
||||
# Best-effort: a bad/removed doc_id must never block the completion.
|
||||
for photo_doc_id in photo_doc_ids or ():
|
||||
await self._link_completion_photo(photo_doc_id, task_id)
|
||||
|
||||
# Update adaptive scheduling if enabled. Gated on is_latest: a pure
|
||||
@@ -1294,6 +1331,10 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
duration=duration,
|
||||
feedback=feedback,
|
||||
completed_by=completed_by,
|
||||
# Recorded readings (#83 scalar / #161 phase 2 slots) so an
|
||||
# automation can forward a meter value without reading history.
|
||||
reading_value=reading_value,
|
||||
reading_values=reading_values,
|
||||
# #133: the history entry's own timestamp — identical to what
|
||||
# the history records, so automations can attribute backdated
|
||||
# completions to the right period instead of time_fired.
|
||||
@@ -1427,10 +1468,6 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
as_missed: bool = False,
|
||||
) -> None:
|
||||
"""Skip the current maintenance cycle for a task."""
|
||||
# Same as reset: skipping restarts the cycle — clear the double-tap
|
||||
# window so a follow-up completion counts.
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
self._clear_notification_state(task_id)
|
||||
merged = self._get_merged_tasks_data()
|
||||
if task_id not in merged:
|
||||
_LOGGER.error("Task %s not found in entry %s", task_id, self.entry.title)
|
||||
@@ -1443,6 +1480,29 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
f"Skipping is disabled for task {merged[task_id].get('name', task_id)!r}"
|
||||
)
|
||||
|
||||
# Same gate as complete_maintenance: a retired / disabled / paused
|
||||
# task must not get a new cycle from a stale notification button or
|
||||
# an old NFC sticker (bug review 2026-09-04).
|
||||
_td = merged[task_id]
|
||||
if (
|
||||
_td.get("archived_at") is not None
|
||||
or _td.get("enabled") is False
|
||||
or self.entry.data.get(CONF_OBJECT, {}).get("paused_at") is not None
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="task_inactive_skip",
|
||||
translation_placeholders={"task_name": str(_td.get("name", task_id))},
|
||||
)
|
||||
|
||||
# Only now that the skip is going to happen: like reset, skipping
|
||||
# restarts the cycle — clear the double-tap window so a follow-up
|
||||
# completion counts, and forget the notification bookkeeping. Doing
|
||||
# this BEFORE the gates re-armed the "task overdue" push for a locked
|
||||
# task on every refused skip (bug review 2026-09-04).
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
self._clear_notification_state(task_id)
|
||||
|
||||
task = MaintenanceTask.from_dict(merged[task_id])
|
||||
# Skipping an already-overdue task means it lapsed → record it as MISSED
|
||||
# (not a deliberate skip). An explicit as_missed=True always wins.
|
||||
@@ -1514,7 +1574,12 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
Guarantees that every EVENT_TASK_COMPLETED/SKIPPED/RESET payload
|
||||
carries the four identification keys (entry_id, task_id, task_name,
|
||||
object_name) — listeners can rely on them being present.
|
||||
object_name) — listeners can rely on them being present — plus
|
||||
``entity_id``: the task's sensor entity (None only when the entity
|
||||
platform has not registered it yet). HA's logbook filters custom
|
||||
events by that key inside the event data, so without it a
|
||||
completion never appeared on the sensor's own timeline, and
|
||||
automations get the entity to act on without a registry lookup.
|
||||
Variant-specific fields (notes, cost, reason, reset_date, …) are
|
||||
passed via **extra.
|
||||
"""
|
||||
@@ -1523,9 +1588,15 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"task_id": task_id,
|
||||
"task_name": task.name,
|
||||
"object_name": self.maintenance_object.name,
|
||||
"entity_id": self.task_sensor_entity_id(task_id),
|
||||
**extra,
|
||||
}
|
||||
|
||||
def task_sensor_entity_id(self, task_id: str) -> str | None:
|
||||
"""The registered entity_id of a task's status sensor, if any."""
|
||||
unique_id = task_unique_id(slugify_object_name(self.maintenance_object.name), task_id)
|
||||
return er.async_get(self.hass).async_get_entity_id("sensor", DOMAIN, unique_id)
|
||||
|
||||
async def async_apply_suggested_interval(self, task_id: str, interval: int) -> None:
|
||||
"""Apply a suggested interval to a task (static config → ConfigEntry)."""
|
||||
tasks_data = dict(self.entry.data.get(CONF_TASKS, {}))
|
||||
|
||||
@@ -371,6 +371,12 @@ class RuntimeTrigger(BaseTrigger):
|
||||
"""Reset accumulated runtime (called after maintenance completion)."""
|
||||
super().reset()
|
||||
self._accumulated_seconds = 0.0
|
||||
# A fresh start is a fresh session for the #149 cap too: the session
|
||||
# that ran up to the completion has been booked (and cleared), the
|
||||
# one starting now may book the cap again. Leaving the old booking in
|
||||
# place froze a still-ON entity at zero until it next turned off
|
||||
# (bug review 2026-09-04).
|
||||
self._session_booked = 0.0
|
||||
# If entity is currently ON, keep tracking from now (fresh start)
|
||||
if self._on_since_dt is not None:
|
||||
now = dt_util.utcnow()
|
||||
|
||||
@@ -92,6 +92,28 @@ class StateChangeTrigger(BaseTrigger):
|
||||
if self._restored_pending_dt is None:
|
||||
self._restored_pending_state = None
|
||||
|
||||
def _in_alert_state(self, state: str | None) -> bool | None:
|
||||
"""Whether *state* is the latch's ALERT state (#167).
|
||||
|
||||
A single-shot trigger (target_changes == 1) is a latch that recovers
|
||||
when the entity leaves its alert state. What "alert" means follows
|
||||
the configured pattern:
|
||||
|
||||
- To-state set: alert == the To-state (leaving it recovers).
|
||||
- Only a From-state set ("from ok to any" — a dock/error sensor with
|
||||
a dozen fault states): alert == anything BUT the From-state, so the
|
||||
latch recovers when the entity returns to it. Before #167 the
|
||||
recovery was hard-wired to the To-state and such a latch never
|
||||
cleared — although the dialog offers both fields as optional and
|
||||
auto-complete-on-recovery right next to them.
|
||||
- Neither set: no latch semantics (None) — a pure change counter.
|
||||
"""
|
||||
if self._to_state is not None:
|
||||
return _norm_state(state) == self._to_state
|
||||
if self._from_state is not None:
|
||||
return _norm_state(state) != self._from_state
|
||||
return None
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up state change trigger.
|
||||
|
||||
@@ -150,7 +172,7 @@ class StateChangeTrigger(BaseTrigger):
|
||||
"""
|
||||
if self._change_count < self._target_changes:
|
||||
return
|
||||
if self._to_state is not None and self._target_changes == 1 and _norm_state(live_state) != self._to_state:
|
||||
if self._target_changes == 1 and self._in_alert_state(live_state) is False:
|
||||
self._change_count = 0
|
||||
self._current_value = 0.0
|
||||
self._triggered = False
|
||||
@@ -386,16 +408,13 @@ class StateChangeTrigger(BaseTrigger):
|
||||
|
||||
# Latch recovery: a single-shot state alarm (target_changes == 1 — an
|
||||
# adopted problem sensor or an appliance event) clears when the entity
|
||||
# leaves its alert state. Reset the counter so the next occurrence can
|
||||
# fire again, and run the deactivation path — which auto-completes on
|
||||
# recovery when opted in. Multi-count triggers keep accumulating and
|
||||
# only reset on manual completion, so they are untouched here.
|
||||
elif (
|
||||
self._to_state is not None
|
||||
and self._target_changes == 1
|
||||
and self._triggered
|
||||
and _norm_state(new_val) != self._to_state
|
||||
):
|
||||
# leaves its alert state (see _in_alert_state for what that means on
|
||||
# a from-only pattern, #167). Reset the counter so the next
|
||||
# occurrence can fire again, and run the deactivation path — which
|
||||
# auto-completes on recovery when opted in. Multi-count triggers keep
|
||||
# accumulating and only reset on manual completion, so they are
|
||||
# untouched here.
|
||||
elif self._target_changes == 1 and self._triggered and self._in_alert_state(new_val) is False:
|
||||
self._change_count = 0
|
||||
self._current_value = 0.0
|
||||
self._persist_runtime_soon()
|
||||
|
||||
@@ -11,9 +11,11 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
BATTERY_FLEET_DUE_WITHOUT_SENSOR,
|
||||
BATTERY_FLEET_EXCLUDED,
|
||||
BATTERY_FLEET_INCLUDED,
|
||||
BATTERY_FLEET_OBJECT_FLAG,
|
||||
BATTERY_FLEET_REMOVED_PARTS,
|
||||
BATTERY_FLEET_TASK_FLAG,
|
||||
BATTERY_FLEET_TRACK_SELF_CHARGING,
|
||||
CONF_OBJECT,
|
||||
@@ -35,13 +37,17 @@ def _export_documents(doc_store: Any, object_id: str) -> list[dict[str, Any]]:
|
||||
file metadata pointing at a missing blob — the storage-hygiene repair issue
|
||||
catches those as dangling. Web-links round-trip fully. ``task_ids`` are
|
||||
carried too (as the OLD task ids); the importer remaps them onto the fresh
|
||||
task ids so a doc's task links survive a backup/restore.
|
||||
task ids so a doc's task links survive a backup/restore. The doc's own
|
||||
``id`` rides along for the same reason: history entries (completion
|
||||
photos) and spare parts (``doc_id``) point at documents by id, and the
|
||||
importer mints fresh ids — without the old one it could not remap them.
|
||||
"""
|
||||
out: list[dict[str, Any]] = []
|
||||
for d in doc_store.for_object(object_id):
|
||||
if d.get("kind") == "weblink":
|
||||
out.append(
|
||||
{
|
||||
"id": d.get("id"),
|
||||
"kind": "weblink",
|
||||
"url": d.get("url"),
|
||||
"title": d.get("title"),
|
||||
@@ -53,6 +59,7 @@ def _export_documents(doc_store: Any, object_id: str) -> list[dict[str, Any]]:
|
||||
else:
|
||||
out.append(
|
||||
{
|
||||
"id": d.get("id"),
|
||||
"kind": "file",
|
||||
"hash": d.get("hash"),
|
||||
"title": d.get("title"),
|
||||
@@ -133,6 +140,9 @@ def _build_export_object(
|
||||
"priority": tdata.get("priority", "normal"),
|
||||
"labels": tdata.get("labels") or [],
|
||||
"earliest_completion_days": tdata.get("earliest_completion_days"),
|
||||
# #170: reference number — a restore keeps "8.3" on the booklets
|
||||
# already printed; a colliding import is renumbered on setup.
|
||||
"ref_no": tdata.get("ref_no"),
|
||||
"on_complete_action": tdata.get("on_complete_action"),
|
||||
"quick_complete_defaults": tdata.get("quick_complete_defaults"),
|
||||
"assignee_pool": tdata.get("assignee_pool") or [],
|
||||
@@ -142,6 +152,8 @@ def _build_export_object(
|
||||
"required_completion_fields": tdata.get("required_completion_fields"),
|
||||
"rotation_strategy": tdata.get("rotation_strategy"),
|
||||
"reading_unit": tdata.get("reading_unit"),
|
||||
# #161 phase 2: reading slots (history carries the per-slot values).
|
||||
"readings": tdata.get("readings"),
|
||||
# Task phases (#139): defs + cycle are static config; the cursor is
|
||||
# Store-merged by merged_tasks, so a restore resumes mid-cycle
|
||||
# instead of silently restarting at step one.
|
||||
@@ -210,6 +222,10 @@ def _build_export_object(
|
||||
# restore — with every task still archived (reason "object") and
|
||||
# object/unarchive refusing because the object "isn't archived".
|
||||
"archived_at": obj_data.get("archived_at"),
|
||||
# #170: the object's reference number + its task counter (the
|
||||
# high-water mark, so numbers are not reused after a restore).
|
||||
"ref_no": obj_data.get("ref_no"),
|
||||
"next_task_ref": obj_data.get("next_task_ref"),
|
||||
}
|
||||
# Battery fleet identity — only emitted for the fleet object so a plain
|
||||
# object's export stays byte-identical to earlier versions. The importer
|
||||
@@ -219,6 +235,9 @@ def _build_export_object(
|
||||
export_obj[BATTERY_FLEET_EXCLUDED] = list(obj_data.get(BATTERY_FLEET_EXCLUDED) or [])
|
||||
export_obj[BATTERY_FLEET_INCLUDED] = list(obj_data.get(BATTERY_FLEET_INCLUDED) or [])
|
||||
export_obj[BATTERY_FLEET_TRACK_SELF_CHARGING] = bool(obj_data.get(BATTERY_FLEET_TRACK_SELF_CHARGING))
|
||||
# D#162: on unless explicitly switched off (absent = on).
|
||||
export_obj[BATTERY_FLEET_DUE_WITHOUT_SENSOR] = obj_data.get(BATTERY_FLEET_DUE_WITHOUT_SENSOR) is not False
|
||||
export_obj[BATTERY_FLEET_REMOVED_PARTS] = list(obj_data.get(BATTERY_FLEET_REMOVED_PARTS) or [])
|
||||
|
||||
return {
|
||||
"entry_id": entry.entry_id,
|
||||
@@ -322,7 +341,7 @@ def export_maintenance_data(
|
||||
# Global settings the export deliberately leaves behind: HA user ids are
|
||||
# instance-bound (and the panel-access allowlist is security-relevant), and
|
||||
# the adopted-task stash is transient re-adopt state.
|
||||
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes", "shopping_list_entity")
|
||||
_NON_PORTABLE_SETTINGS = ("admin_panel_user_ids", "adopted_task_notes", "shopping_list_entity", "member_display")
|
||||
|
||||
|
||||
def build_settings_export(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
@@ -65,6 +65,7 @@ export function obj(entryId: string, tasks: unknown[], name = "Pool Pump") {
|
||||
export async function mountPanel(
|
||||
objects: unknown[],
|
||||
extraHandlers: Record<string, WsHandler> = {},
|
||||
opts: { user?: { id: string; is_admin: boolean } } = {},
|
||||
) {
|
||||
const { hass, sent, subscriptions } = createMockHass({
|
||||
handlers: {
|
||||
@@ -83,7 +84,7 @@ export async function mountPanel(
|
||||
},
|
||||
});
|
||||
// The panel derives write access from hass.user (no user → read-only).
|
||||
(hass as Record<string, unknown>).user = { id: "admin-1", is_admin: true };
|
||||
(hass as Record<string, unknown>).user = opts.user ?? { id: "admin-1", is_admin: true };
|
||||
(hass as Record<string, unknown>).areas = {};
|
||||
|
||||
const el = await fixture<HTMLElement & { updateComplete: Promise<unknown> }>(html`
|
||||
|
||||
+128
-21
@@ -4,7 +4,7 @@
|
||||
* The completion dialog is the money path of the whole product; until now it
|
||||
* was only covered by the lazy-load tripwire. These tests pin the exact
|
||||
* outgoing WS payload (notes / cost / duration / feedback / checklist_state /
|
||||
* photo_doc_id) and the error path (server refusal keeps the dialog open).
|
||||
* photo_doc_ids) and the error path (server refusal keeps the dialog open).
|
||||
*/
|
||||
|
||||
import { expect, fixture, html } from "@open-wc/testing";
|
||||
@@ -100,6 +100,7 @@ describe("complete-dialog", () => {
|
||||
expect("duration" in msg).to.be.false;
|
||||
expect("feedback" in msg).to.be.false;
|
||||
expect("checklist_state" in msg).to.be.false;
|
||||
expect("photo_doc_ids" in msg).to.be.false;
|
||||
expect("photo_doc_id" in msg).to.be.false;
|
||||
expect("completed_at" in msg).to.be.false;
|
||||
});
|
||||
@@ -153,43 +154,149 @@ describe("complete-dialog", () => {
|
||||
expect(el.shadowRoot!.textContent).to.include("future");
|
||||
});
|
||||
|
||||
it("attaches an uploaded photo as photo_doc_id", async () => {
|
||||
const { el, sent } = await mount();
|
||||
|
||||
// Stub the document-upload endpoint the photo picker posts to.
|
||||
/** #161: stub the multipart upload route; every call mints the next id. */
|
||||
function stubUpload(ids: string[]) {
|
||||
const realFetch = window.fetch;
|
||||
const uploads: RequestInit[] = [];
|
||||
let n = 0;
|
||||
window.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => {
|
||||
uploads.push(init!);
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ id: "doc-photo-1", deduped: false }),
|
||||
} as Response;
|
||||
const id = ids[n++] ?? `doc-photo-${n}`;
|
||||
return { ok: true, status: 200, json: async () => ({ id, deduped: false }) } as Response;
|
||||
}) as typeof window.fetch;
|
||||
return { uploads, restore: () => { window.fetch = realFetch; } };
|
||||
}
|
||||
|
||||
function pickFiles(el: MaintenanceCompleteDialog, selector: string, names: string[]) {
|
||||
const fileInput = el.shadowRoot!.querySelector<HTMLInputElement>(selector)!;
|
||||
const dt = new DataTransfer();
|
||||
for (const name of names) dt.items.add(new File(["fake-png"], name, { type: "image/png" }));
|
||||
fileInput.files = dt.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
const CAMERA = '.photo-pick-camera input[type="file"]';
|
||||
const GALLERY = '.photo-pick-gallery input[type="file"]';
|
||||
|
||||
it("falls back to one photo per pick inside the Android Companion app (#161 follow-up)", async () => {
|
||||
// The Android app's file chooser drops a multi-select on the floor
|
||||
// (parseResult ignores ClipData) — the gallery picker is single there.
|
||||
(window as unknown as { externalApp?: unknown }).externalApp = {};
|
||||
try {
|
||||
const fileInput = el.shadowRoot!.querySelector<HTMLInputElement>(
|
||||
'.photo-pick input[type="file"]',
|
||||
)!;
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File(["fake-png"], "done.png", { type: "image/png" }));
|
||||
fileInput.files = dt.files;
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
const { el } = await mount();
|
||||
const gallery = el.shadowRoot!.querySelector<HTMLInputElement>(GALLERY)!;
|
||||
expect(gallery.multiple, "no multi-select affordance on Android").to.equal(false);
|
||||
expect(el.shadowRoot!.querySelector(".photo-pick-gallery span")!.textContent!.trim()).to.equal("Choose photo");
|
||||
expect(el.shadowRoot!.querySelector(".photo-android-hint")!.textContent).to.contain("one photo per pick");
|
||||
const { uploads, restore } = stubUpload(["p1", "p2"]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, ["a.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
pickFiles(el, GALLERY, ["b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
expect(uploads.length, "each single pick is added").to.equal(2);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(2);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
} finally {
|
||||
delete (window as unknown as { externalApp?: unknown }).externalApp;
|
||||
}
|
||||
const { el: plain } = await mount();
|
||||
expect(plain.shadowRoot!.querySelector<HTMLInputElement>(GALLERY)!.multiple, "browsers keep multi-select").to.equal(true);
|
||||
expect(plain.shadowRoot!.querySelector(".photo-android-hint")).to.equal(null);
|
||||
});
|
||||
|
||||
it("attaches the uploaded photos as photo_doc_ids (#161)", async () => {
|
||||
const { el, sent } = await mount();
|
||||
const { uploads, restore } = stubUpload(["doc-photo-1", "doc-photo-2", "doc-photo-3"]);
|
||||
try {
|
||||
// The camera picker takes one shot; the gallery picker several at once.
|
||||
pickFiles(el, CAMERA, ["done.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await el.updateComplete;
|
||||
pickFiles(el, GALLERY, ["a.png", "b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
|
||||
// Preview replaces the picker once the upload returns an id.
|
||||
expect(el.shadowRoot!.querySelector(".photo-preview img")).to.exist;
|
||||
expect(uploads.length).to.equal(1);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(3);
|
||||
expect(uploads.length).to.equal(3);
|
||||
const form = uploads[0].body as FormData;
|
||||
expect(form.get("entry_id")).to.equal("entry1");
|
||||
expect(form.get("tags")).to.equal("photo");
|
||||
// The pickers stay available while there is room under the cap.
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.exist;
|
||||
|
||||
clickComplete(el);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
const msg = sent.find((m) => m.type === "maintenance_supporter/task/complete")!;
|
||||
expect(msg.photo_doc_id).to.equal("doc-photo-1");
|
||||
expect(msg.photo_doc_ids).to.deep.equal(["doc-photo-1", "doc-photo-2", "doc-photo-3"]);
|
||||
expect("photo_doc_id" in msg).to.be.false;
|
||||
// Completing keeps the uploads — no delete goes out.
|
||||
expect(sent.some((m) => m.type === "maintenance_supporter/documents/delete")).to.be.false;
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("caps a completion at 10 photos and says so (#161)", async () => {
|
||||
const { el } = await mount();
|
||||
const { uploads, restore } = stubUpload([]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, Array.from({ length: 12 }, (_, i) => `p${i}.png`));
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
await el.updateComplete;
|
||||
expect(uploads.length).to.equal(10);
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(10);
|
||||
// At the cap the pickers give way to the limit note.
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.equal(null);
|
||||
expect(el.shadowRoot!.querySelector(".photo-limit")!.textContent).to.include("10");
|
||||
expect(el.shadowRoot!.querySelector(".error")!.textContent).to.include("10");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("removing a tile deletes that upload; cancelling drops the rest (#161)", async () => {
|
||||
const { el, sent } = await mount();
|
||||
const { restore } = stubUpload(["doc-photo-1", "doc-photo-2"]);
|
||||
try {
|
||||
pickFiles(el, GALLERY, ["a.png", "b.png"]);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await el.updateComplete;
|
||||
|
||||
const removeButtons = el.shadowRoot!.querySelectorAll<HTMLElement>(".photo-remove");
|
||||
expect(removeButtons.length).to.equal(2);
|
||||
removeButtons[0].click();
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(1);
|
||||
let deletes = sent.filter((m) => m.type === "maintenance_supporter/documents/delete");
|
||||
expect(deletes.map((m) => m.doc_id)).to.deep.equal(["doc-photo-1"]);
|
||||
|
||||
// Cancel: the remaining upload is an orphan nobody references.
|
||||
const cancel = el.shadowRoot!.querySelector<HTMLElement>(".dialog-actions ha-button")!;
|
||||
cancel.click();
|
||||
await el.updateComplete;
|
||||
deletes = sent.filter((m) => m.type === "maintenance_supporter/documents/delete");
|
||||
expect(deletes.map((m) => m.doc_id)).to.deep.equal(["doc-photo-1", "doc-photo-2"]);
|
||||
expect(sent.some((m) => m.type === "maintenance_supporter/task/complete")).to.be.false;
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces a 413 as the too-large message and keeps the picker (#161)", async () => {
|
||||
const { el } = await mount();
|
||||
const realFetch = window.fetch;
|
||||
window.fetch = (async () => ({ ok: false, status: 413, json: async () => ({}) }) as Response) as typeof window.fetch;
|
||||
try {
|
||||
pickFiles(el, CAMERA, ["huge.png"]);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelectorAll(".photo-preview img").length).to.equal(0);
|
||||
expect(el.shadowRoot!.querySelector(".error")!.textContent).to.include("large");
|
||||
expect(el.shadowRoot!.querySelector(CAMERA)).to.exist;
|
||||
} finally {
|
||||
window.fetch = realFetch;
|
||||
}
|
||||
|
||||
+26
@@ -213,4 +213,30 @@ describe("documents-section", () => {
|
||||
label.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
expect(clicked, "Enter triggers the hidden file input").to.be.true;
|
||||
});
|
||||
|
||||
it("offers a local filter from 8 documents up and filters tolerantly (#171)", async () => {
|
||||
const many = Array.from({ length: 9 }, (_, i) => ({
|
||||
...FILE_DOC, id: `f${i}`, title: i === 4 ? "Garantie Spülmaschine" : `Rechnung ${i}`, filename: `r${i}.pdf`, tags: i === 4 ? ["warranty"] : ["invoice"],
|
||||
}));
|
||||
const { el } = await mount(true, many);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".doc-filter input");
|
||||
expect(input, "filter box shown").to.exist;
|
||||
expect(el.shadowRoot!.querySelectorAll(".doc-row").length).to.equal(9);
|
||||
input!.value = "spuel garant";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const rows = [...el.shadowRoot!.querySelectorAll(".doc-row")];
|
||||
expect(rows.length).to.equal(1);
|
||||
expect(rows[0].textContent).to.contain("Garantie Spülmaschine");
|
||||
input!.value = "nichts";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
expect(el.shadowRoot!.querySelector(".doc-empty")!.textContent).to.contain("No matching documents");
|
||||
expect(el.shadowRoot!.querySelector("h3")!.textContent, "count stays the full count").to.contain("(9)");
|
||||
});
|
||||
|
||||
it("keeps the list uncluttered below 8 documents — no filter box", async () => {
|
||||
const { el } = await mount();
|
||||
expect(el.shadowRoot!.querySelector(".doc-filter")).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { setViewport } from "@web/test-runner-commands";
|
||||
import { mountPanel, obj, resetTaskSeq, sr, task } from "./_panel-utils.js";
|
||||
describe("panel shell", () => {
|
||||
beforeEach(() => {
|
||||
@@ -156,6 +157,63 @@ describe("panel shell", () => {
|
||||
expect(textOf(overdueSection)).to.include("Late");
|
||||
});
|
||||
|
||||
it("Today rows name the responsible person, except under the person filter (#169)", async () => {
|
||||
localStorage.setItem("msp-overview-tab", "today");
|
||||
const { el } = await mountPanel(
|
||||
[
|
||||
obj("e1", [
|
||||
task({ name: "Assigned", status: "overdue", days_until_due: -1, responsible_user_id: "u-anna" }),
|
||||
task({ name: "Nobody", status: "overdue", days_until_due: -2 }),
|
||||
]),
|
||||
],
|
||||
{ "maintenance_supporter/users/list": () => ({ users: [{ id: "u-anna", name: "Anna" }] }) },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const rowOf = (name: string) =>
|
||||
[...sr(el).querySelectorAll(".today-row")].find((r) => r.querySelector(".today-task")?.textContent?.trim() === name)!;
|
||||
expect(rowOf("Assigned").querySelector(".today-person")!.textContent).to.include("Anna");
|
||||
expect(rowOf("Nobody").querySelector(".today-person")).to.equal(null);
|
||||
|
||||
// Filtering by that person makes the chip redundant — every row is hers.
|
||||
(el as unknown as { _filterUser: string | null })._filterUser = "u-anna";
|
||||
await el.updateComplete;
|
||||
expect(sr(el).querySelector(".today-person")).to.equal(null);
|
||||
});
|
||||
|
||||
it("Today person chip stays readable on a phone next to a long object name (#169)", async () => {
|
||||
localStorage.setItem("msp-overview-tab", "today");
|
||||
await setViewport({ width: 360, height: 800 });
|
||||
try {
|
||||
const { el } = await mountPanel(
|
||||
[
|
||||
obj("e1", [task({ name: "Descale and clean the filter thoroughly", status: "overdue", days_until_due: -248, responsible_user_id: "u-max" })],
|
||||
"Dishwasher in the basement utility room 12345"),
|
||||
],
|
||||
{ "maintenance_supporter/users/list": () => ({ users: [{ id: "u-max", name: "Maximiliane Schneider-Hoffmann" }] }) },
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const line = sr(el).querySelector<HTMLElement>(".today-object")!;
|
||||
const chip = line.querySelector<HTMLElement>(".today-person")!;
|
||||
const lineBox = line.getBoundingClientRect();
|
||||
const chipBox = chip.getBoundingClientRect();
|
||||
// Narrow: the avatar alone (initials in the member's colour), fully inside the line.
|
||||
const avatar = chip.querySelector<HTMLElement>(".person-avatar")!;
|
||||
expect(avatar.textContent!.trim()).to.equal("MS");
|
||||
expect(getComputedStyle(chip.querySelector(".person-name")!).display, "name hidden on narrow").to.equal("none");
|
||||
const visible = Math.min(chipBox.right, lineBox.right) - Math.max(chipBox.left, lineBox.left);
|
||||
expect(visible, "avatar visible inside the line").to.be.greaterThan(16);
|
||||
expect(chipBox.right, "chip does not overflow the line").to.be.at.most(lineBox.right + 1);
|
||||
// The object text gave way (ellipsis), not the person.
|
||||
const text = line.querySelector<HTMLElement>(".today-object-text")!;
|
||||
expect(text.scrollWidth).to.be.greaterThan(text.clientWidth);
|
||||
expect(chip.getAttribute("title")).to.equal("Maximiliane Schneider-Hoffmann");
|
||||
} finally {
|
||||
await setViewport({ width: 1280, height: 800 });
|
||||
}
|
||||
});
|
||||
|
||||
it("virtualizes the table above the threshold and moves the window on scroll", async () => {
|
||||
const many = Array.from({ length: 150 }, (_, i) =>
|
||||
task({ name: `Bulk ${String(i).padStart(3, "0")}`, days_until_due: (i % 40) + 1 }),
|
||||
|
||||
+40
-1
@@ -21,6 +21,7 @@ function mockHass(opts: {
|
||||
vacationStart?: string | null;
|
||||
vacationEnd?: string | null;
|
||||
exemptIds?: string[];
|
||||
previewRows?: Array<Record<string, unknown>>;
|
||||
} = {}) {
|
||||
const settingsResponse = {
|
||||
...DEFAULT_SETTINGS_RESPONSE,
|
||||
@@ -51,7 +52,7 @@ function mockHass(opts: {
|
||||
"maintenance_supporter/vacation/end_now": () => ({
|
||||
...settingsResponse.vacation, enabled: false, is_active: false,
|
||||
}),
|
||||
"maintenance_supporter/vacation/preview": () => ({ rows: [], window_end: null }),
|
||||
"maintenance_supporter/vacation/preview": () => ({ rows: opts.previewRows ?? [], window_end: null }),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -144,4 +145,42 @@ describe("settings-view vacation section", () => {
|
||||
const endNow = vacationSection(el)!.querySelector(".vac-end-now");
|
||||
expect(endNow, "no end-now in default state").to.not.exist;
|
||||
});
|
||||
it("the preview hides Skip for a task whose skip rule is off (#150 rule in the preview)", async () => {
|
||||
// Bug review 2026-09-04: every time-based row offered Skip — the
|
||||
// backend then rejected it for allow_skip=false tasks.
|
||||
const row = (task_id: string, extra: Record<string, unknown>) => ({
|
||||
task_id,
|
||||
entry_id: "e1",
|
||||
object_name: "Pool",
|
||||
task_name: task_id,
|
||||
kind: "time_based",
|
||||
confidence: "deterministic",
|
||||
events: [{ date: "2099-06-12", status: "due_soon" }],
|
||||
will_suppress: true,
|
||||
...extra,
|
||||
});
|
||||
const { el } = await mount({
|
||||
vacationStart: "2099-06-10",
|
||||
vacationEnd: "2099-06-20",
|
||||
previewRows: [
|
||||
row("Open", { allow_skip: true }),
|
||||
row("Locked", { allow_skip: false }),
|
||||
row("Legacy", {}),
|
||||
row("Sensor", { kind: "sensor_based", allow_skip: true }),
|
||||
],
|
||||
});
|
||||
const section = vacationSection(el)!;
|
||||
section.querySelector<HTMLButtonElement>(".vac-preview-toolbar button")!.click();
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await el.updateComplete;
|
||||
const rows = Array.from(section.querySelectorAll(".vac-preview-row"));
|
||||
expect(rows.length).to.equal(4);
|
||||
const skipLabel = rows[0].querySelectorAll(".vac-preview-actions button")[1].textContent!.trim();
|
||||
const hasSkip = (r: Element) =>
|
||||
Array.from(r.querySelectorAll(".vac-preview-actions button")).some((b) => b.textContent!.trim() === skipLabel);
|
||||
expect(hasSkip(rows[0]), "allow_skip=true keeps Skip").to.be.true;
|
||||
expect(hasSkip(rows[1]), "allow_skip=false hides Skip").to.be.false;
|
||||
expect(hasSkip(rows[2]), "absent field = allowed (older backend)").to.be.true;
|
||||
expect(hasSkip(rows[3]), "sensor rows never had Skip").to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
+11
@@ -125,4 +125,15 @@ describe("storage-section-card", () => {
|
||||
expect(el.shadowRoot!.querySelector(".obj-name")!.textContent!.trim()).to.equal("01234567");
|
||||
expect(el.shadowRoot!.querySelector(".obj-row.clickable"), "unknown object not clickable").to.not.exist;
|
||||
});
|
||||
|
||||
it("shows how much of the library the full-text search can see (#171)", async () => {
|
||||
const el = await mount({ ...SUMMARY, search_index: { total: 12, indexed: 9, no_text: 2, unsupported: 1, pending: 0 } });
|
||||
await expand(el);
|
||||
const status = el.shadowRoot!.querySelector(".index-status")!;
|
||||
expect(status.textContent!.replace(/\s+/g, " ")).to.contain("9 of 12 files indexed");
|
||||
expect(status.textContent).to.contain("3 without text layer");
|
||||
const none = await mount({ ...SUMMARY, search_index: { total: 0, indexed: 0, no_text: 0, unsupported: 0, pending: 0 } });
|
||||
await expand(none);
|
||||
expect(none.shadowRoot!.querySelector(".index-status"), "no line without files").to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
+58
-2
@@ -13,8 +13,8 @@ import "../components/task-dialog.js";
|
||||
import type { MaintenanceTaskDialog } from "../components/task-dialog";
|
||||
import { createMockHass } from "./_test-utils.js";
|
||||
|
||||
async function mountCreate(states: Record<string, unknown>): Promise<MaintenanceTaskDialog> {
|
||||
const { hass } = createMockHass({ states });
|
||||
async function mountCreate(states: Record<string, unknown>, language = "en"): Promise<MaintenanceTaskDialog> {
|
||||
const { hass } = createMockHass({ states, language });
|
||||
const el = await fixture<MaintenanceTaskDialog>(html`
|
||||
<maintenance-task-dialog .hass=${hass}></maintenance-task-dialog>
|
||||
`);
|
||||
@@ -59,6 +59,37 @@ describe("task-dialog live trigger hint", () => {
|
||||
expect(text).to.include("1.5 bar");
|
||||
});
|
||||
|
||||
it("formats the figures in the user's number format (bug review 2026-09-04)", async () => {
|
||||
// Decimal comma + thousands separators for a de user — like the entity
|
||||
// card next to the dialog; the hint used raw String(n).
|
||||
const el = await mountCreate(
|
||||
{
|
||||
"sensor.odometer": { state: "100000", attributes: { unit_of_measurement: "km" } },
|
||||
"sensor.pressure": { state: "1.25", attributes: { unit_of_measurement: "bar" } },
|
||||
},
|
||||
"de",
|
||||
);
|
||||
(el as any)._triggerEntityId = "sensor.odometer";
|
||||
(el as any)._triggerEntityIds = ["sensor.odometer"];
|
||||
(el as any)._triggerType = "counter";
|
||||
(el as any)._triggerDeltaMode = true;
|
||||
(el as any)._triggerTargetValue = "15000";
|
||||
await el.updateComplete;
|
||||
let text = hint(el)!;
|
||||
expect(text).to.include("100.000 km");
|
||||
expect(text).to.include("115.000 km");
|
||||
expect(text).to.include("15.000 km");
|
||||
|
||||
(el as any)._triggerEntityId = "sensor.pressure";
|
||||
(el as any)._triggerEntityIds = ["sensor.pressure"];
|
||||
(el as any)._triggerType = "threshold";
|
||||
(el as any)._triggerAbove = "1.5";
|
||||
await el.updateComplete;
|
||||
text = hint(el)!;
|
||||
expect(text).to.include("1,3 bar"); // 1.25 rounded to one decimal, decimal comma
|
||||
expect(text).to.include("1,5 bar");
|
||||
});
|
||||
|
||||
it("renders nothing without a bound entity or without targets", async () => {
|
||||
const el = await mountCreate({
|
||||
"sensor.pressure": { state: "1.2", attributes: {} },
|
||||
@@ -137,3 +168,28 @@ describe("task-dialog live trigger hint", () => {
|
||||
expect(text).to.include("100 h");
|
||||
});
|
||||
});
|
||||
|
||||
describe("task-dialog state-change latch hint (#167)", () => {
|
||||
const latchHint = (el: MaintenanceTaskDialog): boolean =>
|
||||
[...el.shadowRoot!.querySelectorAll(".field-help")].some((n) => /latch/i.test(n.textContent || ""));
|
||||
|
||||
it("explains the latch (incl. the from-only recovery) for a single transition with a pattern", async () => {
|
||||
const el = await mountCreate({ "sensor.dock_error": { state: "ok", attributes: {} } });
|
||||
(el as any)._triggerEntityId = "sensor.dock_error";
|
||||
(el as any)._triggerEntityIds = ["sensor.dock_error"];
|
||||
(el as any)._triggerType = "state_change";
|
||||
(el as any)._triggerFromState = "ok";
|
||||
(el as any)._triggerTargetChanges = "1";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "from-only, target 1").to.equal(true);
|
||||
|
||||
(el as any)._triggerTargetChanges = "3";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "a counter is no latch").to.equal(false);
|
||||
|
||||
(el as any)._triggerTargetChanges = "";
|
||||
(el as any)._triggerFromState = "";
|
||||
await el.updateComplete;
|
||||
expect(latchHint(el), "no pattern = no latch semantics").to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,4 +183,17 @@ describe("task-documents in part mode", () => {
|
||||
const { el } = await mountPart([P_LINKED]);
|
||||
expect(el.shadowRoot!.querySelector(".tdoc-page")).to.not.exist;
|
||||
});
|
||||
|
||||
it("offers a local filter once 8+ documents are linked (#171)", async () => {
|
||||
const many = Array.from({ length: 8 }, (_, i) => ({ ...LINKED, id: `l${i}`, title: i === 2 ? "Schaltplan" : `Manual ${i}`, filename: `m${i}.pdf` }));
|
||||
const { el } = await mount(true, many);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>(".doc-filter input");
|
||||
expect(input, "filter box shown").to.exist;
|
||||
input!.value = "schaltpl";
|
||||
input!.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const rows = [...el.shadowRoot!.querySelectorAll(".tdoc-row")];
|
||||
expect(rows.length).to.equal(1);
|
||||
expect(rows[0].textContent).to.contain("Schaltplan");
|
||||
});
|
||||
});
|
||||
|
||||
+75
-4
@@ -35,6 +35,14 @@ interface BatteryRow {
|
||||
* healthy. Never escalates to low/task — the row just shows the
|
||||
* discrepancy (usual cause: an unrecorded swap). */
|
||||
forecast_overdue?: boolean;
|
||||
/** D#162: a Battery Notes note whose device has NO battery level at
|
||||
* all — read from its type sensor. No level bar, never offline; the
|
||||
* forecast is the whole signal and (option) a passed one is `low`. */
|
||||
no_sensor?: boolean;
|
||||
/** D#162 follow-up: the note has a replaced button → per-row Replaced action. */
|
||||
can_mark_replaced?: boolean;
|
||||
/** ISO date the forecast anchors on (Battery Notes' last replaced). */
|
||||
last_replaced?: string | null;
|
||||
}
|
||||
interface RosterRow extends BatteryRow {
|
||||
status: "low" | "soon" | "ok";
|
||||
@@ -69,6 +77,9 @@ interface Overview {
|
||||
excluded?: { entity_id: string; device_name: string }[];
|
||||
// #135 follow-up: fleet-wide "track self-charging devices" opt-in.
|
||||
track_self_charging?: boolean;
|
||||
// D#162: a passed forecast on a sensorless note counts as due (default
|
||||
// on — an older backend omits the field, which renders as on too).
|
||||
due_without_sensor?: boolean;
|
||||
}
|
||||
|
||||
export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
@@ -213,13 +224,22 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
// #135 follow-up: fleet-wide opt-in that keeps self-charging devices
|
||||
// (phones, vacuums, smart rings) in the roster as rechargeables.
|
||||
private async _setTrackSelf(e: Event): Promise<void> {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
await this._setFleetOption("set_track_self_charging", (e.target as HTMLInputElement).checked);
|
||||
}
|
||||
|
||||
// D#162: fleet-wide option — a passed forecast on a sensorless note is
|
||||
// due (fires the task). Default on; only the opt-out is stored.
|
||||
private async _setDueWithoutSensor(e: Event): Promise<void> {
|
||||
await this._setFleetOption("set_due_without_sensor", (e.target as HTMLInputElement).checked);
|
||||
}
|
||||
|
||||
private async _setFleetOption(command: "set_track_self_charging" | "set_due_without_sensor", enabled: boolean): Promise<void> {
|
||||
if (this._marking) return;
|
||||
this._marking = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/battery_fleet/set_track_self_charging",
|
||||
type: `maintenance_supporter/battery_fleet/${command}`,
|
||||
enabled,
|
||||
});
|
||||
await this._load();
|
||||
@@ -428,7 +448,9 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
<span class="bf-dev">${b.device_name}</span>
|
||||
${b.available === false
|
||||
? html`<span class="bf-offline">${t("battery_fleet_offline", L)}</span>`
|
||||
: nothing}
|
||||
: b.no_sensor
|
||||
? html`<span class="bf-offline bf-nosensor">${t("battery_fleet_no_sensor", L)}</span>`
|
||||
: nothing}
|
||||
<span class="bf-type">${b.quantity}× ${b.battery_type}</span>
|
||||
${b.rechargeable
|
||||
? html`<span class="bf-recharge" title=${t("battery_fleet_rechargeable", L)}
|
||||
@@ -496,8 +518,15 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
(b) => html`
|
||||
<div class="bf-row">
|
||||
<span class="bf-dev">${b.device_name}</span>
|
||||
<span class="bf-status bf-${b.status}">${t("battery_fleet_status_" + b.status, L)}</span>
|
||||
<span class="bf-status bf-${b.status}"
|
||||
>${b.no_sensor && b.status === "low"
|
||||
? t("battery_fleet_status_due", L)
|
||||
: t("battery_fleet_status_" + b.status, L)}</span
|
||||
>
|
||||
<span class="bf-type">${b.quantity}× ${b.battery_type}</span>
|
||||
${b.no_sensor
|
||||
? html`<span class="bf-offline bf-nosensor">${t("battery_fleet_no_sensor", L)}</span>`
|
||||
: nothing}
|
||||
${b.rechargeable
|
||||
? html`<span class="bf-recharge" title=${t("battery_fleet_rechargeable", L)}
|
||||
><ha-icon icon="mdi:battery-charging-outline"></ha-icon
|
||||
@@ -506,6 +535,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
${this._sparkline(b)}
|
||||
${this._levelBar(b)}
|
||||
${b.level != null ? html`<span class="bf-level">${b.level}%</span>` : nothing}
|
||||
${b.no_sensor || b.can_mark_replaced
|
||||
? html`<button
|
||||
class="bf-mark bf-replaced"
|
||||
title=${t("battery_fleet_mark_one", L)}
|
||||
.disabled=${this._marking}
|
||||
@click=${() => this._mark([b.entity_id])}
|
||||
>
|
||||
<ha-icon icon="mdi:battery-sync"></ha-icon>
|
||||
</button>`
|
||||
: nothing}
|
||||
${(() => {
|
||||
const jump = this._history?.[b.entity_id]?.jump;
|
||||
if (!jump || this._recorded.includes(b.entity_id)) return nothing;
|
||||
@@ -564,6 +603,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
${t("battery_fleet_track_self", L)}
|
||||
</label>
|
||||
<div class="bf-roster-hint">${t("battery_fleet_track_self_hint", L)}</div>
|
||||
<label class="bf-track-self bf-due-nosensor">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${ov.due_without_sensor !== false}
|
||||
.disabled=${this._marking}
|
||||
@change=${this._setDueWithoutSensor}
|
||||
/>
|
||||
${t("battery_fleet_due_without_sensor", L)}
|
||||
</label>
|
||||
<div class="bf-roster-hint">${t("battery_fleet_due_without_sensor_hint", L)}</div>
|
||||
</details>
|
||||
`
|
||||
: nothing}
|
||||
@@ -712,6 +761,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
.bf-bar {
|
||||
grid-column: 6;
|
||||
}
|
||||
/* D#162: the roster's "No sensor" chip takes the level bar's slot
|
||||
* (such a row has no bar); column 2 stays the status chip's. */
|
||||
.bf-status + .bf-type + .bf-nosensor {
|
||||
grid-column: 6;
|
||||
justify-self: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bf-level {
|
||||
grid-column: 7;
|
||||
justify-self: end;
|
||||
@@ -719,6 +775,14 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
.bf-row .bf-mark {
|
||||
grid-column: 8;
|
||||
}
|
||||
/* D#162: the sensorless row's Replaced action sits in the percentage
|
||||
* slot (always empty for such a row) instead of the row-action column
|
||||
* - a new max-content track there widens EVERY row via the subgrid
|
||||
* and pushed the phone roster 34 px past its edge. */
|
||||
.bf-row .bf-mark.bf-replaced {
|
||||
grid-column: 7;
|
||||
justify-self: end;
|
||||
}
|
||||
.bf-predicted {
|
||||
grid-column: 9;
|
||||
justify-self: end;
|
||||
@@ -791,6 +855,13 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
.bf-row .bf-mark {
|
||||
grid-row: 2;
|
||||
}
|
||||
/* The roster's "No sensor" chip would widen the shared bar track
|
||||
* for EVERY row (subgrid) - on phones the missing percentage and
|
||||
* the "Due" status already tell the story, so it yields like the
|
||||
* bar and the sparkline do. */
|
||||
.bf-status + .bf-type + .bf-nosensor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.bf-spark-line {
|
||||
fill: none;
|
||||
|
||||
+172
-54
@@ -2,11 +2,18 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant, TaskPartLink } from "../types";
|
||||
import { t, nativeFieldStyles, formatCost } from "../styles";
|
||||
import type { HomeAssistant, ReadingSlot, TaskPartLink } from "../types";
|
||||
import { lastReadingBefore, type ReadingHistoryEntry } from "../helpers/reading-slots";
|
||||
import { t, nativeFieldStyles, formatCost, formatNumber } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { partLinkKey, type LinkedPart } from "../helpers/shared-parts";
|
||||
import { isAndroidCompanion } from "../helpers/companion";
|
||||
import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
|
||||
import {
|
||||
MAX_COMPLETION_PHOTOS,
|
||||
discardUploadedPhotos,
|
||||
uploadCompletionPhoto,
|
||||
} from "../helpers/photo-upload";
|
||||
import "./ms-date-field";
|
||||
|
||||
export class MaintenanceCompleteDialog extends LitElement {
|
||||
@@ -20,6 +27,14 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
// v2.20 (#83): task type + unit drive the reading-value field below.
|
||||
@property() public taskType = "";
|
||||
@property() public readingUnit = "";
|
||||
/** #161 phase 2: the task's reading slots — one field each instead of
|
||||
* the single value; [] keeps the scalar field. */
|
||||
@property({ attribute: false }) public readings: ReadingSlot[] = [];
|
||||
/** The task's dated slot snapshots (oldest first) — the "last: …" hint
|
||||
* and the lower-than-last warning compare against the last value BEFORE
|
||||
* the completion moment, so a backdated entry is judged by its own
|
||||
* date rather than by the newest reading. */
|
||||
@property({ attribute: false }) public readingHistory: ReadingHistoryEntry[] = [];
|
||||
/** Buy task (part_ref): default restock quantity — shows an editable qty field. */
|
||||
@property({ attribute: false }) public restockDefault: number | null = null;
|
||||
/** #104 follow-up: the buy task's part unit cost — powers the cost
|
||||
@@ -61,10 +76,15 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
@state() private _error = "";
|
||||
@state() private _checklistState: Record<string, boolean> = {};
|
||||
@state() private _feedback: string = "needed";
|
||||
@state() private _photoDocId = "";
|
||||
@state() private _photoPreview = "";
|
||||
/** #161: the photos attached so far, in pick order (preview = object URL). */
|
||||
@state() private _photos: Array<{ id: string; preview: string }> = [];
|
||||
/** Docs uploaded by THIS dialog session; dropped again on Cancel so an
|
||||
* abandoned completion leaves no orphan files behind. */
|
||||
private _uploadedIds: string[] = [];
|
||||
@state() private _photoUploading = false;
|
||||
@state() private _readingValue = "";
|
||||
/** #161 phase 2: typed text per slot id (parsed on save; "" = unread). */
|
||||
@state() private _readingValues: Record<string, string> = {};
|
||||
@state() private _restockQty = "";
|
||||
/** #133: optional backdated completion moment ("YYYY-MM-DDTHH:MM:SS" local; "" = now). */
|
||||
@state() private _completedAt = "";
|
||||
@@ -94,10 +114,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
.filter(([, done]) => done),
|
||||
);
|
||||
this._feedback = "needed";
|
||||
this._photoDocId = "";
|
||||
this._photoPreview = "";
|
||||
this._photos.forEach((p) => URL.revokeObjectURL(p.preview));
|
||||
this._photos = [];
|
||||
this._uploadedIds = [];
|
||||
this._photoUploading = false;
|
||||
this._readingValue = "";
|
||||
this._readingValues = {};
|
||||
this._restockQty = this.restockDefault !== null ? String(this.restockDefault) : "";
|
||||
this._completedAt = "";
|
||||
// #99: prefill "parts used" with the task's fixed links — the user can
|
||||
@@ -118,45 +140,52 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
this._feedback = value;
|
||||
}
|
||||
|
||||
/** The Android Companion app answers a multi-select with an empty file
|
||||
* list (its chooser ignores the intent's ClipData — #161 follow-up), so
|
||||
* the gallery picker is single-select there and a hint says each pick
|
||||
* is added. Evaluated once per dialog; the host does not change. */
|
||||
private readonly _singlePick = isAndroidCompanion();
|
||||
|
||||
/** #161: both pickers (camera = one shot, gallery = multiple) land here.
|
||||
* Files upload one after another so a slow connection still shows
|
||||
* progress tile by tile; anything beyond the cap is dropped with a
|
||||
* note rather than silently. */
|
||||
private async _onPhotoInput(e: Event): Promise<void> {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
const files = Array.from(input.files ?? []);
|
||||
input.value = ""; // allow re-picking the same file
|
||||
if (!file) return;
|
||||
if (files.length === 0) return;
|
||||
const room = MAX_COMPLETION_PHOTOS - this._photos.length;
|
||||
const accepted = files.slice(0, Math.max(room, 0));
|
||||
this._photoUploading = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("entry_id", this.entryId);
|
||||
form.append("tags", "photo");
|
||||
form.append("file", file, file.name);
|
||||
const resp = await fetch("/api/maintenance_supporter/document/upload", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${this.hass.auth?.data?.access_token ?? ""}` },
|
||||
body: form,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
this._error = resp.status === 413
|
||||
? t("doc_too_large", this.lang)
|
||||
: t("doc_upload_failed", this.lang);
|
||||
return;
|
||||
for (const file of accepted) {
|
||||
const id = await uploadCompletionPhoto(this.hass, this.entryId, file);
|
||||
this._uploadedIds = [...this._uploadedIds, id];
|
||||
this._photos = [...this._photos, { id, preview: URL.createObjectURL(file) }];
|
||||
}
|
||||
const doc = (await resp.json()) as { id?: string };
|
||||
if (doc.id) {
|
||||
this._photoDocId = doc.id;
|
||||
this._photoPreview = URL.createObjectURL(file);
|
||||
if (files.length > accepted.length) {
|
||||
this._error = t("photos_limit", this.lang).replace("{max}", String(MAX_COMPLETION_PHOTOS));
|
||||
}
|
||||
} catch {
|
||||
this._error = t("doc_upload_failed", this.lang);
|
||||
} catch (e) {
|
||||
const key = e instanceof Error && e.message === "doc_too_large" ? "doc_too_large" : "doc_upload_failed";
|
||||
this._error = t(key, this.lang);
|
||||
} finally {
|
||||
this._photoUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _removePhoto(): void {
|
||||
if (this._photoPreview) URL.revokeObjectURL(this._photoPreview);
|
||||
this._photoDocId = "";
|
||||
this._photoPreview = "";
|
||||
/** ✕ on a tile: drop it from the completion AND delete the upload —
|
||||
* the file only ever existed for this dialog session. */
|
||||
private _removePhoto(id: string): void {
|
||||
const gone = this._photos.find((p) => p.id === id);
|
||||
if (gone) URL.revokeObjectURL(gone.preview);
|
||||
this._photos = this._photos.filter((p) => p.id !== id);
|
||||
if (this._uploadedIds.includes(id)) {
|
||||
this._uploadedIds = this._uploadedIds.filter((x) => x !== id);
|
||||
void discardUploadedPhotos(this.hass, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
private async _complete(): Promise<void> {
|
||||
@@ -183,8 +212,8 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
if (this.adaptiveEnabled) {
|
||||
data.feedback = this._feedback;
|
||||
}
|
||||
if (this._photoDocId) {
|
||||
data.photo_doc_id = this._photoDocId;
|
||||
if (this._photos.length > 0) {
|
||||
data.photo_doc_ids = this._photos.map((p) => p.id);
|
||||
}
|
||||
// Scan fallback: the backend accepts via_tag_scan on task/complete so a
|
||||
// require_tag_scan task can still be finished from the dialog the scan
|
||||
@@ -204,7 +233,18 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
// normalisation as the history-edit dialog).
|
||||
data.completed_at = this._completedAt.length === 16 ? `${this._completedAt}:00` : this._completedAt;
|
||||
}
|
||||
if (this._readingValue !== "") {
|
||||
if (this.readings.length > 0) {
|
||||
// #161 phase 2: only the meters actually read go out; the backend
|
||||
// snapshots name + unit per slot id.
|
||||
const values: Record<string, number> = {};
|
||||
for (const slot of this.readings) {
|
||||
const raw = (this._readingValues[slot.id] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const num = parseFloat(raw.replace(",", "."));
|
||||
if (!isNaN(num)) values[slot.id] = num;
|
||||
}
|
||||
if (Object.keys(values).length > 0) data.reading_values = values;
|
||||
} else if (this._readingValue !== "") {
|
||||
const rv = parseFloat(this._readingValue);
|
||||
if (!isNaN(rv)) data.reading_value = rv;
|
||||
}
|
||||
@@ -226,6 +266,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
);
|
||||
}
|
||||
await this.hass.connection.sendMessagePromise(data);
|
||||
this._uploadedIds = []; // attached now — Cancel cleanup must not touch them
|
||||
this._open = false;
|
||||
this.dispatchEvent(new CustomEvent("task-completed"));
|
||||
} catch (e) {
|
||||
@@ -235,13 +276,37 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** #161 phase 2: one field per slot with the previous value as a hint;
|
||||
* a value below the last one gets a warning (meters count up — a
|
||||
* replaced meter or a typo), never a block. */
|
||||
private _renderReadingField(slot: ReadingSlot, L: string) {
|
||||
const at = this._completedAt ? new Date(this._completedAt).getTime() : NaN;
|
||||
const last = lastReadingBefore(this.readingHistory, slot.id, isNaN(at) ? undefined : at);
|
||||
const unit = slot.unit || this.readingUnit;
|
||||
const raw = (this._readingValues[slot.id] ?? "").trim();
|
||||
const num = raw === "" ? NaN : parseFloat(raw.replace(",", "."));
|
||||
const below = last !== undefined && !isNaN(num) && num < last.value;
|
||||
const lastText = last !== undefined ? formatNumber(last.value, L, { maximumFractionDigits: 3 }) : "";
|
||||
return html`
|
||||
<label class="field reading-field">
|
||||
<span class="field-label">${slot.name}${unit ? ` (${unit})` : ""}</span>
|
||||
<input type="text" inputmode="decimal" class="field-input"
|
||||
placeholder=${last !== undefined ? t("reading_last", L).replace("{value}", lastText) : ""}
|
||||
.value=${this._readingValues[slot.id] ?? ""}
|
||||
@input=${(e: Event) => {
|
||||
this._readingValues = { ...this._readingValues, [slot.id]: (e.target as HTMLInputElement).value };
|
||||
}} />
|
||||
${below ? html`<span class="reading-warn">${t("reading_below_last", L).replace("{value}", lastText)}</span>` : nothing}
|
||||
</label>`;
|
||||
}
|
||||
|
||||
/** Required details the user has not supplied yet (drives Save + markers). */
|
||||
private get _missingRequired(): string[] {
|
||||
const filled: Record<string, boolean> = {
|
||||
notes: this._notes.trim() !== "",
|
||||
cost: this._cost.trim() !== "",
|
||||
duration: this._duration.trim() !== "",
|
||||
photo: this._photoDocId !== "",
|
||||
photo: this._photos.length > 0,
|
||||
// "Who did it" is filled in server-side from the authenticated
|
||||
// connection (websocket/tasks_actions.py), so the dialog satisfies it
|
||||
// as long as we ARE a logged-in user. Claiming it is always satisfied
|
||||
@@ -300,6 +365,11 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
|
||||
private _close(): void {
|
||||
this._open = false;
|
||||
if (this._uploadedIds.length > 0) {
|
||||
const orphans = this._uploadedIds;
|
||||
this._uploadedIds = [];
|
||||
void discardUploadedPhotos(this.hass, orphans);
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed the backdate field with the current minute (local, seconds zeroed). */
|
||||
@@ -330,7 +400,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
${this.taskType === "reading"
|
||||
${this.readings.length > 0
|
||||
? html`<div class="readings-block">
|
||||
<span class="field-label">${t("readings_section", L)}</span>
|
||||
${this.readings.map((slot) => this._renderReadingField(slot, L))}
|
||||
</div>`
|
||||
: this.taskType === "reading"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span class="field-label">${t("reading_value_label", L)}${this.readingUnit ? ` (${this.readingUnit})` : ""}</span>
|
||||
@@ -433,22 +508,36 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
</button>`}
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">${t("completion_photo_optional", L)}${this._req("photo")}</span>
|
||||
${this._photoPreview
|
||||
? html`
|
||||
<div class="photo-preview">
|
||||
<img src=${this._photoPreview} alt="" />
|
||||
<button type="button" class="photo-remove" @click=${this._removePhoto}
|
||||
title="${t("remove", L)}">✕</button>
|
||||
<span class="field-label">${t("completion_photos_optional", L)}${this._req("photo")}</span>
|
||||
${this._photos.length > 0
|
||||
? html`<div class="photo-strip">
|
||||
${this._photos.map((p) => html`
|
||||
<div class="photo-preview">
|
||||
<img src=${p.preview} alt="" />
|
||||
<button type="button" class="photo-remove" @click=${() => this._removePhoto(p.id)}
|
||||
title="${t("remove", L)}">✕</button>
|
||||
</div>`)}
|
||||
</div>`
|
||||
: html`
|
||||
<label class="photo-pick">
|
||||
<ha-icon icon="mdi:camera"></ha-icon>
|
||||
<span>${this._photoUploading ? t("uploading", L) : t("add_photo", L)}</span>
|
||||
<input type="file" accept="image/*" capture="environment"
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>`}
|
||||
: nothing}
|
||||
${this._photos.length < MAX_COMPLETION_PHOTOS
|
||||
? html`<div class="photo-pickers">
|
||||
<label class="photo-pick photo-pick-camera">
|
||||
<ha-icon icon="mdi:camera"></ha-icon>
|
||||
<span>${this._photoUploading ? t("uploading", L) : t("doc_camera", L)}</span>
|
||||
<input type="file" accept="image/*" capture="environment"
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>
|
||||
<label class="photo-pick photo-pick-gallery">
|
||||
<ha-icon icon="mdi:image-multiple"></ha-icon>
|
||||
<span>${t(this._singlePick ? "choose_photo" : "choose_photos", L)}</span>
|
||||
<input type="file" accept="image/*" ?multiple=${!this._singlePick}
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>
|
||||
</div>
|
||||
${this._singlePick ? html`<div class="photo-limit photo-android-hint">${t("photos_android_hint", L)}</div>` : nothing}`
|
||||
: html`<div class="photo-limit">${t("photos_limit", L).replace("{max}", String(MAX_COMPLETION_PHOTOS))}</div>`}
|
||||
</div>
|
||||
${this.adaptiveEnabled ? html`
|
||||
<div class="feedback-section">
|
||||
@@ -567,6 +656,13 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* .field/.field-label/.field-input come from nativeFieldStyles */
|
||||
/* #161 phase 2: the per-slot reading fields */
|
||||
.readings-block { display: flex; flex-direction: column; gap: 8px; }
|
||||
.readings-block > .field-label { margin-bottom: -4px; }
|
||||
.reading-warn {
|
||||
font-size: 12px;
|
||||
color: var(--warning-color, #ff9800);
|
||||
}
|
||||
.photo-pick {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -601,15 +697,37 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
.backdate-pick:hover { border-color: var(--primary-color); }
|
||||
.photo-pick input[type="file"] { display: none; }
|
||||
/* #161: several photos per completion — tiles wrap into a strip,
|
||||
the two pickers (camera / gallery) sit underneath while there is
|
||||
room left under the cap. */
|
||||
.photo-pickers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.photo-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 8px 0; /* room for the remove badges above the tiles */
|
||||
}
|
||||
.photo-limit {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.photo-android-hint { margin-top: 4px; }
|
||||
.photo-preview {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
/* Uniform tiles: a tiny or portrait shot must not collapse the strip. */
|
||||
.photo-preview img {
|
||||
max-width: 160px;
|
||||
max-height: 160px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
}
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
|
||||
+24
-5
@@ -16,6 +16,7 @@ import { downloadUrl } from "../helpers/download";
|
||||
import { downloadSignedDocument, openSignedDocument, signDocumentPath } from "../helpers/document-url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import { DOC_FILTER_MIN, filterDocuments } from "../helpers/document-filter";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface MaintenanceDocument {
|
||||
@@ -36,6 +37,8 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
@property({ type: Boolean }) public canWrite = false;
|
||||
|
||||
@state() private _docs: MaintenanceDocument[] = [];
|
||||
// Local filter (#171) — shown once the list is long enough to need one.
|
||||
@state() private _filter = "";
|
||||
@state() private _loaded = false;
|
||||
@state() private _busy = false;
|
||||
@state() private _error = "";
|
||||
@@ -389,15 +392,23 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
${this._loaded && this._docs.length >= DOC_FILTER_MIN
|
||||
? html`<div class="doc-filter">
|
||||
<ha-icon icon="mdi:magnify"></ha-icon>
|
||||
<input type="search" aria-label=${t("doc_search", L)} placeholder=${t("doc_search", L)}
|
||||
.value=${this._filter} @input=${(e: Event) => (this._filter = (e.target as HTMLInputElement).value)} />
|
||||
</div>`
|
||||
: nothing}
|
||||
${!this._loaded
|
||||
? html`<div class="doc-empty">${t("loading", L)}</div>`
|
||||
: this._docs.length === 0
|
||||
? html`<div class="doc-empty">${t("documents_empty", L)}</div>`
|
||||
: html`
|
||||
<div class="doc-list">
|
||||
${this._docs.map((doc) => this._renderDoc(doc, L))}
|
||||
</div>
|
||||
`}
|
||||
: (() => {
|
||||
const shown = filterDocuments(this._docs, this._filter);
|
||||
return shown.length === 0
|
||||
? html`<div class="doc-empty">${t("doc_search_none", L)}</div>`
|
||||
: html`<div class="doc-list">${shown.map((doc) => this._renderDoc(doc, L))}</div>`;
|
||||
})()}
|
||||
|
||||
${this._lightboxUrl
|
||||
? html`<div class="lightbox" @click=${() => (this._lightboxUrl = "")}>
|
||||
@@ -561,6 +572,14 @@ export class MaintenanceDocumentsSection extends LitElement {
|
||||
.doc-msg.error { color: var(--error-color, #f44336); }
|
||||
.doc-msg.hint { color: var(--secondary-text-color, #888); }
|
||||
.doc-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 8px 0; }
|
||||
.doc-filter { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; }
|
||||
.doc-filter ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); }
|
||||
.doc-filter input {
|
||||
flex: 1; min-width: 0; font: inherit; font-size: 13px; padding: 6px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--divider-color); background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.doc-filter input:focus { outline: none; border-color: var(--primary-color); }
|
||||
.doc-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.doc-row {
|
||||
display: flex; align-items: center; gap: 12px; padding: 8px 10px;
|
||||
|
||||
+225
-3
@@ -1,5 +1,5 @@
|
||||
/** Dialog for editing an existing history entry (timestamp / notes / cost /
|
||||
* duration / completed_by). Backed by maintenance_supporter/task/history/update.
|
||||
* duration / completed_by / photos). Backed by maintenance_supporter/task/history/update.
|
||||
*
|
||||
* Opened from:
|
||||
* - Task detail page → history tab → Edit button per entry
|
||||
@@ -9,10 +9,16 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, langOf } from "../styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { t, langOf, formatNumber } from "../styles";
|
||||
import type { HomeAssistant, ReadingSlot, ReadingValue } from "../types";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import {
|
||||
MAX_COMPLETION_PHOTOS,
|
||||
discardUploadedPhotos,
|
||||
uploadCompletionPhoto,
|
||||
} from "../helpers/photo-upload";
|
||||
import "./ms-date-field";
|
||||
import "./history-photo";
|
||||
|
||||
export interface HistoryEntryDraft {
|
||||
entry_id: string;
|
||||
@@ -27,6 +33,19 @@ export interface HistoryEntryDraft {
|
||||
// #130: the entry's recorded part consumption ({part_id, name, quantity,
|
||||
// entry_id? for pooled parts}); absent/empty = nothing consumed.
|
||||
used_parts?: Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null;
|
||||
// #161: the entry's completion photos (callers fold the pre-multi-photo
|
||||
// `photo_doc_id` scalar in via helpers/history-photos.historyPhotoIds).
|
||||
photo_doc_ids?: string[] | null;
|
||||
// #161 phase 2: the entry's readings — the v2.20 scalar and/or the
|
||||
// per-slot snapshot — plus the task's current slots, so a meter that
|
||||
// was skipped at completion can be filled in afterwards. task_type /
|
||||
// reading_unit decide whether the scalar field shows on an entry
|
||||
// that recorded nothing.
|
||||
reading_value?: number | null;
|
||||
reading_values?: ReadingValue[] | null;
|
||||
readings?: ReadingSlot[] | null;
|
||||
task_type?: string | null;
|
||||
reading_unit?: string | null;
|
||||
}
|
||||
|
||||
/** One selectable part option in the edit dialog — the object's own parts
|
||||
@@ -60,6 +79,21 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
@state() private _partQty: Record<string, number> = {};
|
||||
private _partQtyOriginal = "";
|
||||
|
||||
// #161: the edited photo list; the original as a JSON key for the
|
||||
// "did it change" check, plus the docs THIS session uploaded so an
|
||||
// abandoned edit can drop them again (a removed pre-existing photo is
|
||||
// only detached — the file stays in the object's documents).
|
||||
@state() private _photos: string[] = [];
|
||||
private _photosOriginal = "";
|
||||
|
||||
// #161 phase 2: the reading rows (entry snapshot ∪ task slots) with the
|
||||
// typed text per slot id; the original numbers as a JSON key.
|
||||
@state() private _readingRows: ReadingSlot[] = [];
|
||||
@state() private _readingText: Record<string, string> = {};
|
||||
private _readingsOriginal = "";
|
||||
private _uploadedIds: string[] = [];
|
||||
@state() private _photoUploading = false;
|
||||
|
||||
/** Open the dialog with the given history-entry data. The caller must
|
||||
* pass `original_timestamp` (the entry's current timestamp before edit)
|
||||
* so the backend can find the entry. */
|
||||
@@ -71,9 +105,83 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
this._partOptions = null;
|
||||
this._partQty = {};
|
||||
this._partQtyOriginal = "";
|
||||
this._photos = [...(draft.photo_doc_ids ?? [])];
|
||||
this._photosOriginal = JSON.stringify(this._photos);
|
||||
this._uploadedIds = [];
|
||||
this._photoUploading = false;
|
||||
this._seedReadings(draft);
|
||||
void this._loadPartOptions();
|
||||
}
|
||||
|
||||
private _seedReadings(draft: HistoryEntryDraft): void {
|
||||
// Task slot order first (matches the complete dialog), then snapshot
|
||||
// slots the task no longer has — with the entry's own name/unit.
|
||||
const rows: ReadingSlot[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const s of draft.readings ?? []) {
|
||||
if (seen.has(s.id)) continue;
|
||||
seen.add(s.id);
|
||||
rows.push({ id: s.id, name: s.name, unit: s.unit ?? null });
|
||||
}
|
||||
for (const v of draft.reading_values ?? []) {
|
||||
if (seen.has(v.id)) continue;
|
||||
seen.add(v.id);
|
||||
rows.push({ id: v.id, name: v.name, unit: v.unit ?? null });
|
||||
}
|
||||
this._readingRows = rows;
|
||||
const text: Record<string, string> = {};
|
||||
for (const v of draft.reading_values ?? []) text[v.id] = String(v.value);
|
||||
this._readingText = text;
|
||||
this._readingsOriginal = JSON.stringify(this._readingNumbers());
|
||||
}
|
||||
|
||||
/** {slot id: number} for every row with a parsable value. */
|
||||
private _readingNumbers(): Record<string, number> {
|
||||
const out: Record<string, number> = {};
|
||||
for (const row of this._readingRows) {
|
||||
const raw = (this._readingText[row.id] ?? "").trim();
|
||||
if (raw === "") continue;
|
||||
const num = parseFloat(raw.replace(",", "."));
|
||||
if (!isNaN(num)) out[row.id] = num;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async _onPhotoInput(e: Event): Promise<void> {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const files = Array.from(input.files ?? []);
|
||||
input.value = "";
|
||||
const draft = this._draft;
|
||||
if (files.length === 0 || !draft) return;
|
||||
const room = MAX_COMPLETION_PHOTOS - this._photos.length;
|
||||
const accepted = files.slice(0, Math.max(room, 0));
|
||||
this._photoUploading = true;
|
||||
this._error = "";
|
||||
try {
|
||||
for (const file of accepted) {
|
||||
const id = await uploadCompletionPhoto(this.hass, draft.entry_id, file);
|
||||
this._uploadedIds = [...this._uploadedIds, id];
|
||||
this._photos = [...this._photos, id];
|
||||
}
|
||||
if (files.length > accepted.length) {
|
||||
this._error = t("photos_limit", this._lang).replace("{max}", String(MAX_COMPLETION_PHOTOS));
|
||||
}
|
||||
} catch (err) {
|
||||
const key = err instanceof Error && err.message === "doc_too_large" ? "doc_too_large" : "doc_upload_failed";
|
||||
this._error = t(key, this._lang);
|
||||
} finally {
|
||||
this._photoUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _removePhoto(id: string): void {
|
||||
this._photos = this._photos.filter((x) => x !== id);
|
||||
if (this._uploadedIds.includes(id)) {
|
||||
this._uploadedIds = this._uploadedIds.filter((x) => x !== id);
|
||||
void discardUploadedPhotos(this.hass, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
/** The object's own parts + pooled parts this task draws on — from the
|
||||
* instance-wide overview so pooled owners resolve without extra calls. */
|
||||
private async _loadPartOptions(): Promise<void> {
|
||||
@@ -140,6 +248,12 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._draft = null;
|
||||
this._originalSnapshot = null;
|
||||
if (this._uploadedIds.length > 0) {
|
||||
// Cancelled after uploading: nothing references those files.
|
||||
const orphans = this._uploadedIds;
|
||||
this._uploadedIds = [];
|
||||
void discardUploadedPhotos(this.hass, orphans);
|
||||
}
|
||||
}
|
||||
|
||||
private _set<K extends keyof HistoryEntryDraft>(
|
||||
@@ -188,6 +302,22 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
...(o.foreign ? { entry_id: o.entry_id } : {}),
|
||||
}));
|
||||
}
|
||||
// #161 phase 2: readings. The scalar rides the changed-field check
|
||||
// like notes; the slot map is sent whole when any value differs —
|
||||
// the backend replaces the snapshot, a missing id means "unread".
|
||||
if (this._draft.reading_value !== this._originalSnapshot.reading_value) {
|
||||
patch.reading_value = this._draft.reading_value ?? null;
|
||||
}
|
||||
if (this._readingRows.length > 0 && JSON.stringify(this._readingNumbers()) !== this._readingsOriginal) {
|
||||
const numbers = this._readingNumbers();
|
||||
const map: Record<string, number | null> = {};
|
||||
for (const row of this._readingRows) map[row.id] = numbers[row.id] ?? null;
|
||||
patch.reading_values = map;
|
||||
}
|
||||
// #161: the photo list, only when it differs from what we opened with.
|
||||
if (JSON.stringify(this._photos) !== this._photosOriginal) {
|
||||
patch.photo_doc_ids = [...this._photos];
|
||||
}
|
||||
// Nothing changed → close without WS call
|
||||
const changedKeys = Object.keys(patch).filter(
|
||||
(k) => !["type", "entry_id", "task_id", "original_timestamp"].includes(k),
|
||||
@@ -197,6 +327,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
await this.hass.connection.sendMessagePromise(patch);
|
||||
this._uploadedIds = []; // saved — they belong to the entry now
|
||||
// Notify upstream so they can refresh
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("history-entry-saved", {
|
||||
@@ -273,6 +404,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
}} />
|
||||
</label>
|
||||
</div>
|
||||
${this._renderReadings(d, L)}
|
||||
${this._partOptions && this._partOptions.length > 0 ? html`
|
||||
<div class="parts-block">
|
||||
<span class="parts-title">${t("complete_parts_used", L)}</span>
|
||||
@@ -300,6 +432,27 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
})}
|
||||
</div>
|
||||
` : nothing}
|
||||
<div class="photos-block">
|
||||
<span class="parts-title">${t("completion_photos", L)}</span>
|
||||
${this._photos.length > 0 ? html`
|
||||
<div class="photo-strip">
|
||||
${this._photos.map((id) => html`
|
||||
<div class="photo-tile">
|
||||
<maintenance-history-photo .hass=${this.hass} .docId=${id}></maintenance-history-photo>
|
||||
<button type="button" class="photo-remove" title=${t("remove", L)}
|
||||
@click=${() => this._removePhoto(id)}>✕</button>
|
||||
</div>`)}
|
||||
</div>` : nothing}
|
||||
${this._photos.length < MAX_COMPLETION_PHOTOS ? html`
|
||||
<label class="photo-add">
|
||||
<ha-icon icon="mdi:image-plus"></ha-icon>
|
||||
<span>${this._photoUploading ? t("uploading", L) : t("add_photos", L)}</span>
|
||||
<input type="file" accept="image/*" multiple
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>` : html`<span class="photos-hint">${t("photos_limit", L).replace("{max}", String(MAX_COMPLETION_PHOTOS))}</span>`}
|
||||
<span class="photos-hint">${t("history_edit_photos_hint", L)}</span>
|
||||
</div>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
<div class="actions">
|
||||
<button class="cancel" @click=${this.close} ?disabled=${this._saving}>
|
||||
@@ -313,6 +466,39 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** #161 phase 2: per-slot rows for a slot task (entry snapshot ∪ current
|
||||
* slots), else the scalar field on a reading task / an entry that has one. */
|
||||
private _renderReadings(d: HistoryEntryDraft, L: string) {
|
||||
if (this._readingRows.length > 0) {
|
||||
return html`
|
||||
<div class="readings-block">
|
||||
<span class="parts-title">${t("readings_section", L)}</span>
|
||||
${this._readingRows.map((row) => html`
|
||||
<label class="reading-row-edit">
|
||||
<span class="reading-row-name">${row.name}${row.unit ? ` (${row.unit})` : ""}</span>
|
||||
<input type="text" inputmode="decimal" class="reading-row-input"
|
||||
.value=${this._readingText[row.id] ?? ""}
|
||||
@input=${(e: Event) => {
|
||||
this._readingText = { ...this._readingText, [row.id]: (e.target as HTMLInputElement).value };
|
||||
}} />
|
||||
</label>
|
||||
`)}
|
||||
</div>`;
|
||||
}
|
||||
if (d.reading_value == null && d.task_type !== "reading") return nothing;
|
||||
return html`
|
||||
<label>
|
||||
<span>${t("reading_value_label", L)}${d.reading_unit ? ` (${d.reading_unit})` : ""}</span>
|
||||
<input type="number" step="any"
|
||||
.value=${d.reading_value != null ? String(d.reading_value) : ""}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
const num = v === "" ? NaN : Number(v);
|
||||
this._set("reading_value", isNaN(num) ? null : num);
|
||||
}} />
|
||||
</label>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host { display: contents; }
|
||||
.backdrop {
|
||||
@@ -389,6 +575,42 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
.part-row-edit input[type="checkbox"] { width: auto; }
|
||||
.part-label { flex: 1; color: var(--primary-text-color); }
|
||||
.part-qty { width: 76px; }
|
||||
/* #161 phase 2: readings on the entry */
|
||||
.readings-block {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
border: 1px solid var(--divider-color, #444);
|
||||
border-radius: 6px; padding: 8px;
|
||||
}
|
||||
.reading-row-edit {
|
||||
display: flex; flex-direction: row; align-items: center; gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.reading-row-name { flex: 1; color: var(--primary-text-color); min-width: 0; }
|
||||
.reading-row-input { width: 140px; font-variant-numeric: tabular-nums; }
|
||||
/* #161: photos on the entry */
|
||||
.photos-block {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
border: 1px solid var(--divider-color, #444);
|
||||
border-radius: 6px; padding: 8px;
|
||||
}
|
||||
.photo-strip { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 6px; }
|
||||
.photo-tile { position: relative; width: fit-content; }
|
||||
.photo-remove {
|
||||
position: absolute; top: -4px; right: -8px;
|
||||
width: 22px; height: 22px; border-radius: 50%; border: none;
|
||||
background: var(--error-color, #db4437); color: #fff;
|
||||
cursor: pointer; font-size: 11px; line-height: 1; padding: 0;
|
||||
}
|
||||
.photo-add {
|
||||
display: inline-flex; flex-direction: row; align-items: center; gap: 8px;
|
||||
width: fit-content; padding: 6px 10px;
|
||||
border: 1px dashed var(--divider-color, #444); border-radius: 8px;
|
||||
cursor: pointer; font-size: 13px; color: var(--secondary-text-color);
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.photo-add:hover { border-color: var(--primary-color); }
|
||||
.photo-add input[type="file"] { display: none; }
|
||||
.photos-hint { font-size: 12px; color: var(--secondary-text-color); }
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,16 +48,20 @@ export class MaintenanceHistoryPhoto extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.wrap { display: inline-block; margin-top: 4px; }
|
||||
/* #161: uniform 96px tiles — several photos sit in a strip, so a
|
||||
tiny or portrait shot must not collapse its slot. */
|
||||
img {
|
||||
max-width: 96px;
|
||||
max-height: 96px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
border: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ph {
|
||||
width: 96px;
|
||||
height: 64px;
|
||||
height: 96px;
|
||||
border-radius: 6px;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
margin-top: 4px;
|
||||
|
||||
@@ -5,12 +5,14 @@ import { property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { live } from "lit/directives/live.js";
|
||||
import type { HomeAssistant, AdvancedFeatures, BudgetStatus, HAUser } from "../types";
|
||||
import { t, langOf } from "../styles";
|
||||
import { t, langOf, personStyles } from "../styles";
|
||||
import { signApiPath } from "../helpers/document-url";
|
||||
import { downloadUrl } from "../helpers/download";
|
||||
import { UserService } from "../user-service";
|
||||
import { AVATAR_PALETTE, personOf, renderPersonAvatar } from "../helpers/person";
|
||||
import { OBJECT_COLUMNS, sanitizeColumns } from "../helpers/object-columns";
|
||||
import { downloadTextFile } from "../helpers/download";
|
||||
import { invalidateSettingsCache } from "../helpers/settings-cache";
|
||||
import "./ms-date-field";
|
||||
|
||||
/** One household member and the notify services they actually resolve to.
|
||||
@@ -26,6 +28,8 @@ interface PersonNotifyTarget {
|
||||
interface SettingsResponse {
|
||||
features: AdvancedFeatures;
|
||||
admin_panel_user_ids?: string[];
|
||||
/** #169 follow-up: per-member avatar overrides. */
|
||||
member_display?: Record<string, { initials?: string; color?: string }>;
|
||||
operator_write_enabled?: boolean;
|
||||
objects_table_columns?: string[];
|
||||
/** v2.21: template-gallery curation — hidden template ids. */
|
||||
@@ -73,6 +77,9 @@ interface SettingsResponse {
|
||||
bundle_threshold: number;
|
||||
reminder_lead_days: number[];
|
||||
scope_view_id: string;
|
||||
/** #165: your own notification rule — event-only delivery + extra-data template. */
|
||||
event_only?: boolean;
|
||||
extra_data?: string;
|
||||
};
|
||||
actions: {
|
||||
complete_enabled: boolean;
|
||||
@@ -117,6 +124,8 @@ interface VacationPreviewRow {
|
||||
confidence: "deterministic" | "estimated" | "unpredictable";
|
||||
events: Array<{ date: string; status: "due_soon" | "overdue" | "triggered_est" }>;
|
||||
will_suppress: boolean;
|
||||
/** The task's own skip rule (#150); absent on older backends = allowed. */
|
||||
allow_skip?: boolean;
|
||||
}
|
||||
|
||||
// Keep in lockstep with const.BUDGET_CURRENCIES (keys + order). Parity is
|
||||
@@ -269,11 +278,18 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
.replace("{n}", String(bn.devices));
|
||||
const rule = (warn ? t("bn_above_floor", L).replace("{name}", "Battery Notes") : t("bn_floor_decides", L))
|
||||
.replace("{floor}", String(floor));
|
||||
// The integration name becomes the link, the rest stays text. ja/zh
|
||||
// punctuate with the fullwidth colon — an ASCII-only split linked the
|
||||
// whole sentence and then duplicated its last character (bug review
|
||||
// 2026-09-04).
|
||||
const cut = summary.search(/[::]/);
|
||||
const linkText = cut < 0 ? summary : summary.slice(0, cut);
|
||||
const rest = cut < 0 ? "" : summary.slice(cut);
|
||||
return html`
|
||||
<div class="bn-note${warn ? " warn" : ""}">
|
||||
<ha-icon icon="${warn ? "mdi:alert-outline" : "mdi:battery-heart-variant"}"></ha-icon>
|
||||
<span>
|
||||
<a class="bn-link" href="/config/integrations/integration/battery_notes">${summary.split(":")[0]}</a>${summary.slice(summary.indexOf(":"))}
|
||||
<a class="bn-link" href="/config/integrations/integration/battery_notes">${linkText}</a>${rest}
|
||||
${bn.overrides.length
|
||||
? html` · ${t("bn_overrides", L).replace("{n}", String(bn.overrides.length + bn.more))}
|
||||
${bn.overrides.map((o, i) => html`${i ? " · " : " "}${o.device_id
|
||||
@@ -286,6 +302,62 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** #169 follow-up: Settings → household members — the avatar each member
|
||||
* gets in task lists (initials + a palette colour). Defaults come from
|
||||
* the backend (name / user id); an override is stored per user id. */
|
||||
private _renderMemberAvatars(L: string) {
|
||||
if (!this._users.length) return nothing;
|
||||
const overrides = this._settings?.member_display || {};
|
||||
return html`
|
||||
<div class="settings-section member-avatars" data-section="member_avatars">
|
||||
<h3>${t("member_avatars", L)}</h3>
|
||||
<p class="section-desc">${t("member_avatars_hint", L)}</p>
|
||||
${this._users.map((u) => {
|
||||
const person = personOf(u)!;
|
||||
const override = overrides[u.id] || {};
|
||||
return html`
|
||||
<div class="member-avatar-row">
|
||||
${renderPersonAvatar(person)}
|
||||
<span class="member-avatar-name">${u.name}</span>
|
||||
<input class="member-initials" type="text" maxlength="3" autocomplete="off"
|
||||
aria-label="${t("member_initials", L)}"
|
||||
placeholder="${person.initials}"
|
||||
.value=${override.initials ?? ""}
|
||||
@change=${(e: Event) => this._setMemberAvatar(u.id, { initials: (e.target as HTMLInputElement).value })} />
|
||||
<span class="member-palette" role="radiogroup" aria-label="${t("member_color", L)}">
|
||||
${AVATAR_PALETTE.map((color) => html`
|
||||
<button type="button" class="member-swatch${person.color === color ? " selected" : ""}"
|
||||
style="--person-color: ${color}" title=${color} aria-label=${color}
|
||||
@click=${() => this._setMemberAvatar(u.id, { color })}></button>`)}
|
||||
</span>
|
||||
${override.initials || override.color
|
||||
? html`<button class="ha-button secondary member-reset" @click=${() => this._setMemberAvatar(u.id, null)}>${t("member_avatar_reset", L)}</button>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Merge one member's override into the map and save; null resets the member. */
|
||||
private async _setMemberAvatar(userId: string, patch: { initials?: string; color?: string } | null): Promise<void> {
|
||||
const current = { ...(this._settings?.member_display || {}) };
|
||||
if (patch === null) {
|
||||
delete current[userId];
|
||||
} else {
|
||||
const next = { ...(current[userId] || {}), ...patch };
|
||||
if ("initials" in patch && !(patch.initials || "").trim()) delete next.initials;
|
||||
if (Object.keys(next).length) current[userId] = next;
|
||||
else delete current[userId];
|
||||
}
|
||||
await this._updateSetting("member_display", current);
|
||||
// The avatars come back resolved through users/list — refresh the cache.
|
||||
try {
|
||||
this._users = (await this._userService?.getUsers(true)) ?? this._users;
|
||||
} catch {
|
||||
/* keep the stale list; the next open reloads it */
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateSetting(key: string, value: unknown): Promise<void> {
|
||||
try {
|
||||
const result = await this.hass.connection.sendMessagePromise({
|
||||
@@ -293,6 +365,9 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
settings: { [key]: value },
|
||||
});
|
||||
this._settings = result as SettingsResponse;
|
||||
// The card / Lovelace dialogs read their copy through a page-wide
|
||||
// cache — drop it so they follow the change without a reload.
|
||||
invalidateSettingsCache();
|
||||
this._showToast(t("settings_saved", this._lang));
|
||||
this.dispatchEvent(new CustomEvent("settings-changed"));
|
||||
} catch {
|
||||
@@ -329,6 +404,23 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
setTimeout(() => { this._toast = ""; }, 3000);
|
||||
}
|
||||
|
||||
/** Bounded integer settings: an out-of-range entry used to be dropped
|
||||
* silently while the field kept showing it (bug review 2026-09-04). Now
|
||||
* it is named in a toast and the field snaps back to the stored value;
|
||||
* a server reject snaps back through `live()` like the selects. */
|
||||
private _onBoundedIntChange(e: Event, key: string, min: number, max: number, current: number): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const v = parseInt(input.value, 10);
|
||||
if (Number.isInteger(v) && v >= min && v <= max) {
|
||||
void this._updateSetting(key, v);
|
||||
return;
|
||||
}
|
||||
this._showToast(
|
||||
t("settings_value_out_of_range", this._lang).replace("{min}", String(min)).replace("{max}", String(max)),
|
||||
);
|
||||
input.value = String(current);
|
||||
}
|
||||
|
||||
private _downloadFile(content: string, filename: string, mime: string): void {
|
||||
// Companion-app safe download (target=_blank + DOM + deferred revoke).
|
||||
downloadTextFile(content, filename, mime);
|
||||
@@ -345,6 +437,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
return html`
|
||||
${this._renderFeatures(L)}
|
||||
${this._renderPanelAccess(L)}
|
||||
${this._renderMemberAvatars(L)}
|
||||
${this._renderGeneral(L)}
|
||||
${this._renderObjectsColumns(L)}
|
||||
${this._settings.general.notifications_enabled ? this._renderNotifications(L) : nothing}
|
||||
@@ -632,28 +725,22 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<h3>${t("settings_general", L)}</h3>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_default_warning", L)}</span>
|
||||
<input type="number" min="0" max="365" .value=${String(g.default_warning_days)}
|
||||
@change=${(e: Event) => {
|
||||
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
<input type="number" min="0" max="365" .value=${live(String(g.default_warning_days))}
|
||||
@change=${(e: Event) =>
|
||||
// 0 = no warning window (due soon only on the due date) — #145.
|
||||
if (v >= 0 && v <= 365) this._updateSetting("default_warning_days", v);
|
||||
}} />
|
||||
this._onBoundedIntChange(e, "default_warning_days", 0, 365, g.default_warning_days)} />
|
||||
</label>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_consumable_threshold", L)}</span>
|
||||
<input type="number" min="1" max="90" .value=${String(g.default_consumable_threshold ?? 10)}
|
||||
@change=${(e: Event) => {
|
||||
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (v >= 1 && v <= 90) this._updateSetting("default_consumable_threshold", v);
|
||||
}} />
|
||||
<input type="number" min="1" max="90" .value=${live(String(g.default_consumable_threshold ?? 10))}
|
||||
@change=${(e: Event) =>
|
||||
this._onBoundedIntChange(e, "default_consumable_threshold", 1, 90, g.default_consumable_threshold ?? 10)} />
|
||||
</label>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_battery_low_percent", L)}</span>
|
||||
<input type="number" min="1" max="90" .value=${String(g.battery_low_percent ?? 20)}
|
||||
@change=${(e: Event) => {
|
||||
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (v >= 1 && v <= 90) this._updateSetting("battery_low_percent", v);
|
||||
}} />
|
||||
<input type="number" min="1" max="90" .value=${live(String(g.battery_low_percent ?? 20))}
|
||||
@change=${(e: Event) =>
|
||||
this._onBoundedIntChange(e, "battery_low_percent", 1, 90, g.battery_low_percent ?? 20)} />
|
||||
</label>
|
||||
${this._renderBatteryNotesHint(L)}
|
||||
<div class="setting-hint">${t("settings_thresholds_hint", L)}</div>
|
||||
@@ -884,6 +971,25 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_notify_scope_hint", L)}</div>
|
||||
|
||||
<div class="notify-rule">
|
||||
<h4 style="margin: 16px 0 8px; font-size: 14px;">${t("settings_notify_rule", L)}</h4>
|
||||
<div class="setting-hint">${t("settings_notify_rule_hint", L)}</div>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_notify_event_only", L)}</span>
|
||||
<input type="checkbox" .checked=${!!n.event_only}
|
||||
@change=${(e: Event) => this._updateSetting("notify_event_only", (e.target as HTMLInputElement).checked)} />
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_notify_event_only_hint", L)}</div>
|
||||
<label class="setting-row setting-row-block">
|
||||
<span class="setting-label">${t("settings_notify_extra_data", L)}</span>
|
||||
<textarea class="import-area notify-extra" .value=${live(n.extra_data || "")} maxlength="2000" spellcheck="false"
|
||||
placeholder=${'{"category": "maintenance", "critical": {{ priority == "high" }}, "navigate_to": "{{ url }}"}'}
|
||||
@change=${(e: Event) => this._updateSetting("notify_extra_data", (e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_notify_extra_data_hint", L)}</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 16px 0 8px; font-size: 14px;">${t("settings_actions", L)}</h4>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_action_complete", L)}</span>
|
||||
@@ -1128,7 +1234,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</div>
|
||||
<div class="vac-preview-actions">
|
||||
<button @click=${() => this._previewActionComplete(row)}>${t("qr_action_complete", L)}</button>
|
||||
${row.kind === "time_based"
|
||||
${row.kind === "time_based" && row.allow_skip !== false
|
||||
? html`<button @click=${() => this._previewActionSkip(row)}>${t("qr_action_skip", L)}</button>`
|
||||
: nothing}
|
||||
<button class=${isExempt ? "vac-notify-on" : ""}
|
||||
@@ -1761,7 +1867,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
static styles = css`
|
||||
static styles = [personStyles, css`
|
||||
.bn-note {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
margin: 6px 0 10px; padding: 10px 12px; border-radius: 8px;
|
||||
@@ -1836,6 +1942,21 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
.notify-person-target.muted {
|
||||
font-style: italic;
|
||||
}
|
||||
/* #169 follow-up: member avatar editor */
|
||||
.member-avatar-row { display: flex; align-items: center; gap: 10px; padding: 6px 0 0; flex-wrap: wrap; }
|
||||
.member-avatar-name { font-weight: 500; min-width: 120px; }
|
||||
.member-initials {
|
||||
width: 4.5em; padding: 4px 6px; text-align: center; text-transform: uppercase;
|
||||
border: 1px solid var(--divider-color, #e0e0e0); border-radius: 6px;
|
||||
background: var(--card-background-color); color: var(--primary-text-color); font: inherit;
|
||||
}
|
||||
.member-palette { display: inline-flex; gap: 4px; flex-wrap: wrap; }
|
||||
.member-swatch {
|
||||
width: 20px; height: 20px; border-radius: 50%; border: 2px solid transparent; padding: 0;
|
||||
background: var(--person-color); cursor: pointer;
|
||||
}
|
||||
.member-swatch.selected { border-color: var(--primary-text-color); box-shadow: 0 0 0 2px var(--card-background-color) inset; }
|
||||
.member-reset { --ha-button-font-size: 12px; }
|
||||
/* v2.27: template gallery clustered by category */
|
||||
.tpl-group { margin-top: 14px; }
|
||||
.tpl-group-head {
|
||||
@@ -1927,6 +2048,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
|
||||
.import-section { margin-top: 16px; }
|
||||
|
||||
.setting-row-block { flex-direction: column; align-items: stretch; gap: 6px; }
|
||||
.notify-extra { min-height: 72px; font-family: var(--code-font-family, monospace); font-size: 12.5px; }
|
||||
.import-area {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
@@ -2259,7 +2382,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
.qr-print-cell .qr-svg { max-width: 48mm; }
|
||||
.qr-label { font-size: 9pt; }
|
||||
}
|
||||
`;
|
||||
`];
|
||||
}
|
||||
|
||||
customElements.define("maintenance-settings-view", MaintenanceSettingsView);
|
||||
|
||||
+17
@@ -23,6 +23,8 @@ interface StorageSummary {
|
||||
link_count: number;
|
||||
document_count: number;
|
||||
by_object: Record<string, { bytes: number; files: number; links: number }>;
|
||||
/** #171: how much of the library the full-text search can see. */
|
||||
search_index?: { total: number; indexed: number; no_text: number; unsupported: number; pending: number };
|
||||
}
|
||||
|
||||
interface PanelObject {
|
||||
@@ -221,6 +223,16 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${s.search_index && s.search_index.total > 0
|
||||
? html`<div class="index-status">
|
||||
<ha-icon icon="mdi:text-search"></ha-icon>
|
||||
${t("search_index_status", L)
|
||||
.replace("{indexed}", String(s.search_index.indexed))
|
||||
.replace("{total}", String(s.search_index.total))
|
||||
.replace("{no_text}", String(s.search_index.no_text + s.search_index.unsupported))
|
||||
.replace("{pending}", String(s.search_index.pending))}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="doc-search">
|
||||
<ha-icon icon="mdi:magnify"></ha-icon>
|
||||
<input
|
||||
@@ -284,6 +296,11 @@ export class MaintenanceStorageSectionCard extends LitElement {
|
||||
static styles = css`
|
||||
ha-card { margin-top: 16px; }
|
||||
.card-content { padding: 16px; }
|
||||
.index-status {
|
||||
display: flex; align-items: center; gap: 6px; margin: 10px 0 0; font-size: 12.5px;
|
||||
color: var(--secondary-text-color, #888);
|
||||
}
|
||||
.index-status ha-icon { --mdc-icon-size: 18px; }
|
||||
.doc-search {
|
||||
display: flex; align-items: center; gap: 6px; margin: 10px 0 4px;
|
||||
padding: 2px 10px; border-radius: 8px;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, t, weekdayName, monthName, langOf } from "../styles";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, ReadingSlot, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, formatNumber, t, weekdayName, monthName, langOf } from "../styles";
|
||||
import { UserService } from "../user-service";
|
||||
import { partLinkKey } from "../helpers/shared-parts";
|
||||
import { MAX_READING_SLOTS, cleanReadingSlots, duplicateReadingSlotIds, newReadingSlotId } from "../helpers/reading-slots";
|
||||
import {
|
||||
ENVIRONMENTAL_PICKER_DEVICE_CLASSES,
|
||||
ENVIRONMENTAL_PICKER_DOMAINS,
|
||||
@@ -21,6 +22,9 @@ const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibra
|
||||
const PRIORITY_KEYS = ["low", "normal", "high"];
|
||||
const SCHEDULE_TYPE_KEYS = ["time_based", "weekdays", "nth_weekday", "day_of_month", "sensor_based", "one_time", "manual"];
|
||||
const CALENDAR_KINDS = ["weekdays", "nth_weekday", "day_of_month"];
|
||||
// #168: kinds that produce a due DATE and therefore take a time of day
|
||||
// (mirrors SCHEDULE_TIME_KINDS in config_flow_helpers.py).
|
||||
const SCHEDULE_TIME_KINDS = ["time_based", "one_time", ...CALENDAR_KINDS];
|
||||
const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"];
|
||||
// The type selector additionally offers "compound" (a group of conditions
|
||||
// joined by AND/OR); its per-condition sub-type is limited to the flat kinds.
|
||||
@@ -249,6 +253,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@state() private _allowSkip = true;
|
||||
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
|
||||
@state() private _readingUnit = "";
|
||||
// #161 phase 2: named reading slots — several values per completion.
|
||||
@state() private _readings: ReadingSlot[] = [];
|
||||
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
|
||||
* since the same part id can exist on two objects (battery fleet). */
|
||||
@state() private _consumesParts: Record<string, TaskPartLink> = {};
|
||||
@@ -285,6 +291,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
// Schedule time (HH:MM, advanced feature)
|
||||
@state() private _scheduleTime = "";
|
||||
// The picker only shows once this is ticked (#168 follow-up) — most tasks
|
||||
// are due "on the day", so the form stays lean. Unticking clears the time.
|
||||
@state() private _scheduleTimeOn = false;
|
||||
|
||||
// v1.3.0: on_complete_action (gated by completionActionsEnabled)
|
||||
// v1.3.1: _actionData is the parsed object (was: _actionDataJson string)
|
||||
@@ -404,6 +413,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requireTagScan = !!task.require_tag_scan;
|
||||
this._allowSkip = task.allow_skip !== false;
|
||||
this._readingUnit = task.reading_unit || "";
|
||||
this._readings = (task.readings || []).map((s) => ({ ...s }));
|
||||
// Whole link, entry_id included — hydrating only part_id would turn every
|
||||
// shared-pool link into an own-part link on the next save (#111).
|
||||
this._consumesParts = Object.fromEntries(
|
||||
@@ -443,6 +453,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = [...(task.phase_sequence || [])];
|
||||
this._requiredCompletion = [...(task.required_completion_fields || [])];
|
||||
this._scheduleTime = task.schedule_time || "";
|
||||
this._scheduleTimeOn = !!task.schedule_time;
|
||||
|
||||
// v1.3.0: hydrate on_complete_action + quick_complete_defaults
|
||||
const oca = task.on_complete_action;
|
||||
@@ -557,6 +568,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requireTagScan = false;
|
||||
this._allowSkip = true;
|
||||
this._readingUnit = "";
|
||||
this._readings = [];
|
||||
this._consumesParts = {};
|
||||
this._responsibleUserId = null;
|
||||
this._assigneePool = [];
|
||||
@@ -566,6 +578,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = [];
|
||||
this._requiredCompletion = [];
|
||||
this._scheduleTime = "";
|
||||
this._scheduleTimeOn = false;
|
||||
this._environmentalEntity = "";
|
||||
this._environmentalAttribute = "";
|
||||
this._environmentalInitial = "";
|
||||
@@ -1052,6 +1065,47 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requiredCompletion = [...next];
|
||||
}
|
||||
|
||||
// ── Reading slots (#161 phase 2) ─────────────────────────────────────
|
||||
|
||||
private _patchReading(id: string, patch: Partial<ReadingSlot>): void {
|
||||
this._readings = this._readings.map((s) => (s.id === id ? { ...s, ...patch } : s));
|
||||
}
|
||||
|
||||
private _renderReadingsEditor(L: string) {
|
||||
const dupes = duplicateReadingSlotIds(this._readings);
|
||||
return html`
|
||||
<div class="readings-editor">
|
||||
<div class="field-label">${t("readings_section", L)}</div>
|
||||
<div class="field-help">${t("readings_hint", L)}</div>
|
||||
${this._readings.map((s) => html`
|
||||
${dupes.has(s.id) ? html`<div class="field-help reading-dup">${t("reading_duplicate_name", L)}</div>` : nothing}
|
||||
<div class="reading-row">
|
||||
<ms-textfield
|
||||
class="reading-name"
|
||||
label="${t("reading_name_label", L)}"
|
||||
.value=${s.name}
|
||||
@input=${(e: Event) => this._patchReading(s.id, { name: (e.target as HTMLInputElement).value })}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
class="reading-unit"
|
||||
label="${t("reading_unit_short", L)}"
|
||||
.value=${s.unit || ""}
|
||||
@input=${(e: Event) => this._patchReading(s.id, { unit: (e.target as HTMLInputElement).value })}
|
||||
></ms-textfield>
|
||||
<mwc-icon-button class="phase-remove reading-remove" @click=${() => (this._readings = this._readings.filter((x) => x.id !== s.id))}>
|
||||
<ha-icon icon="mdi:delete-outline"></ha-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
`)}
|
||||
${this._readings.length < MAX_READING_SLOTS ? html`
|
||||
<ha-button appearance="plain" class="reading-add"
|
||||
@click=${() => (this._readings = [...this._readings, { id: newReadingSlotId(), name: "", unit: this._readings.length ? this._readings[this._readings.length - 1].unit : this._readingUnit }])}>
|
||||
<ha-icon icon="mdi:plus"></ha-icon> ${t("reading_add", L)}
|
||||
</ha-button>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Task phases (#139) ───────────────────────────────────────────────
|
||||
|
||||
private _phaseSlug(name: string): string {
|
||||
@@ -1265,6 +1319,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.require_tag_scan = this._requireTagScan;
|
||||
data.allow_skip = this._allowSkip;
|
||||
data.reading_unit = this._readingUnit.trim() || null;
|
||||
// #161 phase 2: always sent — [] clears the slots (single-value task).
|
||||
// Sent as hydrated even when the editor is hidden (type != reading), so
|
||||
// a no-op edit never wipes stored slots (the #42/#50 wipe class).
|
||||
data.readings = cleanReadingSlots(this._readings);
|
||||
// Task phases (#139): always sent — null clears a removed cycle.
|
||||
{
|
||||
const defs: Record<string, unknown> = {};
|
||||
@@ -1388,9 +1446,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.trigger_config = null;
|
||||
}
|
||||
|
||||
// Schedule time only sent when feature is enabled — empty string clears.
|
||||
if (this.scheduleTimeEnabled && this._scheduleType === "time_based") {
|
||||
const t = this._scheduleTime.trim();
|
||||
// Schedule time only sent when feature is enabled — null clears.
|
||||
// Every date-driven kind takes one (#168), not only the interval; an
|
||||
// unticked checkbox (or a ticked one with an empty picker) clears it.
|
||||
if (this.scheduleTimeEnabled && SCHEDULE_TIME_KINDS.includes(this._scheduleType)) {
|
||||
const t = this._scheduleTimeOn ? this._scheduleTime.trim() : "";
|
||||
data.schedule_time = /^([01]\d|2[0-3]):[0-5]\d$/.test(t) ? t : null;
|
||||
}
|
||||
|
||||
@@ -2103,7 +2163,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
private _renderSchedulePreview() {
|
||||
if (this._schedulePreview.length === 0) return nothing;
|
||||
const L = this._lang;
|
||||
const time = this.scheduleTimeEnabled && this._scheduleTime ? ` ${this._scheduleTime}` : "";
|
||||
const time = this.scheduleTimeEnabled && this._scheduleTimeOn && this._scheduleTime ? ` ${this._scheduleTime}` : "";
|
||||
const chips = this._schedulePreview
|
||||
.map((iso, i) => {
|
||||
const js = new Date(`${iso}T12:00:00`).getDay(); // 0=Sun
|
||||
@@ -2331,7 +2391,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
: st.state;
|
||||
const num = typeof raw === "number" ? raw : parseFloat(String(raw));
|
||||
const hasNum = raw !== "unknown" && raw !== "unavailable" && raw != null && !isNaN(num);
|
||||
const fmt = (v: number) => (Number.isInteger(v) ? String(v) : String(Math.round(v * 10) / 10));
|
||||
// Profile number format like every other figure (#163) — the hint used
|
||||
// to print "1.2" next to a "1,2 bar" entity card (bug review 2026-09-04).
|
||||
const fmt = (v: number) => formatNumber(v, L, { maximumFractionDigits: 1 });
|
||||
|
||||
const parts: string[] = [];
|
||||
if (this._triggerType === "threshold") {
|
||||
@@ -2487,6 +2549,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => (this._triggerTargetChanges = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("target_changes_help", L)}</div>
|
||||
${(this._triggerTargetChanges || "1") === "1" && (this._triggerFromState || this._triggerToState)
|
||||
? html`<div class="field-help">${t("state_latch_help", L)}</div>`
|
||||
: nothing}
|
||||
<ms-textfield
|
||||
label="${t("for_at_least_minutes", L)}"
|
||||
type="number"
|
||||
@@ -2579,6 +2644,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => (this._readingUnit = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("reading_unit_help", L)}</div>
|
||||
${this._renderReadingsEditor(L)}
|
||||
`
|
||||
: nothing}
|
||||
${this._partsLoadFailed
|
||||
@@ -2659,18 +2725,6 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<option value="planned" ?selected=${this._intervalAnchor === "planned"}>${t("anchor_planned", L)}</option>
|
||||
</select>
|
||||
</div>
|
||||
${this.scheduleTimeEnabled ? html`
|
||||
<ms-date-field
|
||||
kind="time"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
label="${t("schedule_time_optional", L)}"
|
||||
.value=${this._scheduleTime}
|
||||
helper="${t("schedule_time_help", L)}"
|
||||
@value-changed=${(e: CustomEvent) => (this._scheduleTime = e.detail.value as string)}
|
||||
></ms-date-field>
|
||||
` : nothing}
|
||||
`
|
||||
: nothing}
|
||||
${this._renderCalendarFields()}
|
||||
@@ -2686,6 +2740,23 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
></ms-date-field>
|
||||
`
|
||||
: nothing}
|
||||
${this.scheduleTimeEnabled && SCHEDULE_TIME_KINDS.includes(this._scheduleType) ? html`
|
||||
<label class="checkbox-row schedule-time-toggle">
|
||||
<input type="checkbox" .checked=${this._scheduleTimeOn}
|
||||
@change=${(e: Event) => (this._scheduleTimeOn = (e.target as HTMLInputElement).checked)} />
|
||||
<span>${t("schedule_time_toggle", L)}</span>
|
||||
</label>
|
||||
${this._scheduleTimeOn ? html`
|
||||
<ms-date-field
|
||||
kind="time"
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.value=${this._scheduleTime}
|
||||
helper="${t("schedule_time_help", L)}"
|
||||
@value-changed=${(e: CustomEvent) => (this._scheduleTime = e.detail.value as string)}
|
||||
></ms-date-field>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
${this._renderRecurrenceExtras()}
|
||||
${this._renderSchedulePreview()}
|
||||
<ms-textfield
|
||||
@@ -3067,6 +3138,17 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
--mdc-icon-button-size: 36px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
/* #161 phase 2: reading slots — name wide, unit narrow, trash. */
|
||||
.readings-editor { margin-top: 14px; }
|
||||
.reading-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.reading-row .reading-name { flex: 2; min-width: 0; }
|
||||
.reading-row .reading-unit { flex: 1; min-width: 0; max-width: 140px; }
|
||||
.reading-dup { color: var(--warning-color, #ff9800); margin-top: 6px; }
|
||||
.phase-seq-label {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -3214,6 +3296,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.schedule-time-toggle {
|
||||
display: flex;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.pool-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { downloadSignedDocument, openSignedDocument } from "../helpers/document-
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { formatBytes } from "../helpers/format-bytes";
|
||||
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
|
||||
import { DOC_FILTER_MIN, filterDocuments } from "../helpers/document-filter";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface Doc {
|
||||
@@ -47,6 +48,8 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
@state() private _busy = false;
|
||||
@state() private _error = "";
|
||||
@state() private _attachId = "";
|
||||
// Local filter (#171) — shown once the linked list is long enough to need one.
|
||||
@state() private _filter = "";
|
||||
|
||||
private _loadedKey = "";
|
||||
private _localeReady = false;
|
||||
@@ -199,9 +202,21 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
<div class="task-docs">
|
||||
<h3><ha-icon icon="mdi:paperclip"></ha-icon> ${t("documents", L)} (${linked.length})</h3>
|
||||
${this._error ? html`<div class="tdoc-error">${this._error}</div>` : nothing}
|
||||
${linked.length >= DOC_FILTER_MIN
|
||||
? html`<div class="doc-filter">
|
||||
<ha-icon icon="mdi:magnify"></ha-icon>
|
||||
<input type="search" aria-label=${t("doc_search", L)} placeholder=${t("doc_search", L)}
|
||||
.value=${this._filter} @input=${(e: Event) => (this._filter = (e.target as HTMLInputElement).value)} />
|
||||
</div>`
|
||||
: nothing}
|
||||
${linked.length === 0
|
||||
? html`<div class="tdoc-empty">${t(this.partId ? "doc_part_none" : "doc_task_none", L)}</div>`
|
||||
: html`<div class="tdoc-list">${linked.map((d) => this._renderRow(d, L))}</div>`}
|
||||
: (() => {
|
||||
const shown = filterDocuments(linked, this._filter);
|
||||
return shown.length === 0
|
||||
? html`<div class="tdoc-empty">${t("doc_search_none", L)}</div>`
|
||||
: html`<div class="tdoc-list">${shown.map((d) => this._renderRow(d, L))}</div>`;
|
||||
})()}
|
||||
${this.canWrite && available.length
|
||||
? html`<div class="tdoc-attach">
|
||||
<select
|
||||
@@ -295,6 +310,14 @@ export class MaintenanceTaskDocuments extends LitElement {
|
||||
.tdoc-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 2px 0 8px; }
|
||||
.tdoc-error { color: var(--error-color, #f44336); font-size: 13px; margin: 4px 0; }
|
||||
.tdoc-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.doc-filter { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; }
|
||||
.doc-filter ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); }
|
||||
.doc-filter input {
|
||||
flex: 1; min-width: 0; font: inherit; font-size: 13px; padding: 6px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--divider-color); background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.doc-filter input:focus { outline: none; border-color: var(--primary-color); }
|
||||
.tdoc-row {
|
||||
display: flex; align-items: center; gap: 10px; padding: 6px 10px;
|
||||
border: 1px solid var(--divider-color); border-radius: 8px;
|
||||
|
||||
+48
@@ -19,6 +19,9 @@ import { describeWsError } from "../ws-errors";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { historyPhotoIds } from "../helpers/history-photos";
|
||||
import { entryReadingValues, readingSlotDelta } from "../helpers/reading-slots";
|
||||
import "./history-photo";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||
@@ -358,6 +361,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
duration: entry.duration ?? null,
|
||||
completed_by: entry.completed_by ?? null,
|
||||
used_parts: entry.used_parts ?? null,
|
||||
photo_doc_ids: historyPhotoIds(entry),
|
||||
reading_value: entry.reading_value ?? null,
|
||||
reading_values: entryReadingValues(entry),
|
||||
readings: this._task?.readings ?? [],
|
||||
task_type: this._task?.type ?? null,
|
||||
reading_unit: this._task?.reading_unit ?? null,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -439,6 +448,30 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** #161: the entry's readings — per-slot lines with the delta against the
|
||||
* previous entry carrying the slot, or the single value of a scalar task.
|
||||
* Parity with the panel timeline for the Lovelace / strategy path. */
|
||||
private _renderHistoryReadings(entry: HistoryEntry, history: HistoryEntry[], L: string) {
|
||||
const values = entryReadingValues(entry);
|
||||
const fmt = (n: number) => formatNumber(n, L, { maximumFractionDigits: 3 });
|
||||
if (values.length > 0) {
|
||||
return html`<div class="history-readings">
|
||||
${values.map((v) => {
|
||||
const d = readingSlotDelta(history, entry, v.id);
|
||||
return html`<span class="history-reading"><span class="history-reading-name">${v.name}</span>
|
||||
<span class="history-reading-value">${fmt(v.value)}${v.unit ? ` ${v.unit}` : ""}${d == null ? "" : ` (${d >= 0 ? "+" : ""}${fmt(d)})`}</span></span>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
if (entry.reading_value != null) {
|
||||
const unit = this._task?.reading_unit ? ` ${this._task.reading_unit}` : "";
|
||||
return html`<div class="history-readings"><span class="history-reading">
|
||||
<span class="history-reading-name">${t("reading_label", L)}</span>
|
||||
<span class="history-reading-value">${fmt(entry.reading_value)}${unit}</span></span></div>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
/** Read-only details panel: stats + history. Shown when the user clicks
|
||||
* "Show details" in the dialog. Edit-buttons on history entries open the
|
||||
* existing history-edit dialog (which lives in the same dialog-mount). */
|
||||
@@ -500,6 +533,15 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
${entry.notes
|
||||
? html`<div class="history-notes">${entry.notes}</div>`
|
||||
: nothing}
|
||||
${this._renderHistoryReadings(entry, history, L)}
|
||||
${(() => {
|
||||
const photos = historyPhotoIds(entry);
|
||||
return photos.length
|
||||
? html`<div class="history-photos">
|
||||
${photos.map((docId) => html`<maintenance-history-photo .hass=${this.hass} .docId=${docId}></maintenance-history-photo>`)}
|
||||
</div>`
|
||||
: nothing;
|
||||
})()}
|
||||
${entry.cost != null || entry.duration != null
|
||||
? html`<div class="history-meta">
|
||||
${entry.cost != null ? html`<span>💰 ${formatNumber(entry.cost, L, 2)}</span>` : nothing}
|
||||
@@ -830,6 +872,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
.history-edit ha-icon { --mdc-icon-size: 14px; }
|
||||
.history-notes { margin-top: 4px; color: var(--primary-text-color); }
|
||||
.history-meta { display: flex; gap: 12px; margin-top: 4px; color: var(--secondary-text-color); font-size: 11px; }
|
||||
/* #161: readings + photos on the entry (panel-timeline parity) */
|
||||
.history-readings { display: flex; flex-wrap: wrap; gap: 2px 16px; margin-top: 4px; font-size: 12px; }
|
||||
.history-reading { display: inline-flex; gap: 6px; }
|
||||
.history-reading-name { color: var(--secondary-text-color); }
|
||||
.history-reading-value { font-variant-numeric: tabular-nums; }
|
||||
.history-photos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
||||
.history-more { padding: 8px; text-align: center; font-size: 12px; color: var(--secondary-text-color); font-style: italic; }
|
||||
|
||||
/* Adaptive section — wraps the panel renderers (which assume sharedStyles
|
||||
|
||||
@@ -46,6 +46,7 @@ import type { MaintenanceObjectQuickActionsDialog } from "./components/object-qu
|
||||
import type { HomeAssistant, MaintenanceObject } from "./types";
|
||||
import { ensureLocale, isLocaleLoaded, langOf, setProfilePrefs } from "./styles";
|
||||
import { fillAndOpenCompleteDialog, type CompleteDialogArgs } from "./helpers/complete-dialog-args";
|
||||
import { fetchSettingsOnce, invalidateSettingsCache } from "./helpers/settings-cache";
|
||||
|
||||
const OBJECT_DIALOG_TAG = "maintenance-object-dialog";
|
||||
const TASK_DIALOG_TAG = "maintenance-task-dialog";
|
||||
@@ -106,40 +107,9 @@ function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Settings cache used to populate task-dialog's feature-gated sections
|
||||
* (checklists / schedule_time / completion_actions) AND its
|
||||
* defaultWarningDays prop when the dialog is mounted from Lovelace.
|
||||
*
|
||||
* Without this, every section that's gated on a feature flag stays
|
||||
* hidden (default false) — that's the "ich habe nicht alles wie im
|
||||
* Panel gefunden" symptom. The panel reads the same settings object
|
||||
* via maintenance-panel.ts and passes it through as element properties.
|
||||
*
|
||||
* Cache lifetime: page session. Invalidated on full HA reload (which
|
||||
* also drops our custom-element registry). The settings WS itself is
|
||||
* cheap (~10ms) so even uncached this isn't a hot path concern, but
|
||||
* caching means re-opening the dialog feels instant.
|
||||
*/
|
||||
interface SettingsCache {
|
||||
features: {
|
||||
adaptive: boolean; predictions: boolean; seasonal: boolean;
|
||||
environmental: boolean; budget: boolean; groups: boolean;
|
||||
checklists: boolean; schedule_time: boolean; completion_actions: boolean;
|
||||
};
|
||||
defaultWarningDays: number;
|
||||
/** #145: global "Task row actions" style (buttons_compact | buttons | icons). */
|
||||
rowActionStyle: string;
|
||||
}
|
||||
|
||||
const FALLBACK_SETTINGS: SettingsCache = {
|
||||
features: {
|
||||
adaptive: false, predictions: false, seasonal: false,
|
||||
environmental: false, budget: false, groups: false,
|
||||
checklists: false, schedule_time: false, completion_actions: false,
|
||||
},
|
||||
defaultWarningDays: 7,
|
||||
rowActionStyle: "buttons_compact",
|
||||
};
|
||||
/** The settings the Lovelace-mounted dialogs read (feature-gated sections,
|
||||
* default warning days): see helpers/settings-cache.ts — one fetch per
|
||||
* page, shared across bundles, invalidated by whoever writes global/update. */
|
||||
|
||||
/** The household's row-action style for surfaces outside the panel (the
|
||||
* Lovelace card follows it unless its own `action_style` is set). */
|
||||
@@ -147,28 +117,10 @@ export function getRowActionStyle(hass: HomeAssistant): Promise<string> {
|
||||
return fetchSettingsOnce(hass).then((s) => s.rowActionStyle);
|
||||
}
|
||||
|
||||
/** Test hook: the settings cache is module-wide (one fetch per page), which
|
||||
/** Test hook: the settings cache is page-wide (one fetch per page), which
|
||||
* makes component tests with DIFFERENT settings share the first answer. */
|
||||
export function __resetSettingsCacheForTests(): void {
|
||||
_cachedSettings = null;
|
||||
}
|
||||
|
||||
let _cachedSettings: Promise<SettingsCache> | null = null;
|
||||
|
||||
function fetchSettingsOnce(hass: HomeAssistant): Promise<SettingsCache> {
|
||||
if (_cachedSettings) return _cachedSettings;
|
||||
_cachedSettings = hass.connection
|
||||
.sendMessagePromise<{
|
||||
features?: SettingsCache["features"];
|
||||
general?: { default_warning_days?: number; row_action_style?: string };
|
||||
}>({ type: "maintenance_supporter/settings" })
|
||||
.then((r) => ({
|
||||
features: r.features ?? FALLBACK_SETTINGS.features,
|
||||
defaultWarningDays: r.general?.default_warning_days ?? 7,
|
||||
rowActionStyle: r.general?.row_action_style ?? FALLBACK_SETTINGS.rowActionStyle,
|
||||
}))
|
||||
.catch(() => FALLBACK_SETTINGS);
|
||||
return _cachedSettings;
|
||||
invalidateSettingsCache();
|
||||
}
|
||||
|
||||
export function openCreateObjectDialog(): boolean {
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface ObjectColumnDef {
|
||||
/** Canonical order + label mapping. Most labels reuse existing i18n keys. */
|
||||
export const OBJECT_COLUMNS: ObjectColumnDef[] = [
|
||||
{ key: "name", labelKey: "name", required: true },
|
||||
// #170: the object's reference number — off by default, one click away.
|
||||
{ key: "ref_no", labelKey: "ref_number" },
|
||||
{ key: "manufacturer", labelKey: "manufacturer" },
|
||||
{ key: "model", labelKey: "model" },
|
||||
{ key: "serial_number", labelKey: "serial_number_label" },
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
|
||||
export const LS_KEYS = {
|
||||
/** #170: service-booklet print options (layout + include switches). */
|
||||
printOptions: "msp-print-options",
|
||||
overviewTab: "msp-overview-tab",
|
||||
collapsedSections: "msp-collapsed-sections",
|
||||
chartRange: "msp-chart-range",
|
||||
|
||||
@@ -61,6 +61,8 @@ export function buildTaskWorksheetHtml(
|
||||
excerpt: WorksheetExcerpt | null,
|
||||
nowIso: string,
|
||||
partsLines: string[] = [],
|
||||
/** #170: the task's reference ("8.3") — printed next to the title. */
|
||||
taskRef: string | null = null,
|
||||
): string {
|
||||
const meta: Array<[string, string]> = [
|
||||
[L.object, esc(objectName)],
|
||||
@@ -97,6 +99,7 @@ export function buildTaskWorksheetHtml(
|
||||
header { display: flex; justify-content: space-between; align-items: flex-start;
|
||||
border-bottom: 3px solid #111; padding-bottom: 8px; margin-bottom: 12px; }
|
||||
h1 { font-size: 22px; margin: 0 0 2px; }
|
||||
h1 .ref { font-size: 12px; font-weight: 500; color: #555; border: 1px solid #ccc; border-radius: 5px; padding: 1px 6px; vertical-align: middle; font-variant-numeric: tabular-nums; }
|
||||
.obj { font-size: 14px; color: #444; }
|
||||
.qr-row { display: flex; gap: 18px; }
|
||||
.qr { margin: 0; text-align: center; }
|
||||
@@ -123,7 +126,7 @@ export function buildTaskWorksheetHtml(
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>${esc(task.name)}</h1>
|
||||
<h1>${esc(task.name)}${taskRef ? ` <span class="ref">#${esc(taskRef)}</span>` : ""}</h1>
|
||||
<div class="obj">${esc(objectName)}</div>
|
||||
</div>
|
||||
<div class="qr-row">
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: neplatná hodnota",
|
||||
"feat_schedule_time": "Plánování podle denní doby",
|
||||
"feat_schedule_time_desc": "Úkoly se stanou po termínu v určenou denní dobu místo o půlnoci.",
|
||||
"schedule_time_optional": "Termín v čase (volitelné)",
|
||||
"schedule_time_toggle": "Termín v určitou hodinu",
|
||||
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
|
||||
"at_time": "v",
|
||||
"notes_optional": "Poznámky (volitelné)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Žádný tag —",
|
||||
"dashboard": "Přehled",
|
||||
"tab_today": "Dnes",
|
||||
"palette_placeholder": "Hledat objekty a úkoly…",
|
||||
"palette_placeholder": "Hledat objekty, úkoly, díly, dokumenty, poznámky…",
|
||||
"palette_no_results": "Žádné výsledky",
|
||||
"palette_hint": "↑↓ pohyb · Enter otevřít · Esc zavřít",
|
||||
"palette_hint": "↑↓ navigace · Enter otevřít · Esc zavřít · / otevře hledání kdekoli",
|
||||
"search_open": "Hledat",
|
||||
"search_group_parts": "Náhradní díly",
|
||||
"search_group_content": "V dokumentech",
|
||||
"search_group_history": "Poznámky v historii",
|
||||
"search_page": "Strana {page}",
|
||||
"search_empty_hint": "Pište pro hledání v objektech, úkolech, dílech, dokumentech a poznámkách",
|
||||
"search_searching": "Prohledávání dokumentů a poznámek…",
|
||||
"search_index_status": "Fulltextové hledání: {indexed} z {total} souborů indexováno · {no_text} bez textové vrstvy · {pending} čeká",
|
||||
"today_all_caught_up": "Vše hotovo! Tento týden nic nezbývá.",
|
||||
"today_overdue": "Po termínu",
|
||||
"today_due_today": "Dnes",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Práh „spotřební materiál nízký“ (%)",
|
||||
"settings_battery_low_percent": "Práh „baterie slabá“ (%)",
|
||||
"settings_thresholds_hint": "Výchozí hodnoty pro nová Navrhovaná nastavení (zbývající procenta) a pro baterie flotily bez vlastního prahu Battery Notes. Stávající úkoly si ponechají svou hodnotu.",
|
||||
"settings_value_out_of_range": "Hodnota musí být mezi {min} a {max}",
|
||||
"bn_summary": "{name}: {pct} % pro {n} baterií",
|
||||
"bn_floor_decides": "Tvůj práh {floor} % rozhoduje pro všechny; u každé baterie platí vyšší z obou prahů.",
|
||||
"bn_above_floor": "To je nad tvým prahem {floor} %, takže o všech poznamenaných bateriích rozhoduje {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testovací oznámení odesláno",
|
||||
"test_notification_failed": "Testovací oznámení se nezdařilo",
|
||||
"notify_per_person": "Doručování podle osoby",
|
||||
"member_avatars": "Avatary členů",
|
||||
"member_avatars_hint": "Zobrazuje se u přiřazených úkolů — iniciály v barvě člena. Výchozí hodnota vychází ze jména; nastavte vlastní iniciály nebo vyberte barvu pro rozlišení členů.",
|
||||
"member_initials": "Iniciály",
|
||||
"member_color": "Barva",
|
||||
"member_avatar_reset": "Obnovit",
|
||||
"notify_no_own_device": "Žádné vlastní zařízení — použije domácí službu",
|
||||
"settings_notify_due_soon": "Oznámit když brzy",
|
||||
"settings_notify_overdue": "Oznámit když po termínu",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Seskupit podle",
|
||||
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
|
||||
"target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).",
|
||||
"state_latch_help": "Při 1 přechodu je spouštěč západka: obnoví se, když entita opustí cílový stav — nebo, je-li nastaven jen výchozí stav, když se do něj vrátí.",
|
||||
"for_minutes_state_help": "0 počítá každou změnu ihned. S minutami musí nový stav nejprve vydržet — krátké bliknutí pak nespustí ani se nepočítá.",
|
||||
"qr_print_title": "Tisk QR kódů",
|
||||
"qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Smazat „{name}“?",
|
||||
"doc_too_large": "Soubor je příliš velký (max. 25 MB).",
|
||||
"doc_upload_failed": "Nahrání selhalo.",
|
||||
"completion_photo_optional": "Fotka dokončení (volitelné)",
|
||||
"add_photo": "Přidat fotku",
|
||||
"completion_photos_optional": "Fotky z dokončení (volitelné)",
|
||||
"completion_photos": "Fotky z dokončení",
|
||||
"choose_photos": "Vybrat fotky",
|
||||
"choose_photo": "Vybrat fotku",
|
||||
"photos_android_hint": "Aplikace pro Android předá jednu fotku na výběr — každý výběr se přidá.",
|
||||
"add_photos": "Přidat fotky",
|
||||
"photos_limit": "Nejvýše {max} fotek na dokončení",
|
||||
"history_edit_photos_hint": "Zde odebraná fotka zůstává v dokumentech objektu.",
|
||||
"uploading": "Nahrávání…",
|
||||
"remove": "Odebrat",
|
||||
"doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Zobrazí se vedle zaznamenané hodnoty při dokončení tohoto úkolu.",
|
||||
"reading_value_label": "Hodnota odečtu",
|
||||
"reading_label": "Odečet",
|
||||
"readings_section": "Odečty",
|
||||
"readings_hint": "Několik pojmenovaných hodnot na dokončení — jeden řádek na měřič. Nechte prázdné pro jedinou hodnotu.",
|
||||
"reading_name_label": "Název odečtu",
|
||||
"reading_unit_short": "Jednotka",
|
||||
"reading_add": "Přidat odečet",
|
||||
"reading_duplicate_name": "Název je již použit",
|
||||
"reading_last": "naposledy: {value}",
|
||||
"reading_below_last": "Nižší než poslední odečet ({value})",
|
||||
"settings_templates_label": "Galerie šablon",
|
||||
"settings_templates_hint": "Odškrtněte šablony, které nikdy nevyužijete — zmizí z výběru „Ze šablony\" (panel i config flow). Nic jiného se nemění; kdykoli je lze znovu zapnout.",
|
||||
"worksheet": "Pracovní list",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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í.",
|
||||
"settings_notify_rule": "Vlastní pravidlo oznámení",
|
||||
"settings_notify_rule_hint": "Každé oznámení navíc vyvolá událost maintenance_supporter_notification (druh, stav, objekt, úkol, referenční čísla, priorita, odkaz, data). Nasměrujte ji automatizací — např. do Tickeru, Telegramu nebo Pushoveru.",
|
||||
"settings_notify_event_only": "Jen událost, nic neposílat",
|
||||
"settings_notify_event_only_hint": "Doručení zajišťuje vaše automatizace. Intervaly opakování, tiché hodiny a denní limit dál rozhodují, kdy se událost vyvolá.",
|
||||
"settings_notify_extra_data": "Další data oznámení (šablona)",
|
||||
"settings_notify_extra_data_hint": "JSON nebo YAML, Jinja povoleno; sloučí se do data volání notify (vaše klíče mají přednost). Proměnné: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Ověřte tlačítkem Odeslat test.",
|
||||
"card_saved_view": "Uložené zobrazení",
|
||||
"card_saved_view_none": "Žádné",
|
||||
"card_saved_view_help": "Použije filtry stavu, uživatele a štítku zobrazení navíc k výše uvedeným filtrům. Řazení a seskupení zobrazení jsou nastavení panelu a na kartě se nepoužijí.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Označit vše jako vyměněné",
|
||||
"battery_fleet_mark_one": "Označit tuto baterii jako vyměněnou",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Bez senzoru",
|
||||
"battery_fleet_trigger_lost": "Senzorový spouštěč této úlohy byl ztracen — nespustí se ani se automaticky nedokončí.",
|
||||
"battery_fleet_repair": "Opravit",
|
||||
"battery_fleet_exclude": "Vyloučit z flotily",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Vyberte senzor baterie, který automatická detekce přehlédla — okamžitě se objeví v seznamu.",
|
||||
"battery_fleet_track_self": "Sledovat samodobíjecí baterie",
|
||||
"battery_fleet_track_self_hint": "Telefony, vysavače a další samodobíjecí zařízení se zobrazují jako akumulátory — při nízkém stavu žádají o nabití, nikdy o nové články.",
|
||||
"battery_fleet_due_without_sensor": "Prošlou předpověď považovat za termín výměny, pokud baterie nemá senzor",
|
||||
"battery_fleet_due_without_sensor_hint": "Některá zařízení vůbec nehlásí stav baterie — Battery Notes zná jen typ a poslední výměnu. Po uplynutí typické životnosti se taková baterie počítá jako vybitá a úkol flotily vás upozorní; označení jako vyměněné předpověď vynuluje.",
|
||||
"battery_fleet_status_low": "Nízká",
|
||||
"battery_fleet_status_soon": "Brzy",
|
||||
"battery_fleet_status_ok": "V pořádku",
|
||||
"battery_fleet_status_due": "K výměně",
|
||||
"battery_fleet_predicted_on": "Očekáváno kolem {date}",
|
||||
"battery_fleet_predicted_trend": "Předpověď z trendu vybíjení této baterie: přibližně {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulátor: nabíjí se místo výměny — nikdy na nákupním seznamu",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servisní knížka (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "záznamů",
|
||||
"ref_number": "Referenční číslo",
|
||||
"print_options_title": "Tisk servisní knížky",
|
||||
"print_layout": "Rozvržení",
|
||||
"print_layout_chronological": "Chronologicky",
|
||||
"print_layout_by_task": "Podle úkolu",
|
||||
"print_include": "Zahrnout",
|
||||
"print_inc_readings": "Odečty",
|
||||
"print_inc_parts": "Použité díly",
|
||||
"print_inc_photos": "Fotky",
|
||||
"print_inc_documents": "Propojené dokumenty",
|
||||
"print_inc_checklist": "Kontrolní seznam",
|
||||
"print_inc_notes": "Poznámky",
|
||||
"print_inc_costs": "Náklady a doba",
|
||||
"print_inc_person": "Provedl",
|
||||
"print_inc_refs": "Referenční čísla",
|
||||
"print_inc_qr": "QR kód pro každý úkol",
|
||||
"print_button": "Tisk",
|
||||
"report_scan_hint": "Naskenujte pro otevření úkolu",
|
||||
"completed_by": "Dokončil",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ugyldig værdi",
|
||||
"feat_schedule_time": "Planlægning efter tidspunkt på dagen",
|
||||
"feat_schedule_time_desc": "Opgaver bliver forfaldne på et bestemt tidspunkt på dagen i stedet for midnat.",
|
||||
"schedule_time_optional": "Forfalder kl. (valgfrit)",
|
||||
"schedule_time_toggle": "Forfalder på et bestemt tidspunkt",
|
||||
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Noter (valgfrit)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Intet tag —",
|
||||
"dashboard": "Oversigt",
|
||||
"tab_today": "I dag",
|
||||
"palette_placeholder": "Søg objekter og opgaver…",
|
||||
"palette_placeholder": "Søg objekter, opgaver, reservedele, dokumenter, noter…",
|
||||
"palette_no_results": "Ingen match",
|
||||
"palette_hint": "↑↓ naviger · Enter åbn · Esc luk",
|
||||
"palette_hint": "↑↓ naviger · Enter åbn · Esc luk · / åbner søgningen overalt",
|
||||
"search_open": "Søg",
|
||||
"search_group_parts": "Reservedele",
|
||||
"search_group_content": "I dokumenter",
|
||||
"search_group_history": "Historiknoter",
|
||||
"search_page": "Side {page}",
|
||||
"search_empty_hint": "Skriv for at søge i objekter, opgaver, reservedele, dokumenter og noter",
|
||||
"search_searching": "Søger i dokumenter og noter…",
|
||||
"search_index_status": "Fuldtekstsøgning: {indexed} af {total} filer indekseret · {no_text} uden tekstlag · {pending} afventer",
|
||||
"today_all_caught_up": "Alt er klaret! Intet forfalder i denne uge.",
|
||||
"today_overdue": "Forfaldne",
|
||||
"today_due_today": "Forfalder i dag",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Tærskel „forbrugsvare lav“ (%)",
|
||||
"settings_battery_low_percent": "Tærskel „batteri lavt“ (%)",
|
||||
"settings_thresholds_hint": "Standardværdier for nye Foreslåede opsætninger (resterende procent) og for flådebatterier uden egen Battery Notes-tærskel. Eksisterende opgaver beholder deres værdi.",
|
||||
"settings_value_out_of_range": "Værdien skal være mellem {min} og {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batterier",
|
||||
"bn_floor_decides": "Din tærskel på {floor} % afgør for alle; pr. batteri gælder den højeste af de to tærskler.",
|
||||
"bn_above_floor": "Det er over din tærskel på {floor} %, så {name} afgør for alle noterede batterier.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testnotifikation sendt",
|
||||
"test_notification_failed": "Testnotifikation mislykkedes",
|
||||
"notify_per_person": "Levering pr. person",
|
||||
"member_avatars": "Medlemmernes avatarer",
|
||||
"member_avatars_hint": "Vises ved siden af tildelte opgaver — initialer i medlemmets farve. Standard kommer fra navnet; angiv egne initialer eller vælg en farve for at skelne medlemmer.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Farve",
|
||||
"member_avatar_reset": "Nulstil",
|
||||
"notify_no_own_device": "Ingen egen enhed — bruger husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Notificer ved snart forfalden",
|
||||
"settings_notify_overdue": "Notificer ved forfalden",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Grupper efter",
|
||||
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).",
|
||||
"state_latch_help": "Med 1 overgang er triggeren en lås: den nulstilles, når enheden forlader Til-tilstanden — eller, hvis kun en Fra-tilstand er sat, når den vender tilbage til den.",
|
||||
"for_minutes_state_help": "0 tæller hver ændring straks. Med minutter skal den nye tilstand først holde — korte blink hverken udløser eller tælles så.",
|
||||
"qr_print_title": "Udskriv QR-koder",
|
||||
"qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Slet \"{name}\"?",
|
||||
"doc_too_large": "Filen er for stor (maks. 25 MB).",
|
||||
"doc_upload_failed": "Upload mislykkedes.",
|
||||
"completion_photo_optional": "Fuldførelsesfoto (valgfrit)",
|
||||
"add_photo": "Tilføj foto",
|
||||
"completion_photos_optional": "Fotos ved fuldførelse (valgfrit)",
|
||||
"completion_photos": "Fotos ved fuldførelse",
|
||||
"choose_photos": "Vælg fotos",
|
||||
"choose_photo": "Vælg foto",
|
||||
"photos_android_hint": "Android-appen afleverer ét foto pr. valg — hvert valg tilføjes.",
|
||||
"add_photos": "Tilføj fotos",
|
||||
"photos_limit": "Højst {max} fotos pr. fuldførelse",
|
||||
"history_edit_photos_hint": "Et foto, der fjernes her, bliver i objektets dokumenter.",
|
||||
"uploading": "Uploader…",
|
||||
"remove": "Fjern",
|
||||
"doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Vises ved siden af den registrerede værdi, når opgaven fuldføres.",
|
||||
"reading_value_label": "Aflæst værdi",
|
||||
"reading_label": "Aflæsning",
|
||||
"readings_section": "Aflæsninger",
|
||||
"readings_hint": "Flere navngivne værdier pr. fuldførelse — én række pr. måler. Lad stå tomt for en enkelt værdi.",
|
||||
"reading_name_label": "Aflæsningens navn",
|
||||
"reading_unit_short": "Enhed",
|
||||
"reading_add": "Tilføj aflæsning",
|
||||
"reading_duplicate_name": "Navnet er allerede i brug",
|
||||
"reading_last": "sidst: {value}",
|
||||
"reading_below_last": "Lavere end sidste aflæsning ({value})",
|
||||
"settings_templates_label": "Skabelongalleri",
|
||||
"settings_templates_hint": "Fravælg skabeloner, du aldrig får brug for — de forsvinder fra \"Fra skabelon\"-vælgerne (panel og config flow). Intet andet ændres; kan genaktiveres når som helst.",
|
||||
"worksheet": "Arbejdsark",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen notifikationsregel",
|
||||
"settings_notify_rule_hint": "Hver notifikation udløser også hændelsen maintenance_supporter_notification (type, status, objekt, opgave, referencenumre, prioritet, link, nyttelast). Videresend den med en automatisering — fx til Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Udløs kun hændelsen, send intet selv",
|
||||
"settings_notify_event_only_hint": "Din automatisering står for hele leveringen. Gentagelsesintervaller, stille timer og dagsloftet gælder stadig for, hvornår hændelsen udløses.",
|
||||
"settings_notify_extra_data": "Ekstra notifikationsdata (skabelon)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tilladt; flettes ind i notify-kaldets data (dine nøgler vinder). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test med knappen Send test.",
|
||||
"card_saved_view": "Gemt visning",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Anvender visningens status-, bruger- og etiketfiltre oven på filtrene ovenfor. Visningens sortering og gruppering er panelvisningsindstillinger og anvendes ikke på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marker alle skiftet",
|
||||
"battery_fleet_mark_one": "Marker dette batteri skiftet",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Opgavens sensorudløser er gået tabt — den udløses ikke og fuldføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparér",
|
||||
"battery_fleet_exclude": "Udeluk fra flåden",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Vælg en batterisensor, som den automatiske registrering overså — den kommer straks på listen.",
|
||||
"battery_fleet_track_self": "Spor selvopladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvopladende enheder vises som genopladelige — ved lavt niveau bedes om opladning, aldrig om nye batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandl en overskredet prognose som forfalden, når et batteri ikke har en sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Nogle enheder rapporterer slet ikke batteriniveau — Battery Notes kender kun typen og seneste udskiftning. Når den typiske levetid er passeret, tæller sådan et batteri som lavt, og flådeopgaven minder dig om det; at markere det som udskiftet nulstiller prognosen.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_status_due": "Forfalden",
|
||||
"battery_fleet_predicted_on": "Forventes omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Forudsagt ud fra batteriets afladningstendens: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Genopladeligt: oplad i stedet for at udskifte — aldrig på indkøbslisten",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicebog (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "poster",
|
||||
"ref_number": "Referencenummer",
|
||||
"print_options_title": "Udskriv servicebog",
|
||||
"print_layout": "Opbygning",
|
||||
"print_layout_chronological": "Kronologisk",
|
||||
"print_layout_by_task": "Efter opgave",
|
||||
"print_include": "Medtag",
|
||||
"print_inc_readings": "Aflæsninger",
|
||||
"print_inc_parts": "Brugte dele",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Tilknyttede dokumenter",
|
||||
"print_inc_checklist": "Tjekliste",
|
||||
"print_inc_notes": "Noter",
|
||||
"print_inc_costs": "Omkostning og varighed",
|
||||
"print_inc_person": "Udført af",
|
||||
"print_inc_refs": "Referencenumre",
|
||||
"print_inc_qr": "QR-kode pr. opgave",
|
||||
"print_button": "Udskriv",
|
||||
"report_scan_hint": "Scan for at åbne opgaven",
|
||||
"completed_by": "Udført af",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ungültiger Wert",
|
||||
"feat_schedule_time": "Uhrzeit-Scheduling",
|
||||
"feat_schedule_time_desc": "Tasks werden zu einer festen Uhrzeit fällig statt um Mitternacht.",
|
||||
"schedule_time_optional": "Fällig um (optional)",
|
||||
"schedule_time_toggle": "Zu einer bestimmten Uhrzeit fällig",
|
||||
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
|
||||
"at_time": "um",
|
||||
"notes_optional": "Notizen (optional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Kein Tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Heute",
|
||||
"palette_placeholder": "Objekte und Aufgaben suchen…",
|
||||
"palette_placeholder": "Objekte, Aufgaben, Ersatzteile, Dokumente, Notizen suchen…",
|
||||
"palette_no_results": "Keine Treffer",
|
||||
"palette_hint": "↑↓ Navigieren · Enter Öffnen · Esc Schließen",
|
||||
"palette_hint": "↑↓ navigieren · Enter öffnen · Esc schließen · / öffnet die Suche überall",
|
||||
"search_open": "Suchen",
|
||||
"search_group_parts": "Ersatzteile",
|
||||
"search_group_content": "In Dokumenten",
|
||||
"search_group_history": "Verlaufsnotizen",
|
||||
"search_page": "Seite {page}",
|
||||
"search_empty_hint": "Tippen, um Objekte, Aufgaben, Ersatzteile, Dokumente und Notizen zu durchsuchen",
|
||||
"search_searching": "Dokumente und Notizen werden durchsucht…",
|
||||
"search_index_status": "Volltextsuche: {indexed} von {total} Dateien indiziert · {no_text} ohne Textebene · {pending} ausstehend",
|
||||
"today_all_caught_up": "Alles erledigt! Diese Woche steht nichts an.",
|
||||
"today_overdue": "Überfällig",
|
||||
"today_due_today": "Heute fällig",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Schwelle „Verbrauchsmaterial niedrig“ (%)",
|
||||
"settings_battery_low_percent": "Schwelle „Batterie niedrig“ (%)",
|
||||
"settings_thresholds_hint": "Vorgaben für neue Vorgeschlagene Einrichtungen (Prozent-Restwert) und für Flotten-Batterien ohne eigene Battery-Notes-Schwelle. Bestehende Aufgaben behalten ihren Wert.",
|
||||
"settings_value_out_of_range": "Der Wert muss zwischen {min} und {max} liegen",
|
||||
"bn_summary": "{name}: {pct} % für {n} Batterien",
|
||||
"bn_floor_decides": "Deine {floor}-%-Schwelle entscheidet für alle; pro Batterie zählt die höhere der beiden Schwellen.",
|
||||
"bn_above_floor": "Das liegt über deiner {floor}-%-Schwelle — für alle notierten Batterien entscheidet {name}.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test-Benachrichtigung gesendet",
|
||||
"test_notification_failed": "Test-Benachrichtigung fehlgeschlagen",
|
||||
"notify_per_person": "Zustellung pro Person",
|
||||
"member_avatars": "Avatare der Mitglieder",
|
||||
"member_avatars_hint": "Wird neben zugewiesenen Aufgaben angezeigt — Initialen in der Farbe des Mitglieds. Standard kommt aus dem Namen; eigene Initialen oder eine Farbe wählen, um Mitglieder zu unterscheiden.",
|
||||
"member_initials": "Initialen",
|
||||
"member_color": "Farbe",
|
||||
"member_avatar_reset": "Zurücksetzen",
|
||||
"notify_no_own_device": "Kein eigenes Gerät — nutzt den Haushaltsdienst",
|
||||
"settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen",
|
||||
"settings_notify_overdue": "Bei Überfälligkeit benachrichtigen",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Gruppieren nach",
|
||||
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
|
||||
"target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).",
|
||||
"state_latch_help": "Bei 1 Übergang ist der Trigger ein Latch: Er erholt sich, wenn die Entität den Nach-Zustand verlässt — oder, wenn nur ein Von-Zustand gesetzt ist, sobald sie dorthin zurückkehrt.",
|
||||
"for_minutes_state_help": "0 zählt jede Änderung sofort. Mit Minuten muss der neue Zustand erst so lange bestehen — kurzes Flackern löst dann weder aus noch zählt es.",
|
||||
"qr_print_title": "QR-Codes drucken",
|
||||
"qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "„{name}“ löschen?",
|
||||
"doc_too_large": "Datei ist zu groß (max. 25 MB).",
|
||||
"doc_upload_failed": "Upload fehlgeschlagen.",
|
||||
"completion_photo_optional": "Abschlussfoto (optional)",
|
||||
"add_photo": "Foto hinzufügen",
|
||||
"completion_photos_optional": "Abschlussfotos (optional)",
|
||||
"completion_photos": "Abschlussfotos",
|
||||
"choose_photos": "Fotos auswählen",
|
||||
"choose_photo": "Foto wählen",
|
||||
"photos_android_hint": "Die Android-App übergibt pro Auswahl ein Foto — jede Auswahl wird hinzugefügt.",
|
||||
"add_photos": "Fotos hinzufügen",
|
||||
"photos_limit": "Bis zu {max} Fotos pro Abschluss",
|
||||
"history_edit_photos_hint": "Ein hier entferntes Foto bleibt in den Dokumenten des Objekts erhalten.",
|
||||
"uploading": "Wird hochgeladen…",
|
||||
"remove": "Entfernen",
|
||||
"doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wird beim Erledigen dieser Aufgabe neben dem erfassten Wert angezeigt.",
|
||||
"reading_value_label": "Ablesewert",
|
||||
"reading_label": "Ablesung",
|
||||
"readings_section": "Ablesewerte",
|
||||
"readings_hint": "Mehrere benannte Werte je Abschluss — eine Zeile pro Zähler. Leer lassen für einen einzelnen Wert.",
|
||||
"reading_name_label": "Name der Ablesung",
|
||||
"reading_unit_short": "Einheit",
|
||||
"reading_add": "Ablesung hinzufügen",
|
||||
"reading_duplicate_name": "Name bereits vergeben",
|
||||
"reading_last": "zuletzt: {value}",
|
||||
"reading_below_last": "Kleiner als der letzte Wert ({value})",
|
||||
"settings_templates_label": "Vorlagen-Galerie",
|
||||
"settings_templates_hint": "Vorlagen abwählen, die du nie brauchst — sie verschwinden aus den \"Aus Vorlage\"-Auswahlen (Panel und Config-Flow). Sonst ändert sich nichts; jederzeit wieder aktivierbar.",
|
||||
"worksheet": "Arbeitsblatt",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Eigene Benachrichtigungsregel",
|
||||
"settings_notify_rule_hint": "Jede Benachrichtigung löst zusätzlich das Ereignis maintenance_supporter_notification aus (Art, Status, Objekt, Aufgabe, Referenznummern, Priorität, Deep-Link, Nutzlast). Leite es per Automation weiter — z. B. an Ticker, Telegram oder Pushover.",
|
||||
"settings_notify_event_only": "Nur das Ereignis auslösen, selbst nichts senden",
|
||||
"settings_notify_event_only_hint": "Deine Automation übernimmt den kompletten Versand. Wiederholintervalle, Ruhezeiten und Tageslimit gelten weiterhin dafür, wann das Ereignis ausgelöst wird.",
|
||||
"settings_notify_extra_data": "Zusätzliche Benachrichtigungsdaten (Template)",
|
||||
"settings_notify_extra_data_hint": "JSON oder YAML, Jinja erlaubt; wird in die data des Notify-Aufrufs gemischt (deine Schlüssel gewinnen). Variablen: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Prüfen mit „Test senden“.",
|
||||
"card_saved_view": "Gespeicherte Ansicht",
|
||||
"card_saved_view_none": "Keine",
|
||||
"card_saved_view_help": "Wendet die Status-, Nutzer- und Label-Filter der Ansicht zusätzlich zu den obigen Filtern an. Sortierung und Gruppierung der Ansicht sind Panel-Darstellung und werden auf der Karte nicht angewendet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Alle als getauscht markieren",
|
||||
"battery_fleet_mark_one": "Diese Batterie als getauscht markieren",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Kein Sensor",
|
||||
"battery_fleet_trigger_lost": "Der Sensor-Trigger dieser Aufgabe ist verloren gegangen — sie wird nicht ausgelöst oder automatisch abgeschlossen.",
|
||||
"battery_fleet_repair": "Reparieren",
|
||||
"battery_fleet_exclude": "Aus der Flotte ausschließen",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Einen Batteriesensor wählen, den die automatische Erkennung übersehen hat — er erscheint sofort in der Liste.",
|
||||
"battery_fleet_track_self": "Selbstladende Batterien mitverfolgen",
|
||||
"battery_fleet_track_self_hint": "Telefone, Staubsauger und andere selbstladende Geräte erscheinen als Akkus — bei niedrigem Stand wird ans Laden erinnert, nie an neue Zellen.",
|
||||
"battery_fleet_due_without_sensor": "Abgelaufene Prognose als fällig werten, wenn eine Batterie keinen Sensor hat",
|
||||
"battery_fleet_due_without_sensor_hint": "Manche Geräte melden gar keinen Batteriestand — Battery Notes kennt nur Typ und letzten Wechsel. Ist die typische Lebensdauer verstrichen, gilt so eine Batterie als leer und die Flotten-Aufgabe erinnert dich; als ersetzt markieren setzt die Prognose zurück.",
|
||||
"battery_fleet_status_low": "Schwach",
|
||||
"battery_fleet_status_soon": "Bald",
|
||||
"battery_fleet_status_ok": "In Ordnung",
|
||||
"battery_fleet_status_due": "Fällig",
|
||||
"battery_fleet_predicted_on": "Voraussichtlich um {date}",
|
||||
"battery_fleet_predicted_trend": "Aus dem Entladetrend dieser Batterie vorhergesagt: etwa {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Wiederaufladbar: laden statt ersetzen — nie auf der Einkaufsliste",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Serviceheft (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "Einträge",
|
||||
"ref_number": "Referenznummer",
|
||||
"print_options_title": "Serviceheft drucken",
|
||||
"print_layout": "Aufbau",
|
||||
"print_layout_chronological": "Chronologisch",
|
||||
"print_layout_by_task": "Nach Aufgabe",
|
||||
"print_include": "Enthalten",
|
||||
"print_inc_readings": "Zählerstände",
|
||||
"print_inc_parts": "Verbrauchte Teile",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Verknüpfte Dokumente",
|
||||
"print_inc_checklist": "Checkliste",
|
||||
"print_inc_notes": "Notizen",
|
||||
"print_inc_costs": "Kosten und Dauer",
|
||||
"print_inc_person": "Erledigt von",
|
||||
"print_inc_refs": "Referenznummern",
|
||||
"print_inc_qr": "QR-Code je Aufgabe",
|
||||
"print_button": "Drucken",
|
||||
"report_scan_hint": "Scannen öffnet die Aufgabe",
|
||||
"completed_by": "Erledigt von",
|
||||
"date_from": "Von",
|
||||
"date_to": "Bis",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: invalid value",
|
||||
"feat_schedule_time": "Time-of-day scheduling",
|
||||
"feat_schedule_time_desc": "Tasks become overdue at a specific time of day instead of midnight.",
|
||||
"schedule_time_optional": "Due at time (optional)",
|
||||
"schedule_time_toggle": "Due at a specific time",
|
||||
"schedule_time_help": "Empty = midnight (default). HA timezone.",
|
||||
"at_time": "at",
|
||||
"notes_optional": "Notes (optional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— No tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Today",
|
||||
"palette_placeholder": "Search objects and tasks…",
|
||||
"palette_placeholder": "Search objects, tasks, spare parts, documents, notes…",
|
||||
"palette_no_results": "No matches",
|
||||
"palette_hint": "↑↓ to navigate · Enter to open · Esc to close",
|
||||
"palette_hint": "↑↓ to navigate · Enter to open · Esc to close · / opens the search anywhere",
|
||||
"search_open": "Search",
|
||||
"search_group_parts": "Spare parts",
|
||||
"search_group_content": "In documents",
|
||||
"search_group_history": "History notes",
|
||||
"search_page": "Page {page}",
|
||||
"search_empty_hint": "Type to search objects, tasks, spare parts, documents and notes",
|
||||
"search_searching": "Searching documents and notes…",
|
||||
"search_index_status": "Full-text search: {indexed} of {total} files indexed · {no_text} without text layer · {pending} pending",
|
||||
"today_all_caught_up": "All caught up! Nothing due this week.",
|
||||
"today_overdue": "Overdue",
|
||||
"today_due_today": "Due today",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Consumable low threshold (%)",
|
||||
"settings_battery_low_percent": "Battery low threshold (%)",
|
||||
"settings_thresholds_hint": "Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.",
|
||||
"settings_value_out_of_range": "Value must be between {min} and {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batteries",
|
||||
"bn_floor_decides": "Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.",
|
||||
"bn_above_floor": "That is above your {floor} % floor, so {name} decides for all noted batteries.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test notification sent",
|
||||
"test_notification_failed": "Test notification failed",
|
||||
"notify_per_person": "Per-person delivery",
|
||||
"member_avatars": "Member avatars",
|
||||
"member_avatars_hint": "Shown next to assigned tasks — initials in the member's colour. Defaults come from the name; set your own initials or pick a colour to tell members apart.",
|
||||
"member_initials": "Initials",
|
||||
"member_color": "Colour",
|
||||
"member_avatar_reset": "Reset",
|
||||
"notify_no_own_device": "No own device — uses the household service",
|
||||
"settings_notify_due_soon": "Notify when due soon",
|
||||
"settings_notify_overdue": "Notify when overdue",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Group by",
|
||||
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
|
||||
"target_changes_help": "Number of matching transitions before the trigger fires (default: 1).",
|
||||
"state_latch_help": "With 1 transition the trigger is a latch: it recovers when the entity leaves the To-state — or, with only a From-state set, when it returns to that state.",
|
||||
"for_minutes_state_help": "0 counts every change immediately. Set minutes and the new state must hold that long first — brief flickers then neither trigger nor count.",
|
||||
"qr_print_title": "Print QR codes",
|
||||
"qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Delete \"{name}\"?",
|
||||
"doc_too_large": "File is too large (max 25 MB).",
|
||||
"doc_upload_failed": "Upload failed.",
|
||||
"completion_photo_optional": "Completion photo (optional)",
|
||||
"add_photo": "Add photo",
|
||||
"completion_photos_optional": "Completion photos (optional)",
|
||||
"completion_photos": "Completion photos",
|
||||
"choose_photos": "Choose photos",
|
||||
"choose_photo": "Choose photo",
|
||||
"photos_android_hint": "The Android app hands over one photo per pick — each pick is added.",
|
||||
"add_photos": "Add photos",
|
||||
"photos_limit": "Up to {max} photos per completion",
|
||||
"history_edit_photos_hint": "Removing a photo here keeps the file in the object's documents.",
|
||||
"uploading": "Uploading…",
|
||||
"remove": "Remove",
|
||||
"doc_deduped": "Already stored elsewhere — shared, no extra space used.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Shown next to the recorded value when completing this task.",
|
||||
"reading_value_label": "Reading value",
|
||||
"reading_label": "Reading",
|
||||
"readings_section": "Readings",
|
||||
"readings_hint": "Several named values per completion — one row per meter. Leave empty to record a single value.",
|
||||
"reading_name_label": "Reading name",
|
||||
"reading_unit_short": "Unit",
|
||||
"reading_add": "Add reading",
|
||||
"reading_duplicate_name": "Name already used",
|
||||
"reading_last": "last: {value}",
|
||||
"reading_below_last": "Lower than the last reading ({value})",
|
||||
"settings_templates_label": "Template gallery",
|
||||
"settings_templates_hint": "Untick templates you'll never need — they disappear from the \"From template\" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.",
|
||||
"worksheet": "Work sheet",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Your own notification rule",
|
||||
"settings_notify_rule_hint": "Every notification also fires the event maintenance_supporter_notification (kind, status, object, task, reference numbers, priority, deep link, the payload). Route it with an automation — e.g. into Ticker, Telegram or Pushover.",
|
||||
"settings_notify_event_only": "Only fire the event, send nothing myself",
|
||||
"settings_notify_event_only_hint": "Your automation is the whole delivery. Rate limits, quiet hours and the daily cap still apply to when the event fires.",
|
||||
"settings_notify_extra_data": "Extra notification data (template)",
|
||||
"settings_notify_extra_data_hint": "JSON or YAML, Jinja allowed; merged into the notify call's data (your keys win). Variables: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test it with the Send test button.",
|
||||
"card_saved_view": "Saved view",
|
||||
"card_saved_view_none": "None",
|
||||
"card_saved_view_help": "Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Mark all replaced",
|
||||
"battery_fleet_mark_one": "Mark this battery replaced",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "No sensor",
|
||||
"battery_fleet_trigger_lost": "This task's sensor trigger was lost — it will not fire or auto-complete.",
|
||||
"battery_fleet_repair": "Repair",
|
||||
"battery_fleet_exclude": "Exclude from the fleet",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Pick a battery sensor the automatic discovery missed — it joins the roster immediately.",
|
||||
"battery_fleet_track_self": "Track self-charging batteries",
|
||||
"battery_fleet_track_self_hint": "Phones, vacuums and other devices that recharge themselves appear as rechargeables — a low one asks for a charge, never for new cells.",
|
||||
"battery_fleet_due_without_sensor": "Treat a passed forecast as due when a battery has no sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Some devices report no battery level at all — Battery Notes only knows the type and the last replacement. Once the typical lifetime has passed, such a battery counts as low and the fleet task reminds you; marking it replaced resets the forecast.",
|
||||
"battery_fleet_status_low": "Low",
|
||||
"battery_fleet_status_soon": "Soon",
|
||||
"battery_fleet_status_ok": "Healthy",
|
||||
"battery_fleet_status_due": "Due",
|
||||
"battery_fleet_predicted_on": "Expected around {date}",
|
||||
"battery_fleet_predicted_trend": "Predicted from this battery's discharge trend: around {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable: charge instead of replacing — never on the shopping list",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Service record (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entries",
|
||||
"ref_number": "Reference number",
|
||||
"print_options_title": "Print service record",
|
||||
"print_layout": "Layout",
|
||||
"print_layout_chronological": "Chronological",
|
||||
"print_layout_by_task": "By task",
|
||||
"print_include": "Include",
|
||||
"print_inc_readings": "Readings",
|
||||
"print_inc_parts": "Parts used",
|
||||
"print_inc_photos": "Photos",
|
||||
"print_inc_documents": "Linked documents",
|
||||
"print_inc_checklist": "Checklist",
|
||||
"print_inc_notes": "Notes",
|
||||
"print_inc_costs": "Cost and duration",
|
||||
"print_inc_person": "Completed by",
|
||||
"print_inc_refs": "Reference numbers",
|
||||
"print_inc_qr": "QR code per task",
|
||||
"print_button": "Print",
|
||||
"report_scan_hint": "Scan to open the task",
|
||||
"completed_by": "Completed by",
|
||||
"date_from": "From",
|
||||
"date_to": "To",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Programación por hora",
|
||||
"feat_schedule_time_desc": "Las tareas vencen a una hora específica en lugar de medianoche.",
|
||||
"schedule_time_optional": "Vence a las (opcional)",
|
||||
"schedule_time_toggle": "Vence a una hora concreta",
|
||||
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
|
||||
"at_time": "a las",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Sin etiqueta —",
|
||||
"dashboard": "Panel",
|
||||
"tab_today": "Hoy",
|
||||
"palette_placeholder": "Buscar objetos y tareas…",
|
||||
"palette_placeholder": "Buscar objetos, tareas, repuestos, documentos, notas…",
|
||||
"palette_no_results": "Sin resultados",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc cerrar",
|
||||
"palette_hint": "↑↓ navegar · Intro abrir · Esc cerrar · / abre la búsqueda en cualquier lugar",
|
||||
"search_open": "Buscar",
|
||||
"search_group_parts": "Repuestos",
|
||||
"search_group_content": "En documentos",
|
||||
"search_group_history": "Notas del historial",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Escribe para buscar objetos, tareas, repuestos, documentos y notas",
|
||||
"search_searching": "Buscando en documentos y notas…",
|
||||
"search_index_status": "Búsqueda de texto completo: {indexed} de {total} archivos indexados · {no_text} sin capa de texto · {pending} pendientes",
|
||||
"today_all_caught_up": "¡Todo al día! Nada pendiente esta semana.",
|
||||
"today_overdue": "Vencidas",
|
||||
"today_due_today": "Vence hoy",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Umbral „consumible bajo“ (%)",
|
||||
"settings_battery_low_percent": "Umbral „pila baja“ (%)",
|
||||
"settings_thresholds_hint": "Valores predeterminados para nuevas Configuraciones sugeridas (porcentaje restante) y para pilas de la flota sin umbral propio de Battery Notes. Las tareas existentes conservan su valor.",
|
||||
"settings_value_out_of_range": "El valor debe estar entre {min} y {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilas",
|
||||
"bn_floor_decides": "Tu umbral del {floor} % decide para todas; por pila cuenta el mayor de los dos umbrales.",
|
||||
"bn_above_floor": "Está por encima de tu umbral del {floor} %, así que {name} decide para todas las pilas anotadas.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notificación de prueba enviada",
|
||||
"test_notification_failed": "La notificación de prueba falló",
|
||||
"notify_per_person": "Entrega por persona",
|
||||
"member_avatars": "Avatares de los miembros",
|
||||
"member_avatars_hint": "Se muestra junto a las tareas asignadas — iniciales en el color del miembro. Por defecto se toman del nombre; elige tus iniciales o un color para distinguir a los miembros.",
|
||||
"member_initials": "Iniciales",
|
||||
"member_color": "Color",
|
||||
"member_avatar_reset": "Restablecer",
|
||||
"notify_no_own_device": "Sin dispositivo propio — usa el servicio del hogar",
|
||||
"settings_notify_due_soon": "Notificar cuando esté próxima",
|
||||
"settings_notify_overdue": "Notificar cuando esté vencida",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
|
||||
"target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).",
|
||||
"state_latch_help": "Con 1 transición el disparador es un cerrojo: se recupera cuando la entidad abandona el estado destino — o, si solo hay un estado origen, cuando vuelve a él.",
|
||||
"for_minutes_state_help": "0 cuenta cada cambio de inmediato. Con minutos, el nuevo estado debe mantenerse primero — un parpadeo breve entonces ni dispara ni cuenta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "¿Eliminar «{name}»?",
|
||||
"doc_too_large": "El archivo es demasiado grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Error al subir.",
|
||||
"completion_photo_optional": "Foto de finalización (opcional)",
|
||||
"add_photo": "Añadir foto",
|
||||
"completion_photos_optional": "Fotos de finalización (opcional)",
|
||||
"completion_photos": "Fotos de finalización",
|
||||
"choose_photos": "Elegir fotos",
|
||||
"choose_photo": "Elegir foto",
|
||||
"photos_android_hint": "La app de Android entrega una foto por selección — cada selección se añade.",
|
||||
"add_photos": "Añadir fotos",
|
||||
"photos_limit": "Hasta {max} fotos por finalización",
|
||||
"history_edit_photos_hint": "Una foto quitada aquí se conserva en los documentos del objeto.",
|
||||
"uploading": "Subiendo…",
|
||||
"remove": "Quitar",
|
||||
"doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Se muestra junto al valor registrado al completar esta tarea.",
|
||||
"reading_value_label": "Valor de lectura",
|
||||
"reading_label": "Lectura",
|
||||
"readings_section": "Lecturas",
|
||||
"readings_hint": "Varios valores con nombre por finalización — una fila por contador. Déjalo vacío para un único valor.",
|
||||
"reading_name_label": "Nombre de la lectura",
|
||||
"reading_unit_short": "Unidad",
|
||||
"reading_add": "Añadir lectura",
|
||||
"reading_duplicate_name": "Nombre ya en uso",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Menor que la última lectura ({value})",
|
||||
"settings_templates_label": "Galería de plantillas",
|
||||
"settings_templates_hint": "Desmarca las plantillas que nunca necesitarás: desaparecen de los selectores \"Desde plantilla\" (panel y flujo de configuración). Nada más cambia; puedes reactivarlas cuando quieras.",
|
||||
"worksheet": "Hoja de trabajo",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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í.",
|
||||
"settings_notify_rule": "Tu propia regla de notificación",
|
||||
"settings_notify_rule_hint": "Cada notificación dispara además el evento maintenance_supporter_notification (tipo, estado, objeto, tarea, números de referencia, prioridad, enlace, carga útil). Enrútalo con una automatización — p. ej. a Ticker, Telegram o Pushover.",
|
||||
"settings_notify_event_only": "Solo disparar el evento, no enviar nada",
|
||||
"settings_notify_event_only_hint": "Tu automatización se encarga de toda la entrega. Los intervalos de repetición, las horas de silencio y el límite diario siguen decidiendo cuándo se dispara el evento.",
|
||||
"settings_notify_extra_data": "Datos de notificación adicionales (plantilla)",
|
||||
"settings_notify_extra_data_hint": "JSON o YAML, se permite Jinja; se fusiona en los data de la llamada notify (tus claves ganan). Variables: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Pruébalo con el botón Enviar prueba.",
|
||||
"card_saved_view": "Vista guardada",
|
||||
"card_saved_view_none": "Ninguna",
|
||||
"card_saved_view_help": "Aplica los filtros de estado, usuario y etiqueta de la vista además de los filtros anteriores. El orden y la agrupación de la vista son ajustes de visualización del panel y no se aplican en la tarjeta.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas como reemplazadas",
|
||||
"battery_fleet_mark_one": "Marcar esta batería como reemplazada",
|
||||
"battery_fleet_offline": "sin conexión",
|
||||
"battery_fleet_no_sensor": "Sin sensor",
|
||||
"battery_fleet_trigger_lost": "El disparador de sensor de esta tarea se perdió — no se activará ni se completará automáticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir de la flota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Elige un sensor de batería que la detección automática pasó por alto — se une a la lista de inmediato.",
|
||||
"battery_fleet_track_self": "Seguir baterías de carga automática",
|
||||
"battery_fleet_track_self_hint": "Teléfonos, aspiradoras y otros dispositivos que se recargan solos aparecen como recargables: si están bajos piden una carga, nunca pilas nuevas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar una previsión vencida como pendiente cuando una pila no tiene sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Algunos dispositivos no informan ningún nivel de pila — Battery Notes solo conoce el tipo y el último cambio. Pasada la vida útil típica, esa pila cuenta como baja y la tarea de la flota te lo recuerda; marcarla como sustituida reinicia la previsión.",
|
||||
"battery_fleet_status_low": "Baja",
|
||||
"battery_fleet_status_soon": "Pronto",
|
||||
"battery_fleet_status_ok": "Correcta",
|
||||
"battery_fleet_status_due": "Pendiente",
|
||||
"battery_fleet_predicted_on": "Previsto hacia {date}",
|
||||
"battery_fleet_predicted_trend": "Predicción según la tendencia de descarga de esta pila: hacia {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recargable: se recarga en lugar de sustituirse — nunca en la lista de compra",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registro de mantenimiento (PDF)",
|
||||
"date": "Fecha",
|
||||
"service_record_entries": "entradas",
|
||||
"ref_number": "Número de referencia",
|
||||
"print_options_title": "Imprimir el historial de servicio",
|
||||
"print_layout": "Diseño",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarea",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Lecturas",
|
||||
"print_inc_parts": "Piezas usadas",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Documentos vinculados",
|
||||
"print_inc_checklist": "Lista de verificación",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Coste y duración",
|
||||
"print_inc_person": "Realizado por",
|
||||
"print_inc_refs": "Números de referencia",
|
||||
"print_inc_qr": "Código QR por tarea",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Escanea para abrir la tarea",
|
||||
"completed_by": "Realizado por",
|
||||
"date_from": "Desde",
|
||||
"date_to": "Hasta",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: virheellinen arvo",
|
||||
"feat_schedule_time": "Kellonajan ajoitus",
|
||||
"feat_schedule_time_desc": "Tehtävät erääntyvät tiettyyn kellonaikaan keskiyön sijaan.",
|
||||
"schedule_time_optional": "Erääntyy klo (valinnainen)",
|
||||
"schedule_time_toggle": "Erääntyy tiettyyn kellonaikaan",
|
||||
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
|
||||
"at_time": "klo",
|
||||
"notes_optional": "Muistiinpanot (valinnainen)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Ei tunnistetta —",
|
||||
"dashboard": "Kojelauta",
|
||||
"tab_today": "Tänään",
|
||||
"palette_placeholder": "Hae kohteita ja tehtäviä…",
|
||||
"palette_placeholder": "Hae kohteita, tehtäviä, varaosia, asiakirjoja, muistiinpanoja…",
|
||||
"palette_no_results": "Ei osumia",
|
||||
"palette_hint": "↑↓ liiku · Enter avaa · Esc sulje",
|
||||
"palette_hint": "↑↓ selaa · Enter avaa · Esc sulje · / avaa haun missä tahansa",
|
||||
"search_open": "Hae",
|
||||
"search_group_parts": "Varaosat",
|
||||
"search_group_content": "Asiakirjoissa",
|
||||
"search_group_history": "Historian muistiinpanot",
|
||||
"search_page": "Sivu {page}",
|
||||
"search_empty_hint": "Kirjoita hakeaksesi kohteista, tehtävistä, varaosista, asiakirjoista ja muistiinpanoista",
|
||||
"search_searching": "Haetaan asiakirjoista ja muistiinpanoista…",
|
||||
"search_index_status": "Kokotekstihaku: {indexed}/{total} tiedostoa indeksoitu · {no_text} ilman tekstikerrosta · {pending} odottaa",
|
||||
"today_all_caught_up": "Kaikki hoidettu! Tällä viikolla ei ole mitään.",
|
||||
"today_overdue": "Myöhässä",
|
||||
"today_due_today": "Erääntyy tänään",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Kynnys „kulutustarvike vähissä“ (%)",
|
||||
"settings_battery_low_percent": "Kynnys „paristo vähissä“ (%)",
|
||||
"settings_thresholds_hint": "Oletukset uusille Ehdotetuille asetuksille (jäljellä oleva prosentti) ja paristokannan paristoille ilman omaa Battery Notes -kynnystä. Olemassa olevat tehtävät säilyttävät arvonsa.",
|
||||
"settings_value_out_of_range": "Arvon on oltava välillä {min}–{max}",
|
||||
"bn_summary": "{name}: {pct} % {n} paristolle",
|
||||
"bn_floor_decides": "Sinun {floor} %:n kynnyksesi ratkaisee kaikille; paristoa kohden pätee korkeampi kahdesta kynnyksestä.",
|
||||
"bn_above_floor": "Se on sinun {floor} %:n kynnyksesi yläpuolella, joten {name} ratkaisee kaikille kirjatuille paristoille.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testi-ilmoitus lähetetty",
|
||||
"test_notification_failed": "Testi-ilmoitus epäonnistui",
|
||||
"notify_per_person": "Toimitus henkilöittäin",
|
||||
"member_avatars": "Jäsenten avatarit",
|
||||
"member_avatars_hint": "Näytetään osoitettujen tehtävien vieressä — nimikirjaimet jäsenen värissä. Oletus tulee nimestä; aseta omat nimikirjaimet tai valitse väri jäsenten erottamiseksi.",
|
||||
"member_initials": "Nimikirjaimet",
|
||||
"member_color": "Väri",
|
||||
"member_avatar_reset": "Palauta",
|
||||
"notify_no_own_device": "Ei omaa laitetta — käyttää kotitalouden palvelua",
|
||||
"settings_notify_due_soon": "Ilmoita, kun erääntyy pian",
|
||||
"settings_notify_overdue": "Ilmoita, kun myöhässä",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Ryhmittele",
|
||||
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
|
||||
"target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).",
|
||||
"state_latch_help": "Yhdellä siirtymällä liipaisin on salpa: se palautuu, kun entiteetti poistuu Kohde-tilasta — tai, jos vain Lähtö-tila on asetettu, kun se palaa siihen.",
|
||||
"for_minutes_state_help": "0 laskee jokaisen muutoksen heti. Minuuteilla uuden tilan on ensin pysyttävä — lyhyet välähdykset eivät silloin laukaise eivätkä kirjaudu.",
|
||||
"qr_print_title": "Tulosta QR-koodit",
|
||||
"qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Poistetaanko \"{name}\"?",
|
||||
"doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).",
|
||||
"doc_upload_failed": "Lataus epäonnistui.",
|
||||
"completion_photo_optional": "Valmistumiskuva (valinnainen)",
|
||||
"add_photo": "Lisää kuva",
|
||||
"completion_photos_optional": "Valokuvat suorituksesta (valinnainen)",
|
||||
"completion_photos": "Valokuvat suorituksesta",
|
||||
"choose_photos": "Valitse kuvat",
|
||||
"choose_photo": "Valitse kuva",
|
||||
"photos_android_hint": "Android-sovellus välittää yhden kuvan per valinta — jokainen valinta lisätään.",
|
||||
"add_photos": "Lisää kuvia",
|
||||
"photos_limit": "Enintään {max} kuvaa suoritusta kohti",
|
||||
"history_edit_photos_hint": "Tästä poistettu kuva säilyy kohteen asiakirjoissa.",
|
||||
"uploading": "Ladataan…",
|
||||
"remove": "Poista",
|
||||
"doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Näytetään kirjatun arvon vieressä tehtävää suoritettaessa.",
|
||||
"reading_value_label": "Lukema",
|
||||
"reading_label": "Lukema",
|
||||
"readings_section": "Lukemat",
|
||||
"readings_hint": "Useita nimettyjä arvoja suoritusta kohti — yksi rivi mittaria kohti. Jätä tyhjäksi yhtä arvoa varten.",
|
||||
"reading_name_label": "Lukeman nimi",
|
||||
"reading_unit_short": "Yksikkö",
|
||||
"reading_add": "Lisää lukema",
|
||||
"reading_duplicate_name": "Nimi on jo käytössä",
|
||||
"reading_last": "viimeksi: {value}",
|
||||
"reading_below_last": "Pienempi kuin edellinen lukema ({value})",
|
||||
"settings_templates_label": "Mallipohjagalleria",
|
||||
"settings_templates_hint": "Poista valinta mallipohjista, joita et koskaan tarvitse — ne katoavat \"Mallipohjasta\"-valitsimista (paneeli ja config flow). Mikään muu ei muutu; voi ottaa käyttöön uudelleen milloin tahansa.",
|
||||
"worksheet": "Työlomake",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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ä.",
|
||||
"settings_notify_rule": "Oma ilmoitussääntö",
|
||||
"settings_notify_rule_hint": "Jokainen ilmoitus laukaisee myös tapahtuman maintenance_supporter_notification (laji, tila, kohde, tehtävä, viitenumerot, prioriteetti, linkki, sisältö). Ohjaa se automaatiolla — esim. Tickeriin, Telegramiin tai Pushoveriin.",
|
||||
"settings_notify_event_only": "Laukaise vain tapahtuma, älä lähetä itse mitään",
|
||||
"settings_notify_event_only_hint": "Automaatiosi hoitaa koko toimituksen. Toistovälit, hiljaiset tunnit ja päiväraja määräävät yhä, milloin tapahtuma laukeaa.",
|
||||
"settings_notify_extra_data": "Ilmoituksen lisätiedot (malli)",
|
||||
"settings_notify_extra_data_hint": "JSON tai YAML, Jinja sallittu; yhdistetään notify-kutsun data-kenttään (omat avaimesi voittavat). Muuttujat: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Kokeile Lähetä testi -painikkeella.",
|
||||
"card_saved_view": "Tallennettu näkymä",
|
||||
"card_saved_view_none": "Ei mitään",
|
||||
"card_saved_view_help": "Soveltaa näkymän tila-, käyttäjä- ja tunnistesuodattimet yllä olevien suodattimien lisäksi. Näkymän lajittelu ja ryhmittely ovat paneelin näyttöasetuksia eivätkä vaikuta korttiin.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Merkitse kaikki vaihdetuiksi",
|
||||
"battery_fleet_mark_one": "Merkitse tämä paristo vaihdetuksi",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ei anturia",
|
||||
"battery_fleet_trigger_lost": "Tehtävän anturiliipaisin on kadonnut — se ei laukea eikä valmistu automaattisesti.",
|
||||
"battery_fleet_repair": "Korjaa",
|
||||
"battery_fleet_exclude": "Sulje pois kannasta",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Valitse paristoanturi, jonka automaattinen tunnistus ohitti — se liittyy luetteloon heti.",
|
||||
"battery_fleet_track_self": "Seuraa itselatautuvia akkuja",
|
||||
"battery_fleet_track_self_hint": "Puhelimet, imurit ja muut itselatautuvat laitteet näkyvät ladattavina — vähissä oleva pyytää latausta, ei koskaan uusia paristoja.",
|
||||
"battery_fleet_due_without_sensor": "Käsittele ohitettu ennuste erääntyneenä, kun paristolla ei ole anturia",
|
||||
"battery_fleet_due_without_sensor_hint": "Jotkin laitteet eivät raportoi paristotasoa lainkaan — Battery Notes tietää vain tyypin ja viimeisen vaihdon. Kun tyypillinen käyttöikä on kulunut, tällainen paristo lasketaan vähissä olevaksi ja kalustotehtävä muistuttaa sinua; vaihdetuksi merkitseminen nollaa ennusteen.",
|
||||
"battery_fleet_status_low": "Vähissä",
|
||||
"battery_fleet_status_soon": "Pian",
|
||||
"battery_fleet_status_ok": "Kunnossa",
|
||||
"battery_fleet_status_due": "Erääntynyt",
|
||||
"battery_fleet_predicted_on": "Odotettavissa noin {date}",
|
||||
"battery_fleet_predicted_trend": "Ennustettu tämän pariston purkautumistrendistä: noin {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ladattava: lataa vaihtamisen sijaan — ei koskaan ostoslistalle",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Huoltokirja (PDF)",
|
||||
"date": "Päivämäärä",
|
||||
"service_record_entries": "merkintää",
|
||||
"ref_number": "Viitenumero",
|
||||
"print_options_title": "Tulosta huoltokirja",
|
||||
"print_layout": "Asettelu",
|
||||
"print_layout_chronological": "Aikajärjestyksessä",
|
||||
"print_layout_by_task": "Tehtävittäin",
|
||||
"print_include": "Sisällytä",
|
||||
"print_inc_readings": "Lukemat",
|
||||
"print_inc_parts": "Käytetyt osat",
|
||||
"print_inc_photos": "Kuvat",
|
||||
"print_inc_documents": "Liitetyt asiakirjat",
|
||||
"print_inc_checklist": "Tarkistuslista",
|
||||
"print_inc_notes": "Muistiinpanot",
|
||||
"print_inc_costs": "Kustannus ja kesto",
|
||||
"print_inc_person": "Suorittaja",
|
||||
"print_inc_refs": "Viitenumerot",
|
||||
"print_inc_qr": "QR-koodi tehtävää kohti",
|
||||
"print_button": "Tulosta",
|
||||
"report_scan_hint": "Skannaa avataksesi tehtävän",
|
||||
"completed_by": "Suorittanut",
|
||||
"date_from": "Alkaen",
|
||||
"date_to": "Asti",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field} : valeur invalide",
|
||||
"feat_schedule_time": "Planification à l'heure",
|
||||
"feat_schedule_time_desc": "Les tâches arrivent à échéance à une heure précise plutôt qu'à minuit.",
|
||||
"schedule_time_optional": "Échéance à l'heure (optionnel)",
|
||||
"schedule_time_toggle": "Échéance à une heure précise",
|
||||
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
|
||||
"at_time": "à",
|
||||
"notes_optional": "Notes (optionnel)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Aucun tag —",
|
||||
"dashboard": "Tableau de bord",
|
||||
"tab_today": "Aujourd'hui",
|
||||
"palette_placeholder": "Rechercher objets et tâches…",
|
||||
"palette_placeholder": "Rechercher objets, tâches, pièces, documents, notes…",
|
||||
"palette_no_results": "Aucun résultat",
|
||||
"palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer",
|
||||
"palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer · / ouvre la recherche partout",
|
||||
"search_open": "Rechercher",
|
||||
"search_group_parts": "Pièces détachées",
|
||||
"search_group_content": "Dans les documents",
|
||||
"search_group_history": "Notes d'historique",
|
||||
"search_page": "p. {page}",
|
||||
"search_empty_hint": "Saisissez un texte pour chercher dans les objets, tâches, pièces, documents et notes",
|
||||
"search_searching": "Recherche dans les documents et notes…",
|
||||
"search_index_status": "Recherche plein texte : {indexed} fichiers sur {total} indexés · {no_text} sans couche texte · {pending} en attente",
|
||||
"today_all_caught_up": "Tout est à jour ! Rien cette semaine.",
|
||||
"today_overdue": "En retard",
|
||||
"today_due_today": "À faire aujourd'hui",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Seuil « consommable bas » (%)",
|
||||
"settings_battery_low_percent": "Seuil « pile faible » (%)",
|
||||
"settings_thresholds_hint": "Valeurs par défaut pour les nouvelles Configurations suggérées (pourcentage restant) et pour les piles de la flotte sans seuil Battery Notes propre. Les tâches existantes gardent leur valeur.",
|
||||
"settings_value_out_of_range": "La valeur doit être comprise entre {min} et {max}",
|
||||
"bn_summary": "{name} : {pct} % pour {n} piles",
|
||||
"bn_floor_decides": "Votre seuil de {floor} % décide pour toutes ; par pile, c'est le plus élevé des deux seuils qui compte.",
|
||||
"bn_above_floor": "C'est au-dessus de votre seuil de {floor} % : {name} décide donc pour toutes les piles notées.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notification de test envoyée",
|
||||
"test_notification_failed": "Échec de la notification de test",
|
||||
"notify_per_person": "Distribution par personne",
|
||||
"member_avatars": "Avatars des membres",
|
||||
"member_avatars_hint": "Affiché à côté des tâches attribuées — initiales dans la couleur du membre. Par défaut d'après le nom ; choisissez vos initiales ou une couleur pour distinguer les membres.",
|
||||
"member_initials": "Initiales",
|
||||
"member_color": "Couleur",
|
||||
"member_avatar_reset": "Réinitialiser",
|
||||
"notify_no_own_device": "Aucun appareil propre — utilise le service du foyer",
|
||||
"settings_notify_due_soon": "Notifier quand bientôt dû",
|
||||
"settings_notify_overdue": "Notifier quand en retard",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Grouper par",
|
||||
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
|
||||
"target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).",
|
||||
"state_latch_help": "Avec 1 transition, le déclencheur est un verrou : il se rétablit quand l'entité quitte l'état « vers » — ou, si seul un état « depuis » est défini, quand elle y revient.",
|
||||
"for_minutes_state_help": "0 compte chaque changement immédiatement. Avec des minutes, le nouvel état doit d'abord se maintenir — un bref clignotement ne déclenche ni ne compte alors.",
|
||||
"qr_print_title": "Imprimer les QR codes",
|
||||
"qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Supprimer « {name} » ?",
|
||||
"doc_too_large": "Fichier trop volumineux (max. 25 Mo).",
|
||||
"doc_upload_failed": "Échec du téléversement.",
|
||||
"completion_photo_optional": "Photo d'achèvement (facultatif)",
|
||||
"add_photo": "Ajouter une photo",
|
||||
"completion_photos_optional": "Photos de fin de tâche (facultatif)",
|
||||
"completion_photos": "Photos de fin de tâche",
|
||||
"choose_photos": "Choisir des photos",
|
||||
"choose_photo": "Choisir une photo",
|
||||
"photos_android_hint": "L'application Android ne transmet qu'une photo par sélection — chaque sélection est ajoutée.",
|
||||
"add_photos": "Ajouter des photos",
|
||||
"photos_limit": "Jusqu'à {max} photos par fin de tâche",
|
||||
"history_edit_photos_hint": "Une photo retirée ici reste dans les documents de l'objet.",
|
||||
"uploading": "Téléversement…",
|
||||
"remove": "Retirer",
|
||||
"doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Affichée à côté de la valeur enregistrée lors de l'exécution de cette tâche.",
|
||||
"reading_value_label": "Valeur relevée",
|
||||
"reading_label": "Relevé",
|
||||
"readings_section": "Relevés",
|
||||
"readings_hint": "Plusieurs valeurs nommées par fin de tâche — une ligne par compteur. Laisser vide pour une valeur unique.",
|
||||
"reading_name_label": "Nom du relevé",
|
||||
"reading_unit_short": "Unité",
|
||||
"reading_add": "Ajouter un relevé",
|
||||
"reading_duplicate_name": "Nom déjà utilisé",
|
||||
"reading_last": "dernier : {value}",
|
||||
"reading_below_last": "Inférieur au dernier relevé ({value})",
|
||||
"settings_templates_label": "Galerie de modèles",
|
||||
"settings_templates_hint": "Décochez les modèles dont vous n'aurez jamais besoin — ils disparaissent des sélecteurs « Depuis un modèle » (panneau et config flow). Rien d'autre ne change ; réactivables à tout moment.",
|
||||
"worksheet": "Fiche de travail",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Votre propre règle de notification",
|
||||
"settings_notify_rule_hint": "Chaque notification déclenche aussi l'événement maintenance_supporter_notification (type, statut, objet, tâche, numéros de référence, priorité, lien, charge utile). Routez-le avec une automatisation — vers Ticker, Telegram ou Pushover par exemple.",
|
||||
"settings_notify_event_only": "Déclencher seulement l'événement, ne rien envoyer",
|
||||
"settings_notify_event_only_hint": "Votre automatisation assure toute la livraison. Intervalles de répétition, heures calmes et plafond quotidien s'appliquent toujours au déclenchement de l'événement.",
|
||||
"settings_notify_extra_data": "Données de notification supplémentaires (modèle)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja autorisé ; fusionné dans les data de l'appel notify (vos clés l'emportent). Variables : kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Vérifiez avec le bouton Envoyer un test.",
|
||||
"card_saved_view": "Vue enregistrée",
|
||||
"card_saved_view_none": "Aucune",
|
||||
"card_saved_view_help": "Applique les filtres statut, utilisateur et étiquette de la vue en plus des filtres ci-dessus. Le tri et le regroupement de la vue sont des réglages d'affichage du panneau et ne s'appliquent pas à la carte.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Tout marquer remplacé",
|
||||
"battery_fleet_mark_one": "Marquer cette pile remplacée",
|
||||
"battery_fleet_offline": "hors ligne",
|
||||
"battery_fleet_no_sensor": "Pas de capteur",
|
||||
"battery_fleet_trigger_lost": "Le déclencheur de capteur de cette tâche a été perdu — elle ne se déclenchera pas et ne se terminera pas automatiquement.",
|
||||
"battery_fleet_repair": "Réparer",
|
||||
"battery_fleet_exclude": "Exclure du parc",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Choisissez un capteur de batterie que la détection automatique a manqué — il rejoint la liste immédiatement.",
|
||||
"battery_fleet_track_self": "Suivre les batteries à recharge automatique",
|
||||
"battery_fleet_track_self_hint": "Les téléphones, aspirateurs et autres appareils qui se rechargent seuls apparaissent comme rechargeables — un niveau bas demande une recharge, jamais de nouvelles piles.",
|
||||
"battery_fleet_due_without_sensor": "Considérer une prévision dépassée comme échue quand une pile n'a pas de capteur",
|
||||
"battery_fleet_due_without_sensor_hint": "Certains appareils ne signalent aucun niveau de pile — Battery Notes ne connaît que le type et le dernier remplacement. Une fois la durée de vie typique dépassée, cette pile compte comme faible et la tâche du parc vous le rappelle ; la marquer remplacée réinitialise la prévision.",
|
||||
"battery_fleet_status_low": "Faible",
|
||||
"battery_fleet_status_soon": "Bientôt",
|
||||
"battery_fleet_status_ok": "Bon état",
|
||||
"battery_fleet_status_due": "À remplacer",
|
||||
"battery_fleet_predicted_on": "Prévu vers {date}",
|
||||
"battery_fleet_predicted_trend": "Prédit à partir de la tendance de décharge de cette pile : vers {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable : à recharger plutôt qu'à remplacer — jamais sur la liste d'achats",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Carnet d'entretien (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entrées",
|
||||
"ref_number": "Numéro de référence",
|
||||
"print_options_title": "Imprimer le carnet d'entretien",
|
||||
"print_layout": "Mise en page",
|
||||
"print_layout_chronological": "Chronologique",
|
||||
"print_layout_by_task": "Par tâche",
|
||||
"print_include": "Inclure",
|
||||
"print_inc_readings": "Relevés",
|
||||
"print_inc_parts": "Pièces utilisées",
|
||||
"print_inc_photos": "Photographies",
|
||||
"print_inc_documents": "Documents liés",
|
||||
"print_inc_checklist": "Liste de contrôle",
|
||||
"print_inc_notes": "Remarques",
|
||||
"print_inc_costs": "Coût et durée",
|
||||
"print_inc_person": "Réalisé par",
|
||||
"print_inc_refs": "Numéros de référence",
|
||||
"print_inc_qr": "QR code par tâche",
|
||||
"print_button": "Imprimer",
|
||||
"report_scan_hint": "Scanner pour ouvrir la tâche",
|
||||
"completed_by": "Réalisé par",
|
||||
"date_from": "Du",
|
||||
"date_to": "Au",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: अमान्य मान",
|
||||
"feat_schedule_time": "दिन का समय निर्धारण",
|
||||
"feat_schedule_time_desc": "कार्य मध्यरात्रि के बजाय दिन के एक विशिष्ट समय पर अतिदेय हो जाते हैं।",
|
||||
"schedule_time_optional": "इस समय देय (वैकल्पिक)",
|
||||
"schedule_time_toggle": "किसी निश्चित समय पर देय",
|
||||
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
|
||||
"at_time": "बजे",
|
||||
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— कोई टैग नहीं —",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"tab_today": "आज",
|
||||
"palette_placeholder": "ऑब्जेक्ट और कार्य खोजें…",
|
||||
"palette_placeholder": "ऑब्जेक्ट, कार्य, स्पेयर पार्ट्स, दस्तावेज़, नोट्स खोजें…",
|
||||
"palette_no_results": "कोई मिलान नहीं",
|
||||
"palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद",
|
||||
"palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद करें · / कहीं से भी खोज खोलता है",
|
||||
"search_open": "खोजें",
|
||||
"search_group_parts": "स्पेयर पार्ट्स",
|
||||
"search_group_content": "दस्तावेज़ों में",
|
||||
"search_group_history": "इतिहास नोट्स",
|
||||
"search_page": "पृष्ठ {page}",
|
||||
"search_empty_hint": "ऑब्जेक्ट, कार्य, स्पेयर पार्ट्स, दस्तावेज़ और नोट्स खोजने के लिए टाइप करें",
|
||||
"search_searching": "दस्तावेज़ और नोट्स खोजे जा रहे हैं…",
|
||||
"search_index_status": "पूर्ण-पाठ खोज: {total} में से {indexed} फ़ाइलें अनुक्रमित · {no_text} बिना टेक्स्ट परत · {pending} लंबित",
|
||||
"today_all_caught_up": "सब पूरा! इस सप्ताह कुछ भी बाकी नहीं।",
|
||||
"today_overdue": "अतिदेय",
|
||||
"today_due_today": "आज देय",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "उपभोग्य „कम“ सीमा (%)",
|
||||
"settings_battery_low_percent": "बैटरी „कम“ सीमा (%)",
|
||||
"settings_thresholds_hint": "नए सुझाए गए सेटअप (शेष प्रतिशत) और अपनी Battery Notes सीमा न रखने वाली बेड़े की बैटरियों के लिए डिफ़ॉल्ट। मौजूदा कार्य अपना मान रखते हैं।",
|
||||
"settings_value_out_of_range": "मान {min} और {max} के बीच होना चाहिए",
|
||||
"bn_summary": "{name}: {n} बैटरियों के लिए {pct} %",
|
||||
"bn_floor_decides": "आपकी {floor} % सीमा सभी के लिए निर्णायक है; प्रत्येक बैटरी के लिए दोनों में से ऊँची सीमा मान्य है।",
|
||||
"bn_above_floor": "यह आपकी {floor} % सीमा से ऊपर है, इसलिए सभी नोट की गई बैटरियों के लिए {name} निर्णायक है।",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "परीक्षण सूचना भेजी गई",
|
||||
"test_notification_failed": "परीक्षण सूचना विफल",
|
||||
"notify_per_person": "प्रति व्यक्ति वितरण",
|
||||
"member_avatars": "सदस्यों के अवतार",
|
||||
"member_avatars_hint": "सौंपे गए कार्यों के बगल में दिखाया जाता है — सदस्य के रंग में आद्याक्षर। डिफ़ॉल्ट नाम से आता है; सदस्यों में अंतर करने के लिए अपने आद्याक्षर या रंग चुनें।",
|
||||
"member_initials": "आद्याक्षर",
|
||||
"member_color": "रंग",
|
||||
"member_avatar_reset": "रीसेट",
|
||||
"notify_no_own_device": "कोई निजी डिवाइस नहीं — घरेलू सेवा का उपयोग करता है",
|
||||
"settings_notify_due_soon": "जल्द देय होने पर सूचित करें",
|
||||
"settings_notify_overdue": "अतिदेय होने पर सूचित करें",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "इसके अनुसार समूहित करें",
|
||||
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
|
||||
"target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।",
|
||||
"state_latch_help": "1 संक्रमण पर ट्रिगर लैच बन जाता है: इकाई 'तक' स्थिति छोड़ते ही यह पुनर्स्थापित होता है — या, केवल 'से' स्थिति सेट हो तो उसमें लौटने पर।",
|
||||
"for_minutes_state_help": "0 हर परिवर्तन को तुरंत गिनता है। मिनट सेट करने पर नई स्थिति को पहले उतनी देर बनी रहनी होगी — क्षणिक झिलमिलाहट तब न ट्रिगर करती है न गिनी जाती है।",
|
||||
"qr_print_title": "QR कोड प्रिंट करें",
|
||||
"qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" हटाएँ?",
|
||||
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।",
|
||||
"doc_upload_failed": "अपलोड विफल रहा।",
|
||||
"completion_photo_optional": "पूर्णता फ़ोटो (वैकल्पिक)",
|
||||
"add_photo": "फ़ोटो जोड़ें",
|
||||
"completion_photos_optional": "पूर्णता की तस्वीरें (वैकल्पिक)",
|
||||
"completion_photos": "पूर्णता की तस्वीरें",
|
||||
"choose_photos": "तस्वीरें चुनें",
|
||||
"choose_photo": "फ़ोटो चुनें",
|
||||
"photos_android_hint": "Android ऐप प्रति चयन एक फ़ोटो देता है — हर चयन जोड़ा जाता है।",
|
||||
"add_photos": "तस्वीरें जोड़ें",
|
||||
"photos_limit": "प्रति पूर्णता अधिकतम {max} तस्वीरें",
|
||||
"history_edit_photos_hint": "यहाँ हटाई गई तस्वीर ऑब्जेक्ट के दस्तावेज़ों में बनी रहती है।",
|
||||
"uploading": "अपलोड हो रहा है…",
|
||||
"remove": "हटाएँ",
|
||||
"doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "इस कार्य को पूरा करते समय दर्ज मान के बगल में दिखाई जाती है।",
|
||||
"reading_value_label": "रीडिंग मान",
|
||||
"reading_label": "रीडिंग",
|
||||
"readings_section": "रीडिंग",
|
||||
"readings_hint": "प्रति पूर्णता कई नामित मान — प्रति मीटर एक पंक्ति। एकल मान के लिए खाली छोड़ें।",
|
||||
"reading_name_label": "रीडिंग का नाम",
|
||||
"reading_unit_short": "इकाई",
|
||||
"reading_add": "रीडिंग जोड़ें",
|
||||
"reading_duplicate_name": "नाम पहले से उपयोग में है",
|
||||
"reading_last": "पिछला: {value}",
|
||||
"reading_below_last": "पिछली रीडिंग ({value}) से कम",
|
||||
"settings_templates_label": "टेम्पलेट गैलरी",
|
||||
"settings_templates_hint": "जिन टेम्पलेट की कभी ज़रूरत नहीं होगी उन्हें अनचेक करें — वे \"टेम्पलेट से\" चयनकर्ताओं (पैनल और कॉन्फ़िग फ़्लो) से गायब हो जाएंगे। और कुछ नहीं बदलता; कभी भी फिर से सक्षम कर सकते हैं।",
|
||||
"worksheet": "कार्य पत्रक",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "केवल इस व्यू के लिए सूचित करें",
|
||||
"settings_notify_scope_all": "सभी कार्य",
|
||||
"settings_notify_scope_hint": "केवल वे कार्य अनुस्मारक भेजते हैं जो चुने गए व्यू के लेबल/उपयोगकर्ता फ़िल्टर से मेल खाते हैं। व्यू की स्थिति, क्रम और समूहन यहाँ अनदेखे रहते हैं।",
|
||||
"settings_notify_rule": "आपका अपना सूचना नियम",
|
||||
"settings_notify_rule_hint": "हर सूचना maintenance_supporter_notification इवेंट भी ट्रिगर करती है (प्रकार, स्थिति, ऑब्जेक्ट, कार्य, संदर्भ संख्याएँ, प्राथमिकता, डीप लिंक, पेलोड)। इसे ऑटोमेशन से रूट करें — जैसे Ticker, Telegram या Pushover में।",
|
||||
"settings_notify_event_only": "केवल इवेंट ट्रिगर करें, स्वयं कुछ न भेजें",
|
||||
"settings_notify_event_only_hint": "आपका ऑटोमेशन पूरी डिलीवरी संभालता है। दोहराव अंतराल, शांत घंटे और दैनिक सीमा अभी भी तय करते हैं कि इवेंट कब ट्रिगर हो।",
|
||||
"settings_notify_extra_data": "अतिरिक्त सूचना डेटा (टेम्पलेट)",
|
||||
"settings_notify_extra_data_hint": "JSON या YAML, Jinja की अनुमति; notify कॉल के data में मिलाया जाता है (आपकी कुंजियाँ प्राथमिक)। वेरिएबल: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message। परीक्षण भेजें बटन से जाँचें।",
|
||||
"card_saved_view": "सहेजा गया व्यू",
|
||||
"card_saved_view_none": "कोई नहीं",
|
||||
"card_saved_view_help": "ऊपर दिए फ़िल्टरों के अतिरिक्त व्यू के स्थिति, उपयोगकर्ता और लेबल फ़िल्टर लागू करता है। व्यू का क्रम और समूहन पैनल की प्रदर्शन सेटिंग हैं और कार्ड पर लागू नहीं होते।",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "सभी बदली हुई चिह्नित करें",
|
||||
"battery_fleet_mark_one": "इस बैटरी को बदली हुई चिह्नित करें",
|
||||
"battery_fleet_offline": "ऑफ़लाइन",
|
||||
"battery_fleet_no_sensor": "कोई सेंसर नहीं",
|
||||
"battery_fleet_trigger_lost": "इस कार्य का सेंसर ट्रिगर खो गया है — यह सक्रिय नहीं होगा और स्वतः पूर्ण नहीं होगा।",
|
||||
"battery_fleet_repair": "मरम्मत करें",
|
||||
"battery_fleet_exclude": "फ्लीट से बाहर करें",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "स्वचालित खोज से छूटा बैटरी सेंसर चुनें — यह तुरंत सूची में जुड़ जाएगा।",
|
||||
"battery_fleet_track_self": "स्वयं चार्ज होने वाली बैटरियों को ट्रैक करें",
|
||||
"battery_fleet_track_self_hint": "फ़ोन, वैक्यूम और अन्य स्वयं चार्ज होने वाले उपकरण रिचार्जेबल के रूप में दिखते हैं — कम होने पर चार्ज करने को कहा जाता है, नई बैटरी कभी नहीं।",
|
||||
"battery_fleet_due_without_sensor": "जब बैटरी में सेंसर न हो तो बीत चुके पूर्वानुमान को देय मानें",
|
||||
"battery_fleet_due_without_sensor_hint": "कुछ डिवाइस बैटरी स्तर बिल्कुल नहीं बताते — Battery Notes को केवल प्रकार और पिछला बदलाव पता होता है। सामान्य आयु बीत जाने पर ऐसी बैटरी कम मानी जाती है और फ़्लीट कार्य आपको याद दिलाता है; बदली हुई चिह्नित करने से पूर्वानुमान रीसेट हो जाता है।",
|
||||
"battery_fleet_status_low": "कम",
|
||||
"battery_fleet_status_soon": "जल्द",
|
||||
"battery_fleet_status_ok": "ठीक",
|
||||
"battery_fleet_status_due": "देय",
|
||||
"battery_fleet_predicted_on": "{date} के आसपास अपेक्षित",
|
||||
"battery_fleet_predicted_trend": "इस बैटरी के डिस्चार्ज रुझान से अनुमानित: लगभग {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "रिचार्जेबल: बदलने के बजाय चार्ज करें — खरीदारी सूची में कभी नहीं",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "सेवा रिकॉर्ड (PDF)",
|
||||
"date": "दिनांक",
|
||||
"service_record_entries": "प्रविष्टियाँ",
|
||||
"ref_number": "संदर्भ संख्या",
|
||||
"print_options_title": "सेवा रिकॉर्ड प्रिंट करें",
|
||||
"print_layout": "लेआउट",
|
||||
"print_layout_chronological": "कालानुक्रमिक",
|
||||
"print_layout_by_task": "कार्य के अनुसार",
|
||||
"print_include": "शामिल करें",
|
||||
"print_inc_readings": "रीडिंग",
|
||||
"print_inc_parts": "प्रयुक्त पुर्जे",
|
||||
"print_inc_photos": "फ़ोटो",
|
||||
"print_inc_documents": "लिंक किए गए दस्तावेज़",
|
||||
"print_inc_checklist": "चेकलिस्ट",
|
||||
"print_inc_notes": "नोट्स",
|
||||
"print_inc_costs": "लागत और अवधि",
|
||||
"print_inc_person": "पूर्ण किया",
|
||||
"print_inc_refs": "संदर्भ संख्याएँ",
|
||||
"print_inc_qr": "प्रति कार्य QR कोड",
|
||||
"print_button": "प्रिंट",
|
||||
"report_scan_hint": "कार्य खोलने के लिए स्कैन करें",
|
||||
"completed_by": "द्वारा पूर्ण",
|
||||
"date_from": "से",
|
||||
"date_to": "तक",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: érvénytelen érték",
|
||||
"feat_schedule_time": "Napszakhoz kötött ütemezés",
|
||||
"feat_schedule_time_desc": "A feladatok nem éjfélkor, hanem egy megadott napszakban válnak lejárttá.",
|
||||
"schedule_time_optional": "Esedékesség időpontja (opcionális)",
|
||||
"schedule_time_toggle": "Adott időpontban esedékes",
|
||||
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
|
||||
"at_time": "ekkor:",
|
||||
"notes_optional": "Megjegyzések (opcionális)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Nincs címke —",
|
||||
"dashboard": "Irányítópult",
|
||||
"tab_today": "Ma",
|
||||
"palette_placeholder": "Objektumok és feladatok keresése…",
|
||||
"palette_placeholder": "Tárgyak, feladatok, alkatrészek, dokumentumok, jegyzetek keresése…",
|
||||
"palette_no_results": "Nincs találat",
|
||||
"palette_hint": "↑↓ navigálás · Enter megnyitás · Esc bezárás",
|
||||
"palette_hint": "↑↓ lépkedés · Enter megnyitás · Esc bezárás · / bárhol megnyitja a keresést",
|
||||
"search_open": "Keresés",
|
||||
"search_group_parts": "Alkatrészek",
|
||||
"search_group_content": "Dokumentumokban",
|
||||
"search_group_history": "Előzmény-jegyzetek",
|
||||
"search_page": "{page}. oldal",
|
||||
"search_empty_hint": "Írjon a tárgyak, feladatok, alkatrészek, dokumentumok és jegyzetek kereséséhez",
|
||||
"search_searching": "Keresés a dokumentumokban és jegyzetekben…",
|
||||
"search_index_status": "Teljes szöveges keresés: {indexed} / {total} fájl indexelve · {no_text} szövegréteg nélkül · {pending} függőben",
|
||||
"today_all_caught_up": "Minden rendben! Ezen a héten nincs esedékes feladat.",
|
||||
"today_overdue": "Lejárt",
|
||||
"today_due_today": "Ma esedékes",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "„Fogyóeszköz alacsony” küszöb (%)",
|
||||
"settings_battery_low_percent": "„Alacsony akkumulátor” küszöb (%)",
|
||||
"settings_thresholds_hint": "Alapértékek az új Javasolt beállításokhoz (százalékos maradék) és a saját Battery Notes-küszöb nélküli flottaelemekhez. A meglévő feladatok megtartják az értéküket.",
|
||||
"settings_value_out_of_range": "Az értéknek {min} és {max} között kell lennie",
|
||||
"bn_summary": "{name}: {pct} % {n} elemhez",
|
||||
"bn_floor_decides": "A te {floor} %-os küszöböd dönt mindegyiknél; elemenként a két küszöb közül a magasabb számít.",
|
||||
"bn_above_floor": "Ez a te {floor} %-os küszöböd felett van, így minden feljegyzett elemnél a {name} dönt.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Tesztértesítés elküldve",
|
||||
"test_notification_failed": "A tesztértesítés nem sikerült",
|
||||
"notify_per_person": "Kézbesítés személyenként",
|
||||
"member_avatars": "Tagok avatárjai",
|
||||
"member_avatars_hint": "A kiosztott feladatok mellett jelenik meg — monogram a tag színében. Alapértelmezés a névből; adjon meg saját monogramot vagy válasszon színt a tagok megkülönböztetéséhez.",
|
||||
"member_initials": "Monogram",
|
||||
"member_color": "Szín",
|
||||
"member_avatar_reset": "Visszaállítás",
|
||||
"notify_no_own_device": "Nincs saját eszköz — a háztartási szolgáltatást használja",
|
||||
"settings_notify_due_soon": "Értesítés, ha hamarosan esedékes",
|
||||
"settings_notify_overdue": "Értesítés lejáratkor",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Csoportosítás",
|
||||
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
|
||||
"target_changes_help": "Az egyező átmenetek száma a kiváltás előtt (alapértelmezés: 1).",
|
||||
"state_latch_help": "1 átmenetnél a trigger reteszként működik: helyreáll, amikor az entitás elhagyja a cél-állapotot — vagy, ha csak kiinduló állapot van megadva, amikor visszatér abba.",
|
||||
"for_minutes_state_help": "A 0 minden változást azonnal számol. Percekkel az új állapotnak előbb fenn kell maradnia — a rövid villanás ekkor se nem indít, se nem számít.",
|
||||
"qr_print_title": "QR-kódok nyomtatása",
|
||||
"qr_print_desc": "Nyomtatható oldal QR-kódokkal — vágja ki és ragassza fel a berendezésekre.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Törli a(z) „{name}” elemet?",
|
||||
"doc_too_large": "A fájl túl nagy (max. 25 MB).",
|
||||
"doc_upload_failed": "A feltöltés nem sikerült.",
|
||||
"completion_photo_optional": "Fotó az elvégzésről (opcionális)",
|
||||
"add_photo": "Fotó hozzáadása",
|
||||
"completion_photos_optional": "Fotók az elvégzésről (nem kötelező)",
|
||||
"completion_photos": "Fotók az elvégzésről",
|
||||
"choose_photos": "Fotók kiválasztása",
|
||||
"choose_photo": "Fotó kiválasztása",
|
||||
"photos_android_hint": "Az Android-alkalmazás választásonként egy fotót ad át — minden választás hozzáadódik.",
|
||||
"add_photos": "Fotók hozzáadása",
|
||||
"photos_limit": "Legfeljebb {max} fotó elvégzésenként",
|
||||
"history_edit_photos_hint": "Az itt eltávolított fotó megmarad az objektum dokumentumai között.",
|
||||
"uploading": "Feltöltés…",
|
||||
"remove": "Eltávolítás",
|
||||
"doc_deduped": "Már máshol is tárolva — megosztott, nem foglal extra helyet.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "A rögzített érték mellett jelenik meg a feladat elvégzésekor.",
|
||||
"reading_value_label": "Leolvasott érték",
|
||||
"reading_label": "Leolvasás",
|
||||
"readings_section": "Leolvasások",
|
||||
"readings_hint": "Több nevesített érték elvégzésenként — mérőnként egy sor. Egyetlen értékhez hagyja üresen.",
|
||||
"reading_name_label": "Leolvasás neve",
|
||||
"reading_unit_short": "Mértékegység",
|
||||
"reading_add": "Leolvasás hozzáadása",
|
||||
"reading_duplicate_name": "A név már foglalt",
|
||||
"reading_last": "legutóbb: {value}",
|
||||
"reading_below_last": "Kisebb az előző leolvasásnál ({value})",
|
||||
"settings_templates_label": "Sablongaléria",
|
||||
"settings_templates_hint": "Vegye ki a pipát a sosem használt sablonoknál — így eltűnnek a „Sablonból” választókból (panel és konfigurációs folyamat). Semmi más nem változik; bármikor újra engedélyezheti őket.",
|
||||
"worksheet": "Munkalap",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Saját értesítési szabály",
|
||||
"settings_notify_rule_hint": "Minden értesítés kiváltja a maintenance_supporter_notification eseményt is (típus, állapot, tárgy, feladat, hivatkozási számok, prioritás, mélylink, adatok). Irányítsa automatizálással — pl. a Tickerbe, Telegramba vagy Pushoverbe.",
|
||||
"settings_notify_event_only": "Csak az esemény, saját küldés nélkül",
|
||||
"settings_notify_event_only_hint": "A kézbesítést teljes egészében az automatizálás végzi. Az ismétlési időközök, a csendes órák és a napi korlát továbbra is meghatározzák, mikor váltódik ki az esemény.",
|
||||
"settings_notify_extra_data": "További értesítési adatok (sablon)",
|
||||
"settings_notify_extra_data_hint": "JSON vagy YAML, Jinja engedélyezett; beolvad a notify hívás data mezőjébe (a saját kulcsai nyernek). Változók: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Ellenőrizze a Teszt küldése gombbal.",
|
||||
"card_saved_view": "Mentett nézet",
|
||||
"card_saved_view_none": "Nincs",
|
||||
"card_saved_view_help": "A nézet állapot-, felhasználó- és címkeszűrőit a fenti szűrőkön felül alkalmazza. A nézet rendezése és csoportosítása a panel megjelenítési beállítása, a kártyán nem érvényesül.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Mind cseréltként jelölése",
|
||||
"battery_fleet_mark_one": "Elem cseréltként jelölése",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Nincs érzékelő",
|
||||
"battery_fleet_trigger_lost": "A feladat érzékelős kiváltója elveszett — nem fog aktiválódni és automatikusan lezáródni sem.",
|
||||
"battery_fleet_repair": "Javítás",
|
||||
"battery_fleet_exclude": "Kizárás a flottából",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Válassz egy elem-érzékelőt, amelyet az automatikus felismerés kihagyott — azonnal felkerül a listára.",
|
||||
"battery_fleet_track_self": "Öntöltő akkumulátorok követése",
|
||||
"battery_fleet_track_self_hint": "A telefonok, porszívók és más öntöltő eszközök akkumulátorként jelennek meg — alacsony töltöttségnél töltést kérnek, sosem új elemet.",
|
||||
"battery_fleet_due_without_sensor": "Lejárt előrejelzés esedékesnek számít, ha az elemnek nincs érzékelője",
|
||||
"battery_fleet_due_without_sensor_hint": "Egyes eszközök egyáltalán nem jelentenek töltöttséget — a Battery Notes csak a típust és az utolsó cserét ismeri. A szokásos élettartam letelte után az ilyen elem lemerültnek számít, és a flottafeladat emlékeztet; a cserélve jelölés visszaállítja az előrejelzést.",
|
||||
"battery_fleet_status_low": "Alacsony",
|
||||
"battery_fleet_status_soon": "Hamarosan",
|
||||
"battery_fleet_status_ok": "Rendben",
|
||||
"battery_fleet_status_due": "Esedékes",
|
||||
"battery_fleet_predicted_on": "Várhatóan {date} körül",
|
||||
"battery_fleet_predicted_trend": "Az elem merülési trendjéből előrejelezve: kb. {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Újratölthető: cserélés helyett töltse fel — soha nem kerül a bevásárlólistára",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Szervizkönyv (PDF)",
|
||||
"date": "Dátum",
|
||||
"service_record_entries": "bejegyzés",
|
||||
"ref_number": "Hivatkozási szám",
|
||||
"print_options_title": "Szervizkönyv nyomtatása",
|
||||
"print_layout": "Elrendezés",
|
||||
"print_layout_chronological": "Időrendben",
|
||||
"print_layout_by_task": "Feladatonként",
|
||||
"print_include": "Tartalmazza",
|
||||
"print_inc_readings": "Mérőállások",
|
||||
"print_inc_parts": "Felhasznált alkatrészek",
|
||||
"print_inc_photos": "Fotók",
|
||||
"print_inc_documents": "Csatolt dokumentumok",
|
||||
"print_inc_checklist": "Ellenőrzőlista",
|
||||
"print_inc_notes": "Jegyzetek",
|
||||
"print_inc_costs": "Költség és időtartam",
|
||||
"print_inc_person": "Elvégezte",
|
||||
"print_inc_refs": "Hivatkozási számok",
|
||||
"print_inc_qr": "QR-kód feladatonként",
|
||||
"print_button": "Nyomtatás",
|
||||
"report_scan_hint": "Olvassa be a feladat megnyitásához",
|
||||
"completed_by": "Elvégezte",
|
||||
"date_from": "Ettől",
|
||||
"date_to": "Eddig",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valore non valido",
|
||||
"feat_schedule_time": "Pianificazione oraria",
|
||||
"feat_schedule_time_desc": "Le attività scadono a un'ora specifica anziché a mezzanotte.",
|
||||
"schedule_time_optional": "Scadenza all'ora (opzionale)",
|
||||
"schedule_time_toggle": "Scadenza a un orario preciso",
|
||||
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
|
||||
"at_time": "alle",
|
||||
"notes_optional": "Note (opzionale)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Nessun tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Oggi",
|
||||
"palette_placeholder": "Cerca oggetti e attività…",
|
||||
"palette_placeholder": "Cerca oggetti, attività, ricambi, documenti, note…",
|
||||
"palette_no_results": "Nessun risultato",
|
||||
"palette_hint": "↑↓ naviga · Invio apri · Esc chiudi",
|
||||
"palette_hint": "↑↓ navigare · Invio aprire · Esc chiudere · / apre la ricerca ovunque",
|
||||
"search_open": "Cerca",
|
||||
"search_group_parts": "Ricambi",
|
||||
"search_group_content": "Nei documenti",
|
||||
"search_group_history": "Note della cronologia",
|
||||
"search_page": "Pagina {page}",
|
||||
"search_empty_hint": "Digita per cercare tra oggetti, attività, ricambi, documenti e note",
|
||||
"search_searching": "Ricerca in documenti e note…",
|
||||
"search_index_status": "Ricerca full-text: {indexed} di {total} file indicizzati · {no_text} senza livello testo · {pending} in attesa",
|
||||
"today_all_caught_up": "Tutto fatto! Niente in scadenza questa settimana.",
|
||||
"today_overdue": "Scadute",
|
||||
"today_due_today": "In scadenza oggi",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Soglia „consumabile basso“ (%)",
|
||||
"settings_battery_low_percent": "Soglia „batteria scarica“ (%)",
|
||||
"settings_thresholds_hint": "Valori predefiniti per le nuove Configurazioni suggerite (percentuale residua) e per le batterie della flotta senza soglia Battery Notes propria. Le attività esistenti mantengono il loro valore.",
|
||||
"settings_value_out_of_range": "Il valore deve essere compreso tra {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % per {n} batterie",
|
||||
"bn_floor_decides": "La tua soglia del {floor} % decide per tutte; per ogni batteria conta la più alta delle due soglie.",
|
||||
"bn_above_floor": "È sopra la tua soglia del {floor} %, quindi {name} decide per tutte le batterie annotate.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notifica di test inviata",
|
||||
"test_notification_failed": "Notifica di test non riuscita",
|
||||
"notify_per_person": "Consegna per persona",
|
||||
"member_avatars": "Avatar dei membri",
|
||||
"member_avatars_hint": "Mostrato accanto alle attività assegnate — iniziali nel colore del membro. Predefinito dal nome; imposta iniziali proprie o scegli un colore per distinguere i membri.",
|
||||
"member_initials": "Iniziali",
|
||||
"member_color": "Colore",
|
||||
"member_avatar_reset": "Ripristina",
|
||||
"notify_no_own_device": "Nessun dispositivo proprio — usa il servizio della casa",
|
||||
"settings_notify_due_soon": "Notifica quando in scadenza",
|
||||
"settings_notify_overdue": "Notifica quando scaduto",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Raggruppa per",
|
||||
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
|
||||
"target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).",
|
||||
"state_latch_help": "Con 1 transizione il trigger è un latch: si ripristina quando l'entità lascia lo stato di destinazione — oppure, se è impostato solo lo stato di partenza, quando vi ritorna.",
|
||||
"for_minutes_state_help": "0 conta ogni cambiamento subito. Con i minuti, il nuovo stato deve prima mantenersi — un breve sfarfallio allora non attiva né conta.",
|
||||
"qr_print_title": "Stampa codici QR",
|
||||
"qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Eliminare «{name}»?",
|
||||
"doc_too_large": "File troppo grande (max 25 MB).",
|
||||
"doc_upload_failed": "Caricamento non riuscito.",
|
||||
"completion_photo_optional": "Foto di completamento (facoltativa)",
|
||||
"add_photo": "Aggiungi foto",
|
||||
"completion_photos_optional": "Foto di completamento (facoltativo)",
|
||||
"completion_photos": "Foto di completamento",
|
||||
"choose_photos": "Scegli foto",
|
||||
"choose_photo": "Scegli foto",
|
||||
"photos_android_hint": "L'app Android passa una foto per selezione — ogni selezione viene aggiunta.",
|
||||
"add_photos": "Aggiungi foto",
|
||||
"photos_limit": "Fino a {max} foto per completamento",
|
||||
"history_edit_photos_hint": "Una foto rimossa qui resta nei documenti dell'oggetto.",
|
||||
"uploading": "Caricamento…",
|
||||
"remove": "Rimuovi",
|
||||
"doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Mostrata accanto al valore registrato al completamento di questa attività.",
|
||||
"reading_value_label": "Valore letto",
|
||||
"reading_label": "Lettura",
|
||||
"readings_section": "Letture",
|
||||
"readings_hint": "Più valori denominati per completamento — una riga per contatore. Lascia vuoto per un valore singolo.",
|
||||
"reading_name_label": "Nome lettura",
|
||||
"reading_unit_short": "Unità",
|
||||
"reading_add": "Aggiungi lettura",
|
||||
"reading_duplicate_name": "Nome già in uso",
|
||||
"reading_last": "ultimo: {value}",
|
||||
"reading_below_last": "Inferiore all'ultima lettura ({value})",
|
||||
"settings_templates_label": "Galleria modelli",
|
||||
"settings_templates_hint": "Deseleziona i modelli che non ti serviranno mai: spariscono dai selettori \"Da modello\" (pannello e config flow). Nient'altro cambia; riattivabili in qualsiasi momento.",
|
||||
"worksheet": "Scheda di lavoro",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "La tua regola di notifica",
|
||||
"settings_notify_rule_hint": "Ogni notifica genera anche l'evento maintenance_supporter_notification (tipo, stato, oggetto, attività, numeri di riferimento, priorità, link, payload). Instradalo con un'automazione — ad es. verso Ticker, Telegram o Pushover.",
|
||||
"settings_notify_event_only": "Genera solo l'evento, non inviare nulla",
|
||||
"settings_notify_event_only_hint": "La tua automazione gestisce l'intera consegna. Intervalli di ripetizione, ore di silenzio e limite giornaliero valgono ancora per quando scatta l'evento.",
|
||||
"settings_notify_extra_data": "Dati di notifica aggiuntivi (template)",
|
||||
"settings_notify_extra_data_hint": "JSON o YAML, Jinja consentito; unito ai data della chiamata notify (le tue chiavi vincono). Variabili: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Prova con il pulsante Invia test.",
|
||||
"card_saved_view": "Vista salvata",
|
||||
"card_saved_view_none": "Nessuna",
|
||||
"card_saved_view_help": "Applica i filtri stato, utente ed etichetta della vista in aggiunta ai filtri sopra. Ordinamento e raggruppamento della vista sono impostazioni di visualizzazione del pannello e non si applicano alla scheda.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Segna tutte sostituite",
|
||||
"battery_fleet_mark_one": "Segna questa batteria sostituita",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Nessun sensore",
|
||||
"battery_fleet_trigger_lost": "Il trigger del sensore di questa attività è andato perso — non si attiverà né si completerà automaticamente.",
|
||||
"battery_fleet_repair": "Ripara",
|
||||
"battery_fleet_exclude": "Escludi dal parco",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Scegli un sensore batteria sfuggito al rilevamento automatico — entra subito nell'elenco.",
|
||||
"battery_fleet_track_self": "Traccia le batterie autoricaricabili",
|
||||
"battery_fleet_track_self_hint": "Telefoni, aspirapolvere e altri dispositivi che si ricaricano da soli appaiono come ricaricabili — se scarichi chiedono una ricarica, mai batterie nuove.",
|
||||
"battery_fleet_due_without_sensor": "Considerare una previsione scaduta come in scadenza se una batteria non ha sensore",
|
||||
"battery_fleet_due_without_sensor_hint": "Alcuni dispositivi non riportano alcun livello di batteria — Battery Notes conosce solo il tipo e l'ultima sostituzione. Trascorsa la durata tipica, la batteria conta come scarica e l'attività del parco te lo ricorda; segnarla come sostituita azzera la previsione.",
|
||||
"battery_fleet_status_low": "Scarica",
|
||||
"battery_fleet_status_soon": "A breve",
|
||||
"battery_fleet_status_ok": "A posto",
|
||||
"battery_fleet_status_due": "In scadenza",
|
||||
"battery_fleet_predicted_on": "Previsto intorno al {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto dalla tendenza di scarica di questa batteria: intorno al {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ricaricabile: da ricaricare anziché sostituire — mai nella lista della spesa",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Libretto di manutenzione (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "voci",
|
||||
"ref_number": "Numero di riferimento",
|
||||
"print_options_title": "Stampa il libretto di servizio",
|
||||
"print_layout": "Impaginazione",
|
||||
"print_layout_chronological": "Cronologico",
|
||||
"print_layout_by_task": "Per attività",
|
||||
"print_include": "Includi",
|
||||
"print_inc_readings": "Letture",
|
||||
"print_inc_parts": "Ricambi usati",
|
||||
"print_inc_photos": "Foto",
|
||||
"print_inc_documents": "Documenti collegati",
|
||||
"print_inc_checklist": "Lista di controllo",
|
||||
"print_inc_notes": "Note",
|
||||
"print_inc_costs": "Costo e durata",
|
||||
"print_inc_person": "Eseguito da",
|
||||
"print_inc_refs": "Numeri di riferimento",
|
||||
"print_inc_qr": "Codice QR per attività",
|
||||
"print_button": "Stampa",
|
||||
"report_scan_hint": "Scansiona per aprire l'attività",
|
||||
"completed_by": "Completato da",
|
||||
"date_from": "Dal",
|
||||
"date_to": "Al",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 無効な値です",
|
||||
"feat_schedule_time": "時刻指定スケジュール",
|
||||
"feat_schedule_time_desc": "タスクが深夜0時ではなく指定した時刻に期限超過となります。",
|
||||
"schedule_time_optional": "期限の時刻(任意)",
|
||||
"schedule_time_toggle": "特定の時刻に期限",
|
||||
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
|
||||
"at_time": "時刻",
|
||||
"notes_optional": "メモ(任意)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— タグなし —",
|
||||
"dashboard": "ダッシュボード",
|
||||
"tab_today": "今日",
|
||||
"palette_placeholder": "オブジェクトとタスクを検索…",
|
||||
"palette_placeholder": "対象・タスク・部品・文書・メモを検索…",
|
||||
"palette_no_results": "一致なし",
|
||||
"palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる",
|
||||
"palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる · / でどこからでも検索",
|
||||
"search_open": "検索",
|
||||
"search_group_parts": "部品",
|
||||
"search_group_content": "文書内",
|
||||
"search_group_history": "履歴メモ",
|
||||
"search_page": "{page} ページ",
|
||||
"search_empty_hint": "入力して対象・タスク・部品・文書・メモを検索",
|
||||
"search_searching": "文書とメモを検索中…",
|
||||
"search_index_status": "全文検索: {total} 件中 {indexed} 件をインデックス済み · テキスト層なし {no_text} 件 · 保留 {pending} 件",
|
||||
"today_all_caught_up": "すべて完了!今週の予定はありません。",
|
||||
"today_overdue": "期限切れ",
|
||||
"today_due_today": "今日期限",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "消耗品「残量少」しきい値(%)",
|
||||
"settings_battery_low_percent": "電池「残量少」しきい値(%)",
|
||||
"settings_thresholds_hint": "新しい「推奨セットアップ」(残量パーセント)と、独自の Battery Notes しきい値を持たないバッテリー一覧の電池に適用される既定値です。既存のタスクは値を保持します。",
|
||||
"settings_value_out_of_range": "値は {min} から {max} の間で指定してください",
|
||||
"bn_summary": "{name}:{n} 個の電池に {pct} %",
|
||||
"bn_floor_decides": "あなたの {floor} % のしきい値がすべてに適用されます。電池ごとに高い方のしきい値が有効です。",
|
||||
"bn_above_floor": "これはあなたの {floor} % のしきい値を上回るため、記録済みのすべての電池は {name} が決定します。",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "テスト通知を送信しました",
|
||||
"test_notification_failed": "テスト通知に失敗しました",
|
||||
"notify_per_person": "メンバーごとの配信",
|
||||
"member_avatars": "メンバーのアバター",
|
||||
"member_avatars_hint": "割り当てられたタスクの横に表示されます — メンバーの色で頭文字を表示。既定は名前から生成。頭文字や色を設定してメンバーを区別できます。",
|
||||
"member_initials": "頭文字",
|
||||
"member_color": "色",
|
||||
"member_avatar_reset": "リセット",
|
||||
"notify_no_own_device": "個人の端末なし — 家庭用サービスを使用",
|
||||
"settings_notify_due_soon": "期限間近で通知",
|
||||
"settings_notify_overdue": "期限超過で通知",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "グループ化",
|
||||
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
|
||||
"target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。",
|
||||
"state_latch_help": "遷移回数が1の場合トリガーはラッチになります。エンティティが「後」の状態を離れると復帰し、「前」の状態のみ設定時はその状態に戻ると復帰します。",
|
||||
"for_minutes_state_help": "0 はすべての変化を即座にカウントします。分を設定すると新しい状態がその時間続く必要があり、一瞬の変動はトリガーもカウントもされません。",
|
||||
"qr_print_title": "QRコードを印刷",
|
||||
"qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "「{name}」を削除しますか?",
|
||||
"doc_too_large": "ファイルが大きすぎます(最大 25 MB)。",
|
||||
"doc_upload_failed": "アップロードに失敗しました。",
|
||||
"completion_photo_optional": "完了写真(任意)",
|
||||
"add_photo": "写真を追加",
|
||||
"completion_photos_optional": "完了写真(任意)",
|
||||
"completion_photos": "完了写真",
|
||||
"choose_photos": "写真を選択",
|
||||
"choose_photo": "写真を選択",
|
||||
"photos_android_hint": "Android アプリは1回の選択につき写真1枚を渡します。選択するたびに追加されます。",
|
||||
"add_photos": "写真を追加",
|
||||
"photos_limit": "1回の完了につき最大{max}枚",
|
||||
"history_edit_photos_hint": "ここで外した写真は対象のドキュメントに残ります。",
|
||||
"uploading": "アップロード中…",
|
||||
"remove": "削除",
|
||||
"doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "このタスクの完了時に記録値の横に表示されます。",
|
||||
"reading_value_label": "検針値",
|
||||
"reading_label": "検針",
|
||||
"readings_section": "検針項目",
|
||||
"readings_hint": "1回の完了で複数の名前付き値を記録 — メーターごとに1行。単一の値だけなら空のままにします。",
|
||||
"reading_name_label": "検針項目名",
|
||||
"reading_unit_short": "単位",
|
||||
"reading_add": "検針項目を追加",
|
||||
"reading_duplicate_name": "この名前は既に使われています",
|
||||
"reading_last": "前回: {value}",
|
||||
"reading_below_last": "前回の値({value})より小さい",
|
||||
"settings_templates_label": "テンプレートギャラリー",
|
||||
"settings_templates_hint": "不要なテンプレートのチェックを外すと、「テンプレートから」の選択肢(パネルと設定フロー)から消えます。それ以外は何も変わらず、いつでも再有効化できます。",
|
||||
"worksheet": "作業シート",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "このビューのみ通知",
|
||||
"settings_notify_scope_all": "すべてのタスク",
|
||||
"settings_notify_scope_hint": "選択した保存済みビューのラベル/ユーザーフィルターに一致するタスクのみがリマインダーを送信します。ビューのステータス・並び順・グループ化はここでは無視されます。",
|
||||
"settings_notify_rule": "独自の通知ルール",
|
||||
"settings_notify_rule_hint": "各通知はイベント maintenance_supporter_notification(種類、状態、対象、タスク、参照番号、優先度、ディープリンク、ペイロード)も発火します。オートメーションで Ticker、Telegram、Pushover などへ転送してください。",
|
||||
"settings_notify_event_only": "イベントのみ発火し、自身では送信しない",
|
||||
"settings_notify_event_only_hint": "配信はすべてオートメーション側で行います。繰り返し間隔、静音時間、1日の上限は引き続きイベント発火の条件になります。",
|
||||
"settings_notify_extra_data": "追加の通知データ(テンプレート)",
|
||||
"settings_notify_extra_data_hint": "JSON または YAML、Jinja 可。notify 呼び出しの data にマージされます(あなたのキーが優先)。変数: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message。「テスト送信」ボタンで確認できます。",
|
||||
"card_saved_view": "保存済みビュー",
|
||||
"card_saved_view_none": "なし",
|
||||
"card_saved_view_help": "上記のフィルターに加えて、ビューのステータス・ユーザー・ラベルのフィルターを適用します。ビューの並び順とグループ化はパネルの表示設定であり、カードには適用されません。",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "すべて交換済みにする",
|
||||
"battery_fleet_mark_one": "この電池を交換済みにする",
|
||||
"battery_fleet_offline": "オフライン",
|
||||
"battery_fleet_no_sensor": "センサーなし",
|
||||
"battery_fleet_trigger_lost": "このタスクのセンサートリガーが失われました — 発動も自動完了もしません。",
|
||||
"battery_fleet_repair": "修復",
|
||||
"battery_fleet_exclude": "フリートから除外",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "自動検出が見逃した電池センサーを選択すると、すぐに一覧に追加されます。",
|
||||
"battery_fleet_track_self": "自己充電式バッテリーを追跡",
|
||||
"battery_fleet_track_self_hint": "スマートフォンや掃除機など自己充電するデバイスは充電式として表示されます。残量が少ないときは充電を促し、電池交換は求めません。",
|
||||
"battery_fleet_due_without_sensor": "センサーのない電池は、予測日を過ぎたら交換時期として扱う",
|
||||
"battery_fleet_due_without_sensor_hint": "電池残量をまったく報告しないデバイスもあります。Battery Notes は種類と最終交換日しか知りません。一般的な寿命を過ぎると、その電池は残量低下として扱われ、一括タスクが通知します。交換済みにすると予測がリセットされます。",
|
||||
"battery_fleet_status_low": "残量低下",
|
||||
"battery_fleet_status_soon": "まもなく",
|
||||
"battery_fleet_status_ok": "良好",
|
||||
"battery_fleet_status_due": "交換時期",
|
||||
"battery_fleet_predicted_on": "{date} 頃の見込み",
|
||||
"battery_fleet_predicted_trend": "この電池の放電傾向から予測: {date} 頃 ({confidence})",
|
||||
"battery_fleet_rechargeable": "充電式:交換ではなく充電——買い物リストには載りません",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "整備記録(PDF)",
|
||||
"date": "日付",
|
||||
"service_record_entries": "件",
|
||||
"ref_number": "参照番号",
|
||||
"print_options_title": "整備記録を印刷",
|
||||
"print_layout": "レイアウト",
|
||||
"print_layout_chronological": "時系列",
|
||||
"print_layout_by_task": "タスク別",
|
||||
"print_include": "含める",
|
||||
"print_inc_readings": "検針値",
|
||||
"print_inc_parts": "使用部品",
|
||||
"print_inc_photos": "写真",
|
||||
"print_inc_documents": "関連文書",
|
||||
"print_inc_checklist": "チェックリスト",
|
||||
"print_inc_notes": "メモ",
|
||||
"print_inc_costs": "費用と所要時間",
|
||||
"print_inc_person": "実施者",
|
||||
"print_inc_refs": "参照番号",
|
||||
"print_inc_qr": "タスクごとのQRコード",
|
||||
"print_button": "印刷",
|
||||
"report_scan_hint": "スキャンしてタスクを開く",
|
||||
"completed_by": "実施者",
|
||||
"date_from": "開始",
|
||||
"date_to": "終了",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 유효하지 않은 값입니다",
|
||||
"feat_schedule_time": "시각 지정 일정",
|
||||
"feat_schedule_time_desc": "작업이 자정이 아닌 지정한 시각에 기한 초과로 전환됩니다.",
|
||||
"schedule_time_optional": "기한 시각 (선택)",
|
||||
"schedule_time_toggle": "특정 시각에 기한",
|
||||
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
|
||||
"at_time": "시각",
|
||||
"notes_optional": "메모 (선택)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— 태그 없음 —",
|
||||
"dashboard": "대시보드",
|
||||
"tab_today": "오늘",
|
||||
"palette_placeholder": "객체와 작업 검색…",
|
||||
"palette_placeholder": "대상, 작업, 부품, 문서, 메모 검색…",
|
||||
"palette_no_results": "일치 항목 없음",
|
||||
"palette_hint": "↑↓ 이동 · Enter 열기 · Esc 닫기",
|
||||
"palette_hint": "↑↓ 이동 · Enter 열기 · Esc 닫기 · / 어디서든 검색 열기",
|
||||
"search_open": "검색",
|
||||
"search_group_parts": "부품",
|
||||
"search_group_content": "문서 내용",
|
||||
"search_group_history": "이력 메모",
|
||||
"search_page": "{page}페이지",
|
||||
"search_empty_hint": "대상, 작업, 부품, 문서, 메모를 검색하려면 입력하세요",
|
||||
"search_searching": "문서와 메모 검색 중…",
|
||||
"search_index_status": "전체 텍스트 검색: {total}개 중 {indexed}개 색인됨 · 텍스트 레이어 없음 {no_text}개 · 대기 {pending}개",
|
||||
"today_all_caught_up": "모두 처리했습니다! 이번 주에 예정된 작업이 없습니다.",
|
||||
"today_overdue": "기한 초과",
|
||||
"today_due_today": "오늘 예정",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "소모품 „부족“ 임계값 (%)",
|
||||
"settings_battery_low_percent": "배터리 „부족“ 임계값 (%)",
|
||||
"settings_thresholds_hint": "새 추천 설정(남은 비율)과 자체 Battery Notes 임계값이 없는 배터리 목록 배터리의 기본값입니다. 기존 작업은 값을 유지합니다.",
|
||||
"settings_value_out_of_range": "값은 {min}에서 {max} 사이여야 합니다",
|
||||
"bn_summary": "{name}: 배터리 {n}개에 {pct} %",
|
||||
"bn_floor_decides": "당신의 {floor} % 임계값이 모두에 적용됩니다. 배터리마다 두 임계값 중 높은 쪽이 적용됩니다.",
|
||||
"bn_above_floor": "이는 당신의 {floor} % 임계값보다 높으므로, 기록된 모든 배터리는 {name}이(가) 결정합니다.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "테스트 알림을 보냈습니다",
|
||||
"test_notification_failed": "테스트 알림 전송 실패",
|
||||
"notify_per_person": "사용자별 전송",
|
||||
"member_avatars": "구성원 아바타",
|
||||
"member_avatars_hint": "할당된 작업 옆에 표시됩니다 — 구성원 색상의 이니셜. 기본값은 이름에서 가져오며, 구성원을 구분하려면 이니셜을 직접 설정하거나 색상을 선택하세요.",
|
||||
"member_initials": "이니셜",
|
||||
"member_color": "색상",
|
||||
"member_avatar_reset": "초기화",
|
||||
"notify_no_own_device": "개인 기기 없음 — 가정 서비스 사용",
|
||||
"settings_notify_due_soon": "기한 임박 시 알림",
|
||||
"settings_notify_overdue": "기한 초과 시 알림",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "그룹화",
|
||||
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
|
||||
"target_changes_help": "트리거가 발동하기까지 필요한 일치 전환 횟수입니다 (기본값: 1).",
|
||||
"state_latch_help": "전환 1회로 설정하면 트리거는 래치가 됩니다. 엔티티가 '이후' 상태를 벗어나면 복구되고, '이전' 상태만 설정한 경우 그 상태로 돌아오면 복구됩니다.",
|
||||
"for_minutes_state_help": "0은 모든 변화를 즉시 집계합니다. 분을 설정하면 새 상태가 먼저 그 시간 동안 유지되어야 하며, 짧은 깜빡임은 트리거되지도 집계되지도 않습니다.",
|
||||
"qr_print_title": "QR 코드 인쇄",
|
||||
"qr_print_desc": "잘라서 장비에 붙일 수 있는 QR 코드 인쇄 페이지를 생성합니다.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "\"{name}\"을(를) 삭제할까요?",
|
||||
"doc_too_large": "파일이 너무 큽니다 (최대 25 MB).",
|
||||
"doc_upload_failed": "업로드에 실패했습니다.",
|
||||
"completion_photo_optional": "완료 사진 (선택)",
|
||||
"add_photo": "사진 추가",
|
||||
"completion_photos_optional": "완료 사진 (선택)",
|
||||
"completion_photos": "완료 사진",
|
||||
"choose_photos": "사진 선택",
|
||||
"choose_photo": "사진 선택",
|
||||
"photos_android_hint": "Android 앱은 선택할 때마다 사진 한 장씩 전달합니다. 선택할 때마다 추가됩니다.",
|
||||
"add_photos": "사진 추가",
|
||||
"photos_limit": "완료당 최대 {max}장",
|
||||
"history_edit_photos_hint": "여기서 제거한 사진은 대상의 문서에 그대로 남습니다.",
|
||||
"uploading": "업로드 중…",
|
||||
"remove": "제거",
|
||||
"doc_deduped": "이미 다른 곳에 저장됨 — 공유되어 추가 공간을 쓰지 않습니다.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "이 작업을 완료할 때 기록 값 옆에 표시됩니다.",
|
||||
"reading_value_label": "검침 값",
|
||||
"reading_label": "검침",
|
||||
"readings_section": "검침 항목",
|
||||
"readings_hint": "완료당 여러 개의 이름 있는 값 — 계량기마다 한 줄. 단일 값만 기록하려면 비워 두세요.",
|
||||
"reading_name_label": "검침 항목 이름",
|
||||
"reading_unit_short": "단위",
|
||||
"reading_add": "검침 항목 추가",
|
||||
"reading_duplicate_name": "이미 사용 중인 이름입니다",
|
||||
"reading_last": "지난번: {value}",
|
||||
"reading_below_last": "지난 검침값({value})보다 작습니다",
|
||||
"settings_templates_label": "템플릿 갤러리",
|
||||
"settings_templates_hint": "필요 없는 템플릿의 체크를 해제하세요 — \"템플릿에서\" 선택 목록(패널 및 설정 흐름)에서 사라집니다. 다른 변화는 없으며 언제든 다시 활성화할 수 있습니다.",
|
||||
"worksheet": "작업 시트",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "특정 보기만 알림",
|
||||
"settings_notify_scope_all": "모든 작업",
|
||||
"settings_notify_scope_hint": "선택한 저장된 보기의 라벨/사용자 필터와 일치하는 작업만 알림을 보냅니다. 보기의 상태·정렬·그룹화는 여기서 무시됩니다.",
|
||||
"settings_notify_rule": "나만의 알림 규칙",
|
||||
"settings_notify_rule_hint": "모든 알림은 maintenance_supporter_notification 이벤트도 발생시킵니다(종류, 상태, 대상, 작업, 참조 번호, 우선순위, 딥 링크, 페이로드). 자동화로 라우팅하세요 — 예: Ticker, Telegram, Pushover.",
|
||||
"settings_notify_event_only": "이벤트만 발생시키고 직접 보내지 않음",
|
||||
"settings_notify_event_only_hint": "전달은 전적으로 자동화가 담당합니다. 반복 간격, 방해 금지 시간, 일일 한도는 여전히 이벤트 발생 시점에 적용됩니다.",
|
||||
"settings_notify_extra_data": "추가 알림 데이터(템플릿)",
|
||||
"settings_notify_extra_data_hint": "JSON 또는 YAML, Jinja 사용 가능. notify 호출의 data에 병합됩니다(사용자 키 우선). 변수: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. 테스트 보내기 버튼으로 확인하세요.",
|
||||
"card_saved_view": "저장된 보기",
|
||||
"card_saved_view_none": "없음",
|
||||
"card_saved_view_help": "위 필터에 더해 보기의 상태·사용자·라벨 필터를 적용합니다. 보기의 정렬과 그룹화는 패널 표시 설정이며 카드에는 적용되지 않습니다.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "모두 교체됨으로 표시",
|
||||
"battery_fleet_mark_one": "이 배터리를 교체됨으로 표시",
|
||||
"battery_fleet_offline": "오프라인",
|
||||
"battery_fleet_no_sensor": "센서 없음",
|
||||
"battery_fleet_trigger_lost": "이 작업의 센서 트리거가 사라졌습니다 — 트리거도 자동 완료도 되지 않습니다.",
|
||||
"battery_fleet_repair": "복구",
|
||||
"battery_fleet_exclude": "플릿에서 제외",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "자동 검색이 놓친 배터리 센서를 선택하면 즉시 목록에 추가됩니다.",
|
||||
"battery_fleet_track_self": "자체 충전 배터리 추적",
|
||||
"battery_fleet_track_self_hint": "휴대폰, 청소기 등 스스로 충전되는 기기는 충전식으로 표시됩니다. 잔량이 낮으면 충전을 요청하며 새 전지를 요구하지 않습니다.",
|
||||
"battery_fleet_due_without_sensor": "센서가 없는 배터리는 예측일이 지나면 교체 시기로 간주",
|
||||
"battery_fleet_due_without_sensor_hint": "일부 기기는 배터리 잔량을 전혀 보고하지 않습니다. Battery Notes는 종류와 마지막 교체일만 알고 있습니다. 일반적인 수명이 지나면 그 배터리는 부족한 것으로 간주되어 배터리 관리 작업이 알려 줍니다. 교체됨으로 표시하면 예측이 초기화됩니다.",
|
||||
"battery_fleet_status_low": "부족",
|
||||
"battery_fleet_status_soon": "곧",
|
||||
"battery_fleet_status_ok": "정상",
|
||||
"battery_fleet_status_due": "교체 시기",
|
||||
"battery_fleet_predicted_on": "{date}쯤 예상",
|
||||
"battery_fleet_predicted_trend": "이 배터리의 방전 추세로 예측: 약 {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "충전식: 교체 대신 충전 — 쇼핑 목록에 오르지 않습니다",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "정비 기록(PDF)",
|
||||
"date": "날짜",
|
||||
"service_record_entries": "건",
|
||||
"ref_number": "참조 번호",
|
||||
"print_options_title": "정비 기록 인쇄",
|
||||
"print_layout": "레이아웃",
|
||||
"print_layout_chronological": "시간순",
|
||||
"print_layout_by_task": "작업별",
|
||||
"print_include": "포함",
|
||||
"print_inc_readings": "측정값",
|
||||
"print_inc_parts": "사용 부품",
|
||||
"print_inc_photos": "사진",
|
||||
"print_inc_documents": "연결된 문서",
|
||||
"print_inc_checklist": "체크리스트",
|
||||
"print_inc_notes": "메모",
|
||||
"print_inc_costs": "비용 및 소요 시간",
|
||||
"print_inc_person": "완료자",
|
||||
"print_inc_refs": "참조 번호",
|
||||
"print_inc_qr": "작업별 QR 코드",
|
||||
"print_button": "인쇄",
|
||||
"report_scan_hint": "스캔하여 작업 열기",
|
||||
"completed_by": "수행자",
|
||||
"date_from": "시작",
|
||||
"date_to": "종료",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ugyldig verdi",
|
||||
"feat_schedule_time": "Planlegging med klokkeslett",
|
||||
"feat_schedule_time_desc": "Oppgaver forfaller på et bestemt klokkeslett i stedet for ved midnatt.",
|
||||
"schedule_time_optional": "Forfaller kl. (valgfritt)",
|
||||
"schedule_time_toggle": "Forfaller på et bestemt klokkeslett",
|
||||
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Notater (valgfritt)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Ingen brikke —",
|
||||
"dashboard": "Dashbord",
|
||||
"tab_today": "I dag",
|
||||
"palette_placeholder": "Søk objekter og oppgaver…",
|
||||
"palette_placeholder": "Søk objekter, oppgaver, reservedeler, dokumenter, notater…",
|
||||
"palette_no_results": "Ingen treff",
|
||||
"palette_hint": "↑↓ naviger · Enter åpne · Esc lukk",
|
||||
"palette_hint": "↑↓ naviger · Enter åpne · Esc lukk · / åpner søket overalt",
|
||||
"search_open": "Søk",
|
||||
"search_group_parts": "Reservedeler",
|
||||
"search_group_content": "I dokumenter",
|
||||
"search_group_history": "Historikknotater",
|
||||
"search_page": "Side {page}",
|
||||
"search_empty_hint": "Skriv for å søke i objekter, oppgaver, reservedeler, dokumenter og notater",
|
||||
"search_searching": "Søker i dokumenter og notater…",
|
||||
"search_index_status": "Fulltekstsøk: {indexed} av {total} filer indeksert · {no_text} uten tekstlag · {pending} venter",
|
||||
"today_all_caught_up": "Alt er ajour! Ingenting forfaller denne uken.",
|
||||
"today_overdue": "Forfalt",
|
||||
"today_due_today": "Forfaller i dag",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Terskel „forbruksvare lav“ (%)",
|
||||
"settings_battery_low_percent": "Terskel „batteri lavt“ (%)",
|
||||
"settings_thresholds_hint": "Standardverdier for nye Foreslåtte oppsett (gjenværende prosent) og for flåtebatterier uten egen Battery Notes-terskel. Eksisterende oppgaver beholder verdien sin.",
|
||||
"settings_value_out_of_range": "Verdien må være mellom {min} og {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batterier",
|
||||
"bn_floor_decides": "Din terskel på {floor} % avgjør for alle; per batteri gjelder den høyeste av de to tersklene.",
|
||||
"bn_above_floor": "Det er over din terskel på {floor} %, så {name} avgjør for alle noterte batterier.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testvarsel sendt",
|
||||
"test_notification_failed": "Testvarsel mislyktes",
|
||||
"notify_per_person": "Levering per person",
|
||||
"member_avatars": "Medlemmenes avatarer",
|
||||
"member_avatars_hint": "Vises ved siden av tildelte oppgaver — initialer i medlemmets farge. Standard kommer fra navnet; angi egne initialer eller velg en farge for å skille medlemmer.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Farge",
|
||||
"member_avatar_reset": "Tilbakestill",
|
||||
"notify_no_own_device": "Ingen egen enhet — bruker husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Varsle når noe forfaller snart",
|
||||
"settings_notify_overdue": "Varsle når noe er forfalt",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Grupper etter",
|
||||
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).",
|
||||
"state_latch_help": "Med 1 overgang er utløseren en lås: den tilbakestilles når entiteten forlater Til-tilstanden — eller, hvis bare en Fra-tilstand er satt, når den går tilbake til den.",
|
||||
"for_minutes_state_help": "0 teller hver endring umiddelbart. Med minutter må den nye tilstanden først holde seg — korte blink verken utløser eller telles da.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Slette \"{name}\"?",
|
||||
"doc_too_large": "Filen er for stor (maks. 25 MB).",
|
||||
"doc_upload_failed": "Opplasting mislyktes.",
|
||||
"completion_photo_optional": "Fullføringsbilde (valgfritt)",
|
||||
"add_photo": "Legg til bilde",
|
||||
"completion_photos_optional": "Bilder ved fullføring (valgfritt)",
|
||||
"completion_photos": "Bilder ved fullføring",
|
||||
"choose_photos": "Velg bilder",
|
||||
"choose_photo": "Velg bilde",
|
||||
"photos_android_hint": "Android-appen leverer ett bilde per valg — hvert valg legges til.",
|
||||
"add_photos": "Legg til bilder",
|
||||
"photos_limit": "Inntil {max} bilder per fullføring",
|
||||
"history_edit_photos_hint": "Et bilde som fjernes her, blir liggende i objektets dokumenter.",
|
||||
"uploading": "Laster opp…",
|
||||
"remove": "Fjern",
|
||||
"doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Vises ved siden av den registrerte verdien når oppgaven fullføres.",
|
||||
"reading_value_label": "Avlest verdi",
|
||||
"reading_label": "Avlesning",
|
||||
"readings_section": "Avlesninger",
|
||||
"readings_hint": "Flere navngitte verdier per fullføring — én rad per måler. La stå tomt for én enkelt verdi.",
|
||||
"reading_name_label": "Avlesningens navn",
|
||||
"reading_unit_short": "Enhet",
|
||||
"reading_add": "Legg til avlesning",
|
||||
"reading_duplicate_name": "Navnet er allerede i bruk",
|
||||
"reading_last": "sist: {value}",
|
||||
"reading_below_last": "Lavere enn forrige avlesning ({value})",
|
||||
"settings_templates_label": "Malgalleri",
|
||||
"settings_templates_hint": "Fjern haken for maler du aldri trenger — de forsvinner fra \"Fra mal\"-velgerne (panel og config flow). Ellers endres ingenting; kan aktiveres igjen når som helst.",
|
||||
"worksheet": "Arbeidsark",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen varslingsregel",
|
||||
"settings_notify_rule_hint": "Hvert varsel utløser også hendelsen maintenance_supporter_notification (type, status, objekt, oppgave, referansenumre, prioritet, lenke, nyttelast). Rut den med en automasjon — f.eks. til Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Bare utløs hendelsen, send ingenting selv",
|
||||
"settings_notify_event_only_hint": "Automasjonen din tar hele leveransen. Gjentakelsesintervaller, stille timer og dagstaket avgjør fortsatt når hendelsen utløses.",
|
||||
"settings_notify_extra_data": "Ekstra varslingsdata (mal)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tillatt; flettes inn i notify-kallets data (dine nøkler vinner). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test med knappen Send test.",
|
||||
"card_saved_view": "Lagret visning",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Bruker visningens status-, bruker- og etikettfiltre i tillegg til filtrene over. Visningens sortering og gruppering er panelvisningsinnstillinger og brukes ikke på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Merk alle byttet",
|
||||
"battery_fleet_mark_one": "Merk dette batteriet byttet",
|
||||
"battery_fleet_offline": "frakoblet",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Oppgavens sensorutløser er tapt — den utløses ikke og fullføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparer",
|
||||
"battery_fleet_exclude": "Utelukk fra flåten",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Velg en batterisensor som den automatiske gjenkjenningen overså — den blir med i listen umiddelbart.",
|
||||
"battery_fleet_track_self": "Spor selvladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvladende enheter vises som oppladbare — ved lavt nivå bes det om lading, aldri om nye batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandle en passert prognose som forfalt når et batteri mangler sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Noen enheter rapporterer ikke batterinivå i det hele tatt — Battery Notes kjenner bare typen og siste bytte. Når den typiske levetiden er passert, regnes et slikt batteri som lavt og flåteoppgaven minner deg på det; å merke det som byttet nullstiller prognosen.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_status_due": "Forfalt",
|
||||
"battery_fleet_predicted_on": "Forventes rundt {date}",
|
||||
"battery_fleet_predicted_trend": "Forutsagt ut fra batteriets utladingstrend: rundt {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oppladbart: lad i stedet for å bytte — aldri på handlelisten",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicehefte (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "oppføringer",
|
||||
"ref_number": "Referansenummer",
|
||||
"print_options_title": "Skriv ut servicehefte",
|
||||
"print_layout": "Oppsett",
|
||||
"print_layout_chronological": "Kronologisk",
|
||||
"print_layout_by_task": "Per oppgave",
|
||||
"print_include": "Ta med",
|
||||
"print_inc_readings": "Avlesninger",
|
||||
"print_inc_parts": "Brukte deler",
|
||||
"print_inc_photos": "Bilder",
|
||||
"print_inc_documents": "Tilknyttede dokumenter",
|
||||
"print_inc_checklist": "Sjekkliste",
|
||||
"print_inc_notes": "Notater",
|
||||
"print_inc_costs": "Kostnad og varighet",
|
||||
"print_inc_person": "Utført av",
|
||||
"print_inc_refs": "Referansenumre",
|
||||
"print_inc_qr": "QR-kode per oppgave",
|
||||
"print_button": "Skriv ut",
|
||||
"report_scan_hint": "Skann for å åpne oppgaven",
|
||||
"completed_by": "Utført av",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: ongeldige waarde",
|
||||
"feat_schedule_time": "Tijd-van-dag-planning",
|
||||
"feat_schedule_time_desc": "Taken vervallen op een specifieke tijd in plaats van middernacht.",
|
||||
"schedule_time_optional": "Vervaldagstijd (optioneel)",
|
||||
"schedule_time_toggle": "Vervalt op een vast tijdstip",
|
||||
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
|
||||
"at_time": "om",
|
||||
"notes_optional": "Notities (optioneel)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Geen tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Vandaag",
|
||||
"palette_placeholder": "Objecten en taken zoeken…",
|
||||
"palette_placeholder": "Objecten, taken, onderdelen, documenten, notities zoeken…",
|
||||
"palette_no_results": "Geen resultaten",
|
||||
"palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten",
|
||||
"palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten · / opent de zoekfunctie overal",
|
||||
"search_open": "Zoeken",
|
||||
"search_group_parts": "Onderdelen",
|
||||
"search_group_content": "In documenten",
|
||||
"search_group_history": "Notities in historie",
|
||||
"search_page": "Pagina {page}",
|
||||
"search_empty_hint": "Typ om objecten, taken, onderdelen, documenten en notities te doorzoeken",
|
||||
"search_searching": "Documenten en notities doorzoeken…",
|
||||
"search_index_status": "Volledige tekst zoeken: {indexed} van {total} bestanden geïndexeerd · {no_text} zonder tekstlaag · {pending} in wachtrij",
|
||||
"today_all_caught_up": "Helemaal bij! Niets te doen deze week.",
|
||||
"today_overdue": "Achterstallig",
|
||||
"today_due_today": "Vandaag te doen",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Drempel „verbruiksartikel laag“ (%)",
|
||||
"settings_battery_low_percent": "Drempel „batterij laag“ (%)",
|
||||
"settings_thresholds_hint": "Standaardwaarden voor nieuwe Voorgestelde installaties (resterend percentage) en voor vlootbatterijen zonder eigen Battery Notes-drempel. Bestaande taken behouden hun waarde.",
|
||||
"settings_value_out_of_range": "De waarde moet tussen {min} en {max} liggen",
|
||||
"bn_summary": "{name}: {pct} % voor {n} batterijen",
|
||||
"bn_floor_decides": "Jouw drempel van {floor} % beslist voor allemaal; per batterij telt de hoogste van de twee drempels.",
|
||||
"bn_above_floor": "Dat ligt boven jouw drempel van {floor} %, dus {name} beslist voor alle genoteerde batterijen.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testmelding verzonden",
|
||||
"test_notification_failed": "Testmelding mislukt",
|
||||
"notify_per_person": "Bezorging per persoon",
|
||||
"member_avatars": "Avatars van leden",
|
||||
"member_avatars_hint": "Wordt naast toegewezen taken getoond — initialen in de kleur van het lid. Standaard uit de naam; kies eigen initialen of een kleur om leden uit elkaar te houden.",
|
||||
"member_initials": "Initialen",
|
||||
"member_color": "Kleur",
|
||||
"member_avatar_reset": "Herstellen",
|
||||
"notify_no_own_device": "Geen eigen apparaat — gebruikt de huishoudelijke dienst",
|
||||
"settings_notify_due_soon": "Melding bij bijna verlopen",
|
||||
"settings_notify_overdue": "Melding bij achterstallig",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Groeperen op",
|
||||
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
|
||||
"target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).",
|
||||
"state_latch_help": "Bij 1 overgang werkt de trigger als vergrendeling: hij herstelt zodra de entiteit de Naar-status verlaat — of, als alleen een Van-status is ingesteld, zodra ze daarnaar terugkeert.",
|
||||
"for_minutes_state_help": "0 telt elke wijziging direct. Met minuten moet de nieuwe status eerst zo lang aanhouden — kort flikkeren triggert dan niet en telt niet mee.",
|
||||
"qr_print_title": "QR-codes afdrukken",
|
||||
"qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" verwijderen?",
|
||||
"doc_too_large": "Bestand is te groot (max. 25 MB).",
|
||||
"doc_upload_failed": "Uploaden mislukt.",
|
||||
"completion_photo_optional": "Voltooiingsfoto (optioneel)",
|
||||
"add_photo": "Foto toevoegen",
|
||||
"completion_photos_optional": "Foto's bij afronding (optioneel)",
|
||||
"completion_photos": "Foto's bij afronding",
|
||||
"choose_photos": "Foto's kiezen",
|
||||
"choose_photo": "Foto kiezen",
|
||||
"photos_android_hint": "De Android-app geeft één foto per keuze door — elke keuze wordt toegevoegd.",
|
||||
"add_photos": "Foto's toevoegen",
|
||||
"photos_limit": "Maximaal {max} foto's per afronding",
|
||||
"history_edit_photos_hint": "Een hier verwijderde foto blijft bewaard in de documenten van het object.",
|
||||
"uploading": "Uploaden…",
|
||||
"remove": "Weghalen",
|
||||
"doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wordt naast de vastgelegde waarde getoond bij het afronden van deze taak.",
|
||||
"reading_value_label": "Meterstand",
|
||||
"reading_label": "Meting",
|
||||
"readings_section": "Meterstanden",
|
||||
"readings_hint": "Meerdere benoemde waarden per afronding — één regel per meter. Leeg laten voor één enkele waarde.",
|
||||
"reading_name_label": "Naam meterstand",
|
||||
"reading_unit_short": "Eenheid",
|
||||
"reading_add": "Meterstand toevoegen",
|
||||
"reading_duplicate_name": "Naam al in gebruik",
|
||||
"reading_last": "laatst: {value}",
|
||||
"reading_below_last": "Lager dan de vorige meterstand ({value})",
|
||||
"settings_templates_label": "Sjabloongalerij",
|
||||
"settings_templates_hint": "Vink sjablonen uit die je nooit nodig hebt — ze verdwijnen uit de \"Vanuit sjabloon\"-keuzes (paneel en config flow). Verder verandert er niets; altijd weer in te schakelen.",
|
||||
"worksheet": "Werkblad",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Eigen meldingsregel",
|
||||
"settings_notify_rule_hint": "Elke melding vuurt ook het event maintenance_supporter_notification af (soort, status, object, taak, referentienummers, prioriteit, deep link, payload). Stuur het door met een automatisering — bijv. naar Ticker, Telegram of Pushover.",
|
||||
"settings_notify_event_only": "Alleen het event afvuren, zelf niets verzenden",
|
||||
"settings_notify_event_only_hint": "Je automatisering verzorgt de volledige bezorging. Herhaalintervallen, stille uren en het daglimiet bepalen nog steeds wanneer het event afgaat.",
|
||||
"settings_notify_extra_data": "Extra meldingsgegevens (sjabloon)",
|
||||
"settings_notify_extra_data_hint": "JSON of YAML, Jinja toegestaan; wordt samengevoegd met de data van de notify-aanroep (jouw sleutels winnen). Variabelen: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test met de knop Test verzenden.",
|
||||
"card_saved_view": "Opgeslagen weergave",
|
||||
"card_saved_view_none": "Geen",
|
||||
"card_saved_view_help": "Past de status-, gebruikers- en labelfilters van de weergave toe bovenop de bovenstaande filters. Sortering en groepering van de weergave zijn paneelweergave-instellingen en gelden niet voor de kaart.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Alles gemarkeerd als vervangen",
|
||||
"battery_fleet_mark_one": "Deze batterij als vervangen markeren",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Geen sensor",
|
||||
"battery_fleet_trigger_lost": "De sensortrigger van deze taak is verloren gegaan — deze wordt niet geactiveerd of automatisch voltooid.",
|
||||
"battery_fleet_repair": "Repareren",
|
||||
"battery_fleet_exclude": "Uitsluiten van de vloot",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Kies een batterijsensor die de automatische detectie heeft gemist — hij verschijnt direct in de lijst.",
|
||||
"battery_fleet_track_self": "Zelfopladende batterijen volgen",
|
||||
"battery_fleet_track_self_hint": "Telefoons, stofzuigers en andere zelfopladende apparaten verschijnen als oplaadbaar — bij een lage stand wordt om opladen gevraagd, nooit om nieuwe batterijen.",
|
||||
"battery_fleet_due_without_sensor": "Een verstreken prognose als aan de beurt beschouwen als een batterij geen sensor heeft",
|
||||
"battery_fleet_due_without_sensor_hint": "Sommige apparaten melden helemaal geen batterijniveau — Battery Notes kent alleen het type en de laatste vervanging. Zodra de gebruikelijke levensduur is verstreken, telt zo'n batterij als leeg en herinnert de vloottaak je eraan; als vervangen markeren zet de prognose terug.",
|
||||
"battery_fleet_status_low": "Laag",
|
||||
"battery_fleet_status_soon": "Binnenkort",
|
||||
"battery_fleet_status_ok": "In orde",
|
||||
"battery_fleet_status_due": "Aan de beurt",
|
||||
"battery_fleet_predicted_on": "Verwacht rond {date}",
|
||||
"battery_fleet_predicted_trend": "Voorspeld uit de ontladingstrend van deze batterij: rond {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oplaadbaar: opladen in plaats van vervangen — nooit op het boodschappenlijstje",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Onderhoudsboekje (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "items",
|
||||
"ref_number": "Referentienummer",
|
||||
"print_options_title": "Serviceoverzicht afdrukken",
|
||||
"print_layout": "Indeling",
|
||||
"print_layout_chronological": "Chronologisch",
|
||||
"print_layout_by_task": "Per taak",
|
||||
"print_include": "Opnemen",
|
||||
"print_inc_readings": "Meterstanden",
|
||||
"print_inc_parts": "Gebruikte onderdelen",
|
||||
"print_inc_photos": "Foto's",
|
||||
"print_inc_documents": "Gekoppelde documenten",
|
||||
"print_inc_checklist": "Controlelijst",
|
||||
"print_inc_notes": "Notities",
|
||||
"print_inc_costs": "Kosten en duur",
|
||||
"print_inc_person": "Uitgevoerd door",
|
||||
"print_inc_refs": "Referentienummers",
|
||||
"print_inc_qr": "QR-code per taak",
|
||||
"print_button": "Afdrukken",
|
||||
"report_scan_hint": "Scan om de taak te openen",
|
||||
"completed_by": "Voltooid door",
|
||||
"date_from": "Van",
|
||||
"date_to": "Tot",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: nieprawidłowa wartość",
|
||||
"feat_schedule_time": "Harmonogram według pory dnia",
|
||||
"feat_schedule_time_desc": "Zadania stają się zaległe o określonej porze dnia zamiast o północy.",
|
||||
"schedule_time_optional": "Termin o godzinie (opcjonalne)",
|
||||
"schedule_time_toggle": "Termin o określonej godzinie",
|
||||
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
|
||||
"at_time": "o",
|
||||
"notes_optional": "Notatki (opcjonalne)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Brak tagu —",
|
||||
"dashboard": "Pulpit",
|
||||
"tab_today": "Dziś",
|
||||
"palette_placeholder": "Szukaj obiektów i zadań…",
|
||||
"palette_placeholder": "Szukaj obiektów, zadań, części, dokumentów, notatek…",
|
||||
"palette_no_results": "Brak wyników",
|
||||
"palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij",
|
||||
"palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij · / otwiera wyszukiwanie wszędzie",
|
||||
"search_open": "Szukaj",
|
||||
"search_group_parts": "Części",
|
||||
"search_group_content": "W dokumentach",
|
||||
"search_group_history": "Notatki z historii",
|
||||
"search_page": "Strona {page}",
|
||||
"search_empty_hint": "Wpisz tekst, aby przeszukać obiekty, zadania, części, dokumenty i notatki",
|
||||
"search_searching": "Przeszukiwanie dokumentów i notatek…",
|
||||
"search_index_status": "Wyszukiwanie pełnotekstowe: {indexed} z {total} plików zindeksowanych · {no_text} bez warstwy tekstu · {pending} oczekujących",
|
||||
"today_all_caught_up": "Wszystko zrobione! Nic w tym tygodniu.",
|
||||
"today_overdue": "Zaległe",
|
||||
"today_due_today": "Na dziś",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Próg „materiał eksploatacyjny niski” (%)",
|
||||
"settings_battery_low_percent": "Próg „bateria słaba” (%)",
|
||||
"settings_thresholds_hint": "Wartości domyślne dla nowych Sugerowanych konfiguracji (procent pozostały) i dla baterii floty bez własnego progu Battery Notes. Istniejące zadania zachowują swoją wartość.",
|
||||
"settings_value_out_of_range": "Wartość musi mieścić się w zakresie od {min} do {max}",
|
||||
"bn_summary": "{name}: {pct} % dla {n} baterii",
|
||||
"bn_floor_decides": "Twój próg {floor} % decyduje dla wszystkich; dla każdej baterii liczy się wyższy z dwóch progów.",
|
||||
"bn_above_floor": "To powyżej twojego progu {floor} %, więc o wszystkich odnotowanych bateriach decyduje {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Powiadomienie testowe wysłane",
|
||||
"test_notification_failed": "Powiadomienie testowe nie powiodło się",
|
||||
"notify_per_person": "Dostarczanie dla każdej osoby",
|
||||
"member_avatars": "Awatary domowników",
|
||||
"member_avatars_hint": "Wyświetlane obok przypisanych zadań — inicjały w kolorze domownika. Domyślnie z imienia; ustaw własne inicjały lub wybierz kolor, aby odróżnić domowników.",
|
||||
"member_initials": "Inicjały",
|
||||
"member_color": "Kolor",
|
||||
"member_avatar_reset": "Resetuj",
|
||||
"notify_no_own_device": "Brak własnego urządzenia — używa usługi domowej",
|
||||
"settings_notify_due_soon": "Powiadom gdy wkrótce",
|
||||
"settings_notify_overdue": "Powiadom gdy zaległe",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Grupuj wg",
|
||||
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
|
||||
"target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).",
|
||||
"state_latch_help": "Przy 1 przejściu wyzwalacz działa jak zatrzask: resetuje się, gdy encja opuści stan docelowy — lub, gdy ustawiono tylko stan początkowy, gdy do niego wróci.",
|
||||
"for_minutes_state_help": "0 liczy każdą zmianę od razu. Z minutami nowy stan musi się najpierw utrzymać — krótkie mignięcia wtedy nie wyzwalają i nie liczą się.",
|
||||
"qr_print_title": "Drukuj kody QR",
|
||||
"qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Usunąć „{name}”?",
|
||||
"doc_too_large": "Plik jest za duży (maks. 25 MB).",
|
||||
"doc_upload_failed": "Przesyłanie nie powiodło się.",
|
||||
"completion_photo_optional": "Zdjęcie ukończenia (opcjonalnie)",
|
||||
"add_photo": "Dodaj zdjęcie",
|
||||
"completion_photos_optional": "Zdjęcia z wykonania (opcjonalnie)",
|
||||
"completion_photos": "Zdjęcia z wykonania",
|
||||
"choose_photos": "Wybierz zdjęcia",
|
||||
"choose_photo": "Wybierz zdjęcie",
|
||||
"photos_android_hint": "Aplikacja na Androida przekazuje jedno zdjęcie na wybór — każdy wybór jest dodawany.",
|
||||
"add_photos": "Dodaj zdjęcia",
|
||||
"photos_limit": "Maksymalnie {max} zdjęć na wykonanie",
|
||||
"history_edit_photos_hint": "Zdjęcie usunięte tutaj pozostaje w dokumentach obiektu.",
|
||||
"uploading": "Przesyłanie…",
|
||||
"remove": "Usuń",
|
||||
"doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wyświetlana obok zarejestrowanej wartości przy ukończeniu tego zadania.",
|
||||
"reading_value_label": "Wartość odczytu",
|
||||
"reading_label": "Odczyt",
|
||||
"readings_section": "Odczyty",
|
||||
"readings_hint": "Kilka nazwanych wartości na wykonanie — jeden wiersz na licznik. Zostaw puste, aby zapisać jedną wartość.",
|
||||
"reading_name_label": "Nazwa odczytu",
|
||||
"reading_unit_short": "Jednostka",
|
||||
"reading_add": "Dodaj odczyt",
|
||||
"reading_duplicate_name": "Nazwa już użyta",
|
||||
"reading_last": "ostatnio: {value}",
|
||||
"reading_below_last": "Mniej niż ostatni odczyt ({value})",
|
||||
"settings_templates_label": "Galeria szablonów",
|
||||
"settings_templates_hint": "Odznacz szablony, których nigdy nie użyjesz — znikną z wyboru „Z szablonu\" (panel i config flow). Nic więcej się nie zmienia; można je włączyć ponownie w każdej chwili.",
|
||||
"worksheet": "Karta pracy",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Własna reguła powiadomień",
|
||||
"settings_notify_rule_hint": "Każde powiadomienie wyzwala dodatkowo zdarzenie maintenance_supporter_notification (rodzaj, status, obiekt, zadanie, numery referencyjne, priorytet, link, dane). Przekieruj je automatyzacją — np. do Tickera, Telegrama lub Pushover.",
|
||||
"settings_notify_event_only": "Tylko zdarzenie, nic nie wysyłaj",
|
||||
"settings_notify_event_only_hint": "Twoja automatyzacja odpowiada za całe dostarczanie. Odstępy powtórzeń, ciche godziny i dzienny limit nadal decydują, kiedy zdarzenie się wyzwala.",
|
||||
"settings_notify_extra_data": "Dodatkowe dane powiadomienia (szablon)",
|
||||
"settings_notify_extra_data_hint": "JSON lub YAML, Jinja dozwolone; scalane z data wywołania notify (Twoje klucze wygrywają). Zmienne: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Sprawdź przyciskiem Wyślij test.",
|
||||
"card_saved_view": "Zapisany widok",
|
||||
"card_saved_view_none": "Brak",
|
||||
"card_saved_view_help": "Stosuje filtry statusu, użytkownika i etykiety widoku oprócz powyższych filtrów. Sortowanie i grupowanie widoku to ustawienia wyświetlania panelu i nie są stosowane na karcie.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Oznacz wszystkie jako wymienione",
|
||||
"battery_fleet_mark_one": "Oznacz tę baterię jako wymienioną",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Brak czujnika",
|
||||
"battery_fleet_trigger_lost": "Wyzwalacz czujnika tego zadania został utracony — nie uruchomi się ani nie zakończy automatycznie.",
|
||||
"battery_fleet_repair": "Napraw",
|
||||
"battery_fleet_exclude": "Wyklucz z floty",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Wybierz czujnik baterii pominięty przez automatyczne wykrywanie — od razu trafi na listę.",
|
||||
"battery_fleet_track_self": "Śledź baterie samoładujące",
|
||||
"battery_fleet_track_self_hint": "Telefony, odkurzacze i inne samoładujące się urządzenia pojawiają się jako akumulatory — przy niskim poziomie proszą o ładowanie, nigdy o nowe baterie.",
|
||||
"battery_fleet_due_without_sensor": "Traktuj przekroczoną prognozę jako termin wymiany, gdy bateria nie ma czujnika",
|
||||
"battery_fleet_due_without_sensor_hint": "Niektóre urządzenia w ogóle nie zgłaszają poziomu baterii — Battery Notes zna tylko typ i ostatnią wymianę. Po upływie typowej żywotności taka bateria liczy się jako słaba i zadanie floty przypomni ci o tym; oznaczenie jako wymienionej resetuje prognozę.",
|
||||
"battery_fleet_status_low": "Niski",
|
||||
"battery_fleet_status_soon": "Wkrótce",
|
||||
"battery_fleet_status_ok": "W porządku",
|
||||
"battery_fleet_status_due": "Do wymiany",
|
||||
"battery_fleet_predicted_on": "Przewidywane około {date}",
|
||||
"battery_fleet_predicted_trend": "Prognoza na podstawie trendu rozładowania tej baterii: około {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulator: ładowanie zamiast wymiany — nigdy na liście zakupów",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Książka serwisowa (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "wpisów",
|
||||
"ref_number": "Numer referencyjny",
|
||||
"print_options_title": "Drukuj książkę serwisową",
|
||||
"print_layout": "Układ",
|
||||
"print_layout_chronological": "Chronologicznie",
|
||||
"print_layout_by_task": "Według zadania",
|
||||
"print_include": "Uwzględnij",
|
||||
"print_inc_readings": "Odczyty",
|
||||
"print_inc_parts": "Zużyte części",
|
||||
"print_inc_photos": "Zdjęcia",
|
||||
"print_inc_documents": "Powiązane dokumenty",
|
||||
"print_inc_checklist": "Lista kontrolna",
|
||||
"print_inc_notes": "Notatki",
|
||||
"print_inc_costs": "Koszt i czas",
|
||||
"print_inc_person": "Wykonał",
|
||||
"print_inc_refs": "Numery referencyjne",
|
||||
"print_inc_qr": "Kod QR dla każdego zadania",
|
||||
"print_button": "Drukuj",
|
||||
"report_scan_hint": "Zeskanuj, aby otworzyć zadanie",
|
||||
"completed_by": "Wykonane przez",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Agendamento por horário",
|
||||
"feat_schedule_time_desc": "As tarefas ficam atrasadas em um horário específico do dia, em vez de à meia-noite.",
|
||||
"schedule_time_optional": "Vence às (opcional)",
|
||||
"schedule_time_toggle": "Vence em um horário específico",
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Observações (opcional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Sem tag —",
|
||||
"dashboard": "Painel",
|
||||
"tab_today": "Hoje",
|
||||
"palette_placeholder": "Pesquisar objetos e tarefas…",
|
||||
"palette_placeholder": "Pesquisar objetos, tarefas, peças, documentos, notas…",
|
||||
"palette_no_results": "Nenhum resultado",
|
||||
"palette_hint": "↑↓ para navegar · Enter para abrir · Esc para fechar",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar · / abre a pesquisa em qualquer lugar",
|
||||
"search_open": "Pesquisar",
|
||||
"search_group_parts": "Peças",
|
||||
"search_group_content": "Nos documentos",
|
||||
"search_group_history": "Notas do histórico",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Digite para pesquisar objetos, tarefas, peças, documentos e notas",
|
||||
"search_searching": "Pesquisando documentos e notas…",
|
||||
"search_index_status": "Busca de texto completo: {indexed} de {total} arquivos indexados · {no_text} sem camada de texto · {pending} pendentes",
|
||||
"today_all_caught_up": "Tudo em dia! Nada vence esta semana.",
|
||||
"today_overdue": "Atrasadas",
|
||||
"today_due_today": "Vencem hoje",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Limite „consumível baixo“ (%)",
|
||||
"settings_battery_low_percent": "Limite „pilha fraca“ (%)",
|
||||
"settings_thresholds_hint": "Padrões para novas Configurações sugeridas (porcentagem restante) e para pilhas da frota sem limite próprio do Battery Notes. As tarefas existentes mantêm seu valor.",
|
||||
"settings_value_out_of_range": "O valor deve estar entre {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilhas",
|
||||
"bn_floor_decides": "Seu limite de {floor} % decide para todas; por pilha vale o maior dos dois limites.",
|
||||
"bn_above_floor": "Está acima do seu limite de {floor} %, então o {name} decide para todas as pilhas anotadas.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"member_avatars": "Avatares dos membros",
|
||||
"member_avatars_hint": "Exibido ao lado das tarefas atribuídas — iniciais na cor do membro. O padrão vem do nome; defina iniciais próprias ou escolha uma cor para distinguir os membros.",
|
||||
"member_initials": "Iniciais",
|
||||
"member_color": "Cor",
|
||||
"member_avatar_reset": "Redefinir",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando vencer em breve",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes de o gatilho disparar (padrão: 1).",
|
||||
"state_latch_help": "Com 1 transição o gatilho é uma trava: ele se recupera quando a entidade sai do estado de destino — ou, se houver apenas um estado de origem, quando volta a ele.",
|
||||
"for_minutes_state_help": "0 conta cada mudança imediatamente. Com minutos, o novo estado deve se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gere uma página imprimível de códigos QR para recortar e colar nos seus equipamentos.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Excluir \"{name}\"?",
|
||||
"doc_too_large": "O arquivo é muito grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Falha no envio.",
|
||||
"completion_photo_optional": "Foto da conclusão (opcional)",
|
||||
"add_photo": "Adicionar foto",
|
||||
"completion_photos_optional": "Fotos de conclusão (opcional)",
|
||||
"completion_photos": "Fotos de conclusão",
|
||||
"choose_photos": "Escolher fotos",
|
||||
"choose_photo": "Escolher foto",
|
||||
"photos_android_hint": "O app Android entrega uma foto por seleção — cada seleção é adicionada.",
|
||||
"add_photos": "Adicionar fotos",
|
||||
"photos_limit": "Até {max} fotos por conclusão",
|
||||
"history_edit_photos_hint": "Uma foto removida aqui continua nos documentos do objeto.",
|
||||
"uploading": "Enviando…",
|
||||
"remove": "Remover",
|
||||
"doc_deduped": "Já armazenado em outro lugar — compartilhado, sem uso de espaço extra.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Exibida ao lado do valor registrado ao concluir esta tarefa.",
|
||||
"reading_value_label": "Valor da leitura",
|
||||
"reading_label": "Leitura",
|
||||
"readings_section": "Leituras",
|
||||
"readings_hint": "Vários valores nomeados por conclusão — uma linha por medidor. Deixe vazio para um único valor.",
|
||||
"reading_name_label": "Nome da leitura",
|
||||
"reading_unit_short": "Unidade",
|
||||
"reading_add": "Adicionar leitura",
|
||||
"reading_duplicate_name": "Nome já em uso",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Menor que a última leitura ({value})",
|
||||
"settings_templates_label": "Galeria de modelos",
|
||||
"settings_templates_hint": "Desmarque os modelos de que você nunca precisará — eles somem dos seletores \"A partir de modelo\" (painel e fluxo de configuração). Nada mais muda; você pode reativá-los a qualquer momento.",
|
||||
"worksheet": "Folha de trabalho",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Sua regra de notificação",
|
||||
"settings_notify_rule_hint": "Cada notificação também dispara o evento maintenance_supporter_notification (tipo, status, objeto, tarefa, números de referência, prioridade, link, payload). Roteie-o com uma automação — por ex. para Ticker, Telegram ou Pushover.",
|
||||
"settings_notify_event_only": "Apenas disparar o evento, não enviar nada",
|
||||
"settings_notify_event_only_hint": "Sua automação cuida de toda a entrega. Intervalos de repetição, horário silencioso e o limite diário continuam valendo para quando o evento dispara.",
|
||||
"settings_notify_extra_data": "Dados extras de notificação (modelo)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja permitido; mesclado nos data da chamada notify (suas chaves vencem). Variáveis: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Teste com o botão Enviar teste.",
|
||||
"card_saved_view": "Visualização salva",
|
||||
"card_saved_view_none": "Nenhuma",
|
||||
"card_saved_view_help": "Aplica os filtros de status, usuário e etiqueta da visualização por cima dos filtros acima. A ordenação e o agrupamento da visualização são configurações de exibição do painel e não se aplicam ao cartão.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas como trocadas",
|
||||
"battery_fleet_mark_one": "Marcar esta bateria como trocada",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Sem sensor",
|
||||
"battery_fleet_trigger_lost": "O gatilho do sensor desta tarefa foi perdido — ela não será acionada nem concluída automaticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Remover da frota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a detecção automática não encontrou — ele entra na lista imediatamente.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem uma recarga, nunca pilhas novas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar uma previsão vencida como pendente quando uma bateria não tem sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Alguns dispositivos não informam nenhum nível de bateria — o Battery Notes só conhece o tipo e a última troca. Passada a vida útil típica, essa bateria conta como fraca e a tarefa da frota lembra você; marcá-la como trocada reinicia a previsão.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_status_due": "Vencida",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta bateria: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: recarregue em vez de substituir — nunca na lista de compras",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registro de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registros",
|
||||
"ref_number": "Número de referência",
|
||||
"print_options_title": "Imprimir o registro de serviço",
|
||||
"print_layout": "Disposição",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarefa",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Leituras",
|
||||
"print_inc_parts": "Peças usadas",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Documentos vinculados",
|
||||
"print_inc_checklist": "Lista de verificação",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Custo e duração",
|
||||
"print_inc_person": "Concluído por",
|
||||
"print_inc_refs": "Números de referência",
|
||||
"print_inc_qr": "Código QR por tarefa",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Escaneie para abrir a tarefa",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Agendamento por hora",
|
||||
"feat_schedule_time_desc": "Tarefas vencem em um horário específico em vez de meia-noite.",
|
||||
"schedule_time_optional": "Vence às (opcional)",
|
||||
"schedule_time_toggle": "Vence a uma hora específica",
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Sem etiqueta —",
|
||||
"dashboard": "Painel",
|
||||
"tab_today": "Hoje",
|
||||
"palette_placeholder": "Pesquisar objetos e tarefas…",
|
||||
"palette_placeholder": "Pesquisar objetos, tarefas, peças, documentos, notas…",
|
||||
"palette_no_results": "Sem resultados",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar · / abre a pesquisa em qualquer lado",
|
||||
"search_open": "Pesquisar",
|
||||
"search_group_parts": "Peças",
|
||||
"search_group_content": "Nos documentos",
|
||||
"search_group_history": "Notas do histórico",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Escreva para pesquisar objetos, tarefas, peças, documentos e notas",
|
||||
"search_searching": "A pesquisar documentos e notas…",
|
||||
"search_index_status": "Pesquisa de texto integral: {indexed} de {total} ficheiros indexados · {no_text} sem camada de texto · {pending} pendentes",
|
||||
"today_all_caught_up": "Tudo em dia! Nada para esta semana.",
|
||||
"today_overdue": "Atrasadas",
|
||||
"today_due_today": "Vence hoje",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Limiar „consumível baixo“ (%)",
|
||||
"settings_battery_low_percent": "Limiar „pilha fraca“ (%)",
|
||||
"settings_thresholds_hint": "Predefinições para novas Configurações sugeridas (percentagem restante) e para pilhas da frota sem limiar Battery Notes próprio. As tarefas existentes mantêm o seu valor.",
|
||||
"settings_value_out_of_range": "O valor tem de estar entre {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilhas",
|
||||
"bn_floor_decides": "O teu limiar de {floor} % decide para todas; por pilha conta o mais alto dos dois limiares.",
|
||||
"bn_above_floor": "Está acima do teu limiar de {floor} %, por isso o {name} decide para todas as pilhas anotadas.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"member_avatars": "Avatares dos membros",
|
||||
"member_avatars_hint": "Mostrado junto às tarefas atribuídas — iniciais na cor do membro. Por defeito vêm do nome; defina iniciais próprias ou escolha uma cor para distinguir os membros.",
|
||||
"member_initials": "Iniciais",
|
||||
"member_color": "Cor",
|
||||
"member_avatar_reset": "Repor",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando próxima",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
@@ -533,6 +547,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).",
|
||||
"state_latch_help": "Com 1 transição o gatilho é um trinco: recupera quando a entidade sai do estado de destino — ou, se só houver um estado de origem, quando volta a ele.",
|
||||
"for_minutes_state_help": "0 conta cada mudança de imediato. Com minutos, o novo estado tem de se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Eliminar «{name}»?",
|
||||
"doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Falha no carregamento.",
|
||||
"completion_photo_optional": "Foto de conclusão (opcional)",
|
||||
"add_photo": "Adicionar foto",
|
||||
"completion_photos_optional": "Fotos de conclusão (opcional)",
|
||||
"completion_photos": "Fotos de conclusão",
|
||||
"choose_photos": "Escolher fotos",
|
||||
"choose_photo": "Escolher foto",
|
||||
"photos_android_hint": "A app Android entrega uma foto por seleção — cada seleção é adicionada.",
|
||||
"add_photos": "Adicionar fotos",
|
||||
"photos_limit": "Até {max} fotos por conclusão",
|
||||
"history_edit_photos_hint": "Uma foto removida aqui continua nos documentos do objeto.",
|
||||
"uploading": "A enviar…",
|
||||
"remove": "Remover",
|
||||
"doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Mostrada ao lado do valor registado ao concluir esta tarefa.",
|
||||
"reading_value_label": "Valor de leitura",
|
||||
"reading_label": "Leitura",
|
||||
"readings_section": "Leituras",
|
||||
"readings_hint": "Vários valores nomeados por conclusão — uma linha por contador. Deixe vazio para um único valor.",
|
||||
"reading_name_label": "Nome da leitura",
|
||||
"reading_unit_short": "Unidade",
|
||||
"reading_add": "Adicionar leitura",
|
||||
"reading_duplicate_name": "Nome já utilizado",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Inferior à última leitura ({value})",
|
||||
"settings_templates_label": "Galeria de modelos",
|
||||
"settings_templates_hint": "Desmarque modelos de que nunca precisará — desaparecem dos seletores \"A partir de modelo\" (painel e config flow). Nada mais muda; reativáveis a qualquer momento.",
|
||||
"worksheet": "Folha de trabalho",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "A sua regra de notificação",
|
||||
"settings_notify_rule_hint": "Cada notificação dispara também o evento maintenance_supporter_notification (tipo, estado, objeto, tarefa, números de referência, prioridade, ligação, conteúdo). Encaminhe-o com uma automação — p. ex. para o Ticker, Telegram ou Pushover.",
|
||||
"settings_notify_event_only": "Apenas disparar o evento, não enviar nada",
|
||||
"settings_notify_event_only_hint": "A sua automação trata de toda a entrega. Intervalos de repetição, horas de silêncio e o limite diário continuam a decidir quando o evento dispara.",
|
||||
"settings_notify_extra_data": "Dados de notificação adicionais (modelo)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja permitido; é fundido nos data da chamada notify (as suas chaves ganham). Variáveis: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Teste com o botão Enviar teste.",
|
||||
"card_saved_view": "Vista guardada",
|
||||
"card_saved_view_none": "Nenhuma",
|
||||
"card_saved_view_help": "Aplica os filtros de estado, utilizador e etiqueta da vista além dos filtros acima. A ordenação e o agrupamento da vista são definições de apresentação do painel e não se aplicam ao cartão.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas substituídas",
|
||||
"battery_fleet_mark_one": "Marcar esta bateria substituída",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Sem sensor",
|
||||
"battery_fleet_trigger_lost": "O gatilho de sensor desta tarefa foi perdido — ela não será acionada nem concluída automaticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir da frota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a deteção automática não encontrou — entra de imediato na lista.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem um carregamento, nunca pilhas novas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar uma previsão ultrapassada como a substituir quando uma pilha não tem sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Alguns dispositivos não reportam qualquer nível de pilha — o Battery Notes só conhece o tipo e a última substituição. Passada a vida útil típica, essa pilha conta como fraca e a tarefa da frota lembra-o; marcá-la como substituída repõe a previsão.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_status_due": "A substituir",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta pilha: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: carrega-se em vez de se substituir — nunca na lista de compras",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registo de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registos",
|
||||
"ref_number": "Número de referência",
|
||||
"print_options_title": "Imprimir o registo de serviço",
|
||||
"print_layout": "Disposição",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarefa",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Leituras",
|
||||
"print_inc_parts": "Peças usadas",
|
||||
"print_inc_photos": "Fotografias",
|
||||
"print_inc_documents": "Documentos associados",
|
||||
"print_inc_checklist": "Lista de verificação",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Custo e duração",
|
||||
"print_inc_person": "Concluído por",
|
||||
"print_inc_refs": "Números de referência",
|
||||
"print_inc_qr": "Código QR por tarefa",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Digitalize para abrir a tarefa",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: неверное значение",
|
||||
"feat_schedule_time": "Планирование по времени дня",
|
||||
"feat_schedule_time_desc": "Задачи становятся просроченными в определённое время дня, а не в полночь.",
|
||||
"schedule_time_optional": "Срок (необязательно)",
|
||||
"schedule_time_toggle": "Срок в определённое время",
|
||||
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
|
||||
"at_time": "в",
|
||||
"notes_optional": "Примечания (опционально)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Нет метки —",
|
||||
"dashboard": "Панель",
|
||||
"tab_today": "Сегодня",
|
||||
"palette_placeholder": "Поиск объектов и задач…",
|
||||
"palette_placeholder": "Поиск объектов, задач, запчастей, документов, заметок…",
|
||||
"palette_no_results": "Нет совпадений",
|
||||
"palette_hint": "↑↓ выбор · Enter открыть · Esc закрыть",
|
||||
"palette_hint": "↑↓ навигация · Enter открыть · Esc закрыть · / открывает поиск где угодно",
|
||||
"search_open": "Поиск",
|
||||
"search_group_parts": "Запчасти",
|
||||
"search_group_content": "В документах",
|
||||
"search_group_history": "Заметки в истории",
|
||||
"search_page": "Стр. {page}",
|
||||
"search_empty_hint": "Введите текст для поиска по объектам, задачам, запчастям, документам и заметкам",
|
||||
"search_searching": "Поиск по документам и заметкам…",
|
||||
"search_index_status": "Полнотекстовый поиск: проиндексировано {indexed} из {total} файлов · {no_text} без текстового слоя · {pending} в очереди",
|
||||
"today_all_caught_up": "Всё сделано! На этой неделе ничего нет.",
|
||||
"today_overdue": "Просрочено",
|
||||
"today_due_today": "Сегодня",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Порог «расходник заканчивается» (%)",
|
||||
"settings_battery_low_percent": "Порог «батарея разряжена» (%)",
|
||||
"settings_thresholds_hint": "Значения по умолчанию для новых «Предлагаемых настроек» (остаток в процентах) и для батарей парка без собственного порога Battery Notes. Существующие задачи сохраняют своё значение.",
|
||||
"settings_value_out_of_range": "Значение должно быть от {min} до {max}",
|
||||
"bn_summary": "{name}: {pct} % для {n} батарей",
|
||||
"bn_floor_decides": "Ваш порог {floor} % действует для всех; для каждой батареи учитывается больший из двух порогов.",
|
||||
"bn_above_floor": "Это выше вашего порога {floor} %, поэтому для всех отмеченных батарей решает {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Тестовое уведомление отправлено",
|
||||
"test_notification_failed": "Не удалось отправить тестовое уведомление",
|
||||
"notify_per_person": "Доставка по пользователям",
|
||||
"member_avatars": "Аватары участников",
|
||||
"member_avatars_hint": "Показывается рядом с назначенными задачами — инициалы в цвете участника. По умолчанию берутся из имени; задайте свои инициалы или выберите цвет, чтобы различать участников.",
|
||||
"member_initials": "Инициалы",
|
||||
"member_color": "Цвет",
|
||||
"member_avatar_reset": "Сбросить",
|
||||
"notify_no_own_device": "Нет своего устройства — используется общий сервис",
|
||||
"settings_notify_due_soon": "Уведомлять, когда срок скоро истекает",
|
||||
"settings_notify_overdue": "Уведомлять при просрочке",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Группировать по",
|
||||
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
|
||||
"target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).",
|
||||
"state_latch_help": "При 1 переходе триггер работает как защёлка: он сбрасывается, когда объект покидает состояние «в» — или, если задано только состояние «из», когда он в него возвращается.",
|
||||
"for_minutes_state_help": "0 — каждое изменение засчитывается сразу. С минутами новое состояние сначала должно удержаться — короткие сбои тогда не срабатывают и не считаются.",
|
||||
"qr_print_title": "Печать QR-кодов",
|
||||
"qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Удалить «{name}»?",
|
||||
"doc_too_large": "Файл слишком большой (макс. 25 МБ).",
|
||||
"doc_upload_failed": "Не удалось загрузить.",
|
||||
"completion_photo_optional": "Фото выполнения (необязательно)",
|
||||
"add_photo": "Добавить фото",
|
||||
"completion_photos_optional": "Фото выполнения (необязательно)",
|
||||
"completion_photos": "Фото выполнения",
|
||||
"choose_photos": "Выбрать фото",
|
||||
"choose_photo": "Выбрать фото",
|
||||
"photos_android_hint": "Приложение для Android передаёт по одному фото за выбор — каждый выбор добавляется.",
|
||||
"add_photos": "Добавить фото",
|
||||
"photos_limit": "До {max} фото на одно выполнение",
|
||||
"history_edit_photos_hint": "Удалённое здесь фото остаётся в документах объекта.",
|
||||
"uploading": "Загрузка…",
|
||||
"remove": "Убрать",
|
||||
"doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Отображается рядом с записанным значением при выполнении этой задачи.",
|
||||
"reading_value_label": "Показание",
|
||||
"reading_label": "Показание",
|
||||
"readings_section": "Показания",
|
||||
"readings_hint": "Несколько именованных значений на одно выполнение — по строке на счётчик. Оставьте пустым для одного значения.",
|
||||
"reading_name_label": "Название показания",
|
||||
"reading_unit_short": "Единица",
|
||||
"reading_add": "Добавить показание",
|
||||
"reading_duplicate_name": "Название уже используется",
|
||||
"reading_last": "последнее: {value}",
|
||||
"reading_below_last": "Меньше предыдущего показания ({value})",
|
||||
"settings_templates_label": "Галерея шаблонов",
|
||||
"settings_templates_hint": "Снимите отметку с шаблонов, которые вам не нужны — они исчезнут из выбора «Из шаблона» (панель и config flow). Больше ничего не меняется; можно включить обратно в любой момент.",
|
||||
"worksheet": "Рабочий лист",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "Уведомлять только для представления",
|
||||
"settings_notify_scope_all": "Все задачи",
|
||||
"settings_notify_scope_hint": "Напоминания отправляют только задачи, совпадающие с фильтрами метки/пользователя выбранного представления. Статус, сортировка и группировка представления здесь игнорируются.",
|
||||
"settings_notify_rule": "Своё правило уведомлений",
|
||||
"settings_notify_rule_hint": "Каждое уведомление дополнительно вызывает событие maintenance_supporter_notification (вид, статус, объект, задача, номера ссылок, приоритет, ссылка, данные). Направьте его автоматизацией — например в Ticker, Telegram или Pushover.",
|
||||
"settings_notify_event_only": "Только событие, ничего не отправлять",
|
||||
"settings_notify_event_only_hint": "Доставку целиком выполняет ваша автоматизация. Интервалы повтора, тихие часы и дневной лимит по-прежнему определяют, когда срабатывает событие.",
|
||||
"settings_notify_extra_data": "Дополнительные данные уведомления (шаблон)",
|
||||
"settings_notify_extra_data_hint": "JSON или YAML, Jinja разрешён; добавляется в data вызова notify (ваши ключи важнее). Переменные: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Проверьте кнопкой «Отправить тест».",
|
||||
"card_saved_view": "Сохранённое представление",
|
||||
"card_saved_view_none": "Нет",
|
||||
"card_saved_view_help": "Применяет фильтры статуса, пользователя и метки представления в дополнение к фильтрам выше. Сортировка и группировка представления — настройки отображения панели и на карточке не применяются.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Отметить все заменёнными",
|
||||
"battery_fleet_mark_one": "Отметить эту батарею заменённой",
|
||||
"battery_fleet_offline": "не в сети",
|
||||
"battery_fleet_no_sensor": "Без датчика",
|
||||
"battery_fleet_trigger_lost": "Сенсорный триггер этой задачи утерян — она не сработает и не завершится автоматически.",
|
||||
"battery_fleet_repair": "Восстановить",
|
||||
"battery_fleet_exclude": "Исключить из парка",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Выберите датчик батареи, который автоматическое обнаружение пропустило — он сразу появится в списке.",
|
||||
"battery_fleet_track_self": "Отслеживать самозаряжающиеся батареи",
|
||||
"battery_fleet_track_self_hint": "Телефоны, пылесосы и другие самозаряжающиеся устройства отображаются как аккумуляторы — при низком заряде нужна зарядка, а не новые батарейки.",
|
||||
"battery_fleet_due_without_sensor": "Считать истёкший прогноз поводом для замены, если у батареи нет датчика",
|
||||
"battery_fleet_due_without_sensor_hint": "Некоторые устройства вообще не сообщают уровень заряда — Battery Notes знает только тип и дату последней замены. Когда типичный срок службы истёк, такая батарея считается разряженной, и задача парка напомнит; отметка «заменена» сбрасывает прогноз.",
|
||||
"battery_fleet_status_low": "Низкий",
|
||||
"battery_fleet_status_soon": "Скоро",
|
||||
"battery_fleet_status_ok": "В норме",
|
||||
"battery_fleet_status_due": "Пора менять",
|
||||
"battery_fleet_predicted_on": "Ожидается примерно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз по тренду разряда этой батареи: примерно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Аккумулятор: заряжается, а не заменяется — никогда не попадает в список покупок",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Сервисная книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записей",
|
||||
"ref_number": "Номер ссылки",
|
||||
"print_options_title": "Печать сервисной книжки",
|
||||
"print_layout": "Структура",
|
||||
"print_layout_chronological": "Хронологически",
|
||||
"print_layout_by_task": "По задачам",
|
||||
"print_include": "Включить",
|
||||
"print_inc_readings": "Показания",
|
||||
"print_inc_parts": "Использованные детали",
|
||||
"print_inc_photos": "Фото",
|
||||
"print_inc_documents": "Связанные документы",
|
||||
"print_inc_checklist": "Чек-лист",
|
||||
"print_inc_notes": "Заметки",
|
||||
"print_inc_costs": "Стоимость и длительность",
|
||||
"print_inc_person": "Выполнил",
|
||||
"print_inc_refs": "Номера ссылок",
|
||||
"print_inc_qr": "QR-код для каждой задачи",
|
||||
"print_button": "Печать",
|
||||
"report_scan_hint": "Отсканируйте, чтобы открыть задачу",
|
||||
"completed_by": "Выполнил",
|
||||
"date_from": "С",
|
||||
"date_to": "По",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: ogiltigt värde",
|
||||
"feat_schedule_time": "Schemaläggning per tid på dygnet",
|
||||
"feat_schedule_time_desc": "Uppgifter blir försenade vid en specifik tid på dygnet istället för midnatt.",
|
||||
"schedule_time_optional": "Förfaller kl. (valfritt)",
|
||||
"schedule_time_toggle": "Förfaller vid en viss tid",
|
||||
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Anteckningar (valfritt)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Ingen tagg —",
|
||||
"dashboard": "Översikt",
|
||||
"tab_today": "Idag",
|
||||
"palette_placeholder": "Sök objekt och uppgifter…",
|
||||
"palette_placeholder": "Sök objekt, uppgifter, reservdelar, dokument, anteckningar…",
|
||||
"palette_no_results": "Inga träffar",
|
||||
"palette_hint": "↑↓ navigera · Enter öppna · Esc stäng",
|
||||
"palette_hint": "↑↓ navigera · Enter öppna · Esc stäng · / öppnar sökningen överallt",
|
||||
"search_open": "Sök",
|
||||
"search_group_parts": "Reservdelar",
|
||||
"search_group_content": "I dokument",
|
||||
"search_group_history": "Historikanteckningar",
|
||||
"search_page": "Sida {page}",
|
||||
"search_empty_hint": "Skriv för att söka bland objekt, uppgifter, reservdelar, dokument och anteckningar",
|
||||
"search_searching": "Söker i dokument och anteckningar…",
|
||||
"search_index_status": "Fulltextsökning: {indexed} av {total} filer indexerade · {no_text} utan textlager · {pending} väntar",
|
||||
"today_all_caught_up": "Allt klart! Inget den här veckan.",
|
||||
"today_overdue": "Försenade",
|
||||
"today_due_today": "Förfaller idag",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Tröskel „förbrukningsartikel låg“ (%)",
|
||||
"settings_battery_low_percent": "Tröskel „batteri lågt“ (%)",
|
||||
"settings_thresholds_hint": "Standardvärden för nya Föreslagna inställningar (återstående procent) och för flottans batterier utan egen Battery Notes-tröskel. Befintliga uppgifter behåller sitt värde.",
|
||||
"settings_value_out_of_range": "Värdet måste vara mellan {min} och {max}",
|
||||
"bn_summary": "{name}: {pct} % för {n} batterier",
|
||||
"bn_floor_decides": "Din tröskel på {floor} % avgör för alla; per batteri gäller den högre av de två trösklarna.",
|
||||
"bn_above_floor": "Det är över din tröskel på {floor} %, så {name} avgör för alla noterade batterier.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testnotifikation skickad",
|
||||
"test_notification_failed": "Testnotifikation misslyckades",
|
||||
"notify_per_person": "Leverans per person",
|
||||
"member_avatars": "Medlemmarnas avatarer",
|
||||
"member_avatars_hint": "Visas bredvid tilldelade uppgifter — initialer i medlemmens färg. Standard hämtas från namnet; ange egna initialer eller välj en färg för att skilja medlemmar åt.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Färg",
|
||||
"member_avatar_reset": "Återställ",
|
||||
"notify_no_own_device": "Ingen egen enhet — använder hushållets tjänst",
|
||||
"settings_notify_due_soon": "Notifiera när snart förfallande",
|
||||
"settings_notify_overdue": "Notifiera när försenad",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Gruppera efter",
|
||||
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
|
||||
"target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).",
|
||||
"state_latch_help": "Med 1 övergång är utlösaren en spärr: den återställs när entiteten lämnar Till-tillståndet — eller, om bara ett Från-tillstånd är satt, när den återgår till det.",
|
||||
"for_minutes_state_help": "0 räknar varje ändring direkt. Med minuter måste det nya tillståndet först hålla i sig — korta blinkningar varken utlöser eller räknas då.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Ta bort \"{name}\"?",
|
||||
"doc_too_large": "Filen är för stor (max 25 MB).",
|
||||
"doc_upload_failed": "Uppladdningen misslyckades.",
|
||||
"completion_photo_optional": "Slutförandefoto (valfritt)",
|
||||
"add_photo": "Lägg till foto",
|
||||
"completion_photos_optional": "Foton vid slutförande (valfritt)",
|
||||
"completion_photos": "Foton vid slutförande",
|
||||
"choose_photos": "Välj foton",
|
||||
"choose_photo": "Välj foto",
|
||||
"photos_android_hint": "Android-appen lämnar över ett foto per val — varje val läggs till.",
|
||||
"add_photos": "Lägg till foton",
|
||||
"photos_limit": "Högst {max} foton per slutförande",
|
||||
"history_edit_photos_hint": "Ett foto som tas bort här finns kvar bland objektets dokument.",
|
||||
"uploading": "Laddar upp…",
|
||||
"remove": "Ta bort",
|
||||
"doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Visas bredvid det registrerade värdet när uppgiften slutförs.",
|
||||
"reading_value_label": "Avläst värde",
|
||||
"reading_label": "Avläsning",
|
||||
"readings_section": "Avläsningar",
|
||||
"readings_hint": "Flera namngivna värden per slutförande — en rad per mätare. Lämna tomt för ett enda värde.",
|
||||
"reading_name_label": "Avläsningens namn",
|
||||
"reading_unit_short": "Enhet",
|
||||
"reading_add": "Lägg till avläsning",
|
||||
"reading_duplicate_name": "Namnet används redan",
|
||||
"reading_last": "senast: {value}",
|
||||
"reading_below_last": "Lägre än senaste avläsningen ({value})",
|
||||
"settings_templates_label": "Mallgalleri",
|
||||
"settings_templates_hint": "Avmarkera mallar du aldrig behöver — de försvinner från \"Från mall\"-väljarna (panel och config flow). Inget annat ändras; kan återaktiveras när som helst.",
|
||||
"worksheet": "Arbetsblad",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen aviseringsregel",
|
||||
"settings_notify_rule_hint": "Varje avisering utlöser även händelsen maintenance_supporter_notification (typ, status, objekt, uppgift, referensnummer, prioritet, länk, nyttolast). Styr den med en automation — t.ex. till Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Utlös bara händelsen, skicka inget själv",
|
||||
"settings_notify_event_only_hint": "Din automation sköter hela leveransen. Upprepningsintervall, tysta timmar och dagstaket avgör fortfarande när händelsen utlöses.",
|
||||
"settings_notify_extra_data": "Extra aviseringsdata (mall)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tillåtet; slås ihop med notify-anropets data (dina nycklar vinner). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Testa med knappen Skicka test.",
|
||||
"card_saved_view": "Sparad vy",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Tillämpar vyns status-, användar- och etikettfilter utöver filtren ovan. Vyns sortering och gruppering är panelvisningsinställningar och tillämpas inte på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Markera alla bytta",
|
||||
"battery_fleet_mark_one": "Markera detta batteri bytt",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Uppgiftens sensorutlösare har gått förlorad — den utlöses inte och slutförs inte automatiskt.",
|
||||
"battery_fleet_repair": "Reparera",
|
||||
"battery_fleet_exclude": "Uteslut från flottan",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Välj en batterisensor som den automatiska identifieringen missade — den läggs till i listan direkt.",
|
||||
"battery_fleet_track_self": "Spåra självladdande batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, dammsugare och andra självladdande enheter visas som laddningsbara — vid låg nivå begärs laddning, aldrig nya batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandla en passerad prognos som dags när ett batteri saknar sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Vissa enheter rapporterar ingen batterinivå alls — Battery Notes känner bara till typ och senaste byte. När den typiska livslängden har passerat räknas ett sådant batteri som lågt och flottuppgiften påminner dig; att markera det som bytt nollställer prognosen.",
|
||||
"battery_fleet_status_low": "Låg",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I ordning",
|
||||
"battery_fleet_status_due": "Dags",
|
||||
"battery_fleet_predicted_on": "Väntas omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Förutspått utifrån batteriets urladdningstrend: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Uppladdningsbart: ladda i stället för att byta — aldrig på inköpslistan",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicebok (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "poster",
|
||||
"ref_number": "Referensnummer",
|
||||
"print_options_title": "Skriv ut servicebok",
|
||||
"print_layout": "Upplägg",
|
||||
"print_layout_chronological": "Kronologiskt",
|
||||
"print_layout_by_task": "Per uppgift",
|
||||
"print_include": "Ta med",
|
||||
"print_inc_readings": "Avläsningar",
|
||||
"print_inc_parts": "Använda delar",
|
||||
"print_inc_photos": "Foton",
|
||||
"print_inc_documents": "Länkade dokument",
|
||||
"print_inc_checklist": "Checklista",
|
||||
"print_inc_notes": "Anteckningar",
|
||||
"print_inc_costs": "Kostnad och tid",
|
||||
"print_inc_person": "Utförd av",
|
||||
"print_inc_refs": "Referensnummer",
|
||||
"print_inc_qr": "QR-kod per uppgift",
|
||||
"print_button": "Skriv ut",
|
||||
"report_scan_hint": "Skanna för att öppna uppgiften",
|
||||
"completed_by": "Utförd av",
|
||||
"date_from": "Från",
|
||||
"date_to": "Till",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: geçersiz değer",
|
||||
"feat_schedule_time": "Gün içi saat zamanlaması",
|
||||
"feat_schedule_time_desc": "Görevler gece yarısı yerine günün belirli bir saatinde gecikmiş olur.",
|
||||
"schedule_time_optional": "Vade saati (isteğe bağlı)",
|
||||
"schedule_time_toggle": "Belirli bir saatte vadesi dolar",
|
||||
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
|
||||
"at_time": "saat",
|
||||
"notes_optional": "Notlar (isteğe bağlı)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Etiket yok —",
|
||||
"dashboard": "Gösterge Paneli",
|
||||
"tab_today": "Bugün",
|
||||
"palette_placeholder": "Nesne ve görevlerde ara…",
|
||||
"palette_placeholder": "Nesne, görev, yedek parça, belge, not ara…",
|
||||
"palette_no_results": "Eşleşme yok",
|
||||
"palette_hint": "↑↓ gezinme · Enter açma · Esc kapatma",
|
||||
"palette_hint": "↑↓ gezin · Enter aç · Esc kapat · / her yerde aramayı açar",
|
||||
"search_open": "Ara",
|
||||
"search_group_parts": "Yedek parçalar",
|
||||
"search_group_content": "Belgelerde",
|
||||
"search_group_history": "Geçmiş notları",
|
||||
"search_page": "Sayfa {page}",
|
||||
"search_empty_hint": "Nesneler, görevler, yedek parçalar, belgeler ve notlarda aramak için yazın",
|
||||
"search_searching": "Belgeler ve notlar aranıyor…",
|
||||
"search_index_status": "Tam metin arama: {total} dosyadan {indexed} tanesi dizinlendi · {no_text} metin katmanı yok · {pending} bekliyor",
|
||||
"today_all_caught_up": "Her şey tamam! Bu hafta vadesi gelen yok.",
|
||||
"today_overdue": "Gecikmiş",
|
||||
"today_due_today": "Bugün vadeli",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "„Sarf malzemesi düşük“ eşiği (%)",
|
||||
"settings_battery_low_percent": "„Pil zayıf“ eşiği (%)",
|
||||
"settings_thresholds_hint": "Yeni Önerilen kurulumlar (kalan yüzde) ve kendi Battery Notes eşiği olmayan filo pilleri için varsayılanlar. Mevcut görevler değerlerini korur.",
|
||||
"settings_value_out_of_range": "Değer {min} ile {max} arasında olmalıdır",
|
||||
"bn_summary": "{name}: {n} pil için % {pct}",
|
||||
"bn_floor_decides": "Senin % {floor} eşiğin hepsi için belirleyici; her pil için iki eşikten yüksek olanı geçerli.",
|
||||
"bn_above_floor": "Bu, senin % {floor} eşiğinin üzerinde; bu yüzden kayıtlı tüm piller için {name} belirleyici.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test bildirimi gönderildi",
|
||||
"test_notification_failed": "Test bildirimi başarısız oldu",
|
||||
"notify_per_person": "Kişi bazında iletim",
|
||||
"member_avatars": "Üye avatarları",
|
||||
"member_avatars_hint": "Atanmış görevlerin yanında gösterilir — üyenin renginde baş harfler. Varsayılan addan gelir; üyeleri ayırt etmek için kendi baş harflerinizi belirleyin veya bir renk seçin.",
|
||||
"member_initials": "Baş harfler",
|
||||
"member_color": "Renk",
|
||||
"member_avatar_reset": "Sıfırla",
|
||||
"notify_no_own_device": "Kendi cihazı yok — ev servisini kullanır",
|
||||
"settings_notify_due_soon": "Vade yaklaştığında bildir",
|
||||
"settings_notify_overdue": "Geciktiğinde bildir",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Grupla",
|
||||
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
|
||||
"target_changes_help": "Tetikleyici çalışmadan önceki eşleşen geçiş sayısı (varsayılan: 1).",
|
||||
"state_latch_help": "1 geçişte tetikleyici bir mandal gibi çalışır: varlık Hedef durumdan çıkınca sıfırlanır — ya da yalnızca Kaynak durum ayarlıysa ona geri döndüğünde.",
|
||||
"for_minutes_state_help": "0 her değişikliği hemen sayar. Dakika ayarlanırsa yeni durumun önce o kadar sürmesi gerekir — kısa titremeler o zaman ne tetikler ne de sayılır.",
|
||||
"qr_print_title": "QR kodlarını yazdır",
|
||||
"qr_print_desc": "Kesip ekipmanınıza yapıştırmak için yazdırılabilir bir QR kodu sayfası oluşturun.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" silinsin mi?",
|
||||
"doc_too_large": "Dosya çok büyük (en fazla 25 MB).",
|
||||
"doc_upload_failed": "Yükleme başarısız oldu.",
|
||||
"completion_photo_optional": "Tamamlama fotoğrafı (isteğe bağlı)",
|
||||
"add_photo": "Fotoğraf ekle",
|
||||
"completion_photos_optional": "Tamamlama fotoğrafları (isteğe bağlı)",
|
||||
"completion_photos": "Tamamlama fotoğrafları",
|
||||
"choose_photos": "Fotoğraf seç",
|
||||
"choose_photo": "Fotoğraf seç",
|
||||
"photos_android_hint": "Android uygulaması her seçimde bir fotoğraf iletir — her seçim eklenir.",
|
||||
"add_photos": "Fotoğraf ekle",
|
||||
"photos_limit": "Tamamlama başına en fazla {max} fotoğraf",
|
||||
"history_edit_photos_hint": "Burada kaldırılan fotoğraf nesnenin belgelerinde kalır.",
|
||||
"uploading": "Yükleniyor…",
|
||||
"remove": "Kaldır",
|
||||
"doc_deduped": "Zaten başka bir yerde saklı — paylaşıldı, ek alan kullanılmadı.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Bu görevi tamamlarken kaydedilen değerin yanında gösterilir.",
|
||||
"reading_value_label": "Okuma değeri",
|
||||
"reading_label": "Okuma",
|
||||
"readings_section": "Sayaç okumaları",
|
||||
"readings_hint": "Tamamlama başına birden fazla adlandırılmış değer — her sayaç için bir satır. Tek bir değer için boş bırakın.",
|
||||
"reading_name_label": "Okuma adı",
|
||||
"reading_unit_short": "Birim",
|
||||
"reading_add": "Okuma ekle",
|
||||
"reading_duplicate_name": "Bu ad zaten kullanılıyor",
|
||||
"reading_last": "son: {value}",
|
||||
"reading_below_last": "Son okumadan ({value}) düşük",
|
||||
"settings_templates_label": "Şablon galerisi",
|
||||
"settings_templates_hint": "Asla ihtiyaç duymayacağınız şablonların işaretini kaldırın — \"Şablondan\" seçicilerinden (panel ve yapılandırma akışı) kaybolurlar. Başka hiçbir şey değişmez; istediğiniz zaman yeniden etkinleştirebilirsiniz.",
|
||||
"worksheet": "Çalışma sayfası",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Kendi bildirim kuralınız",
|
||||
"settings_notify_rule_hint": "Her bildirim ayrıca maintenance_supporter_notification olayını tetikler (tür, durum, nesne, görev, referans numaraları, öncelik, derin bağlantı, yük). Bir otomasyonla yönlendirin — ör. Ticker, Telegram veya Pushover'a.",
|
||||
"settings_notify_event_only": "Yalnızca olayı tetikle, kendin gönderme",
|
||||
"settings_notify_event_only_hint": "Teslimatın tamamını otomasyonunuz yapar. Tekrar aralıkları, sessiz saatler ve günlük sınır, olayın ne zaman tetikleneceği için geçerli kalır.",
|
||||
"settings_notify_extra_data": "Ek bildirim verileri (şablon)",
|
||||
"settings_notify_extra_data_hint": "JSON veya YAML, Jinja kullanılabilir; notify çağrısının data alanına birleştirilir (sizin anahtarlarınız öncelikli). Değişkenler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test gönder düğmesiyle deneyin.",
|
||||
"card_saved_view": "Kayıtlı görünüm",
|
||||
"card_saved_view_none": "Yok",
|
||||
"card_saved_view_help": "Görünümün durum, kullanıcı ve etiket filtrelerini yukarıdaki filtrelerin üzerine uygular. Görünümün sıralaması ve gruplaması panel görüntüleme ayarlarıdır ve kartta uygulanmaz.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Tümünü değiştirildi olarak işaretle",
|
||||
"battery_fleet_mark_one": "Bu pili değiştirildi olarak işaretle",
|
||||
"battery_fleet_offline": "çevrimdışı",
|
||||
"battery_fleet_no_sensor": "Sensör yok",
|
||||
"battery_fleet_trigger_lost": "Bu görevin sensör tetikleyicisi kayboldu — tetiklenmeyecek ve otomatik tamamlanmayacak.",
|
||||
"battery_fleet_repair": "Onar",
|
||||
"battery_fleet_exclude": "Filodan hariç tut",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Otomatik keşfin atladığı bir pil sensörü seçin — hemen listeye katılır.",
|
||||
"battery_fleet_track_self": "Kendi kendine şarj olan pilleri izle",
|
||||
"battery_fleet_track_self_hint": "Telefonlar, süpürgeler ve kendi kendine şarj olan diğer cihazlar şarj edilebilir olarak görünür — seviye düşükken şarj istenir, asla yeni pil istenmez.",
|
||||
"battery_fleet_due_without_sensor": "Sensörü olmayan bir pilde geçmiş tahmini süresi dolmuş say",
|
||||
"battery_fleet_due_without_sensor_hint": "Bazı cihazlar hiç pil seviyesi bildirmez — Battery Notes yalnızca türü ve son değişimi bilir. Tipik ömür geçtiğinde böyle bir pil zayıf sayılır ve filo görevi size hatırlatır; değiştirildi olarak işaretlemek tahmini sıfırlar.",
|
||||
"battery_fleet_status_low": "Düşük",
|
||||
"battery_fleet_status_soon": "Yakında",
|
||||
"battery_fleet_status_ok": "İyi durumda",
|
||||
"battery_fleet_status_due": "Süresi doldu",
|
||||
"battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor",
|
||||
"battery_fleet_predicted_trend": "Bu pilin deşarj eğiliminden tahmin edildi: yaklaşık {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Şarj edilebilir: değiştirmek yerine şarj edin — alışveriş listesine asla girmez",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servis kaydı (PDF)",
|
||||
"date": "Tarih",
|
||||
"service_record_entries": "kayıt",
|
||||
"ref_number": "Referans numarası",
|
||||
"print_options_title": "Servis defterini yazdır",
|
||||
"print_layout": "Düzen",
|
||||
"print_layout_chronological": "Kronolojik",
|
||||
"print_layout_by_task": "Göreve göre",
|
||||
"print_include": "Dahil et",
|
||||
"print_inc_readings": "Okumalar",
|
||||
"print_inc_parts": "Kullanılan parçalar",
|
||||
"print_inc_photos": "Fotoğraflar",
|
||||
"print_inc_documents": "Bağlı belgeler",
|
||||
"print_inc_checklist": "Kontrol listesi",
|
||||
"print_inc_notes": "Notlar",
|
||||
"print_inc_costs": "Maliyet ve süre",
|
||||
"print_inc_person": "Tamamlayan",
|
||||
"print_inc_refs": "Referans numaraları",
|
||||
"print_inc_qr": "Görev başına QR kodu",
|
||||
"print_button": "Yazdır",
|
||||
"report_scan_hint": "Görevi açmak için tarayın",
|
||||
"completed_by": "Tamamlayan",
|
||||
"date_from": "Başlangıç",
|
||||
"date_to": "Bitiş",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: невірне значення",
|
||||
"feat_schedule_time": "Планування за часом доби",
|
||||
"feat_schedule_time_desc": "Задачі стають простроченими у певний час доби, а не опівночі.",
|
||||
"schedule_time_optional": "Час прострочення (необов'язково)",
|
||||
"schedule_time_toggle": "Термін у визначений час",
|
||||
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
|
||||
"at_time": "о",
|
||||
"notes_optional": "Примітки (необов'язково)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Без тега —",
|
||||
"dashboard": "Дашборд",
|
||||
"tab_today": "Сьогодні",
|
||||
"palette_placeholder": "Пошук об'єктів і завдань…",
|
||||
"palette_placeholder": "Пошук об'єктів, завдань, запчастин, документів, нотаток…",
|
||||
"palette_no_results": "Немає збігів",
|
||||
"palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити",
|
||||
"palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити · / відкриває пошук будь-де",
|
||||
"search_open": "Пошук",
|
||||
"search_group_parts": "Запчастини",
|
||||
"search_group_content": "У документах",
|
||||
"search_group_history": "Нотатки в історії",
|
||||
"search_page": "Стор. {page}",
|
||||
"search_empty_hint": "Введіть текст для пошуку об'єктів, завдань, запчастин, документів і нотаток",
|
||||
"search_searching": "Пошук у документах і нотатках…",
|
||||
"search_index_status": "Повнотекстовий пошук: проіндексовано {indexed} із {total} файлів · {no_text} без текстового шару · {pending} у черзі",
|
||||
"today_all_caught_up": "Усе зроблено! Цього тижня нічого немає.",
|
||||
"today_overdue": "Прострочені",
|
||||
"today_due_today": "Сьогодні",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Поріг «витратний матеріал закінчується» (%)",
|
||||
"settings_battery_low_percent": "Поріг «батарея розряджена» (%)",
|
||||
"settings_thresholds_hint": "Значення за замовчуванням для нових «Запропонованих налаштувань» (залишок у відсотках) і для батарей парку без власного порогу Battery Notes. Наявні завдання зберігають своє значення.",
|
||||
"settings_value_out_of_range": "Значення має бути від {min} до {max}",
|
||||
"bn_summary": "{name}: {pct} % для {n} батарей",
|
||||
"bn_floor_decides": "Ваш поріг {floor} % діє для всіх; для кожної батареї враховується більший із двох порогів.",
|
||||
"bn_above_floor": "Це вище за ваш поріг {floor} %, тому для всіх занотованих батарей вирішує {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Тестове сповіщення надіслано",
|
||||
"test_notification_failed": "Не вдалося надіслати тестове сповіщення",
|
||||
"notify_per_person": "Доставка за особами",
|
||||
"member_avatars": "Аватари учасників",
|
||||
"member_avatars_hint": "Показується поруч із призначеними завданнями — ініціали в кольорі учасника. Типово беруться з імені; задайте власні ініціали або оберіть колір, щоб розрізняти учасників.",
|
||||
"member_initials": "Ініціали",
|
||||
"member_color": "Колір",
|
||||
"member_avatar_reset": "Скинути",
|
||||
"notify_no_own_device": "Немає власного пристрою — використовується загальна служба",
|
||||
"settings_notify_due_soon": "Сповіщати, коли термін наближається",
|
||||
"settings_notify_overdue": "Сповіщати про прострочення",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Групувати за",
|
||||
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
|
||||
"target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).",
|
||||
"state_latch_help": "При 1 переході тригер працює як засувка: він скидається, коли сутність залишає стан «до» — або, якщо задано лише стан «з», коли вона до нього повертається.",
|
||||
"for_minutes_state_help": "0 — кожна зміна зараховується одразу. З хвилинами новий стан спершу має утриматися — короткі збої тоді не спрацьовують і не рахуються.",
|
||||
"qr_print_title": "Друкувати QR-коди",
|
||||
"qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Видалити «{name}»?",
|
||||
"doc_too_large": "Файл завеликий (макс. 25 МБ).",
|
||||
"doc_upload_failed": "Не вдалося завантажити.",
|
||||
"completion_photo_optional": "Фото виконання (необов'язково)",
|
||||
"add_photo": "Додати фото",
|
||||
"completion_photos_optional": "Фото виконання (необов'язково)",
|
||||
"completion_photos": "Фото виконання",
|
||||
"choose_photos": "Вибрати фото",
|
||||
"choose_photo": "Вибрати фото",
|
||||
"photos_android_hint": "Застосунок для Android передає по одному фото за вибір — кожен вибір додається.",
|
||||
"add_photos": "Додати фото",
|
||||
"photos_limit": "До {max} фото на одне виконання",
|
||||
"history_edit_photos_hint": "Видалене тут фото залишається в документах об'єкта.",
|
||||
"uploading": "Завантаження…",
|
||||
"remove": "Прибрати",
|
||||
"doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Показується поруч із записаним значенням під час виконання цієї задачі.",
|
||||
"reading_value_label": "Показання",
|
||||
"reading_label": "Показання",
|
||||
"readings_section": "Показання",
|
||||
"readings_hint": "Кілька іменованих значень на одне виконання — по рядку на лічильник. Залиште порожнім для одного значення.",
|
||||
"reading_name_label": "Назва показання",
|
||||
"reading_unit_short": "Одиниця",
|
||||
"reading_add": "Додати показання",
|
||||
"reading_duplicate_name": "Назва вже використовується",
|
||||
"reading_last": "останнє: {value}",
|
||||
"reading_below_last": "Менше за попереднє показання ({value})",
|
||||
"settings_templates_label": "Галерея шаблонів",
|
||||
"settings_templates_hint": "Зніміть позначку з шаблонів, які вам не потрібні — вони зникнуть із вибору «З шаблону» (панель і config flow). Більше нічого не змінюється; можна ввімкнути знову будь-коли.",
|
||||
"worksheet": "Робочий аркуш",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "Сповіщати лише для подання",
|
||||
"settings_notify_scope_all": "Усі завдання",
|
||||
"settings_notify_scope_hint": "Нагадування надсилають лише завдання, що відповідають фільтрам мітки/користувача обраного подання. Статус, сортування та групування подання тут ігноруються.",
|
||||
"settings_notify_rule": "Власне правило сповіщень",
|
||||
"settings_notify_rule_hint": "Кожне сповіщення додатково викликає подію maintenance_supporter_notification (вид, статус, об'єкт, завдання, номери посилань, пріоритет, посилання, дані). Спрямуйте її автоматизацією — наприклад у Ticker, Telegram чи Pushover.",
|
||||
"settings_notify_event_only": "Лише подія, нічого не надсилати",
|
||||
"settings_notify_event_only_hint": "Доставку повністю виконує ваша автоматизація. Інтервали повтору, тихі години та денний ліміт і далі визначають, коли спрацьовує подія.",
|
||||
"settings_notify_extra_data": "Додаткові дані сповіщення (шаблон)",
|
||||
"settings_notify_extra_data_hint": "JSON або YAML, Jinja дозволено; додається до data виклику notify (ваші ключі мають перевагу). Змінні: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Перевірте кнопкою «Надіслати тест».",
|
||||
"card_saved_view": "Збережене подання",
|
||||
"card_saved_view_none": "Немає",
|
||||
"card_saved_view_help": "Застосовує фільтри статусу, користувача та мітки подання додатково до фільтрів вище. Сортування та групування подання — налаштування відображення панелі й на картці не застосовуються.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Позначити всі заміненими",
|
||||
"battery_fleet_mark_one": "Позначити цю батарею заміненою",
|
||||
"battery_fleet_offline": "не в мережі",
|
||||
"battery_fleet_no_sensor": "Без датчика",
|
||||
"battery_fleet_trigger_lost": "Сенсорний тригер цього завдання втрачено — воно не спрацює і не завершиться автоматично.",
|
||||
"battery_fleet_repair": "Відновити",
|
||||
"battery_fleet_exclude": "Виключити з парку",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Виберіть датчик батареї, який автоматичне виявлення пропустило — він одразу з'явиться у списку.",
|
||||
"battery_fleet_track_self": "Відстежувати самозарядні батареї",
|
||||
"battery_fleet_track_self_hint": "Телефони, пилососи та інші самозарядні пристрої відображаються як акумулятори — за низького заряду потрібна зарядка, а не нові батарейки.",
|
||||
"battery_fleet_due_without_sensor": "Вважати прострочений прогноз приводом для заміни, якщо батарея не має датчика",
|
||||
"battery_fleet_due_without_sensor_hint": "Деякі пристрої взагалі не повідомляють рівень заряду — Battery Notes знає лише тип і дату останньої заміни. Коли типовий термін служби минув, така батарея вважається розрядженою, і завдання парку нагадає; позначка «замінено» скидає прогноз.",
|
||||
"battery_fleet_status_low": "Низький",
|
||||
"battery_fleet_status_soon": "Незабаром",
|
||||
"battery_fleet_status_ok": "У нормі",
|
||||
"battery_fleet_status_due": "Час замінити",
|
||||
"battery_fleet_predicted_on": "Очікується приблизно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз за трендом розряду цієї батареї: приблизно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Акумулятор: заряджається, а не замінюється — ніколи не потрапляє до списку покупок",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Сервісна книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записів",
|
||||
"ref_number": "Номер посилання",
|
||||
"print_options_title": "Друк сервісної книжки",
|
||||
"print_layout": "Структура",
|
||||
"print_layout_chronological": "Хронологічно",
|
||||
"print_layout_by_task": "За завданнями",
|
||||
"print_include": "Включити",
|
||||
"print_inc_readings": "Показники",
|
||||
"print_inc_parts": "Використані деталі",
|
||||
"print_inc_photos": "Фото",
|
||||
"print_inc_documents": "Пов'язані документи",
|
||||
"print_inc_checklist": "Чек-лист",
|
||||
"print_inc_notes": "Нотатки",
|
||||
"print_inc_costs": "Вартість і тривалість",
|
||||
"print_inc_person": "Виконав",
|
||||
"print_inc_refs": "Номери посилань",
|
||||
"print_inc_qr": "QR-код для кожного завдання",
|
||||
"print_button": "Друк",
|
||||
"report_scan_hint": "Скануйте, щоб відкрити завдання",
|
||||
"completed_by": "Виконав",
|
||||
"date_from": "Від",
|
||||
"date_to": "До",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 无效数值",
|
||||
"feat_schedule_time": "具体时间调度",
|
||||
"feat_schedule_time_desc": "任务将在特定时间点变为逾期,而非默认的午夜。",
|
||||
"schedule_time_optional": "到期时间 (可选)",
|
||||
"schedule_time_toggle": "在特定时间到期",
|
||||
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
|
||||
"at_time": "于",
|
||||
"notes_optional": "备注 (可选)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— 无标签 —",
|
||||
"dashboard": "仪表盘",
|
||||
"tab_today": "今天",
|
||||
"palette_placeholder": "搜索对象和任务…",
|
||||
"palette_placeholder": "搜索对象、任务、备件、文档、备注…",
|
||||
"palette_no_results": "无匹配",
|
||||
"palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭",
|
||||
"palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭 · / 在任意位置打开搜索",
|
||||
"search_open": "搜索",
|
||||
"search_group_parts": "备件",
|
||||
"search_group_content": "文档内容",
|
||||
"search_group_history": "历史备注",
|
||||
"search_page": "第 {page} 页",
|
||||
"search_empty_hint": "输入以搜索对象、任务、备件、文档和备注",
|
||||
"search_searching": "正在搜索文档和备注…",
|
||||
"search_index_status": "全文搜索:已索引 {indexed}/{total} 个文件 · {no_text} 个无文本层 · {pending} 个待处理",
|
||||
"today_all_caught_up": "全部完成!本周没有待办。",
|
||||
"today_overdue": "逾期",
|
||||
"today_due_today": "今天到期",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "耗材“不足”阈值(%)",
|
||||
"settings_battery_low_percent": "电池“电量低”阈值(%)",
|
||||
"settings_thresholds_hint": "用于新的“建议设置”(剩余百分比)以及没有自身 Battery Notes 阈值的电池组电池。现有任务保留其值。",
|
||||
"settings_value_out_of_range": "数值必须介于 {min} 和 {max} 之间",
|
||||
"bn_summary": "{name}:{n} 块电池为 {pct}%",
|
||||
"bn_floor_decides": "你的 {floor}% 阈值对所有电池生效;每块电池取两个阈值中较高者。",
|
||||
"bn_above_floor": "这高于你的 {floor}% 阈值,因此所有已记录的电池由 {name} 决定。",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "测试通知已发送",
|
||||
"test_notification_failed": "测试通知发送失败",
|
||||
"notify_per_person": "按成员分别投递",
|
||||
"member_avatars": "成员头像",
|
||||
"member_avatars_hint": "显示在已分配任务旁——以成员颜色显示首字母。默认取自姓名;可自定义首字母或选择颜色以区分成员。",
|
||||
"member_initials": "首字母",
|
||||
"member_color": "颜色",
|
||||
"member_avatar_reset": "重置",
|
||||
"notify_no_own_device": "无个人设备 — 使用家庭通知服务",
|
||||
"settings_notify_due_soon": "到期前通知提醒",
|
||||
"settings_notify_overdue": "超期后通知提醒",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "分组依据",
|
||||
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
|
||||
"target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。",
|
||||
"state_latch_help": "设为 1 次转换时触发器为锁存:实体离开“到”状态时恢复;若只设置了“从”状态,则在实体回到该状态时恢复。",
|
||||
"for_minutes_state_help": "0 表示每次变化立即计数。设置分钟后,新状态需先保持该时长——短暂闪变既不触发也不计数。",
|
||||
"qr_print_title": "打印二维码",
|
||||
"qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "删除“{name}”?",
|
||||
"doc_too_large": "文件过大(最大 25 MB)。",
|
||||
"doc_upload_failed": "上传失败。",
|
||||
"completion_photo_optional": "完成照片(可选)",
|
||||
"add_photo": "添加照片",
|
||||
"completion_photos_optional": "完成照片(可选)",
|
||||
"completion_photos": "完成照片",
|
||||
"choose_photos": "选择照片",
|
||||
"choose_photo": "选择照片",
|
||||
"photos_android_hint": "Android 应用每次只传一张照片——每次选择都会添加。",
|
||||
"add_photos": "添加照片",
|
||||
"photos_limit": "每次完成最多 {max} 张照片",
|
||||
"history_edit_photos_hint": "在此移除的照片仍保留在对象的文档中。",
|
||||
"uploading": "上传中…",
|
||||
"remove": "移除",
|
||||
"doc_deduped": "已在别处存储——共享,不占用额外空间。",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "完成此任务时显示在记录值旁边。",
|
||||
"reading_value_label": "读数值",
|
||||
"reading_label": "读数",
|
||||
"readings_section": "读数",
|
||||
"readings_hint": "每次完成可记录多个命名值——每个表计一行。留空则只记录单个值。",
|
||||
"reading_name_label": "读数名称",
|
||||
"reading_unit_short": "单位",
|
||||
"reading_add": "添加读数",
|
||||
"reading_duplicate_name": "名称已被使用",
|
||||
"reading_last": "上次:{value}",
|
||||
"reading_below_last": "低于上次读数({value})",
|
||||
"settings_templates_label": "模板库",
|
||||
"settings_templates_hint": "取消勾选你永远不需要的模板——它们会从\"从模板\"选择器(面板和配置流程)中消失。其他一切不变;可随时重新启用。",
|
||||
"worksheet": "工作单",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "仅为该视图发送通知",
|
||||
"settings_notify_scope_all": "所有任务",
|
||||
"settings_notify_scope_hint": "只有匹配所选视图的标签/用户筛选的任务才会发送提醒。视图的状态、排序和分组在此处被忽略。",
|
||||
"settings_notify_rule": "自定义通知规则",
|
||||
"settings_notify_rule_hint": "每条通知还会触发事件 maintenance_supporter_notification(类型、状态、对象、任务、参考编号、优先级、深层链接、载荷)。用自动化转发它——例如到 Ticker、Telegram 或 Pushover。",
|
||||
"settings_notify_event_only": "仅触发事件,自身不发送",
|
||||
"settings_notify_event_only_hint": "由你的自动化完成全部发送。重复间隔、免打扰时段和每日上限仍决定事件何时触发。",
|
||||
"settings_notify_extra_data": "附加通知数据(模板)",
|
||||
"settings_notify_extra_data_hint": "JSON 或 YAML,可用 Jinja;合并进 notify 调用的 data(你的键优先)。变量:kind、status、priority、object_name、task_name、object_ref、task_ref、days_until_due、next_due、responsible_user_id、url、target、title、message。用“发送测试”按钮验证。",
|
||||
"card_saved_view": "已保存的视图",
|
||||
"card_saved_view_none": "无",
|
||||
"card_saved_view_help": "在上述筛选之外再应用该视图的状态、用户和标签筛选。视图的排序和分组属于面板显示设置,不会应用到卡片上。",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "全部标记为已更换",
|
||||
"battery_fleet_mark_one": "标记此电池为已更换",
|
||||
"battery_fleet_offline": "离线",
|
||||
"battery_fleet_no_sensor": "无传感器",
|
||||
"battery_fleet_trigger_lost": "此任务的传感器触发器已丢失——它不会触发,也不会自动完成。",
|
||||
"battery_fleet_repair": "修复",
|
||||
"battery_fleet_exclude": "从电池群中排除",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "选择自动发现遗漏的电池传感器——它会立即加入列表。",
|
||||
"battery_fleet_track_self": "跟踪自充电电池",
|
||||
"battery_fleet_track_self_hint": "手机、吸尘器等自充电设备显示为可充电电池——电量低时提示充电,而不是更换电池。",
|
||||
"battery_fleet_due_without_sensor": "电池没有传感器时,将已过期的预测视为已到期",
|
||||
"battery_fleet_due_without_sensor_hint": "有些设备完全不报告电池电量——Battery Notes 只知道类型和上次更换时间。一旦超过典型寿命,这样的电池就会被视为电量低,电池群任务会提醒你;标记为已更换会重置预测。",
|
||||
"battery_fleet_status_low": "电量低",
|
||||
"battery_fleet_status_soon": "即将",
|
||||
"battery_fleet_status_ok": "正常",
|
||||
"battery_fleet_status_due": "已到期",
|
||||
"battery_fleet_predicted_on": "预计在 {date} 前后",
|
||||
"battery_fleet_predicted_trend": "根据此电池的放电趋势预测:约 {date}({confidence})",
|
||||
"battery_fleet_rechargeable": "可充电电池:充电即可,无需更换——不会出现在购物清单中",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "维护记录(PDF)",
|
||||
"date": "日期",
|
||||
"service_record_entries": "条记录",
|
||||
"ref_number": "参考编号",
|
||||
"print_options_title": "打印保养记录",
|
||||
"print_layout": "版式",
|
||||
"print_layout_chronological": "按时间顺序",
|
||||
"print_layout_by_task": "按任务",
|
||||
"print_include": "包含",
|
||||
"print_inc_readings": "读数",
|
||||
"print_inc_parts": "使用的零件",
|
||||
"print_inc_photos": "照片",
|
||||
"print_inc_documents": "关联文档",
|
||||
"print_inc_checklist": "检查清单",
|
||||
"print_inc_notes": "备注",
|
||||
"print_inc_costs": "费用和用时",
|
||||
"print_inc_person": "完成者",
|
||||
"print_inc_refs": "参考编号",
|
||||
"print_inc_qr": "每个任务的二维码",
|
||||
"print_button": "打印",
|
||||
"report_scan_hint": "扫描以打开任务",
|
||||
"completed_by": "完成人",
|
||||
"date_from": "从",
|
||||
"date_to": "至",
|
||||
|
||||
@@ -37,10 +37,13 @@ import {
|
||||
import { calendarStyles } from "./calendar-styles";
|
||||
import { syncLocaleFromHass, sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, formatWeekday, formatMonth, langOf, formatCost } from "./styles";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
import { historyPhotoIds } from "./helpers/history-photos";
|
||||
import { entryReadingValues } from "./helpers/reading-slots";
|
||||
import { openHistoryEditDialog, openTaskQuickActions } from "./dialog-mount";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
MaintenanceObjectResponse,
|
||||
ReadingSlot,
|
||||
StatisticsResponse,
|
||||
} from "./types";
|
||||
|
||||
@@ -226,11 +229,16 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
private async _openHistoryEntry(ev: CalendarEvent): Promise<void> {
|
||||
try {
|
||||
const resp = await this.hass.connection.sendMessagePromise<{
|
||||
tasks?: Array<{ id: string; history?: Array<Record<string, unknown>> }>;
|
||||
tasks?: Array<{
|
||||
id: string;
|
||||
type?: string;
|
||||
reading_unit?: string | null;
|
||||
readings?: ReadingSlot[] | null;
|
||||
history?: Array<Record<string, unknown>>;
|
||||
}>;
|
||||
}>({ type: "maintenance_supporter/object", entry_id: ev.entry_id });
|
||||
const entry = resp.tasks
|
||||
?.find((tk) => tk.id === ev.task_id)
|
||||
?.history?.find((h) => h.timestamp === ev.history_timestamp);
|
||||
const histTask = resp.tasks?.find((tk) => tk.id === ev.task_id);
|
||||
const entry = histTask?.history?.find((h) => h.timestamp === ev.history_timestamp);
|
||||
if (!entry) return;
|
||||
const opened = openHistoryEditDialog({
|
||||
entry_id: ev.entry_id,
|
||||
@@ -244,6 +252,12 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
completed_by: (entry.completed_by as string | null) ?? null,
|
||||
used_parts:
|
||||
(entry.used_parts as Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null) ?? null,
|
||||
photo_doc_ids: historyPhotoIds(entry),
|
||||
reading_value: (entry.reading_value as number | null) ?? null,
|
||||
reading_values: entryReadingValues(entry),
|
||||
readings: histTask?.readings ?? [],
|
||||
task_type: histTask?.type ?? null,
|
||||
reading_unit: histTask?.reading_unit ?? null,
|
||||
});
|
||||
if (opened) return;
|
||||
} catch {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
SavedViewFilters,
|
||||
} from "./types";
|
||||
import { UserService } from "./user-service";
|
||||
import { personOf, renderPersonAvatar, type PersonDisplay } from "./helpers/person";
|
||||
import { phaseLabel } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import "./maintenance-card-editor";
|
||||
@@ -58,6 +59,8 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
/** id → display name for the assignee badge. Empty until users/list
|
||||
* resolves; the badge stays hidden rather than showing a raw uuid. */
|
||||
@state() private _userNames: Record<string, string> = {};
|
||||
/** #169 follow-up: id → avatar (initials + colour) for the badge. */
|
||||
private _userPersons: Record<string, PersonDisplay> = {};
|
||||
private _userService: UserService | null = null;
|
||||
private _userNamesLoaded = false;
|
||||
/** entry_id → (task_id → documents) for the row chips. */
|
||||
@@ -199,6 +202,7 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
else this._userService.updateHass(this.hass);
|
||||
try {
|
||||
const users = await this._userService.getUsers();
|
||||
this._userPersons = Object.fromEntries(users.map((u) => [u.id, personOf(u)!]));
|
||||
this._userNames = Object.fromEntries(users.map((u) => [u.id, u.name]));
|
||||
} catch {
|
||||
// leave whatever we had; the badge hides for unresolved ids
|
||||
@@ -481,13 +485,13 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
? html`<div class="task-meta">
|
||||
${object_name} · ${t(task.type, L)}${phaseLabel(task) ? html` · ${phaseLabel(task)}` : nothing}${this._assigneeName(task)
|
||||
? html` · <span class="assignee"
|
||||
><ha-icon icon="mdi:account"></ha-icon>${this._assigneeName(task)}</span
|
||||
>${renderPersonAvatar(this._userPersons[task.responsible_user_id!])}${this._assigneeName(task)}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: this._assigneeName(task)
|
||||
? html`<div class="task-meta compact-assignee" title="${this._assigneeName(task)}">
|
||||
<ha-icon icon="mdi:account"></ha-icon>${this._assigneeName(task)}
|
||||
${renderPersonAvatar(this._userPersons[task.responsible_user_id!])}${this._assigneeName(task)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -673,9 +677,11 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
.assignee, .compact-assignee {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.assignee .person-avatar, .compact-assignee .person-avatar { width: 16px; height: 16px; font-size: 8.5px; }
|
||||
.assignee ha-icon, .compact-assignee ha-icon {
|
||||
--mdc-icon-size: 13px;
|
||||
width: 13px;
|
||||
|
||||
+15
-1
@@ -31,6 +31,8 @@
|
||||
*/
|
||||
|
||||
import { STATUS_ICONS } from "./status-constants";
|
||||
import { historyPhotoIds } from "./helpers/history-photos";
|
||||
import { entryReadingValues } from "./helpers/reading-slots";
|
||||
|
||||
interface MaintenanceObjectResp {
|
||||
entry_id: string;
|
||||
@@ -1142,7 +1144,13 @@ function registerLlCustomHandler(): void {
|
||||
if (!hass) return;
|
||||
try {
|
||||
const r = await hass.connection.sendMessagePromise<{
|
||||
tasks?: Array<{ id?: string; history?: Array<Record<string, unknown>> }>;
|
||||
tasks?: Array<{
|
||||
id?: string;
|
||||
type?: string;
|
||||
reading_unit?: string | null;
|
||||
readings?: Array<{ id: string; name: string; unit?: string | null }> | null;
|
||||
history?: Array<Record<string, unknown>>;
|
||||
}>;
|
||||
}>({
|
||||
type: "maintenance_supporter/object",
|
||||
entry_id: detail.entry_id,
|
||||
@@ -1163,6 +1171,12 @@ function registerLlCustomHandler(): void {
|
||||
duration: (histEntry.duration as number | undefined) ?? null,
|
||||
completed_by: (histEntry.completed_by as string) ?? null,
|
||||
used_parts: (histEntry.used_parts as Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null) ?? null,
|
||||
photo_doc_ids: historyPhotoIds(histEntry),
|
||||
reading_value: (histEntry.reading_value as number | null) ?? null,
|
||||
reading_values: entryReadingValues(histEntry),
|
||||
readings: task?.readings ?? [],
|
||||
task_type: task?.type ?? null,
|
||||
reading_unit: task?.reading_unit ?? null,
|
||||
});
|
||||
} catch {
|
||||
deepLink("/maintenance-supporter");
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { LitElement, html, nothing, type TemplateResult } from "lit";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { queryTokens, scoreFields } from "./helpers/search-match";
|
||||
import { objectRef, parseRef, renderRefChip, taskRef } from "./helpers/reference";
|
||||
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
|
||||
import { isStaleBundle } from "./helpers/bundle-version";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
@@ -22,6 +24,37 @@ import { effectivePhase } from "./helpers/phases";
|
||||
import { buildCompleteDialogArgs, fillAndOpenCompleteDialog } from "./helpers/complete-dialog-args";
|
||||
import { describeWsError } from "./ws-errors";
|
||||
import { panelStyles } from "./panel-styles";
|
||||
|
||||
// Global search (#171): what the panel needs to know about a hit — enough
|
||||
// to render one row and to act on it (open the page / the document).
|
||||
interface SearchHit {
|
||||
kind: "object" | "task" | "part" | "document" | "content" | "history";
|
||||
entryId: string;
|
||||
taskId?: string;
|
||||
docId?: string;
|
||||
docKind?: string | null;
|
||||
url?: string | null;
|
||||
page?: number | null;
|
||||
/** #170: the hit's reference ("8.3", "8.3-2") when numbered. */
|
||||
ref?: string | null;
|
||||
label: string;
|
||||
sub: string;
|
||||
snippet?: string;
|
||||
score: number;
|
||||
icon: string;
|
||||
}
|
||||
interface RemoteDocHit {
|
||||
id: string; entry_id: string; object_name: string; kind: string; title?: string | null; filename?: string | null;
|
||||
url?: string | null; tags?: string[]; match: "meta" | "content"; page?: number | null; snippet?: string; score: number;
|
||||
}
|
||||
interface RemoteHistoryHit {
|
||||
entry_id: string; task_id: string; task_name: string; object_name: string; timestamp?: string | null;
|
||||
type?: string | null; snippet: string; score: number; ref?: string | null;
|
||||
}
|
||||
const SEARCH_MIN_CHARS = 2;
|
||||
const SEARCH_DEBOUNCE_MS = 250;
|
||||
const SEARCH_GROUP_CAP = { objects: 6, tasks: 10, parts: 6, documents: 8, history: 6 } as const;
|
||||
const MDI_MAGNIFY = "M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
MaintenanceObjectResponse,
|
||||
@@ -78,12 +111,16 @@ import { buildCalendarBuckets, isoDateLocal, type CalendarEvent } from "./helper
|
||||
import { renderTriggerProgress, renderMiniSparkline, computeTrend } from "./renderers/progress";
|
||||
import { type HistoryContext } from "./renderers/history";
|
||||
import { renderUserBadge, type TaskDetailContext } from "./renderers/task-detail";
|
||||
import { renderPersonChip, type PersonDisplay } from "./helpers/person";
|
||||
// The task-detail sub-view as a web component (light-DOM, panel styles apply).
|
||||
// Side-effect import: type-only imports get tree-shaken and the element
|
||||
// would never register.
|
||||
import "./components/task-detail-view";
|
||||
import { computeWindow, VIRTUAL_MIN_ROWS } from "./helpers/virtual-window";
|
||||
import { INITIAL_STICKY, nextStickyState, stickyStateOnSelect, stickyTop, type StickyState } from "./helpers/sticky-pane";
|
||||
import { invalidateSettingsCache } from "./helpers/settings-cache";
|
||||
import { historyPhotoIds } from "./helpers/history-photos";
|
||||
import { entryReadingValues, readingSlotDelta } from "./helpers/reading-slots";
|
||||
|
||||
type View = "overview" | "object" | "task" | "all_objects" | "all_parts";
|
||||
|
||||
@@ -270,10 +307,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
catch { return new Set(); }
|
||||
})();
|
||||
// v2.15.0: command palette ("/" since 2.18.1 — Ctrl+K clashed with HA's own
|
||||
// global search) — global fuzzy search over objects + tasks.
|
||||
// global search); 2.78 (#171): the panel's global search — a magnifier in
|
||||
// the header opens it too, results are grouped (objects / tasks / parts
|
||||
// from the loaded data, documents + history notes from the server).
|
||||
@state() private _paletteOpen = false;
|
||||
@state() private _paletteQuery = "";
|
||||
@state() private _paletteActive = 0;
|
||||
@state() private _searchRemote: { query: string; documents: RemoteDocHit[]; history: RemoteHistoryHit[] } | null = null;
|
||||
private _searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private _searchSeq = 0;
|
||||
// v2.15.0: template gallery (surfaces the config-flow object templates).
|
||||
@state() private _templateGalleryOpen = false;
|
||||
@state() private _templates: Array<{ id: string; name: string; category: string; tasks: unknown[]; disabled?: boolean }> = [];
|
||||
@@ -628,8 +670,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
// A data refresh means the open task's history may have grown (complete,
|
||||
// history edit) — the truncated list payload can't tell, so refetch.
|
||||
if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) {
|
||||
this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
|
||||
if (this._detailOpen()) {
|
||||
this._fetchFullHistory(this._selectedEntryId!, this._selectedTaskId!);
|
||||
}
|
||||
// Battery Fleet availability (batteries present + not yet set up).
|
||||
// The slim status check, NOT the overview: the full overview runs the
|
||||
@@ -693,6 +735,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
|
||||
private _deepLinkHandled = false;
|
||||
/** Set while an in-app deep link (see _onLocationChanged) is routed:
|
||||
* _pushPanelState replaces HA's freshly pushed entry instead of pushing. */
|
||||
private _deepLinkInPlace = false;
|
||||
/** Objects + saved views have arrived at least once — deep links that
|
||||
* reference them are routable. */
|
||||
private _initialLoadDone = false;
|
||||
@@ -710,7 +755,24 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const path = window.location.pathname;
|
||||
if (path !== base && !path.startsWith(`${base}/`)) return;
|
||||
this._deepLinkHandled = false;
|
||||
this._handleDeepLink();
|
||||
// HA's navigate() already pushed the entry for this URL (state null), so
|
||||
// the show*() helpers must take that entry over instead of stacking a
|
||||
// second one — Back from a deep-linked task page needed two presses, the
|
||||
// first landing on the state-less entry _onPopState ignores (bug review
|
||||
// 2026-09-04 follow-up, same class as the overview branch).
|
||||
this._deepLinkInPlace = true;
|
||||
try {
|
||||
this._handleDeepLink();
|
||||
} finally {
|
||||
this._deepLinkInPlace = false;
|
||||
}
|
||||
// Whatever the link did — a tab switch, a dialog, an unknown value — the
|
||||
// entry has to describe the view now on screen; a later Back onto a
|
||||
// state-less entry would restore nothing and leave the task page up.
|
||||
const state = history.state as { msp_view?: View } | null;
|
||||
if (!state?.msp_view) {
|
||||
history.replaceState({ msp_view: this._view, msp_entry: this._selectedEntryId, msp_task: this._selectedTaskId }, "");
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDeepLink(): void {
|
||||
@@ -742,6 +804,11 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|| msAction === "open_settings") {
|
||||
this._deepLinkHandled = true;
|
||||
cleanMsActionUrl();
|
||||
// The Settings tab is admin-only (the tab bar hides it, the server
|
||||
// refuses its writes) — a non-admin following a banner link lands on
|
||||
// the dashboard instead of a tab-less settings view (bug review
|
||||
// 2026-09-04).
|
||||
if (!this.hass?.user?.is_admin) return;
|
||||
// Switch to the Settings tab — that's where Vacation/Budget/Groups live.
|
||||
this._overviewTab = "settings";
|
||||
// Hint the settings-view which sub-section to scroll to (its own
|
||||
@@ -774,8 +841,21 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const status = params.get("status");
|
||||
if (tab !== null || view !== null || sort !== null || status !== null) {
|
||||
cleanMsActionUrl();
|
||||
if (this._view !== "overview") this._showOverview();
|
||||
if ((OVERVIEW_TABS as readonly string[]).includes(tab ?? "")) {
|
||||
if (this._view !== "overview") {
|
||||
// In-app navigation to the overview from a task/object page: HA's
|
||||
// navigate() already pushed the entry for this location, so switch
|
||||
// in place — _showOverview()'s own push put a blank entry between
|
||||
// the two pages and Back needed two presses (bug review 2026-09-04).
|
||||
this._view = "overview";
|
||||
this._selectedEntryId = null;
|
||||
this._selectedTaskId = null;
|
||||
this._moreMenuOpen = false;
|
||||
this._scrollContentToTop();
|
||||
}
|
||||
// `?tab=settings` for a non-admin is an unknown value: the tab bar
|
||||
// hides Settings for them, and _setOverviewTab would have persisted
|
||||
// the choice across reloads (bug review 2026-09-04).
|
||||
if ((OVERVIEW_TABS as readonly string[]).includes(tab ?? "") && (tab !== "settings" || this.hass?.user?.is_admin)) {
|
||||
this._setOverviewTab(tab as OverviewTab);
|
||||
}
|
||||
if (view !== null) {
|
||||
@@ -921,10 +1001,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
// truncated list payload never wins the "longer list" comparison
|
||||
// in _historyCtx, so a pushed completion stayed invisible there.
|
||||
if (
|
||||
this._view === "task" && this._selectedEntryId && this._selectedTaskId
|
||||
this._detailOpen()
|
||||
&& (!!ev.objects || (ev.delta || []).some((d) => d.entry_id === this._selectedEntryId))
|
||||
) {
|
||||
void this._fetchFullHistory(this._selectedEntryId, this._selectedTaskId);
|
||||
void this._fetchFullHistory(this._selectedEntryId!, this._selectedTaskId!);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1089,10 +1169,13 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
|
||||
// --- Navigation ---
|
||||
|
||||
/** Push a browser history entry so the back button navigates within the panel. */
|
||||
/** Push a browser history entry so the back button navigates within the
|
||||
* panel. An in-app deep link arrives on the entry HA's navigate() just
|
||||
* pushed — that one is taken over instead (see _onLocationChanged). */
|
||||
private _pushPanelState(view: View, entryId?: string | null, taskId?: string | null): void {
|
||||
const state = { msp_view: view, msp_entry: entryId || null, msp_task: taskId || null };
|
||||
history.pushState(state, "");
|
||||
if (this._deepLinkInPlace) history.replaceState(state, "");
|
||||
else history.pushState(state, "");
|
||||
}
|
||||
|
||||
/** Handle browser back/forward button. */
|
||||
@@ -1271,6 +1354,28 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
return this.split && !this.narrow && !this.tight && this._view === "overview" && this._overviewTab === "dashboard" && !this._bulkMode;
|
||||
}
|
||||
|
||||
/** A task detail is on screen — the full page OR the docked split pane.
|
||||
* The refetch gates keyed on `_view === "task"` alone left the docked
|
||||
* detail's history frozen after a pushed completion or a data refresh
|
||||
* (bug review 2026-09-04). */
|
||||
private _detailOpen(): boolean {
|
||||
return (this._view === "task" || this._splitActive()) && !!this._selectedEntryId && !!this._selectedTaskId;
|
||||
}
|
||||
|
||||
/** The docked detail's breadcrumb opens the full task page (a no-op on
|
||||
* the page itself). Assigning `_view` alone pushed no history entry —
|
||||
* Back then left the panel instead of returning to the list — and kept
|
||||
* the list's scroll offset (bug review 2026-09-04). The detail's tab,
|
||||
* history filter and full history carry over: they are already loaded. */
|
||||
private _showFullTaskPage(entryId: string, taskId: string): void {
|
||||
if (this._view === "task") return;
|
||||
this._pushPanelState("task", entryId, taskId);
|
||||
this._view = "task";
|
||||
this._selectedEntryId = entryId;
|
||||
this._selectedTaskId = taskId;
|
||||
this._scrollContentToTop();
|
||||
}
|
||||
|
||||
private _showTask(entryId: string, taskId: string): void {
|
||||
// Master-detail split (2026-09-01): same data prep, but no view switch
|
||||
// and no history entry — the detail docks on the right like a mail
|
||||
@@ -1345,10 +1450,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
undo?.();
|
||||
}
|
||||
|
||||
// --- Command palette ("/") ---
|
||||
// --- Global search ("/" or the header's magnifier; #171) ---
|
||||
|
||||
private _paletteKeydown = (e: KeyboardEvent): void => {
|
||||
// "/" opens the palette (GitHub/Discourse convention; Shift+7 on a German
|
||||
// "/" opens the search (GitHub/Discourse convention; Shift+7 on a German
|
||||
// layout still yields key === "/"). Deliberately NOT Ctrl/Cmd+K: that is
|
||||
// HA's own global-search hotkey, and this window-level preventDefault
|
||||
// would shadow it whenever the panel is open.
|
||||
@@ -1378,6 +1483,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
this._paletteQuery = "";
|
||||
this._paletteActive = 0;
|
||||
this._paletteOpen = true;
|
||||
this._searchRemote = null;
|
||||
this.updateComplete.then(() => {
|
||||
this.shadowRoot?.querySelector<HTMLInputElement>(".palette-input")?.focus();
|
||||
});
|
||||
@@ -1386,62 +1492,211 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private _closePalette(): void {
|
||||
this._paletteOpen = false;
|
||||
this._paletteQuery = "";
|
||||
this._searchRemote = null;
|
||||
if (this._searchTimer) { clearTimeout(this._searchTimer); this._searchTimer = null; }
|
||||
}
|
||||
|
||||
private get _paletteResults(): Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> {
|
||||
const q = this._paletteQuery.trim().toLowerCase();
|
||||
const out: Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> = [];
|
||||
private _onPaletteInput(value: string): void {
|
||||
this._paletteQuery = value;
|
||||
this._paletteActive = 0;
|
||||
// Documents and history notes live on the server — ask once the user
|
||||
// pauses, and drop answers that arrive for an older query.
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer);
|
||||
const q = value.trim();
|
||||
if (q.length < SEARCH_MIN_CHARS) { this._searchRemote = null; return; }
|
||||
this._searchTimer = setTimeout(() => {
|
||||
this._searchTimer = null;
|
||||
const seq = ++this._searchSeq;
|
||||
this.hass.connection.sendMessagePromise<{ documents: RemoteDocHit[]; history: RemoteHistoryHit[] }>({
|
||||
type: "maintenance_supporter/search", query: q, limit: SEARCH_GROUP_CAP.documents,
|
||||
}).then((res) => {
|
||||
if (seq !== this._searchSeq || !this._paletteOpen) return;
|
||||
this._searchRemote = { query: q, documents: res.documents || [], history: res.history || [] };
|
||||
}).catch(() => { /* the local groups still answer; the server groups just stay empty */ });
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/** Objects, tasks and parts from the data the panel already holds, plus the
|
||||
* server's document and history-note hits — grouped, each group ranked
|
||||
* and capped. The flat array is what the keyboard walks. */
|
||||
private get _paletteResults(): SearchHit[] {
|
||||
const L = this._lang;
|
||||
const q = this._paletteQuery.trim();
|
||||
// A one-digit reference ("8") is a complete query; text needs two chars.
|
||||
if (q.length < SEARCH_MIN_CHARS && !parseRef(q)) return [];
|
||||
const tokens = queryTokens(q);
|
||||
if (!tokens.length) return [];
|
||||
const objects: SearchHit[] = [];
|
||||
const tasks: SearchHit[] = [];
|
||||
const parts: SearchHit[] = [];
|
||||
// #170: a typed reference ("8", "8.3", "8.3-2") names things exactly —
|
||||
// the object (with its tasks), one task, or (via the server) one
|
||||
// completion. Only when nothing carries that number does the text
|
||||
// matching below get its turn.
|
||||
const ref = parseRef(q);
|
||||
if (ref) {
|
||||
for (const obj of this._objects) {
|
||||
const o = obj.object;
|
||||
if (o.ref_no !== ref.object) continue;
|
||||
const oref = objectRef(o)!;
|
||||
if (ref.task == null) {
|
||||
objects.push({ kind: "object", entryId: obj.entry_id, label: o.name || "", sub: t("object", L), score: 1000, icon: "mdi:package-variant-closed", ref: oref });
|
||||
}
|
||||
for (const task of obj.tasks) {
|
||||
if (task.archived || (ref.task != null && task.ref_no !== ref.task)) continue;
|
||||
tasks.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: task.name || "", sub: o.name || "", score: ref.task == null ? 900 : 1000, icon: "mdi:clipboard-check-outline", ref: taskRef(o, task) });
|
||||
}
|
||||
}
|
||||
if (objects.length || tasks.length) {
|
||||
const remoteRef = this._searchRemote && this._searchRemote.query === q ? this._searchRemote : null;
|
||||
const hits: SearchHit[] = [...objects, ...tasks.slice(0, SEARCH_GROUP_CAP.tasks)];
|
||||
for (const h of remoteRef?.history.slice(0, 1) ?? []) {
|
||||
hits.push({ kind: "history", entryId: h.entry_id, taskId: h.task_id, label: h.task_name || "", sub: [h.object_name, h.timestamp ? formatDate(h.timestamp, L) : ""].filter(Boolean).join(" · "), snippet: h.snippet || "", score: h.score, icon: "mdi:note-text-outline", ref: h.ref ?? null });
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
}
|
||||
for (const obj of this._objects) {
|
||||
const oname = obj.object.name || "";
|
||||
if (!q || oname.toLowerCase().includes(q)) {
|
||||
out.push({ kind: "object", entryId: obj.entry_id, label: oname, sub: t("object", this._lang) });
|
||||
const o = obj.object;
|
||||
if (o.archived) continue;
|
||||
const oname = o.name || "";
|
||||
const oscore = scoreFields(tokens, [
|
||||
{ text: oname, weight: 3 }, { text: o.manufacturer, weight: 2 }, { text: o.model, weight: 2 },
|
||||
{ text: o.serial_number, weight: 2 }, { text: o.notes, weight: 1 },
|
||||
]);
|
||||
if (oscore > 0) {
|
||||
const detail = [o.manufacturer, o.model].filter(Boolean).join(" ");
|
||||
objects.push({ kind: "object", entryId: obj.entry_id, label: oname, sub: detail || t("object", L), score: oscore, icon: "mdi:package-variant-closed", ref: objectRef(o) });
|
||||
}
|
||||
for (const task of obj.tasks) {
|
||||
if (task.archived) continue;
|
||||
const tname = task.name || "";
|
||||
const labelHit = (task.labels || []).some((lb) => lb.toLowerCase().includes(q));
|
||||
if (!q || tname.toLowerCase().includes(q) || oname.toLowerCase().includes(q) || labelHit) {
|
||||
const labels = (task.labels || []).join(" ");
|
||||
const tscore = scoreFields(tokens, [
|
||||
{ text: task.name, weight: 3 }, { text: oname, weight: 2 }, { text: labels, weight: 2 }, { text: task.notes, weight: 1 },
|
||||
]);
|
||||
if (tscore > 0) {
|
||||
const labelSub = (task.labels || []).length ? ` #${(task.labels || []).join(" #")}` : "";
|
||||
out.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: tname, sub: oname + labelSub });
|
||||
tasks.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: task.name || "", sub: oname + labelSub, score: tscore, icon: "mdi:clipboard-check-outline", ref: taskRef(o, task) });
|
||||
}
|
||||
}
|
||||
for (const part of obj.parts || []) {
|
||||
const pscore = scoreFields(tokens, [
|
||||
{ text: part.name, weight: 3 }, { text: part.mpn, weight: 2 }, { text: part.vendor, weight: 1 },
|
||||
{ text: part.storage_location, weight: 1 }, { text: part.notes, weight: 1 },
|
||||
]);
|
||||
if (pscore > 0) {
|
||||
parts.push({ kind: "part", entryId: obj.entry_id, label: part.name || "", sub: [oname, part.mpn].filter(Boolean).join(" · "), score: pscore, icon: "mdi:cog-outline" });
|
||||
}
|
||||
}
|
||||
if (out.length > 60) break; // cap the working set; sliced below
|
||||
}
|
||||
return out.slice(0, 40);
|
||||
const byScore = (a: SearchHit, b: SearchHit) => b.score - a.score || a.label.localeCompare(b.label);
|
||||
const out: SearchHit[] = [
|
||||
...objects.sort(byScore).slice(0, SEARCH_GROUP_CAP.objects),
|
||||
...tasks.sort(byScore).slice(0, SEARCH_GROUP_CAP.tasks),
|
||||
...parts.sort(byScore).slice(0, SEARCH_GROUP_CAP.parts),
|
||||
];
|
||||
const remote = this._searchRemote && this._searchRemote.query === q ? this._searchRemote : null;
|
||||
if (remote) {
|
||||
// Metadata hits ("Documents") before content hits ("In documents") —
|
||||
// the server ranks across both, the groups keep each ranked run.
|
||||
const docs = remote.documents.slice(0, SEARCH_GROUP_CAP.documents);
|
||||
for (const d of [...docs.filter((x) => x.match !== "content"), ...docs.filter((x) => x.match === "content")]) {
|
||||
out.push({
|
||||
kind: d.match === "content" ? "content" : "document",
|
||||
entryId: d.entry_id, docId: d.id, docKind: d.kind, url: d.url, page: d.page ?? null,
|
||||
label: d.title || d.filename || d.url || "", sub: d.object_name || "", snippet: d.snippet || "",
|
||||
score: d.score, icon: d.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline",
|
||||
});
|
||||
}
|
||||
for (const h of remote.history.slice(0, SEARCH_GROUP_CAP.history)) {
|
||||
out.push({
|
||||
kind: "history", entryId: h.entry_id, taskId: h.task_id, label: h.task_name || "", ref: h.ref ?? null,
|
||||
sub: [h.object_name, h.timestamp ? formatDate(h.timestamp, L) : ""].filter(Boolean).join(" · "),
|
||||
snippet: h.snippet || "", score: h.score, icon: h.type === "skipped" ? "mdi:skip-next-circle-outline" : "mdi:note-text-outline",
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private _selectPaletteResult(r: { kind: "object" | "task"; entryId: string; taskId?: string }): void {
|
||||
private _selectPaletteResult(r: SearchHit): void {
|
||||
const query = this._paletteQuery.trim();
|
||||
this._closePalette();
|
||||
if (r.kind === "task" && r.taskId) this._showTask(r.entryId, r.taskId);
|
||||
else this._showObject(r.entryId);
|
||||
switch (r.kind) {
|
||||
case "task":
|
||||
if (r.taskId) this._showTask(r.entryId, r.taskId);
|
||||
return;
|
||||
case "history":
|
||||
if (!r.taskId) return;
|
||||
this._showTask(r.entryId, r.taskId);
|
||||
// Land on the history tab with the notes filter pre-filled, so the
|
||||
// matching entry is on screen instead of somewhere down the timeline.
|
||||
this._activeTab = "history";
|
||||
this._historySearch = query;
|
||||
return;
|
||||
case "document":
|
||||
case "content":
|
||||
if (r.docKind === "weblink") {
|
||||
if (isSafeHttpUrl(r.url)) window.open(r.url!, "_blank", "noopener");
|
||||
} else if (r.docId) {
|
||||
// A content hit opens the PDF on the matching page (browser viewers
|
||||
// honour #page=N; the Companion app's external viewer may not).
|
||||
void openSignedDocument(this.hass, r.docId, r.page ? `#page=${r.page}` : "").catch(() => { /* helper closed its tab */ });
|
||||
}
|
||||
return;
|
||||
default:
|
||||
this._showObject(r.entryId);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderPalette() {
|
||||
if (!this._paletteOpen) return nothing;
|
||||
const L = this._lang;
|
||||
const results = this._paletteResults;
|
||||
const q = this._paletteQuery.trim();
|
||||
const groupTitle: Record<SearchHit["kind"], string> = {
|
||||
object: t("objects", L), task: t("tasks", L), part: t("search_group_parts", L),
|
||||
document: t("documents", L), content: t("search_group_content", L), history: t("search_group_history", L),
|
||||
};
|
||||
const waiting = q.length >= SEARCH_MIN_CHARS && (!this._searchRemote || this._searchRemote.query !== q);
|
||||
let lastKind: SearchHit["kind"] | null = null;
|
||||
return html`
|
||||
<div class="palette-backdrop" @click=${() => this._closePalette()}>
|
||||
<div class="palette" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="palette" role="dialog" aria-label=${t("search_open", L)} @click=${(e: Event) => e.stopPropagation()}>
|
||||
<input
|
||||
class="palette-input"
|
||||
type="text"
|
||||
placeholder="${t("palette_placeholder", L)}"
|
||||
.value=${this._paletteQuery}
|
||||
@input=${(e: Event) => { this._paletteQuery = (e.target as HTMLInputElement).value; this._paletteActive = 0; }}
|
||||
@input=${(e: Event) => this._onPaletteInput((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<div class="palette-results">
|
||||
${results.length === 0
|
||||
? html`<div class="palette-empty">${t("palette_no_results", L)}</div>`
|
||||
: results.map((r, i) => html`
|
||||
<div class="palette-item ${i === this._paletteActive ? "active" : ""}"
|
||||
@mouseenter=${() => { this._paletteActive = i; }}
|
||||
@click=${() => this._selectPaletteResult(r)}>
|
||||
<ha-icon icon="${r.kind === "task" ? "mdi:clipboard-check-outline" : "mdi:package-variant-closed"}"></ha-icon>
|
||||
<span class="palette-label">${r.label}</span>
|
||||
<span class="palette-sub">${r.sub}</span>
|
||||
</div>
|
||||
`)}
|
||||
${q.length < SEARCH_MIN_CHARS && !parseRef(q)
|
||||
? html`<div class="palette-empty">${t("search_empty_hint", L)}</div>`
|
||||
: results.length === 0
|
||||
? html`<div class="palette-empty">${waiting ? t("search_searching", L) : t("palette_no_results", L)}</div>`
|
||||
: results.map((r, i) => {
|
||||
const header = r.kind !== lastKind ? html`<div class="palette-group">${groupTitle[r.kind]}</div>` : nothing;
|
||||
lastKind = r.kind;
|
||||
return html`
|
||||
${header}
|
||||
<div class="palette-item ${i === this._paletteActive ? "active" : ""} ${r.snippet ? "has-snippet" : ""}"
|
||||
@mouseenter=${() => { this._paletteActive = i; }}
|
||||
@click=${() => this._selectPaletteResult(r)}>
|
||||
<ha-icon icon="${r.icon}"></ha-icon>
|
||||
<div class="palette-main">
|
||||
<div class="palette-line">
|
||||
<span class="palette-label">${r.label}</span>
|
||||
${r.ref ? html`<span class="ref-chip">#${r.ref}</span>` : nothing}
|
||||
${r.page ? html`<span class="palette-page">${t("search_page", L).replace("{page}", String(r.page))}</span>` : nothing}
|
||||
<span class="palette-sub">${r.sub}</span>
|
||||
</div>
|
||||
${r.snippet ? html`<div class="palette-snippet">${r.snippet}</div>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${results.length > 0 && waiting ? html`<div class="palette-group palette-waiting">${t("search_searching", L)}</div>` : nothing}
|
||||
</div>
|
||||
<div class="palette-hint">${t("palette_hint", L)}</div>
|
||||
</div>
|
||||
@@ -2127,6 +2382,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
excerpt,
|
||||
new Date().toISOString(),
|
||||
partsLines,
|
||||
taskRef(obj.object, task),
|
||||
);
|
||||
openHtmlInNewTab(html);
|
||||
} finally {
|
||||
@@ -2319,6 +2575,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** The magnifier that opens the global search (#171) — in the header
|
||||
* wherever the header is on screen, in the overview's tab bar on wide
|
||||
* screens (no header there), so it is one tap away on every view. */
|
||||
private _renderSearchButton(cls: string) {
|
||||
const L = this._lang;
|
||||
return html`<ha-icon-button
|
||||
class=${cls}
|
||||
.path=${MDI_MAGNIFY}
|
||||
.label=${t("search_open", L)}
|
||||
title=${t("search_open", L)}
|
||||
@click=${() => this._openPalette()}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
private _renderHeader() {
|
||||
const crumbs: { label: string; action?: () => void }[] = [
|
||||
{ label: t("maintenance", this._lang), action: () => this._showOverview() },
|
||||
@@ -2359,6 +2629,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this._renderSearchButton("header-search")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2431,6 +2702,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
${t("settings", L)}
|
||||
</div>
|
||||
` : nothing}
|
||||
${this.narrow ? nothing : this._renderSearchButton("tab-search")}
|
||||
</div>
|
||||
${this._overviewTab === "today"
|
||||
? this._renderToday()
|
||||
@@ -2528,6 +2800,15 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private _renderTodaySection(titleKey: string, rows: TaskRow[], cls: string) {
|
||||
if (rows.length === 0) return nothing;
|
||||
const L = this._lang;
|
||||
// #169: name the responsible person like the dashboard rows do — as the
|
||||
// member's avatar (initials in their colour) plus the name; narrow
|
||||
// layouts keep only the avatar. With the person filter active every
|
||||
// row belongs to the same person, so the chip would only repeat the
|
||||
// filter — leave it out then.
|
||||
const personOf = (row: TaskRow): PersonDisplay | null =>
|
||||
this._filterUser || !row.responsible_user_id
|
||||
? null
|
||||
: this._userService?.getPerson(row.responsible_user_id) ?? null;
|
||||
return html`
|
||||
<div class="today-section">
|
||||
<div class="today-section-header ${cls}">
|
||||
@@ -2538,7 +2819,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<span class="today-dot ${row.trigger_active ? "triggered" : row.status}"></span>
|
||||
<div class="today-main">
|
||||
<div class="today-task">${row.task_name}</div>
|
||||
<div class="today-object">${row.object_name} · ${formatDueDays(row.days_until_due, L)}</div>
|
||||
<div class="today-object">
|
||||
<span class="today-object-text">${row.object_name} · ${formatDueDays(row.days_until_due, L)}</span>
|
||||
${renderPersonChip(personOf(row), "today-person")}
|
||||
</div>
|
||||
</div>
|
||||
${this._actionStyle() === "icons"
|
||||
? html`
|
||||
@@ -3326,6 +3610,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
return html`<td class="oc-notes" title=${o.notes || ""}>${o.notes || "—"}</td>`;
|
||||
case "task_count":
|
||||
return html`<td class="oc-task_count">${obj.tasks.length}</td>`;
|
||||
case "ref_no":
|
||||
return html`<td class="oc-ref_no">${objectRef(o) ? `#${objectRef(o)}` : "—"}</td>`;
|
||||
case "actions":
|
||||
return html`<td class="oc-actions">
|
||||
<mwc-icon-button title="${t("qr_code", L)}" @click=${(e: Event) => { e.stopPropagation(); this._openQrForObject(obj.entry_id, o.name); }}>
|
||||
@@ -3508,10 +3794,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
<span class="sub-chip">
|
||||
<ha-icon icon="mdi:map-marker-outline"></ha-icon>${areaName}
|
||||
</span>` : nothing}
|
||||
${userName ? html`
|
||||
<span class="sub-chip" title="${t("responsible_user", L)}">
|
||||
<ha-icon icon="mdi:account-outline"></ha-icon>${userName}
|
||||
</span>` : nothing}
|
||||
${userName ? renderPersonChip(this._userService?.getPerson(row.responsible_user_id) ?? null, "sub-chip") : nothing}
|
||||
${(row.labels || []).map((label) => html`
|
||||
<span class="sub-chip label-chip" title="${t("labels", L)}">
|
||||
<ha-icon icon="mdi:tag-outline"></ha-icon>${label}
|
||||
@@ -3552,6 +3835,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/global/update", settings });
|
||||
this._rowActionNotice = false;
|
||||
if (backToIcons) this._rowActionStyle = "icons";
|
||||
invalidateSettingsCache();
|
||||
} catch (err) {
|
||||
console.warn("[maintenance-supporter] row-action notice update failed", err);
|
||||
}
|
||||
@@ -3635,7 +3919,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
return html`
|
||||
<div class="detail-section">
|
||||
<div class="detail-header">
|
||||
<h2>${o.name}</h2>
|
||||
<h2>${o.name} ${renderRefChip(objectRef(o), t("ref_number", L))}</h2>
|
||||
<div class="action-buttons">
|
||||
${!isOperator ? html`
|
||||
<ha-button appearance="filled" @click=${() => {
|
||||
@@ -3734,7 +4018,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
: nothing}
|
||||
</span>
|
||||
<span class="cell task-name" @click=${() => this._showTask(obj.entry_id, task.id)}>${task.name}</span>
|
||||
<span class="task-sub${task.responsible_user_id ? '' : ' task-sub-empty'}">${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null)}</span>
|
||||
<span class="task-sub${task.responsible_user_id ? '' : ' task-sub-empty'}">${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null, (id) => this._userService?.getPerson(id) ?? null)}</span>
|
||||
<span class="cell type">${t(task.type, L)}</span>
|
||||
<span class="due-cell" @click=${() => this._showTask(obj.entry_id, task.id)}>
|
||||
<span class="due-text">${formatDueDays(task.days_until_due, L)}</span>
|
||||
@@ -4003,6 +4287,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
setSearch: (s) => { this._historySearch = s; },
|
||||
openEdit: (entry) => this._openHistoryEdit(entry),
|
||||
readingUnit: task?.reading_unit ?? null,
|
||||
taskRef: taskRef(this._selectedEntryId ? this._getObject(this._selectedEntryId)?.object : null, task),
|
||||
// #139: name the phase badge on entries stamped with a phase_id.
|
||||
phaseNames: Object.fromEntries(
|
||||
Object.entries(task?.phases || {}).map(([id, def]) => [id, def.name]),
|
||||
@@ -4012,6 +4297,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
if (idx <= 0) return null;
|
||||
return (entry.reading_value as number) - (readings[idx - 1].reading_value as number);
|
||||
},
|
||||
// #161 phase 2: per-slot delta against the previous entry that
|
||||
// carried the same slot (a skipped meter does not break the chain).
|
||||
readingSlotDelta: (entry, slotId) => readingSlotDelta(fullHistory, entry, slotId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4028,6 +4316,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
entryId,
|
||||
taskId,
|
||||
objectName: obj?.object.name || "",
|
||||
taskRef: taskRef(obj?.object, obj?.tasks.find((tk) => tk.id === taskId)),
|
||||
objectDocUrl: obj?.object?.documentation_url ?? null,
|
||||
objectManualDocs: obj?.object?.manual_docs ?? [],
|
||||
openManualDoc: (doc) => this._openManualDoc(doc),
|
||||
@@ -4050,10 +4339,11 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
sparkline: this._sparklineCtx,
|
||||
history: this._historyCtx(),
|
||||
getUserName: (id) => this._userService?.getUserName(id) ?? null,
|
||||
getPerson: (id) => this._userService?.getPerson(id) ?? null,
|
||||
setActiveTab: (tab) => { this._activeTab = tab; },
|
||||
toggleSection: (key) => this._toggleSection(key),
|
||||
setCostDurationToggle: (v) => { this._costDurationToggle = v; },
|
||||
showTaskView: () => { this._view = "task"; },
|
||||
showTaskView: () => this._showFullTaskPage(entryId, taskId),
|
||||
showObject: () => this._showObject(entryId),
|
||||
toggleMoreMenu: () => this._toggleMoreMenu(),
|
||||
closeMoreMenu: () => this._closeMoreMenu(),
|
||||
@@ -4115,6 +4405,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
* for in the template to trigger a data refresh. */
|
||||
private _openHistoryEdit(entry: HistoryEntry): void {
|
||||
if (!this._selectedEntryId || !this._selectedTaskId) return;
|
||||
const editTask = this._getTask(this._selectedEntryId, this._selectedTaskId);
|
||||
const draft: HistoryEntryDraft = {
|
||||
entry_id: this._selectedEntryId,
|
||||
task_id: this._selectedTaskId,
|
||||
@@ -4126,6 +4417,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
duration: entry.duration ?? null,
|
||||
completed_by: entry.completed_by ?? null,
|
||||
used_parts: entry.used_parts ?? null,
|
||||
photo_doc_ids: historyPhotoIds(entry),
|
||||
reading_value: entry.reading_value ?? null,
|
||||
reading_values: entryReadingValues(entry),
|
||||
readings: editTask?.readings ?? [],
|
||||
task_type: editTask?.type ?? null,
|
||||
reading_unit: editTask?.reading_unit ?? null,
|
||||
};
|
||||
this.shadowRoot
|
||||
?.querySelector<MaintenanceHistoryEditDialog>("maintenance-history-edit-dialog")
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@types/react": "^19.2.18",
|
||||
"@web/dev-server-esbuild": "^2.0.0",
|
||||
"@web/test-runner": "^1.0.0",
|
||||
"@web/test-runner-commands": "^1.0.1",
|
||||
"@web/test-runner-playwright": "^1.0.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"lit": "^3.3.3",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@types/react": "^19.2.18",
|
||||
"@web/dev-server-esbuild": "^2.0.0",
|
||||
"@web/test-runner": "^1.0.0",
|
||||
"@web/test-runner-commands": "^1.0.1",
|
||||
"@web/test-runner-playwright": "^1.0.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"lit": "^3.3.3",
|
||||
|
||||
@@ -399,7 +399,21 @@ export const panelStyles = css`
|
||||
.today-dot.triggered { background: #ff5722; }
|
||||
.today-main { flex: 1; min-width: 0; }
|
||||
.today-task { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.today-object { color: var(--secondary-text-color); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* #169: the sub-line is a flex row of two parts that truncate on their
|
||||
own — a long object name shortens itself instead of pushing the
|
||||
person chip out of the row on a phone (a single nowrap text line cut
|
||||
the chip first, i.e. entirely). The chip keeps a readable minimum. */
|
||||
.today-object {
|
||||
color: var(--secondary-text-color); font-size: 12.5px;
|
||||
display: flex; align-items: center; gap: 8px; min-width: 0;
|
||||
}
|
||||
.today-object-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.today-object .today-person { flex: 0 0 auto; min-width: 0; max-width: 45%; font-size: 12px; }
|
||||
.today-object .today-person .person-avatar { width: 18px; height: 18px; font-size: 9px; }
|
||||
/* Narrow: the avatar alone — initials + colour identify the member,
|
||||
the object name keeps its room. */
|
||||
:host([narrow]) .today-object .today-person .person-name,
|
||||
:host([tight]) .today-object .today-person .person-name { display: none; }
|
||||
.today-row .btn-complete { color: var(--success-color, #4caf50); flex: none; }
|
||||
.today-row .today-complete { flex: none; --ha-button-font-size: 13px; white-space: nowrap; }
|
||||
.today-row .today-complete ha-icon { --mdc-icon-size: 18px; }
|
||||
@@ -411,7 +425,10 @@ export const panelStyles = css`
|
||||
}
|
||||
.today-empty ha-icon { --mdc-icon-size: 56px; color: var(--success-color, #4caf50); }
|
||||
|
||||
/* Command palette (Ctrl/Cmd+K). */
|
||||
.header .header-search { margin-left: auto; color: var(--app-header-text-color, white); --ha-icon-button-size: 40px; flex: none; }
|
||||
.tab-bar .tab-search { margin-left: auto; align-self: center; color: var(--secondary-text-color); --ha-icon-button-size: 40px; flex: none; }
|
||||
|
||||
/* Global search overlay ("/" or the header magnifier; #171). */
|
||||
.palette-backdrop {
|
||||
position: fixed; inset: 0; z-index: 1100; background: rgba(0,0,0,.4);
|
||||
display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh;
|
||||
@@ -433,8 +450,15 @@ export const panelStyles = css`
|
||||
}
|
||||
.palette-item.active { background: color-mix(in srgb, var(--primary-color) 14%, transparent); }
|
||||
.palette-item ha-icon { --mdc-icon-size: 20px; color: var(--secondary-text-color); flex: none; }
|
||||
.palette-main { min-width: 0; flex: 1; }
|
||||
.palette-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.palette-label { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.palette-sub { margin-left: auto; color: var(--secondary-text-color); font-size: 12.5px; flex: none; padding-left: 10px; }
|
||||
.palette-sub { margin-left: auto; color: var(--secondary-text-color); font-size: 12.5px; flex: none; padding-left: 10px; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.palette-page { flex: none; font-size: 11.5px; padding: 1px 7px; border-radius: 10px; background: color-mix(in srgb, var(--primary-color) 14%, transparent); color: var(--primary-color); }
|
||||
.palette-snippet { font-size: 12.5px; color: var(--secondary-text-color); margin-top: 2px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.palette-group { padding: 10px 16px 4px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--secondary-text-color); }
|
||||
.palette-group + .palette-item { border-top: none; }
|
||||
.palette-waiting { font-weight: 400; text-transform: none; letter-spacing: 0; padding-bottom: 10px; }
|
||||
.palette-empty { padding: 20px 16px; color: var(--secondary-text-color); text-align: center; }
|
||||
.palette-hint { padding: 8px 16px; font-size: 12px; color: var(--secondary-text-color); border-top: 1px solid var(--divider-color); }
|
||||
|
||||
@@ -532,6 +556,8 @@ export const panelStyles = css`
|
||||
--mdc-icon-size: 14px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
/* #169 follow-up: the member avatar sits inside the pill at chip height */
|
||||
.sub-chip .person-avatar { width: 16px; height: 16px; font-size: 8.5px; margin-left: -4px; }
|
||||
|
||||
/* Row action buttons (Complete / Skip): right-aligned in their column and a
|
||||
bit larger — the default mwc glyph reads small inside its padded button. */
|
||||
@@ -858,16 +884,17 @@ export const panelStyles = css`
|
||||
.user-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
gap: 5px;
|
||||
padding: 2px 8px 2px 3px;
|
||||
margin-left: 8px;
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
border-radius: 10px;
|
||||
background: var(--secondary-background-color, rgba(127, 127, 127, 0.12));
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.user-badge .person-avatar { width: 18px; height: 18px; font-size: 9px; }
|
||||
|
||||
.user-badge ha-icon {
|
||||
--mdc-icon-size: 12px;
|
||||
|
||||
@@ -10,6 +10,9 @@ import { html, nothing } from "lit";
|
||||
import { t, formatDateTime, formatNumber, formatCost, STATUS_ICONS } from "../styles";
|
||||
import type { MaintenanceTask, HistoryEntry, HomeAssistant } from "../types";
|
||||
import "../components/history-photo";
|
||||
import { historyPhotoIds } from "../helpers/history-photos";
|
||||
import { renderRefChip } from "../helpers/reference";
|
||||
import { entryReadingValues } from "../helpers/reading-slots";
|
||||
|
||||
export interface HistoryContext {
|
||||
lang: string;
|
||||
@@ -27,8 +30,12 @@ export interface HistoryContext {
|
||||
/** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */
|
||||
readingUnit?: string | null;
|
||||
readingDelta?: (entry: HistoryEntry) => number | null;
|
||||
/** #161 phase 2: delta of one slot vs. the previous entry carrying it. */
|
||||
readingSlotDelta?: (entry: HistoryEntry, slotId: string) => number | null;
|
||||
/** #139: phase id → display name, for entries stamped with a phase_id. */
|
||||
phaseNames?: Record<string, string>;
|
||||
/** #170: the task's reference ("8.3") — completions render "#8.3-<n>". */
|
||||
taskRef?: string | null;
|
||||
}
|
||||
|
||||
// Every HistoryEntryType is filterable (drift audit 2026-08: the repair-flow
|
||||
@@ -70,7 +77,8 @@ export function renderHistoryList(task: MaintenanceTask, ctx: HistoryContext) {
|
||||
// Apply search filter
|
||||
if (ctx.search) {
|
||||
const search = ctx.search.toLowerCase();
|
||||
filtered = filtered.filter((h) => h.notes?.toLowerCase().includes(search));
|
||||
// #170: "8.3-2" (or just "-2") in the notes filter finds the entry by number.
|
||||
filtered = filtered.filter((h) => h.notes?.toLowerCase().includes(search) || (entryRefOf(ctx, h) ?? "").toLowerCase().endsWith(search));
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
@@ -84,6 +92,11 @@ export function renderHistoryList(task: MaintenanceTask, ctx: HistoryContext) {
|
||||
`;
|
||||
}
|
||||
|
||||
/** "8.3-2" for a numbered completion, null otherwise. */
|
||||
function entryRefOf(ctx: HistoryContext, entry: HistoryEntry): string | null {
|
||||
return ctx.taskRef && entry.ref_no ? `${ctx.taskRef}-${entry.ref_no}` : null;
|
||||
}
|
||||
|
||||
export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) {
|
||||
const L = ctx.lang;
|
||||
// v2.2.0: only "lifecycle + cost-bearing" entries are user-editable.
|
||||
@@ -98,6 +111,7 @@ export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) {
|
||||
<div class="history-content">
|
||||
<div class="history-row">
|
||||
<strong>${t(entry.type, L)}</strong>
|
||||
${renderRefChip(entryRefOf(ctx, entry), t("ref_number", L))}
|
||||
${entry.phase_id ? html`<span class="history-phase-badge">${ctx.phaseNames?.[entry.phase_id] || entry.phase_id}</span>` : nothing}
|
||||
${entry.auto ? html`<span class="history-auto-badge">${t("history_auto", L)}</span>` : nothing}
|
||||
${editable
|
||||
@@ -110,9 +124,30 @@ export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) {
|
||||
</div>
|
||||
<div class="history-date">${formatDateTime(entry.timestamp, L)}</div>
|
||||
${entry.notes ? html`<div>${entry.notes}</div>` : nothing}
|
||||
${entry.photo_doc_id
|
||||
? html`<maintenance-history-photo .hass=${ctx.hass} .docId=${entry.photo_doc_id}></maintenance-history-photo>`
|
||||
: nothing}
|
||||
${(() => {
|
||||
// #161: photo_doc_ids (new) or the pre-multi-photo photo_doc_id scalar.
|
||||
const photos = historyPhotoIds(entry);
|
||||
return photos.length > 0
|
||||
? html`<div class="history-photos">
|
||||
${photos.map((docId) => html`<maintenance-history-photo .hass=${ctx.hass} .docId=${docId}></maintenance-history-photo>`)}
|
||||
</div>`
|
||||
: nothing;
|
||||
})()}
|
||||
${(() => {
|
||||
// #161 phase 2: one line per recorded slot, delta vs. the previous
|
||||
// completion that carried the same slot.
|
||||
const values = entryReadingValues(entry);
|
||||
if (values.length === 0) return nothing;
|
||||
return html`<div class="history-readings">
|
||||
${values.map((v) => {
|
||||
const d = ctx.readingSlotDelta?.(entry, v.id);
|
||||
return html`<span class="history-reading">
|
||||
<span class="history-reading-name">${v.name}</span>
|
||||
<span class="history-reading-value">${formatNumber(v.value, L, { maximumFractionDigits: 3 })}${v.unit ? ` ${v.unit}` : ""}${d == null ? "" : ` (${d >= 0 ? "+" : ""}${formatNumber(d, L, { maximumFractionDigits: 3 })})`}</span>
|
||||
</span>`;
|
||||
})}
|
||||
</div>`;
|
||||
})()}
|
||||
<div class="history-details">
|
||||
${entry.cost != null ? html`<span>${t("cost", L)}: ${formatCost(entry.cost, ctx.currencySymbol, L)}</span>` : nothing}
|
||||
${entry.duration != null ? html`<span>${t("duration", L)}: ${entry.duration} min</span>` : nothing}
|
||||
|
||||
@@ -48,12 +48,20 @@ export function computeTrend(
|
||||
return null; // compound: conditions may point both ways
|
||||
}
|
||||
const statsPoints = miniStatsData.get(tc.entity_id) || [];
|
||||
// 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
|
||||
// (100 h at the trigger, 20 h now) as "easing" while the hours climb
|
||||
// (bug review 2026-09-04). Recorder stats and the live value only.
|
||||
const resetsOnCompletion = type === "runtime" || type === "state_change";
|
||||
let points: { ts: number; val: number }[] =
|
||||
statsPoints.length >= 2
|
||||
? statsPoints.map((p) => ({ ts: p.ts, val: p.val }))
|
||||
: (row.history || [])
|
||||
.filter((h) => h.trigger_value != null)
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value as number }));
|
||||
: resetsOnCompletion
|
||||
? []
|
||||
: (row.history || [])
|
||||
.filter((h) => h.trigger_value != null)
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value as number }));
|
||||
if (row.trigger_current_value != null) points = [...points, { ts: Date.now(), val: row.trigger_current_value }];
|
||||
if (points.length < 2) return null;
|
||||
points.sort((a, b) => a.ts - b.ts);
|
||||
@@ -63,9 +71,17 @@ export function computeTrend(
|
||||
// Significance is judged against the JOURNEY to the threshold, not the
|
||||
// series' own noise band: a sensor crawling 50.0 -> 50.15 under a
|
||||
// 60-threshold is flat, even though that move spans most of its range.
|
||||
// A delta counter's series is the RAW reading (odometer), so its finish
|
||||
// line is baseline + interval — judged against the bare interval, a car
|
||||
// at 100 000 km with a 15 000 km service interval read "stable" forever
|
||||
// (bug review 2026-09-04).
|
||||
const counterTarget =
|
||||
tc.trigger_delta_mode && row.trigger_baseline_value != null && tc.trigger_target_value != null
|
||||
? row.trigger_baseline_value + tc.trigger_target_value
|
||||
: tc.trigger_target_value;
|
||||
const target =
|
||||
type === "threshold" ? (tc.trigger_above ?? tc.trigger_below)
|
||||
: type === "counter" ? tc.trigger_target_value
|
||||
: type === "counter" ? counterTarget
|
||||
: type === "runtime" ? tc.trigger_runtime_hours
|
||||
: tc.trigger_target_changes;
|
||||
const base = typeof target === "number" ? Math.max(Math.abs(target - vals[0]), range) : range;
|
||||
@@ -88,18 +104,35 @@ export function renderTriggerProgress(row: TaskRow | MaintenanceTask, opts?: { t
|
||||
if (val == null) return nothing;
|
||||
const above = tc.trigger_above;
|
||||
const below = tc.trigger_below;
|
||||
if (above != null) {
|
||||
// Progress toward upper limit
|
||||
const low = below ?? 0;
|
||||
if (above != null && below != null) {
|
||||
// Both limits = a band the reading must stay inside (OR-ed on the
|
||||
// server): the bar measures the distance from the band's centre, so a
|
||||
// reading near EITHER edge fills it. Measuring from `below` up to
|
||||
// `above` painted a reading just under `below` as 0 % / green
|
||||
// although it had already triggered (bug review 2026-09-04).
|
||||
const mid = (above + below) / 2;
|
||||
const half = Math.abs(above - below) / 2 || 1;
|
||||
pct = Math.min(100, Math.max(0, (Math.abs(val - mid) / half) * 100));
|
||||
const nearer = Math.abs(val - above) <= Math.abs(val - below) ? above : below;
|
||||
label = `${formatNumber(val, opts?.lang, 1)} / ${formatNumber(nearer, opts?.lang)} ${unit}`;
|
||||
} else if (above != null) {
|
||||
// Progress toward the upper limit, from a sign-aware floor: 0 for a
|
||||
// positive limit; for a negative one (freezer "warn above −15 °C")
|
||||
// the entity's min or 2× the limit — anchoring at 0 put the floor
|
||||
// ABOVE the limit and rendered every safe reading as a full red bar
|
||||
// (bug review 2026-09-04).
|
||||
const entityMin = row.trigger_entity_info?.min;
|
||||
const low = above > 0 ? 0 : entityMin != null && entityMin < above ? entityMin : above < 0 ? 2 * above : -100;
|
||||
const range = above - low || 1;
|
||||
pct = Math.min(100, Math.max(0, ((val - low) / range) * 100));
|
||||
label = `${formatNumber(val, opts?.lang, 1)} / ${formatNumber(above, opts?.lang)} ${unit}`;
|
||||
} else if (below != null) {
|
||||
// Progress toward lower limit (inverted: lower is worse)
|
||||
// Use entity max, or 2x the threshold as a stable "safe" reference.
|
||||
// Using val*2 caused a dynamic ceiling that distorted the bar.
|
||||
// Progress toward the lower limit (inverted: lower is worse), from a
|
||||
// sign-aware ceiling: the entity max, else 2× the limit for a
|
||||
// positive one and 0 for a negative one (2× a negative limit is BELOW
|
||||
// it). A fixed ceiling — val*2 made the bar wander with the reading.
|
||||
const entityMax = row.trigger_entity_info?.max;
|
||||
const high = entityMax ?? ((below * 2) || 100);
|
||||
const high = entityMax != null && entityMax > below ? entityMax : below > 0 ? below * 2 : below < 0 ? 0 : 100;
|
||||
const range = high - below || 1;
|
||||
pct = Math.min(100, Math.max(0, ((high - val) / range) * 100));
|
||||
label = `${formatNumber(val, opts?.lang, 1)} / ${formatNumber(below, opts?.lang)} ${unit}`;
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { isSafeHttpUrl } from "../helpers/url";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
import { renderPersonAvatar, type PersonDisplay } from "../helpers/person";
|
||||
import { t, formatDate, formatDateTime, formatRecurrence, formatNumber, formatCost } from "../styles";
|
||||
import { renderRefChip } from "../helpers/reference";
|
||||
import type { AdvancedFeatures, HomeAssistant, MaintenanceTask, ManualDocRef } from "../types";
|
||||
import { renderTriggerSection, type SparklineContext } from "./sparkline";
|
||||
import { renderPredictionSection } from "./prediction";
|
||||
@@ -32,6 +34,8 @@ export interface TaskDetailContext {
|
||||
taskId: string;
|
||||
/** Parent object's display name (breadcrumb + object-manual label). */
|
||||
objectName: string;
|
||||
/** #170: the task's reference ("8.3"), null until the backend numbered it. */
|
||||
taskRef?: string | null;
|
||||
/** Parent object's documentation_url (raw; sanitised here). */
|
||||
objectDocUrl: string | null | undefined;
|
||||
/** Parent object's manual-tagged documents — the fallback for the manual
|
||||
@@ -57,6 +61,8 @@ export interface TaskDetailContext {
|
||||
sparkline: SparklineContext;
|
||||
history: HistoryContext;
|
||||
getUserName: (userId: string) => string | null;
|
||||
/** #169 follow-up: avatar + name of a member (null = unknown / not loaded). */
|
||||
getPerson?: (userId: string) => PersonDisplay | null;
|
||||
// ── Panel-owned state mutations ────────────────────────────────────────
|
||||
setActiveTab: (tab: "overview" | "history") => void;
|
||||
toggleSection: (key: string) => void;
|
||||
@@ -89,8 +95,13 @@ export interface TaskDetailContext {
|
||||
export function renderUserBadge(
|
||||
task: MaintenanceTask,
|
||||
getUserName: (userId: string) => string | null,
|
||||
getPerson?: (userId: string) => PersonDisplay | null,
|
||||
) {
|
||||
if (!task.responsible_user_id) return nothing;
|
||||
const person = getPerson?.(task.responsible_user_id) ?? null;
|
||||
if (person) {
|
||||
return html`<span class="user-badge">${renderPersonAvatar(person)}${person.name}</span>`;
|
||||
}
|
||||
const userName = getUserName(task.responsible_user_id);
|
||||
if (!userName) return nothing;
|
||||
return html`
|
||||
@@ -114,13 +125,14 @@ function renderTaskHeader(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||
<div class="task-header">
|
||||
<div class="task-header-title">
|
||||
<span class="task-name-breadcrumb" @click=${() => ctx.showTaskView()}>${task.name}</span>
|
||||
${renderRefChip(ctx.taskRef ?? null, t("ref_number", L))}
|
||||
<span class="breadcrumb-separator">·</span>
|
||||
<span class="object-name-breadcrumb" @click=${() => ctx.showObject()}>${ctx.objectName}</span>
|
||||
<span class="status-chip ${statusClass}">${statusText}</span>
|
||||
${task.due_override ? html`<span class="postponed-badge" title="${t("postponed_to", L)}">
|
||||
<ha-icon icon="mdi:calendar-arrow-right"></ha-icon>${formatDate(task.due_override, L)}
|
||||
</span>` : nothing}
|
||||
${renderUserBadge(task, ctx.getUserName)}
|
||||
${renderUserBadge(task, ctx.getUserName, ctx.getPerson)}
|
||||
${task.nfc_tag_id
|
||||
? html`<span class="nfc-badge" title="${t("nfc_tag_id", L)}: ${task.nfc_tag_id}"><ha-icon icon="mdi:nfc-variant"></ha-icon> NFC</span>`
|
||||
: !isOperator ? html`<span class="nfc-badge unlinked" title="${t("nfc_link_hint", L)}"
|
||||
|
||||
@@ -472,7 +472,34 @@ export const nativeFieldStyles = css`
|
||||
.field-input:focus { outline: none; border-color: var(--primary-color); }
|
||||
`;
|
||||
|
||||
/** Household member avatar (#169 follow-up): initials in the member's
|
||||
* palette colour. Part of sharedStyles (panel, card, dialogs) and pulled
|
||||
* in on its own by surfaces with their own stylesheet (settings view). */
|
||||
export const personStyles = css`
|
||||
.person-chip { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; vertical-align: middle; }
|
||||
.person-avatar {
|
||||
width: 22px; height: 22px; border-radius: 50%; flex: none;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: var(--person-color, #546e7a); color: #fff;
|
||||
font-size: 10.5px; font-weight: 600; letter-spacing: 0.02em; line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.person-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
`;
|
||||
|
||||
/** #170: the muted reference-number chip ("#8.3") — shared by every surface. */
|
||||
export const refStyles = css`
|
||||
.ref-chip {
|
||||
display: inline-block; font-size: 11.5px; font-weight: 500; line-height: 1; padding: 3px 6px;
|
||||
border-radius: 6px; border: 1px solid var(--divider-color); color: var(--secondary-text-color);
|
||||
font-variant-numeric: tabular-nums; letter-spacing: .02em; vertical-align: middle; white-space: nowrap;
|
||||
user-select: all;
|
||||
}
|
||||
`;
|
||||
|
||||
export const sharedStyles = css`
|
||||
${personStyles}
|
||||
${refStyles}
|
||||
:host {
|
||||
--maint-ok-color: var(--success-color, #4caf50);
|
||||
--maint-due-soon-color: var(--warning-color, #ff9800);
|
||||
@@ -731,6 +758,37 @@ export const sharedStyles = css`
|
||||
.history-phase-badge:has(+ .history-auto-badge) {
|
||||
margin-right: 0;
|
||||
}
|
||||
/* #161 — a completion can carry several photos; thumbnails wrap. */
|
||||
.history-photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
/* #161 phase 2 — named readings of a completion: a compact
|
||||
name / value grid, wrapping into columns on wide screens. */
|
||||
.history-readings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 280px));
|
||||
gap: 2px 24px;
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.history-reading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.history-reading-name {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.history-reading-value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.history-edit-btn {
|
||||
background: transparent;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -1625,15 +1683,24 @@ export const sharedStyles = css`
|
||||
.sparkline-container { max-width: 100%; overflow: hidden; }
|
||||
.sparkline-svg { height: 100px; }
|
||||
|
||||
.stats-bar { gap: 8px; padding: 12px; }
|
||||
/* #150 — phones get a FIXED 10-track grid instead of auto-fit: the five
|
||||
KPI tiles span 2 tracks each (= one full row on any width ≥ 320 px),
|
||||
the two budget tiles span 5 each (= a full second row). auto-fit with
|
||||
a px floor rewrapped to 3-4 columns on 360-412 px phones and left one
|
||||
or two tiles orphaned on their own row. */
|
||||
.stats-bar { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 6px; padding: 12px; }
|
||||
/* min-width: 0 (NOT a fixed floor) — a fixed min-width re-enables the
|
||||
grid's auto minimum, so the 5 KPI tracks couldn't shrink below their
|
||||
label text and the last KPI clipped off-screen on phones (the header
|
||||
only *looked* cut — .content scrolls sideways, but nothing hints so).
|
||||
With 0 the tracks compress and the labels wrap to a second line. */
|
||||
.stat-item { min-width: 0; }
|
||||
.stat-item.clickable { padding: 4px 4px; }
|
||||
.stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; }
|
||||
.stat-item { min-width: 0; grid-column: span 2; }
|
||||
.stat-item.budget-tile { grid-column: span 5; }
|
||||
.stat-item.clickable { padding: 4px 2px; }
|
||||
/* ~60 px tracks: multi-word labels wrap at the space, single overlong
|
||||
words (nl "Achterstallig") hyphenate where the browser can and hard-
|
||||
break as the last resort — never clip. */
|
||||
.stat-item .stat-label { font-size: 11px; white-space: normal; text-align: center; line-height: 1.2; hyphens: auto; overflow-wrap: anywhere; }
|
||||
.stat-value { font-size: 20px; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
export interface MaintenanceObject {
|
||||
id: string;
|
||||
name: string;
|
||||
/** #170: reference number ("8") — assigned once by the backend, never reused. */
|
||||
ref_no?: number | null;
|
||||
area_id?: string | null;
|
||||
manufacturer?: string | null;
|
||||
model?: string | null;
|
||||
@@ -87,8 +89,26 @@ export interface TriggerEntityInfo {
|
||||
step?: number | null;
|
||||
}
|
||||
|
||||
/** #161 phase 2: one named reading a `reading` task records per completion. */
|
||||
export interface ReadingSlot {
|
||||
id: string;
|
||||
name: string;
|
||||
unit?: string | null;
|
||||
}
|
||||
|
||||
/** #161 phase 2: a slot's value on a completion — name/unit are a snapshot
|
||||
* taken at completion time, `id` is what deltas match on. */
|
||||
export interface ReadingValue {
|
||||
id: string;
|
||||
name: string;
|
||||
unit?: string | null;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
timestamp: string;
|
||||
/** #170: completion number within the task ("8.3-2"); skips carry none. */
|
||||
ref_no?: number | null;
|
||||
type: string; // "completed" | "skipped" | "reset" | "triggered"
|
||||
notes?: string | null;
|
||||
cost?: number | null;
|
||||
@@ -97,9 +117,14 @@ export interface HistoryEntry {
|
||||
checklist_state?: Record<string, boolean> | null;
|
||||
feedback?: string | null;
|
||||
completed_by?: string | null;
|
||||
/** Pre-#161 single completion photo — read for old entries, never written. */
|
||||
photo_doc_id?: string | null;
|
||||
/** #161: every photo attached to this completion (see helpers/history-photos). */
|
||||
photo_doc_ids?: string[] | null;
|
||||
/** v2.20 (#83): recorded value for `reading`-type tasks. */
|
||||
reading_value?: number | null;
|
||||
/** #161 phase 2: per-slot snapshot of a task with reading slots. */
|
||||
reading_values?: ReadingValue[] | null;
|
||||
/** #99/#130: the completion's part consumption (entry_id set for pooled). */
|
||||
used_parts?: Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null;
|
||||
/** v2.37: completion recorded by the system itself (trigger recovered),
|
||||
@@ -173,6 +198,8 @@ export interface TaskSchedule {
|
||||
export interface MaintenanceTask {
|
||||
id: string;
|
||||
name: string;
|
||||
/** #170: reference number within the object ("8.3" = object 8, task 3). */
|
||||
ref_no?: number | null;
|
||||
type: string; // "cleaning" | "inspection" | "replacement" | "calibration" | "service" | "reading" | "custom"
|
||||
enabled: boolean;
|
||||
/** #150: false = the skip lock — Skip hidden everywhere, server refuses. */
|
||||
@@ -272,6 +299,8 @@ export interface MaintenanceTask {
|
||||
document_count?: number;
|
||||
/** v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). */
|
||||
reading_unit?: string | null;
|
||||
/** #161 phase 2: reading slots ([] = single value with reading_unit). */
|
||||
readings?: ReadingSlot[] | null;
|
||||
priority?: string | null;
|
||||
entity_slug?: string | null;
|
||||
// Auto-derived sensor + binary_sensor entity_ids (since 1.0.45)
|
||||
@@ -587,6 +616,9 @@ export interface HomeAssistant {
|
||||
export interface HAUser {
|
||||
id: string;
|
||||
name: string;
|
||||
/** #169 follow-up: avatar (override or derived), sent to every caller. */
|
||||
initials?: string;
|
||||
color?: string;
|
||||
// Only returned to admin callers of users/list; absent for non-admins.
|
||||
is_admin?: boolean;
|
||||
is_owner?: boolean;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { HomeAssistant, HAUser } from "./types";
|
||||
import { personOf, type PersonDisplay } from "./helpers/person";
|
||||
|
||||
export class UserService {
|
||||
private hass: HomeAssistant;
|
||||
@@ -92,6 +93,11 @@ export class UserService {
|
||||
/**
|
||||
* Get full user object by ID (from cache)
|
||||
*/
|
||||
/** Avatar + name for a user id (null until users are loaded / unknown id). */
|
||||
getPerson(userId: string | null): PersonDisplay | null {
|
||||
return personOf(this.getUser(userId));
|
||||
}
|
||||
|
||||
getUser(userId: string | null): HAUser | null {
|
||||
if (!userId || !this.usersCache) {
|
||||
return null;
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: neplatná hodnota",
|
||||
"feat_schedule_time": "Plánování podle denní doby",
|
||||
"feat_schedule_time_desc": "Úkoly se stanou po termínu v určenou denní dobu místo o půlnoci.",
|
||||
"schedule_time_optional": "Termín v čase (volitelné)",
|
||||
"schedule_time_toggle": "Termín v určitou hodinu",
|
||||
"schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.",
|
||||
"at_time": "v",
|
||||
"notes_optional": "Poznámky (volitelné)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Žádný tag —",
|
||||
"dashboard": "Přehled",
|
||||
"tab_today": "Dnes",
|
||||
"palette_placeholder": "Hledat objekty a úkoly…",
|
||||
"palette_placeholder": "Hledat objekty, úkoly, díly, dokumenty, poznámky…",
|
||||
"palette_no_results": "Žádné výsledky",
|
||||
"palette_hint": "↑↓ pohyb · Enter otevřít · Esc zavřít",
|
||||
"palette_hint": "↑↓ navigace · Enter otevřít · Esc zavřít · / otevře hledání kdekoli",
|
||||
"search_open": "Hledat",
|
||||
"search_group_parts": "Náhradní díly",
|
||||
"search_group_content": "V dokumentech",
|
||||
"search_group_history": "Poznámky v historii",
|
||||
"search_page": "Strana {page}",
|
||||
"search_empty_hint": "Pište pro hledání v objektech, úkolech, dílech, dokumentech a poznámkách",
|
||||
"search_searching": "Prohledávání dokumentů a poznámek…",
|
||||
"search_index_status": "Fulltextové hledání: {indexed} z {total} souborů indexováno · {no_text} bez textové vrstvy · {pending} čeká",
|
||||
"today_all_caught_up": "Vše hotovo! Tento týden nic nezbývá.",
|
||||
"today_overdue": "Po termínu",
|
||||
"today_due_today": "Dnes",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Práh „spotřební materiál nízký“ (%)",
|
||||
"settings_battery_low_percent": "Práh „baterie slabá“ (%)",
|
||||
"settings_thresholds_hint": "Výchozí hodnoty pro nová Navrhovaná nastavení (zbývající procenta) a pro baterie flotily bez vlastního prahu Battery Notes. Stávající úkoly si ponechají svou hodnotu.",
|
||||
"settings_value_out_of_range": "Hodnota musí být mezi {min} a {max}",
|
||||
"bn_summary": "{name}: {pct} % pro {n} baterií",
|
||||
"bn_floor_decides": "Tvůj práh {floor} % rozhoduje pro všechny; u každé baterie platí vyšší z obou prahů.",
|
||||
"bn_above_floor": "To je nad tvým prahem {floor} %, takže o všech poznamenaných bateriích rozhoduje {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testovací oznámení odesláno",
|
||||
"test_notification_failed": "Testovací oznámení se nezdařilo",
|
||||
"notify_per_person": "Doručování podle osoby",
|
||||
"member_avatars": "Avatary členů",
|
||||
"member_avatars_hint": "Zobrazuje se u přiřazených úkolů — iniciály v barvě člena. Výchozí hodnota vychází ze jména; nastavte vlastní iniciály nebo vyberte barvu pro rozlišení členů.",
|
||||
"member_initials": "Iniciály",
|
||||
"member_color": "Barva",
|
||||
"member_avatar_reset": "Obnovit",
|
||||
"notify_no_own_device": "Žádné vlastní zařízení — použije domácí službu",
|
||||
"settings_notify_due_soon": "Oznámit když brzy",
|
||||
"settings_notify_overdue": "Oznámit když po termínu",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Seskupit podle",
|
||||
"state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.",
|
||||
"target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).",
|
||||
"state_latch_help": "Při 1 přechodu je spouštěč západka: obnoví se, když entita opustí cílový stav — nebo, je-li nastaven jen výchozí stav, když se do něj vrátí.",
|
||||
"for_minutes_state_help": "0 počítá každou změnu ihned. S minutami musí nový stav nejprve vydržet — krátké bliknutí pak nespustí ani se nepočítá.",
|
||||
"qr_print_title": "Tisk QR kódů",
|
||||
"qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Smazat „{name}“?",
|
||||
"doc_too_large": "Soubor je příliš velký (max. 25 MB).",
|
||||
"doc_upload_failed": "Nahrání selhalo.",
|
||||
"completion_photo_optional": "Fotka dokončení (volitelné)",
|
||||
"add_photo": "Přidat fotku",
|
||||
"completion_photos_optional": "Fotky z dokončení (volitelné)",
|
||||
"completion_photos": "Fotky z dokončení",
|
||||
"choose_photos": "Vybrat fotky",
|
||||
"choose_photo": "Vybrat fotku",
|
||||
"photos_android_hint": "Aplikace pro Android předá jednu fotku na výběr — každý výběr se přidá.",
|
||||
"add_photos": "Přidat fotky",
|
||||
"photos_limit": "Nejvýše {max} fotek na dokončení",
|
||||
"history_edit_photos_hint": "Zde odebraná fotka zůstává v dokumentech objektu.",
|
||||
"uploading": "Nahrávání…",
|
||||
"remove": "Odebrat",
|
||||
"doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Zobrazí se vedle zaznamenané hodnoty při dokončení tohoto úkolu.",
|
||||
"reading_value_label": "Hodnota odečtu",
|
||||
"reading_label": "Odečet",
|
||||
"readings_section": "Odečty",
|
||||
"readings_hint": "Několik pojmenovaných hodnot na dokončení — jeden řádek na měřič. Nechte prázdné pro jedinou hodnotu.",
|
||||
"reading_name_label": "Název odečtu",
|
||||
"reading_unit_short": "Jednotka",
|
||||
"reading_add": "Přidat odečet",
|
||||
"reading_duplicate_name": "Název je již použit",
|
||||
"reading_last": "naposledy: {value}",
|
||||
"reading_below_last": "Nižší než poslední odečet ({value})",
|
||||
"settings_templates_label": "Galerie šablon",
|
||||
"settings_templates_hint": "Odškrtněte šablony, které nikdy nevyužijete — zmizí z výběru „Ze šablony\" (panel i config flow). Nic jiného se nemění; kdykoli je lze znovu zapnout.",
|
||||
"worksheet": "Pracovní list",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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í.",
|
||||
"settings_notify_rule": "Vlastní pravidlo oznámení",
|
||||
"settings_notify_rule_hint": "Každé oznámení navíc vyvolá událost maintenance_supporter_notification (druh, stav, objekt, úkol, referenční čísla, priorita, odkaz, data). Nasměrujte ji automatizací — např. do Tickeru, Telegramu nebo Pushoveru.",
|
||||
"settings_notify_event_only": "Jen událost, nic neposílat",
|
||||
"settings_notify_event_only_hint": "Doručení zajišťuje vaše automatizace. Intervaly opakování, tiché hodiny a denní limit dál rozhodují, kdy se událost vyvolá.",
|
||||
"settings_notify_extra_data": "Další data oznámení (šablona)",
|
||||
"settings_notify_extra_data_hint": "JSON nebo YAML, Jinja povoleno; sloučí se do data volání notify (vaše klíče mají přednost). Proměnné: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Ověřte tlačítkem Odeslat test.",
|
||||
"card_saved_view": "Uložené zobrazení",
|
||||
"card_saved_view_none": "Žádné",
|
||||
"card_saved_view_help": "Použije filtry stavu, uživatele a štítku zobrazení navíc k výše uvedeným filtrům. Řazení a seskupení zobrazení jsou nastavení panelu a na kartě se nepoužijí.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Označit vše jako vyměněné",
|
||||
"battery_fleet_mark_one": "Označit tuto baterii jako vyměněnou",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Bez senzoru",
|
||||
"battery_fleet_trigger_lost": "Senzorový spouštěč této úlohy byl ztracen — nespustí se ani se automaticky nedokončí.",
|
||||
"battery_fleet_repair": "Opravit",
|
||||
"battery_fleet_exclude": "Vyloučit z flotily",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Vyberte senzor baterie, který automatická detekce přehlédla — okamžitě se objeví v seznamu.",
|
||||
"battery_fleet_track_self": "Sledovat samodobíjecí baterie",
|
||||
"battery_fleet_track_self_hint": "Telefony, vysavače a další samodobíjecí zařízení se zobrazují jako akumulátory — při nízkém stavu žádají o nabití, nikdy o nové články.",
|
||||
"battery_fleet_due_without_sensor": "Prošlou předpověď považovat za termín výměny, pokud baterie nemá senzor",
|
||||
"battery_fleet_due_without_sensor_hint": "Některá zařízení vůbec nehlásí stav baterie — Battery Notes zná jen typ a poslední výměnu. Po uplynutí typické životnosti se taková baterie počítá jako vybitá a úkol flotily vás upozorní; označení jako vyměněné předpověď vynuluje.",
|
||||
"battery_fleet_status_low": "Nízká",
|
||||
"battery_fleet_status_soon": "Brzy",
|
||||
"battery_fleet_status_ok": "V pořádku",
|
||||
"battery_fleet_status_due": "K výměně",
|
||||
"battery_fleet_predicted_on": "Očekáváno kolem {date}",
|
||||
"battery_fleet_predicted_trend": "Předpověď z trendu vybíjení této baterie: přibližně {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulátor: nabíjí se místo výměny — nikdy na nákupním seznamu",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servisní knížka (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "záznamů",
|
||||
"ref_number": "Referenční číslo",
|
||||
"print_options_title": "Tisk servisní knížky",
|
||||
"print_layout": "Rozvržení",
|
||||
"print_layout_chronological": "Chronologicky",
|
||||
"print_layout_by_task": "Podle úkolu",
|
||||
"print_include": "Zahrnout",
|
||||
"print_inc_readings": "Odečty",
|
||||
"print_inc_parts": "Použité díly",
|
||||
"print_inc_photos": "Fotky",
|
||||
"print_inc_documents": "Propojené dokumenty",
|
||||
"print_inc_checklist": "Kontrolní seznam",
|
||||
"print_inc_notes": "Poznámky",
|
||||
"print_inc_costs": "Náklady a doba",
|
||||
"print_inc_person": "Provedl",
|
||||
"print_inc_refs": "Referenční čísla",
|
||||
"print_inc_qr": "QR kód pro každý úkol",
|
||||
"print_button": "Tisk",
|
||||
"report_scan_hint": "Naskenujte pro otevření úkolu",
|
||||
"completed_by": "Dokončil",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ugyldig værdi",
|
||||
"feat_schedule_time": "Planlægning efter tidspunkt på dagen",
|
||||
"feat_schedule_time_desc": "Opgaver bliver forfaldne på et bestemt tidspunkt på dagen i stedet for midnat.",
|
||||
"schedule_time_optional": "Forfalder kl. (valgfrit)",
|
||||
"schedule_time_toggle": "Forfalder på et bestemt tidspunkt",
|
||||
"schedule_time_help": "Tom = midnat (standard). HA-tidszone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Noter (valgfrit)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Intet tag —",
|
||||
"dashboard": "Oversigt",
|
||||
"tab_today": "I dag",
|
||||
"palette_placeholder": "Søg objekter og opgaver…",
|
||||
"palette_placeholder": "Søg objekter, opgaver, reservedele, dokumenter, noter…",
|
||||
"palette_no_results": "Ingen match",
|
||||
"palette_hint": "↑↓ naviger · Enter åbn · Esc luk",
|
||||
"palette_hint": "↑↓ naviger · Enter åbn · Esc luk · / åbner søgningen overalt",
|
||||
"search_open": "Søg",
|
||||
"search_group_parts": "Reservedele",
|
||||
"search_group_content": "I dokumenter",
|
||||
"search_group_history": "Historiknoter",
|
||||
"search_page": "Side {page}",
|
||||
"search_empty_hint": "Skriv for at søge i objekter, opgaver, reservedele, dokumenter og noter",
|
||||
"search_searching": "Søger i dokumenter og noter…",
|
||||
"search_index_status": "Fuldtekstsøgning: {indexed} af {total} filer indekseret · {no_text} uden tekstlag · {pending} afventer",
|
||||
"today_all_caught_up": "Alt er klaret! Intet forfalder i denne uge.",
|
||||
"today_overdue": "Forfaldne",
|
||||
"today_due_today": "Forfalder i dag",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Tærskel „forbrugsvare lav“ (%)",
|
||||
"settings_battery_low_percent": "Tærskel „batteri lavt“ (%)",
|
||||
"settings_thresholds_hint": "Standardværdier for nye Foreslåede opsætninger (resterende procent) og for flådebatterier uden egen Battery Notes-tærskel. Eksisterende opgaver beholder deres værdi.",
|
||||
"settings_value_out_of_range": "Værdien skal være mellem {min} og {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batterier",
|
||||
"bn_floor_decides": "Din tærskel på {floor} % afgør for alle; pr. batteri gælder den højeste af de to tærskler.",
|
||||
"bn_above_floor": "Det er over din tærskel på {floor} %, så {name} afgør for alle noterede batterier.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testnotifikation sendt",
|
||||
"test_notification_failed": "Testnotifikation mislykkedes",
|
||||
"notify_per_person": "Levering pr. person",
|
||||
"member_avatars": "Medlemmernes avatarer",
|
||||
"member_avatars_hint": "Vises ved siden af tildelte opgaver — initialer i medlemmets farve. Standard kommer fra navnet; angiv egne initialer eller vælg en farve for at skelne medlemmer.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Farve",
|
||||
"member_avatar_reset": "Nulstil",
|
||||
"notify_no_own_device": "Ingen egen enhed — bruger husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Notificer ved snart forfalden",
|
||||
"settings_notify_overdue": "Notificer ved forfalden",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Grupper efter",
|
||||
"state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).",
|
||||
"state_latch_help": "Med 1 overgang er triggeren en lås: den nulstilles, når enheden forlader Til-tilstanden — eller, hvis kun en Fra-tilstand er sat, når den vender tilbage til den.",
|
||||
"for_minutes_state_help": "0 tæller hver ændring straks. Med minutter skal den nye tilstand først holde — korte blink hverken udløser eller tælles så.",
|
||||
"qr_print_title": "Udskriv QR-koder",
|
||||
"qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Slet \"{name}\"?",
|
||||
"doc_too_large": "Filen er for stor (maks. 25 MB).",
|
||||
"doc_upload_failed": "Upload mislykkedes.",
|
||||
"completion_photo_optional": "Fuldførelsesfoto (valgfrit)",
|
||||
"add_photo": "Tilføj foto",
|
||||
"completion_photos_optional": "Fotos ved fuldførelse (valgfrit)",
|
||||
"completion_photos": "Fotos ved fuldførelse",
|
||||
"choose_photos": "Vælg fotos",
|
||||
"choose_photo": "Vælg foto",
|
||||
"photos_android_hint": "Android-appen afleverer ét foto pr. valg — hvert valg tilføjes.",
|
||||
"add_photos": "Tilføj fotos",
|
||||
"photos_limit": "Højst {max} fotos pr. fuldførelse",
|
||||
"history_edit_photos_hint": "Et foto, der fjernes her, bliver i objektets dokumenter.",
|
||||
"uploading": "Uploader…",
|
||||
"remove": "Fjern",
|
||||
"doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Vises ved siden af den registrerede værdi, når opgaven fuldføres.",
|
||||
"reading_value_label": "Aflæst værdi",
|
||||
"reading_label": "Aflæsning",
|
||||
"readings_section": "Aflæsninger",
|
||||
"readings_hint": "Flere navngivne værdier pr. fuldførelse — én række pr. måler. Lad stå tomt for en enkelt værdi.",
|
||||
"reading_name_label": "Aflæsningens navn",
|
||||
"reading_unit_short": "Enhed",
|
||||
"reading_add": "Tilføj aflæsning",
|
||||
"reading_duplicate_name": "Navnet er allerede i brug",
|
||||
"reading_last": "sidst: {value}",
|
||||
"reading_below_last": "Lavere end sidste aflæsning ({value})",
|
||||
"settings_templates_label": "Skabelongalleri",
|
||||
"settings_templates_hint": "Fravælg skabeloner, du aldrig får brug for — de forsvinder fra \"Fra skabelon\"-vælgerne (panel og config flow). Intet andet ændres; kan genaktiveres når som helst.",
|
||||
"worksheet": "Arbejdsark",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen notifikationsregel",
|
||||
"settings_notify_rule_hint": "Hver notifikation udløser også hændelsen maintenance_supporter_notification (type, status, objekt, opgave, referencenumre, prioritet, link, nyttelast). Videresend den med en automatisering — fx til Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Udløs kun hændelsen, send intet selv",
|
||||
"settings_notify_event_only_hint": "Din automatisering står for hele leveringen. Gentagelsesintervaller, stille timer og dagsloftet gælder stadig for, hvornår hændelsen udløses.",
|
||||
"settings_notify_extra_data": "Ekstra notifikationsdata (skabelon)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tilladt; flettes ind i notify-kaldets data (dine nøgler vinder). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test med knappen Send test.",
|
||||
"card_saved_view": "Gemt visning",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Anvender visningens status-, bruger- og etiketfiltre oven på filtrene ovenfor. Visningens sortering og gruppering er panelvisningsindstillinger og anvendes ikke på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marker alle skiftet",
|
||||
"battery_fleet_mark_one": "Marker dette batteri skiftet",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Opgavens sensorudløser er gået tabt — den udløses ikke og fuldføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparér",
|
||||
"battery_fleet_exclude": "Udeluk fra flåden",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Vælg en batterisensor, som den automatiske registrering overså — den kommer straks på listen.",
|
||||
"battery_fleet_track_self": "Spor selvopladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvopladende enheder vises som genopladelige — ved lavt niveau bedes om opladning, aldrig om nye batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandl en overskredet prognose som forfalden, når et batteri ikke har en sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Nogle enheder rapporterer slet ikke batteriniveau — Battery Notes kender kun typen og seneste udskiftning. Når den typiske levetid er passeret, tæller sådan et batteri som lavt, og flådeopgaven minder dig om det; at markere det som udskiftet nulstiller prognosen.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_status_due": "Forfalden",
|
||||
"battery_fleet_predicted_on": "Forventes omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Forudsagt ud fra batteriets afladningstendens: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Genopladeligt: oplad i stedet for at udskifte — aldrig på indkøbslisten",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicebog (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "poster",
|
||||
"ref_number": "Referencenummer",
|
||||
"print_options_title": "Udskriv servicebog",
|
||||
"print_layout": "Opbygning",
|
||||
"print_layout_chronological": "Kronologisk",
|
||||
"print_layout_by_task": "Efter opgave",
|
||||
"print_include": "Medtag",
|
||||
"print_inc_readings": "Aflæsninger",
|
||||
"print_inc_parts": "Brugte dele",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Tilknyttede dokumenter",
|
||||
"print_inc_checklist": "Tjekliste",
|
||||
"print_inc_notes": "Noter",
|
||||
"print_inc_costs": "Omkostning og varighed",
|
||||
"print_inc_person": "Udført af",
|
||||
"print_inc_refs": "Referencenumre",
|
||||
"print_inc_qr": "QR-kode pr. opgave",
|
||||
"print_button": "Udskriv",
|
||||
"report_scan_hint": "Scan for at åbne opgaven",
|
||||
"completed_by": "Udført af",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ungültiger Wert",
|
||||
"feat_schedule_time": "Uhrzeit-Scheduling",
|
||||
"feat_schedule_time_desc": "Tasks werden zu einer festen Uhrzeit fällig statt um Mitternacht.",
|
||||
"schedule_time_optional": "Fällig um (optional)",
|
||||
"schedule_time_toggle": "Zu einer bestimmten Uhrzeit fällig",
|
||||
"schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.",
|
||||
"at_time": "um",
|
||||
"notes_optional": "Notizen (optional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Kein Tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Heute",
|
||||
"palette_placeholder": "Objekte und Aufgaben suchen…",
|
||||
"palette_placeholder": "Objekte, Aufgaben, Ersatzteile, Dokumente, Notizen suchen…",
|
||||
"palette_no_results": "Keine Treffer",
|
||||
"palette_hint": "↑↓ Navigieren · Enter Öffnen · Esc Schließen",
|
||||
"palette_hint": "↑↓ navigieren · Enter öffnen · Esc schließen · / öffnet die Suche überall",
|
||||
"search_open": "Suchen",
|
||||
"search_group_parts": "Ersatzteile",
|
||||
"search_group_content": "In Dokumenten",
|
||||
"search_group_history": "Verlaufsnotizen",
|
||||
"search_page": "Seite {page}",
|
||||
"search_empty_hint": "Tippen, um Objekte, Aufgaben, Ersatzteile, Dokumente und Notizen zu durchsuchen",
|
||||
"search_searching": "Dokumente und Notizen werden durchsucht…",
|
||||
"search_index_status": "Volltextsuche: {indexed} von {total} Dateien indiziert · {no_text} ohne Textebene · {pending} ausstehend",
|
||||
"today_all_caught_up": "Alles erledigt! Diese Woche steht nichts an.",
|
||||
"today_overdue": "Überfällig",
|
||||
"today_due_today": "Heute fällig",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Schwelle „Verbrauchsmaterial niedrig“ (%)",
|
||||
"settings_battery_low_percent": "Schwelle „Batterie niedrig“ (%)",
|
||||
"settings_thresholds_hint": "Vorgaben für neue Vorgeschlagene Einrichtungen (Prozent-Restwert) und für Flotten-Batterien ohne eigene Battery-Notes-Schwelle. Bestehende Aufgaben behalten ihren Wert.",
|
||||
"settings_value_out_of_range": "Der Wert muss zwischen {min} und {max} liegen",
|
||||
"bn_summary": "{name}: {pct} % für {n} Batterien",
|
||||
"bn_floor_decides": "Deine {floor}-%-Schwelle entscheidet für alle; pro Batterie zählt die höhere der beiden Schwellen.",
|
||||
"bn_above_floor": "Das liegt über deiner {floor}-%-Schwelle — für alle notierten Batterien entscheidet {name}.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test-Benachrichtigung gesendet",
|
||||
"test_notification_failed": "Test-Benachrichtigung fehlgeschlagen",
|
||||
"notify_per_person": "Zustellung pro Person",
|
||||
"member_avatars": "Avatare der Mitglieder",
|
||||
"member_avatars_hint": "Wird neben zugewiesenen Aufgaben angezeigt — Initialen in der Farbe des Mitglieds. Standard kommt aus dem Namen; eigene Initialen oder eine Farbe wählen, um Mitglieder zu unterscheiden.",
|
||||
"member_initials": "Initialen",
|
||||
"member_color": "Farbe",
|
||||
"member_avatar_reset": "Zurücksetzen",
|
||||
"notify_no_own_device": "Kein eigenes Gerät — nutzt den Haushaltsdienst",
|
||||
"settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen",
|
||||
"settings_notify_overdue": "Bei Überfälligkeit benachrichtigen",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Gruppieren nach",
|
||||
"state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.",
|
||||
"target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).",
|
||||
"state_latch_help": "Bei 1 Übergang ist der Trigger ein Latch: Er erholt sich, wenn die Entität den Nach-Zustand verlässt — oder, wenn nur ein Von-Zustand gesetzt ist, sobald sie dorthin zurückkehrt.",
|
||||
"for_minutes_state_help": "0 zählt jede Änderung sofort. Mit Minuten muss der neue Zustand erst so lange bestehen — kurzes Flackern löst dann weder aus noch zählt es.",
|
||||
"qr_print_title": "QR-Codes drucken",
|
||||
"qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "„{name}“ löschen?",
|
||||
"doc_too_large": "Datei ist zu groß (max. 25 MB).",
|
||||
"doc_upload_failed": "Upload fehlgeschlagen.",
|
||||
"completion_photo_optional": "Abschlussfoto (optional)",
|
||||
"add_photo": "Foto hinzufügen",
|
||||
"completion_photos_optional": "Abschlussfotos (optional)",
|
||||
"completion_photos": "Abschlussfotos",
|
||||
"choose_photos": "Fotos auswählen",
|
||||
"choose_photo": "Foto wählen",
|
||||
"photos_android_hint": "Die Android-App übergibt pro Auswahl ein Foto — jede Auswahl wird hinzugefügt.",
|
||||
"add_photos": "Fotos hinzufügen",
|
||||
"photos_limit": "Bis zu {max} Fotos pro Abschluss",
|
||||
"history_edit_photos_hint": "Ein hier entferntes Foto bleibt in den Dokumenten des Objekts erhalten.",
|
||||
"uploading": "Wird hochgeladen…",
|
||||
"remove": "Entfernen",
|
||||
"doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wird beim Erledigen dieser Aufgabe neben dem erfassten Wert angezeigt.",
|
||||
"reading_value_label": "Ablesewert",
|
||||
"reading_label": "Ablesung",
|
||||
"readings_section": "Ablesewerte",
|
||||
"readings_hint": "Mehrere benannte Werte je Abschluss — eine Zeile pro Zähler. Leer lassen für einen einzelnen Wert.",
|
||||
"reading_name_label": "Name der Ablesung",
|
||||
"reading_unit_short": "Einheit",
|
||||
"reading_add": "Ablesung hinzufügen",
|
||||
"reading_duplicate_name": "Name bereits vergeben",
|
||||
"reading_last": "zuletzt: {value}",
|
||||
"reading_below_last": "Kleiner als der letzte Wert ({value})",
|
||||
"settings_templates_label": "Vorlagen-Galerie",
|
||||
"settings_templates_hint": "Vorlagen abwählen, die du nie brauchst — sie verschwinden aus den \"Aus Vorlage\"-Auswahlen (Panel und Config-Flow). Sonst ändert sich nichts; jederzeit wieder aktivierbar.",
|
||||
"worksheet": "Arbeitsblatt",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Eigene Benachrichtigungsregel",
|
||||
"settings_notify_rule_hint": "Jede Benachrichtigung löst zusätzlich das Ereignis maintenance_supporter_notification aus (Art, Status, Objekt, Aufgabe, Referenznummern, Priorität, Deep-Link, Nutzlast). Leite es per Automation weiter — z. B. an Ticker, Telegram oder Pushover.",
|
||||
"settings_notify_event_only": "Nur das Ereignis auslösen, selbst nichts senden",
|
||||
"settings_notify_event_only_hint": "Deine Automation übernimmt den kompletten Versand. Wiederholintervalle, Ruhezeiten und Tageslimit gelten weiterhin dafür, wann das Ereignis ausgelöst wird.",
|
||||
"settings_notify_extra_data": "Zusätzliche Benachrichtigungsdaten (Template)",
|
||||
"settings_notify_extra_data_hint": "JSON oder YAML, Jinja erlaubt; wird in die data des Notify-Aufrufs gemischt (deine Schlüssel gewinnen). Variablen: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Prüfen mit „Test senden“.",
|
||||
"card_saved_view": "Gespeicherte Ansicht",
|
||||
"card_saved_view_none": "Keine",
|
||||
"card_saved_view_help": "Wendet die Status-, Nutzer- und Label-Filter der Ansicht zusätzlich zu den obigen Filtern an. Sortierung und Gruppierung der Ansicht sind Panel-Darstellung und werden auf der Karte nicht angewendet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Alle als getauscht markieren",
|
||||
"battery_fleet_mark_one": "Diese Batterie als getauscht markieren",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Kein Sensor",
|
||||
"battery_fleet_trigger_lost": "Der Sensor-Trigger dieser Aufgabe ist verloren gegangen — sie wird nicht ausgelöst oder automatisch abgeschlossen.",
|
||||
"battery_fleet_repair": "Reparieren",
|
||||
"battery_fleet_exclude": "Aus der Flotte ausschließen",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Einen Batteriesensor wählen, den die automatische Erkennung übersehen hat — er erscheint sofort in der Liste.",
|
||||
"battery_fleet_track_self": "Selbstladende Batterien mitverfolgen",
|
||||
"battery_fleet_track_self_hint": "Telefone, Staubsauger und andere selbstladende Geräte erscheinen als Akkus — bei niedrigem Stand wird ans Laden erinnert, nie an neue Zellen.",
|
||||
"battery_fleet_due_without_sensor": "Abgelaufene Prognose als fällig werten, wenn eine Batterie keinen Sensor hat",
|
||||
"battery_fleet_due_without_sensor_hint": "Manche Geräte melden gar keinen Batteriestand — Battery Notes kennt nur Typ und letzten Wechsel. Ist die typische Lebensdauer verstrichen, gilt so eine Batterie als leer und die Flotten-Aufgabe erinnert dich; als ersetzt markieren setzt die Prognose zurück.",
|
||||
"battery_fleet_status_low": "Schwach",
|
||||
"battery_fleet_status_soon": "Bald",
|
||||
"battery_fleet_status_ok": "In Ordnung",
|
||||
"battery_fleet_status_due": "Fällig",
|
||||
"battery_fleet_predicted_on": "Voraussichtlich um {date}",
|
||||
"battery_fleet_predicted_trend": "Aus dem Entladetrend dieser Batterie vorhergesagt: etwa {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Wiederaufladbar: laden statt ersetzen — nie auf der Einkaufsliste",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Serviceheft (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "Einträge",
|
||||
"ref_number": "Referenznummer",
|
||||
"print_options_title": "Serviceheft drucken",
|
||||
"print_layout": "Aufbau",
|
||||
"print_layout_chronological": "Chronologisch",
|
||||
"print_layout_by_task": "Nach Aufgabe",
|
||||
"print_include": "Enthalten",
|
||||
"print_inc_readings": "Zählerstände",
|
||||
"print_inc_parts": "Verbrauchte Teile",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Verknüpfte Dokumente",
|
||||
"print_inc_checklist": "Checkliste",
|
||||
"print_inc_notes": "Notizen",
|
||||
"print_inc_costs": "Kosten und Dauer",
|
||||
"print_inc_person": "Erledigt von",
|
||||
"print_inc_refs": "Referenznummern",
|
||||
"print_inc_qr": "QR-Code je Aufgabe",
|
||||
"print_button": "Drucken",
|
||||
"report_scan_hint": "Scannen öffnet die Aufgabe",
|
||||
"completed_by": "Erledigt von",
|
||||
"date_from": "Von",
|
||||
"date_to": "Bis",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: invalid value",
|
||||
"feat_schedule_time": "Time-of-day scheduling",
|
||||
"feat_schedule_time_desc": "Tasks become overdue at a specific time of day instead of midnight.",
|
||||
"schedule_time_optional": "Due at time (optional)",
|
||||
"schedule_time_toggle": "Due at a specific time",
|
||||
"schedule_time_help": "Empty = midnight (default). HA timezone.",
|
||||
"at_time": "at",
|
||||
"notes_optional": "Notes (optional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— No tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Today",
|
||||
"palette_placeholder": "Search objects and tasks…",
|
||||
"palette_placeholder": "Search objects, tasks, spare parts, documents, notes…",
|
||||
"palette_no_results": "No matches",
|
||||
"palette_hint": "↑↓ to navigate · Enter to open · Esc to close",
|
||||
"palette_hint": "↑↓ to navigate · Enter to open · Esc to close · / opens the search anywhere",
|
||||
"search_open": "Search",
|
||||
"search_group_parts": "Spare parts",
|
||||
"search_group_content": "In documents",
|
||||
"search_group_history": "History notes",
|
||||
"search_page": "Page {page}",
|
||||
"search_empty_hint": "Type to search objects, tasks, spare parts, documents and notes",
|
||||
"search_searching": "Searching documents and notes…",
|
||||
"search_index_status": "Full-text search: {indexed} of {total} files indexed · {no_text} without text layer · {pending} pending",
|
||||
"today_all_caught_up": "All caught up! Nothing due this week.",
|
||||
"today_overdue": "Overdue",
|
||||
"today_due_today": "Due today",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Consumable low threshold (%)",
|
||||
"settings_battery_low_percent": "Battery low threshold (%)",
|
||||
"settings_thresholds_hint": "Defaults for new Suggested setups (percent-remaining consumables) and for fleet batteries without their own Battery Notes threshold. Existing tasks keep their value.",
|
||||
"settings_value_out_of_range": "Value must be between {min} and {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batteries",
|
||||
"bn_floor_decides": "Your {floor} % floor decides for all of them; the higher of the two thresholds counts per battery.",
|
||||
"bn_above_floor": "That is above your {floor} % floor, so {name} decides for all noted batteries.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test notification sent",
|
||||
"test_notification_failed": "Test notification failed",
|
||||
"notify_per_person": "Per-person delivery",
|
||||
"member_avatars": "Member avatars",
|
||||
"member_avatars_hint": "Shown next to assigned tasks — initials in the member's colour. Defaults come from the name; set your own initials or pick a colour to tell members apart.",
|
||||
"member_initials": "Initials",
|
||||
"member_color": "Colour",
|
||||
"member_avatar_reset": "Reset",
|
||||
"notify_no_own_device": "No own device — uses the household service",
|
||||
"settings_notify_due_soon": "Notify when due soon",
|
||||
"settings_notify_overdue": "Notify when overdue",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Group by",
|
||||
"state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.",
|
||||
"target_changes_help": "Number of matching transitions before the trigger fires (default: 1).",
|
||||
"state_latch_help": "With 1 transition the trigger is a latch: it recovers when the entity leaves the To-state — or, with only a From-state set, when it returns to that state.",
|
||||
"for_minutes_state_help": "0 counts every change immediately. Set minutes and the new state must hold that long first — brief flickers then neither trigger nor count.",
|
||||
"qr_print_title": "Print QR codes",
|
||||
"qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Delete \"{name}\"?",
|
||||
"doc_too_large": "File is too large (max 25 MB).",
|
||||
"doc_upload_failed": "Upload failed.",
|
||||
"completion_photo_optional": "Completion photo (optional)",
|
||||
"add_photo": "Add photo",
|
||||
"completion_photos_optional": "Completion photos (optional)",
|
||||
"completion_photos": "Completion photos",
|
||||
"choose_photos": "Choose photos",
|
||||
"choose_photo": "Choose photo",
|
||||
"photos_android_hint": "The Android app hands over one photo per pick — each pick is added.",
|
||||
"add_photos": "Add photos",
|
||||
"photos_limit": "Up to {max} photos per completion",
|
||||
"history_edit_photos_hint": "Removing a photo here keeps the file in the object's documents.",
|
||||
"uploading": "Uploading…",
|
||||
"remove": "Remove",
|
||||
"doc_deduped": "Already stored elsewhere — shared, no extra space used.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Shown next to the recorded value when completing this task.",
|
||||
"reading_value_label": "Reading value",
|
||||
"reading_label": "Reading",
|
||||
"readings_section": "Readings",
|
||||
"readings_hint": "Several named values per completion — one row per meter. Leave empty to record a single value.",
|
||||
"reading_name_label": "Reading name",
|
||||
"reading_unit_short": "Unit",
|
||||
"reading_add": "Add reading",
|
||||
"reading_duplicate_name": "Name already used",
|
||||
"reading_last": "last: {value}",
|
||||
"reading_below_last": "Lower than the last reading ({value})",
|
||||
"settings_templates_label": "Template gallery",
|
||||
"settings_templates_hint": "Untick templates you'll never need — they disappear from the \"From template\" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.",
|
||||
"worksheet": "Work sheet",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Your own notification rule",
|
||||
"settings_notify_rule_hint": "Every notification also fires the event maintenance_supporter_notification (kind, status, object, task, reference numbers, priority, deep link, the payload). Route it with an automation — e.g. into Ticker, Telegram or Pushover.",
|
||||
"settings_notify_event_only": "Only fire the event, send nothing myself",
|
||||
"settings_notify_event_only_hint": "Your automation is the whole delivery. Rate limits, quiet hours and the daily cap still apply to when the event fires.",
|
||||
"settings_notify_extra_data": "Extra notification data (template)",
|
||||
"settings_notify_extra_data_hint": "JSON or YAML, Jinja allowed; merged into the notify call's data (your keys win). Variables: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test it with the Send test button.",
|
||||
"card_saved_view": "Saved view",
|
||||
"card_saved_view_none": "None",
|
||||
"card_saved_view_help": "Applies the view's status, user and label filters on top of the filters above. The view's sorting and grouping are panel display settings and are not applied on the card.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Mark all replaced",
|
||||
"battery_fleet_mark_one": "Mark this battery replaced",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "No sensor",
|
||||
"battery_fleet_trigger_lost": "This task's sensor trigger was lost — it will not fire or auto-complete.",
|
||||
"battery_fleet_repair": "Repair",
|
||||
"battery_fleet_exclude": "Exclude from the fleet",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Pick a battery sensor the automatic discovery missed — it joins the roster immediately.",
|
||||
"battery_fleet_track_self": "Track self-charging batteries",
|
||||
"battery_fleet_track_self_hint": "Phones, vacuums and other devices that recharge themselves appear as rechargeables — a low one asks for a charge, never for new cells.",
|
||||
"battery_fleet_due_without_sensor": "Treat a passed forecast as due when a battery has no sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Some devices report no battery level at all — Battery Notes only knows the type and the last replacement. Once the typical lifetime has passed, such a battery counts as low and the fleet task reminds you; marking it replaced resets the forecast.",
|
||||
"battery_fleet_status_low": "Low",
|
||||
"battery_fleet_status_soon": "Soon",
|
||||
"battery_fleet_status_ok": "Healthy",
|
||||
"battery_fleet_status_due": "Due",
|
||||
"battery_fleet_predicted_on": "Expected around {date}",
|
||||
"battery_fleet_predicted_trend": "Predicted from this battery's discharge trend: around {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable: charge instead of replacing — never on the shopping list",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Service record (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entries",
|
||||
"ref_number": "Reference number",
|
||||
"print_options_title": "Print service record",
|
||||
"print_layout": "Layout",
|
||||
"print_layout_chronological": "Chronological",
|
||||
"print_layout_by_task": "By task",
|
||||
"print_include": "Include",
|
||||
"print_inc_readings": "Readings",
|
||||
"print_inc_parts": "Parts used",
|
||||
"print_inc_photos": "Photos",
|
||||
"print_inc_documents": "Linked documents",
|
||||
"print_inc_checklist": "Checklist",
|
||||
"print_inc_notes": "Notes",
|
||||
"print_inc_costs": "Cost and duration",
|
||||
"print_inc_person": "Completed by",
|
||||
"print_inc_refs": "Reference numbers",
|
||||
"print_inc_qr": "QR code per task",
|
||||
"print_button": "Print",
|
||||
"report_scan_hint": "Scan to open the task",
|
||||
"completed_by": "Completed by",
|
||||
"date_from": "From",
|
||||
"date_to": "To",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Programación por hora",
|
||||
"feat_schedule_time_desc": "Las tareas vencen a una hora específica en lugar de medianoche.",
|
||||
"schedule_time_optional": "Vence a las (opcional)",
|
||||
"schedule_time_toggle": "Vence a una hora concreta",
|
||||
"schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.",
|
||||
"at_time": "a las",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Sin etiqueta —",
|
||||
"dashboard": "Panel",
|
||||
"tab_today": "Hoy",
|
||||
"palette_placeholder": "Buscar objetos y tareas…",
|
||||
"palette_placeholder": "Buscar objetos, tareas, repuestos, documentos, notas…",
|
||||
"palette_no_results": "Sin resultados",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc cerrar",
|
||||
"palette_hint": "↑↓ navegar · Intro abrir · Esc cerrar · / abre la búsqueda en cualquier lugar",
|
||||
"search_open": "Buscar",
|
||||
"search_group_parts": "Repuestos",
|
||||
"search_group_content": "En documentos",
|
||||
"search_group_history": "Notas del historial",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Escribe para buscar objetos, tareas, repuestos, documentos y notas",
|
||||
"search_searching": "Buscando en documentos y notas…",
|
||||
"search_index_status": "Búsqueda de texto completo: {indexed} de {total} archivos indexados · {no_text} sin capa de texto · {pending} pendientes",
|
||||
"today_all_caught_up": "¡Todo al día! Nada pendiente esta semana.",
|
||||
"today_overdue": "Vencidas",
|
||||
"today_due_today": "Vence hoy",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Umbral „consumible bajo“ (%)",
|
||||
"settings_battery_low_percent": "Umbral „pila baja“ (%)",
|
||||
"settings_thresholds_hint": "Valores predeterminados para nuevas Configuraciones sugeridas (porcentaje restante) y para pilas de la flota sin umbral propio de Battery Notes. Las tareas existentes conservan su valor.",
|
||||
"settings_value_out_of_range": "El valor debe estar entre {min} y {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilas",
|
||||
"bn_floor_decides": "Tu umbral del {floor} % decide para todas; por pila cuenta el mayor de los dos umbrales.",
|
||||
"bn_above_floor": "Está por encima de tu umbral del {floor} %, así que {name} decide para todas las pilas anotadas.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notificación de prueba enviada",
|
||||
"test_notification_failed": "La notificación de prueba falló",
|
||||
"notify_per_person": "Entrega por persona",
|
||||
"member_avatars": "Avatares de los miembros",
|
||||
"member_avatars_hint": "Se muestra junto a las tareas asignadas — iniciales en el color del miembro. Por defecto se toman del nombre; elige tus iniciales o un color para distinguir a los miembros.",
|
||||
"member_initials": "Iniciales",
|
||||
"member_color": "Color",
|
||||
"member_avatar_reset": "Restablecer",
|
||||
"notify_no_own_device": "Sin dispositivo propio — usa el servicio del hogar",
|
||||
"settings_notify_due_soon": "Notificar cuando esté próxima",
|
||||
"settings_notify_overdue": "Notificar cuando esté vencida",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.",
|
||||
"target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).",
|
||||
"state_latch_help": "Con 1 transición el disparador es un cerrojo: se recupera cuando la entidad abandona el estado destino — o, si solo hay un estado origen, cuando vuelve a él.",
|
||||
"for_minutes_state_help": "0 cuenta cada cambio de inmediato. Con minutos, el nuevo estado debe mantenerse primero — un parpadeo breve entonces ni dispara ni cuenta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "¿Eliminar «{name}»?",
|
||||
"doc_too_large": "El archivo es demasiado grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Error al subir.",
|
||||
"completion_photo_optional": "Foto de finalización (opcional)",
|
||||
"add_photo": "Añadir foto",
|
||||
"completion_photos_optional": "Fotos de finalización (opcional)",
|
||||
"completion_photos": "Fotos de finalización",
|
||||
"choose_photos": "Elegir fotos",
|
||||
"choose_photo": "Elegir foto",
|
||||
"photos_android_hint": "La app de Android entrega una foto por selección — cada selección se añade.",
|
||||
"add_photos": "Añadir fotos",
|
||||
"photos_limit": "Hasta {max} fotos por finalización",
|
||||
"history_edit_photos_hint": "Una foto quitada aquí se conserva en los documentos del objeto.",
|
||||
"uploading": "Subiendo…",
|
||||
"remove": "Quitar",
|
||||
"doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Se muestra junto al valor registrado al completar esta tarea.",
|
||||
"reading_value_label": "Valor de lectura",
|
||||
"reading_label": "Lectura",
|
||||
"readings_section": "Lecturas",
|
||||
"readings_hint": "Varios valores con nombre por finalización — una fila por contador. Déjalo vacío para un único valor.",
|
||||
"reading_name_label": "Nombre de la lectura",
|
||||
"reading_unit_short": "Unidad",
|
||||
"reading_add": "Añadir lectura",
|
||||
"reading_duplicate_name": "Nombre ya en uso",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Menor que la última lectura ({value})",
|
||||
"settings_templates_label": "Galería de plantillas",
|
||||
"settings_templates_hint": "Desmarca las plantillas que nunca necesitarás: desaparecen de los selectores \"Desde plantilla\" (panel y flujo de configuración). Nada más cambia; puedes reactivarlas cuando quieras.",
|
||||
"worksheet": "Hoja de trabajo",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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í.",
|
||||
"settings_notify_rule": "Tu propia regla de notificación",
|
||||
"settings_notify_rule_hint": "Cada notificación dispara además el evento maintenance_supporter_notification (tipo, estado, objeto, tarea, números de referencia, prioridad, enlace, carga útil). Enrútalo con una automatización — p. ej. a Ticker, Telegram o Pushover.",
|
||||
"settings_notify_event_only": "Solo disparar el evento, no enviar nada",
|
||||
"settings_notify_event_only_hint": "Tu automatización se encarga de toda la entrega. Los intervalos de repetición, las horas de silencio y el límite diario siguen decidiendo cuándo se dispara el evento.",
|
||||
"settings_notify_extra_data": "Datos de notificación adicionales (plantilla)",
|
||||
"settings_notify_extra_data_hint": "JSON o YAML, se permite Jinja; se fusiona en los data de la llamada notify (tus claves ganan). Variables: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Pruébalo con el botón Enviar prueba.",
|
||||
"card_saved_view": "Vista guardada",
|
||||
"card_saved_view_none": "Ninguna",
|
||||
"card_saved_view_help": "Aplica los filtros de estado, usuario y etiqueta de la vista además de los filtros anteriores. El orden y la agrupación de la vista son ajustes de visualización del panel y no se aplican en la tarjeta.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas como reemplazadas",
|
||||
"battery_fleet_mark_one": "Marcar esta batería como reemplazada",
|
||||
"battery_fleet_offline": "sin conexión",
|
||||
"battery_fleet_no_sensor": "Sin sensor",
|
||||
"battery_fleet_trigger_lost": "El disparador de sensor de esta tarea se perdió — no se activará ni se completará automáticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir de la flota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Elige un sensor de batería que la detección automática pasó por alto — se une a la lista de inmediato.",
|
||||
"battery_fleet_track_self": "Seguir baterías de carga automática",
|
||||
"battery_fleet_track_self_hint": "Teléfonos, aspiradoras y otros dispositivos que se recargan solos aparecen como recargables: si están bajos piden una carga, nunca pilas nuevas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar una previsión vencida como pendiente cuando una pila no tiene sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Algunos dispositivos no informan ningún nivel de pila — Battery Notes solo conoce el tipo y el último cambio. Pasada la vida útil típica, esa pila cuenta como baja y la tarea de la flota te lo recuerda; marcarla como sustituida reinicia la previsión.",
|
||||
"battery_fleet_status_low": "Baja",
|
||||
"battery_fleet_status_soon": "Pronto",
|
||||
"battery_fleet_status_ok": "Correcta",
|
||||
"battery_fleet_status_due": "Pendiente",
|
||||
"battery_fleet_predicted_on": "Previsto hacia {date}",
|
||||
"battery_fleet_predicted_trend": "Predicción según la tendencia de descarga de esta pila: hacia {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recargable: se recarga en lugar de sustituirse — nunca en la lista de compra",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registro de mantenimiento (PDF)",
|
||||
"date": "Fecha",
|
||||
"service_record_entries": "entradas",
|
||||
"ref_number": "Número de referencia",
|
||||
"print_options_title": "Imprimir el historial de servicio",
|
||||
"print_layout": "Diseño",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarea",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Lecturas",
|
||||
"print_inc_parts": "Piezas usadas",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Documentos vinculados",
|
||||
"print_inc_checklist": "Lista de verificación",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Coste y duración",
|
||||
"print_inc_person": "Realizado por",
|
||||
"print_inc_refs": "Números de referencia",
|
||||
"print_inc_qr": "Código QR por tarea",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Escanea para abrir la tarea",
|
||||
"completed_by": "Realizado por",
|
||||
"date_from": "Desde",
|
||||
"date_to": "Hasta",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: virheellinen arvo",
|
||||
"feat_schedule_time": "Kellonajan ajoitus",
|
||||
"feat_schedule_time_desc": "Tehtävät erääntyvät tiettyyn kellonaikaan keskiyön sijaan.",
|
||||
"schedule_time_optional": "Erääntyy klo (valinnainen)",
|
||||
"schedule_time_toggle": "Erääntyy tiettyyn kellonaikaan",
|
||||
"schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.",
|
||||
"at_time": "klo",
|
||||
"notes_optional": "Muistiinpanot (valinnainen)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Ei tunnistetta —",
|
||||
"dashboard": "Kojelauta",
|
||||
"tab_today": "Tänään",
|
||||
"palette_placeholder": "Hae kohteita ja tehtäviä…",
|
||||
"palette_placeholder": "Hae kohteita, tehtäviä, varaosia, asiakirjoja, muistiinpanoja…",
|
||||
"palette_no_results": "Ei osumia",
|
||||
"palette_hint": "↑↓ liiku · Enter avaa · Esc sulje",
|
||||
"palette_hint": "↑↓ selaa · Enter avaa · Esc sulje · / avaa haun missä tahansa",
|
||||
"search_open": "Hae",
|
||||
"search_group_parts": "Varaosat",
|
||||
"search_group_content": "Asiakirjoissa",
|
||||
"search_group_history": "Historian muistiinpanot",
|
||||
"search_page": "Sivu {page}",
|
||||
"search_empty_hint": "Kirjoita hakeaksesi kohteista, tehtävistä, varaosista, asiakirjoista ja muistiinpanoista",
|
||||
"search_searching": "Haetaan asiakirjoista ja muistiinpanoista…",
|
||||
"search_index_status": "Kokotekstihaku: {indexed}/{total} tiedostoa indeksoitu · {no_text} ilman tekstikerrosta · {pending} odottaa",
|
||||
"today_all_caught_up": "Kaikki hoidettu! Tällä viikolla ei ole mitään.",
|
||||
"today_overdue": "Myöhässä",
|
||||
"today_due_today": "Erääntyy tänään",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Kynnys „kulutustarvike vähissä“ (%)",
|
||||
"settings_battery_low_percent": "Kynnys „paristo vähissä“ (%)",
|
||||
"settings_thresholds_hint": "Oletukset uusille Ehdotetuille asetuksille (jäljellä oleva prosentti) ja paristokannan paristoille ilman omaa Battery Notes -kynnystä. Olemassa olevat tehtävät säilyttävät arvonsa.",
|
||||
"settings_value_out_of_range": "Arvon on oltava välillä {min}–{max}",
|
||||
"bn_summary": "{name}: {pct} % {n} paristolle",
|
||||
"bn_floor_decides": "Sinun {floor} %:n kynnyksesi ratkaisee kaikille; paristoa kohden pätee korkeampi kahdesta kynnyksestä.",
|
||||
"bn_above_floor": "Se on sinun {floor} %:n kynnyksesi yläpuolella, joten {name} ratkaisee kaikille kirjatuille paristoille.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testi-ilmoitus lähetetty",
|
||||
"test_notification_failed": "Testi-ilmoitus epäonnistui",
|
||||
"notify_per_person": "Toimitus henkilöittäin",
|
||||
"member_avatars": "Jäsenten avatarit",
|
||||
"member_avatars_hint": "Näytetään osoitettujen tehtävien vieressä — nimikirjaimet jäsenen värissä. Oletus tulee nimestä; aseta omat nimikirjaimet tai valitse väri jäsenten erottamiseksi.",
|
||||
"member_initials": "Nimikirjaimet",
|
||||
"member_color": "Väri",
|
||||
"member_avatar_reset": "Palauta",
|
||||
"notify_no_own_device": "Ei omaa laitetta — käyttää kotitalouden palvelua",
|
||||
"settings_notify_due_soon": "Ilmoita, kun erääntyy pian",
|
||||
"settings_notify_overdue": "Ilmoita, kun myöhässä",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Ryhmittele",
|
||||
"state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.",
|
||||
"target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).",
|
||||
"state_latch_help": "Yhdellä siirtymällä liipaisin on salpa: se palautuu, kun entiteetti poistuu Kohde-tilasta — tai, jos vain Lähtö-tila on asetettu, kun se palaa siihen.",
|
||||
"for_minutes_state_help": "0 laskee jokaisen muutoksen heti. Minuuteilla uuden tilan on ensin pysyttävä — lyhyet välähdykset eivät silloin laukaise eivätkä kirjaudu.",
|
||||
"qr_print_title": "Tulosta QR-koodit",
|
||||
"qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Poistetaanko \"{name}\"?",
|
||||
"doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).",
|
||||
"doc_upload_failed": "Lataus epäonnistui.",
|
||||
"completion_photo_optional": "Valmistumiskuva (valinnainen)",
|
||||
"add_photo": "Lisää kuva",
|
||||
"completion_photos_optional": "Valokuvat suorituksesta (valinnainen)",
|
||||
"completion_photos": "Valokuvat suorituksesta",
|
||||
"choose_photos": "Valitse kuvat",
|
||||
"choose_photo": "Valitse kuva",
|
||||
"photos_android_hint": "Android-sovellus välittää yhden kuvan per valinta — jokainen valinta lisätään.",
|
||||
"add_photos": "Lisää kuvia",
|
||||
"photos_limit": "Enintään {max} kuvaa suoritusta kohti",
|
||||
"history_edit_photos_hint": "Tästä poistettu kuva säilyy kohteen asiakirjoissa.",
|
||||
"uploading": "Ladataan…",
|
||||
"remove": "Poista",
|
||||
"doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Näytetään kirjatun arvon vieressä tehtävää suoritettaessa.",
|
||||
"reading_value_label": "Lukema",
|
||||
"reading_label": "Lukema",
|
||||
"readings_section": "Lukemat",
|
||||
"readings_hint": "Useita nimettyjä arvoja suoritusta kohti — yksi rivi mittaria kohti. Jätä tyhjäksi yhtä arvoa varten.",
|
||||
"reading_name_label": "Lukeman nimi",
|
||||
"reading_unit_short": "Yksikkö",
|
||||
"reading_add": "Lisää lukema",
|
||||
"reading_duplicate_name": "Nimi on jo käytössä",
|
||||
"reading_last": "viimeksi: {value}",
|
||||
"reading_below_last": "Pienempi kuin edellinen lukema ({value})",
|
||||
"settings_templates_label": "Mallipohjagalleria",
|
||||
"settings_templates_hint": "Poista valinta mallipohjista, joita et koskaan tarvitse — ne katoavat \"Mallipohjasta\"-valitsimista (paneeli ja config flow). Mikään muu ei muutu; voi ottaa käyttöön uudelleen milloin tahansa.",
|
||||
"worksheet": "Työlomake",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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ä.",
|
||||
"settings_notify_rule": "Oma ilmoitussääntö",
|
||||
"settings_notify_rule_hint": "Jokainen ilmoitus laukaisee myös tapahtuman maintenance_supporter_notification (laji, tila, kohde, tehtävä, viitenumerot, prioriteetti, linkki, sisältö). Ohjaa se automaatiolla — esim. Tickeriin, Telegramiin tai Pushoveriin.",
|
||||
"settings_notify_event_only": "Laukaise vain tapahtuma, älä lähetä itse mitään",
|
||||
"settings_notify_event_only_hint": "Automaatiosi hoitaa koko toimituksen. Toistovälit, hiljaiset tunnit ja päiväraja määräävät yhä, milloin tapahtuma laukeaa.",
|
||||
"settings_notify_extra_data": "Ilmoituksen lisätiedot (malli)",
|
||||
"settings_notify_extra_data_hint": "JSON tai YAML, Jinja sallittu; yhdistetään notify-kutsun data-kenttään (omat avaimesi voittavat). Muuttujat: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Kokeile Lähetä testi -painikkeella.",
|
||||
"card_saved_view": "Tallennettu näkymä",
|
||||
"card_saved_view_none": "Ei mitään",
|
||||
"card_saved_view_help": "Soveltaa näkymän tila-, käyttäjä- ja tunnistesuodattimet yllä olevien suodattimien lisäksi. Näkymän lajittelu ja ryhmittely ovat paneelin näyttöasetuksia eivätkä vaikuta korttiin.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Merkitse kaikki vaihdetuiksi",
|
||||
"battery_fleet_mark_one": "Merkitse tämä paristo vaihdetuksi",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ei anturia",
|
||||
"battery_fleet_trigger_lost": "Tehtävän anturiliipaisin on kadonnut — se ei laukea eikä valmistu automaattisesti.",
|
||||
"battery_fleet_repair": "Korjaa",
|
||||
"battery_fleet_exclude": "Sulje pois kannasta",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Valitse paristoanturi, jonka automaattinen tunnistus ohitti — se liittyy luetteloon heti.",
|
||||
"battery_fleet_track_self": "Seuraa itselatautuvia akkuja",
|
||||
"battery_fleet_track_self_hint": "Puhelimet, imurit ja muut itselatautuvat laitteet näkyvät ladattavina — vähissä oleva pyytää latausta, ei koskaan uusia paristoja.",
|
||||
"battery_fleet_due_without_sensor": "Käsittele ohitettu ennuste erääntyneenä, kun paristolla ei ole anturia",
|
||||
"battery_fleet_due_without_sensor_hint": "Jotkin laitteet eivät raportoi paristotasoa lainkaan — Battery Notes tietää vain tyypin ja viimeisen vaihdon. Kun tyypillinen käyttöikä on kulunut, tällainen paristo lasketaan vähissä olevaksi ja kalustotehtävä muistuttaa sinua; vaihdetuksi merkitseminen nollaa ennusteen.",
|
||||
"battery_fleet_status_low": "Vähissä",
|
||||
"battery_fleet_status_soon": "Pian",
|
||||
"battery_fleet_status_ok": "Kunnossa",
|
||||
"battery_fleet_status_due": "Erääntynyt",
|
||||
"battery_fleet_predicted_on": "Odotettavissa noin {date}",
|
||||
"battery_fleet_predicted_trend": "Ennustettu tämän pariston purkautumistrendistä: noin {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ladattava: lataa vaihtamisen sijaan — ei koskaan ostoslistalle",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Huoltokirja (PDF)",
|
||||
"date": "Päivämäärä",
|
||||
"service_record_entries": "merkintää",
|
||||
"ref_number": "Viitenumero",
|
||||
"print_options_title": "Tulosta huoltokirja",
|
||||
"print_layout": "Asettelu",
|
||||
"print_layout_chronological": "Aikajärjestyksessä",
|
||||
"print_layout_by_task": "Tehtävittäin",
|
||||
"print_include": "Sisällytä",
|
||||
"print_inc_readings": "Lukemat",
|
||||
"print_inc_parts": "Käytetyt osat",
|
||||
"print_inc_photos": "Kuvat",
|
||||
"print_inc_documents": "Liitetyt asiakirjat",
|
||||
"print_inc_checklist": "Tarkistuslista",
|
||||
"print_inc_notes": "Muistiinpanot",
|
||||
"print_inc_costs": "Kustannus ja kesto",
|
||||
"print_inc_person": "Suorittaja",
|
||||
"print_inc_refs": "Viitenumerot",
|
||||
"print_inc_qr": "QR-koodi tehtävää kohti",
|
||||
"print_button": "Tulosta",
|
||||
"report_scan_hint": "Skannaa avataksesi tehtävän",
|
||||
"completed_by": "Suorittanut",
|
||||
"date_from": "Alkaen",
|
||||
"date_to": "Asti",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field} : valeur invalide",
|
||||
"feat_schedule_time": "Planification à l'heure",
|
||||
"feat_schedule_time_desc": "Les tâches arrivent à échéance à une heure précise plutôt qu'à minuit.",
|
||||
"schedule_time_optional": "Échéance à l'heure (optionnel)",
|
||||
"schedule_time_toggle": "Échéance à une heure précise",
|
||||
"schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.",
|
||||
"at_time": "à",
|
||||
"notes_optional": "Notes (optionnel)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Aucun tag —",
|
||||
"dashboard": "Tableau de bord",
|
||||
"tab_today": "Aujourd'hui",
|
||||
"palette_placeholder": "Rechercher objets et tâches…",
|
||||
"palette_placeholder": "Rechercher objets, tâches, pièces, documents, notes…",
|
||||
"palette_no_results": "Aucun résultat",
|
||||
"palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer",
|
||||
"palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer · / ouvre la recherche partout",
|
||||
"search_open": "Rechercher",
|
||||
"search_group_parts": "Pièces détachées",
|
||||
"search_group_content": "Dans les documents",
|
||||
"search_group_history": "Notes d'historique",
|
||||
"search_page": "p. {page}",
|
||||
"search_empty_hint": "Saisissez un texte pour chercher dans les objets, tâches, pièces, documents et notes",
|
||||
"search_searching": "Recherche dans les documents et notes…",
|
||||
"search_index_status": "Recherche plein texte : {indexed} fichiers sur {total} indexés · {no_text} sans couche texte · {pending} en attente",
|
||||
"today_all_caught_up": "Tout est à jour ! Rien cette semaine.",
|
||||
"today_overdue": "En retard",
|
||||
"today_due_today": "À faire aujourd'hui",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Seuil « consommable bas » (%)",
|
||||
"settings_battery_low_percent": "Seuil « pile faible » (%)",
|
||||
"settings_thresholds_hint": "Valeurs par défaut pour les nouvelles Configurations suggérées (pourcentage restant) et pour les piles de la flotte sans seuil Battery Notes propre. Les tâches existantes gardent leur valeur.",
|
||||
"settings_value_out_of_range": "La valeur doit être comprise entre {min} et {max}",
|
||||
"bn_summary": "{name} : {pct} % pour {n} piles",
|
||||
"bn_floor_decides": "Votre seuil de {floor} % décide pour toutes ; par pile, c'est le plus élevé des deux seuils qui compte.",
|
||||
"bn_above_floor": "C'est au-dessus de votre seuil de {floor} % : {name} décide donc pour toutes les piles notées.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notification de test envoyée",
|
||||
"test_notification_failed": "Échec de la notification de test",
|
||||
"notify_per_person": "Distribution par personne",
|
||||
"member_avatars": "Avatars des membres",
|
||||
"member_avatars_hint": "Affiché à côté des tâches attribuées — initiales dans la couleur du membre. Par défaut d'après le nom ; choisissez vos initiales ou une couleur pour distinguer les membres.",
|
||||
"member_initials": "Initiales",
|
||||
"member_color": "Couleur",
|
||||
"member_avatar_reset": "Réinitialiser",
|
||||
"notify_no_own_device": "Aucun appareil propre — utilise le service du foyer",
|
||||
"settings_notify_due_soon": "Notifier quand bientôt dû",
|
||||
"settings_notify_overdue": "Notifier quand en retard",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Grouper par",
|
||||
"state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.",
|
||||
"target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).",
|
||||
"state_latch_help": "Avec 1 transition, le déclencheur est un verrou : il se rétablit quand l'entité quitte l'état « vers » — ou, si seul un état « depuis » est défini, quand elle y revient.",
|
||||
"for_minutes_state_help": "0 compte chaque changement immédiatement. Avec des minutes, le nouvel état doit d'abord se maintenir — un bref clignotement ne déclenche ni ne compte alors.",
|
||||
"qr_print_title": "Imprimer les QR codes",
|
||||
"qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Supprimer « {name} » ?",
|
||||
"doc_too_large": "Fichier trop volumineux (max. 25 Mo).",
|
||||
"doc_upload_failed": "Échec du téléversement.",
|
||||
"completion_photo_optional": "Photo d'achèvement (facultatif)",
|
||||
"add_photo": "Ajouter une photo",
|
||||
"completion_photos_optional": "Photos de fin de tâche (facultatif)",
|
||||
"completion_photos": "Photos de fin de tâche",
|
||||
"choose_photos": "Choisir des photos",
|
||||
"choose_photo": "Choisir une photo",
|
||||
"photos_android_hint": "L'application Android ne transmet qu'une photo par sélection — chaque sélection est ajoutée.",
|
||||
"add_photos": "Ajouter des photos",
|
||||
"photos_limit": "Jusqu'à {max} photos par fin de tâche",
|
||||
"history_edit_photos_hint": "Une photo retirée ici reste dans les documents de l'objet.",
|
||||
"uploading": "Téléversement…",
|
||||
"remove": "Retirer",
|
||||
"doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Affichée à côté de la valeur enregistrée lors de l'exécution de cette tâche.",
|
||||
"reading_value_label": "Valeur relevée",
|
||||
"reading_label": "Relevé",
|
||||
"readings_section": "Relevés",
|
||||
"readings_hint": "Plusieurs valeurs nommées par fin de tâche — une ligne par compteur. Laisser vide pour une valeur unique.",
|
||||
"reading_name_label": "Nom du relevé",
|
||||
"reading_unit_short": "Unité",
|
||||
"reading_add": "Ajouter un relevé",
|
||||
"reading_duplicate_name": "Nom déjà utilisé",
|
||||
"reading_last": "dernier : {value}",
|
||||
"reading_below_last": "Inférieur au dernier relevé ({value})",
|
||||
"settings_templates_label": "Galerie de modèles",
|
||||
"settings_templates_hint": "Décochez les modèles dont vous n'aurez jamais besoin — ils disparaissent des sélecteurs « Depuis un modèle » (panneau et config flow). Rien d'autre ne change ; réactivables à tout moment.",
|
||||
"worksheet": "Fiche de travail",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Votre propre règle de notification",
|
||||
"settings_notify_rule_hint": "Chaque notification déclenche aussi l'événement maintenance_supporter_notification (type, statut, objet, tâche, numéros de référence, priorité, lien, charge utile). Routez-le avec une automatisation — vers Ticker, Telegram ou Pushover par exemple.",
|
||||
"settings_notify_event_only": "Déclencher seulement l'événement, ne rien envoyer",
|
||||
"settings_notify_event_only_hint": "Votre automatisation assure toute la livraison. Intervalles de répétition, heures calmes et plafond quotidien s'appliquent toujours au déclenchement de l'événement.",
|
||||
"settings_notify_extra_data": "Données de notification supplémentaires (modèle)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja autorisé ; fusionné dans les data de l'appel notify (vos clés l'emportent). Variables : kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Vérifiez avec le bouton Envoyer un test.",
|
||||
"card_saved_view": "Vue enregistrée",
|
||||
"card_saved_view_none": "Aucune",
|
||||
"card_saved_view_help": "Applique les filtres statut, utilisateur et étiquette de la vue en plus des filtres ci-dessus. Le tri et le regroupement de la vue sont des réglages d'affichage du panneau et ne s'appliquent pas à la carte.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Tout marquer remplacé",
|
||||
"battery_fleet_mark_one": "Marquer cette pile remplacée",
|
||||
"battery_fleet_offline": "hors ligne",
|
||||
"battery_fleet_no_sensor": "Pas de capteur",
|
||||
"battery_fleet_trigger_lost": "Le déclencheur de capteur de cette tâche a été perdu — elle ne se déclenchera pas et ne se terminera pas automatiquement.",
|
||||
"battery_fleet_repair": "Réparer",
|
||||
"battery_fleet_exclude": "Exclure du parc",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Choisissez un capteur de batterie que la détection automatique a manqué — il rejoint la liste immédiatement.",
|
||||
"battery_fleet_track_self": "Suivre les batteries à recharge automatique",
|
||||
"battery_fleet_track_self_hint": "Les téléphones, aspirateurs et autres appareils qui se rechargent seuls apparaissent comme rechargeables — un niveau bas demande une recharge, jamais de nouvelles piles.",
|
||||
"battery_fleet_due_without_sensor": "Considérer une prévision dépassée comme échue quand une pile n'a pas de capteur",
|
||||
"battery_fleet_due_without_sensor_hint": "Certains appareils ne signalent aucun niveau de pile — Battery Notes ne connaît que le type et le dernier remplacement. Une fois la durée de vie typique dépassée, cette pile compte comme faible et la tâche du parc vous le rappelle ; la marquer remplacée réinitialise la prévision.",
|
||||
"battery_fleet_status_low": "Faible",
|
||||
"battery_fleet_status_soon": "Bientôt",
|
||||
"battery_fleet_status_ok": "Bon état",
|
||||
"battery_fleet_status_due": "À remplacer",
|
||||
"battery_fleet_predicted_on": "Prévu vers {date}",
|
||||
"battery_fleet_predicted_trend": "Prédit à partir de la tendance de décharge de cette pile : vers {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Rechargeable : à recharger plutôt qu'à remplacer — jamais sur la liste d'achats",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Carnet d'entretien (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entrées",
|
||||
"ref_number": "Numéro de référence",
|
||||
"print_options_title": "Imprimer le carnet d'entretien",
|
||||
"print_layout": "Mise en page",
|
||||
"print_layout_chronological": "Chronologique",
|
||||
"print_layout_by_task": "Par tâche",
|
||||
"print_include": "Inclure",
|
||||
"print_inc_readings": "Relevés",
|
||||
"print_inc_parts": "Pièces utilisées",
|
||||
"print_inc_photos": "Photographies",
|
||||
"print_inc_documents": "Documents liés",
|
||||
"print_inc_checklist": "Liste de contrôle",
|
||||
"print_inc_notes": "Remarques",
|
||||
"print_inc_costs": "Coût et durée",
|
||||
"print_inc_person": "Réalisé par",
|
||||
"print_inc_refs": "Numéros de référence",
|
||||
"print_inc_qr": "QR code par tâche",
|
||||
"print_button": "Imprimer",
|
||||
"report_scan_hint": "Scanner pour ouvrir la tâche",
|
||||
"completed_by": "Réalisé par",
|
||||
"date_from": "Du",
|
||||
"date_to": "Au",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: अमान्य मान",
|
||||
"feat_schedule_time": "दिन का समय निर्धारण",
|
||||
"feat_schedule_time_desc": "कार्य मध्यरात्रि के बजाय दिन के एक विशिष्ट समय पर अतिदेय हो जाते हैं।",
|
||||
"schedule_time_optional": "इस समय देय (वैकल्पिक)",
|
||||
"schedule_time_toggle": "किसी निश्चित समय पर देय",
|
||||
"schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।",
|
||||
"at_time": "बजे",
|
||||
"notes_optional": "टिप्पणियाँ (वैकल्पिक)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— कोई टैग नहीं —",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"tab_today": "आज",
|
||||
"palette_placeholder": "ऑब्जेक्ट और कार्य खोजें…",
|
||||
"palette_placeholder": "ऑब्जेक्ट, कार्य, स्पेयर पार्ट्स, दस्तावेज़, नोट्स खोजें…",
|
||||
"palette_no_results": "कोई मिलान नहीं",
|
||||
"palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद",
|
||||
"palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद करें · / कहीं से भी खोज खोलता है",
|
||||
"search_open": "खोजें",
|
||||
"search_group_parts": "स्पेयर पार्ट्स",
|
||||
"search_group_content": "दस्तावेज़ों में",
|
||||
"search_group_history": "इतिहास नोट्स",
|
||||
"search_page": "पृष्ठ {page}",
|
||||
"search_empty_hint": "ऑब्जेक्ट, कार्य, स्पेयर पार्ट्स, दस्तावेज़ और नोट्स खोजने के लिए टाइप करें",
|
||||
"search_searching": "दस्तावेज़ और नोट्स खोजे जा रहे हैं…",
|
||||
"search_index_status": "पूर्ण-पाठ खोज: {total} में से {indexed} फ़ाइलें अनुक्रमित · {no_text} बिना टेक्स्ट परत · {pending} लंबित",
|
||||
"today_all_caught_up": "सब पूरा! इस सप्ताह कुछ भी बाकी नहीं।",
|
||||
"today_overdue": "अतिदेय",
|
||||
"today_due_today": "आज देय",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "उपभोग्य „कम“ सीमा (%)",
|
||||
"settings_battery_low_percent": "बैटरी „कम“ सीमा (%)",
|
||||
"settings_thresholds_hint": "नए सुझाए गए सेटअप (शेष प्रतिशत) और अपनी Battery Notes सीमा न रखने वाली बेड़े की बैटरियों के लिए डिफ़ॉल्ट। मौजूदा कार्य अपना मान रखते हैं।",
|
||||
"settings_value_out_of_range": "मान {min} और {max} के बीच होना चाहिए",
|
||||
"bn_summary": "{name}: {n} बैटरियों के लिए {pct} %",
|
||||
"bn_floor_decides": "आपकी {floor} % सीमा सभी के लिए निर्णायक है; प्रत्येक बैटरी के लिए दोनों में से ऊँची सीमा मान्य है।",
|
||||
"bn_above_floor": "यह आपकी {floor} % सीमा से ऊपर है, इसलिए सभी नोट की गई बैटरियों के लिए {name} निर्णायक है।",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "परीक्षण सूचना भेजी गई",
|
||||
"test_notification_failed": "परीक्षण सूचना विफल",
|
||||
"notify_per_person": "प्रति व्यक्ति वितरण",
|
||||
"member_avatars": "सदस्यों के अवतार",
|
||||
"member_avatars_hint": "सौंपे गए कार्यों के बगल में दिखाया जाता है — सदस्य के रंग में आद्याक्षर। डिफ़ॉल्ट नाम से आता है; सदस्यों में अंतर करने के लिए अपने आद्याक्षर या रंग चुनें।",
|
||||
"member_initials": "आद्याक्षर",
|
||||
"member_color": "रंग",
|
||||
"member_avatar_reset": "रीसेट",
|
||||
"notify_no_own_device": "कोई निजी डिवाइस नहीं — घरेलू सेवा का उपयोग करता है",
|
||||
"settings_notify_due_soon": "जल्द देय होने पर सूचित करें",
|
||||
"settings_notify_overdue": "अतिदेय होने पर सूचित करें",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "इसके अनुसार समूहित करें",
|
||||
"state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।",
|
||||
"target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।",
|
||||
"state_latch_help": "1 संक्रमण पर ट्रिगर लैच बन जाता है: इकाई 'तक' स्थिति छोड़ते ही यह पुनर्स्थापित होता है — या, केवल 'से' स्थिति सेट हो तो उसमें लौटने पर।",
|
||||
"for_minutes_state_help": "0 हर परिवर्तन को तुरंत गिनता है। मिनट सेट करने पर नई स्थिति को पहले उतनी देर बनी रहनी होगी — क्षणिक झिलमिलाहट तब न ट्रिगर करती है न गिनी जाती है।",
|
||||
"qr_print_title": "QR कोड प्रिंट करें",
|
||||
"qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" हटाएँ?",
|
||||
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।",
|
||||
"doc_upload_failed": "अपलोड विफल रहा।",
|
||||
"completion_photo_optional": "पूर्णता फ़ोटो (वैकल्पिक)",
|
||||
"add_photo": "फ़ोटो जोड़ें",
|
||||
"completion_photos_optional": "पूर्णता की तस्वीरें (वैकल्पिक)",
|
||||
"completion_photos": "पूर्णता की तस्वीरें",
|
||||
"choose_photos": "तस्वीरें चुनें",
|
||||
"choose_photo": "फ़ोटो चुनें",
|
||||
"photos_android_hint": "Android ऐप प्रति चयन एक फ़ोटो देता है — हर चयन जोड़ा जाता है।",
|
||||
"add_photos": "तस्वीरें जोड़ें",
|
||||
"photos_limit": "प्रति पूर्णता अधिकतम {max} तस्वीरें",
|
||||
"history_edit_photos_hint": "यहाँ हटाई गई तस्वीर ऑब्जेक्ट के दस्तावेज़ों में बनी रहती है।",
|
||||
"uploading": "अपलोड हो रहा है…",
|
||||
"remove": "हटाएँ",
|
||||
"doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "इस कार्य को पूरा करते समय दर्ज मान के बगल में दिखाई जाती है।",
|
||||
"reading_value_label": "रीडिंग मान",
|
||||
"reading_label": "रीडिंग",
|
||||
"readings_section": "रीडिंग",
|
||||
"readings_hint": "प्रति पूर्णता कई नामित मान — प्रति मीटर एक पंक्ति। एकल मान के लिए खाली छोड़ें।",
|
||||
"reading_name_label": "रीडिंग का नाम",
|
||||
"reading_unit_short": "इकाई",
|
||||
"reading_add": "रीडिंग जोड़ें",
|
||||
"reading_duplicate_name": "नाम पहले से उपयोग में है",
|
||||
"reading_last": "पिछला: {value}",
|
||||
"reading_below_last": "पिछली रीडिंग ({value}) से कम",
|
||||
"settings_templates_label": "टेम्पलेट गैलरी",
|
||||
"settings_templates_hint": "जिन टेम्पलेट की कभी ज़रूरत नहीं होगी उन्हें अनचेक करें — वे \"टेम्पलेट से\" चयनकर्ताओं (पैनल और कॉन्फ़िग फ़्लो) से गायब हो जाएंगे। और कुछ नहीं बदलता; कभी भी फिर से सक्षम कर सकते हैं।",
|
||||
"worksheet": "कार्य पत्रक",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "केवल इस व्यू के लिए सूचित करें",
|
||||
"settings_notify_scope_all": "सभी कार्य",
|
||||
"settings_notify_scope_hint": "केवल वे कार्य अनुस्मारक भेजते हैं जो चुने गए व्यू के लेबल/उपयोगकर्ता फ़िल्टर से मेल खाते हैं। व्यू की स्थिति, क्रम और समूहन यहाँ अनदेखे रहते हैं।",
|
||||
"settings_notify_rule": "आपका अपना सूचना नियम",
|
||||
"settings_notify_rule_hint": "हर सूचना maintenance_supporter_notification इवेंट भी ट्रिगर करती है (प्रकार, स्थिति, ऑब्जेक्ट, कार्य, संदर्भ संख्याएँ, प्राथमिकता, डीप लिंक, पेलोड)। इसे ऑटोमेशन से रूट करें — जैसे Ticker, Telegram या Pushover में।",
|
||||
"settings_notify_event_only": "केवल इवेंट ट्रिगर करें, स्वयं कुछ न भेजें",
|
||||
"settings_notify_event_only_hint": "आपका ऑटोमेशन पूरी डिलीवरी संभालता है। दोहराव अंतराल, शांत घंटे और दैनिक सीमा अभी भी तय करते हैं कि इवेंट कब ट्रिगर हो।",
|
||||
"settings_notify_extra_data": "अतिरिक्त सूचना डेटा (टेम्पलेट)",
|
||||
"settings_notify_extra_data_hint": "JSON या YAML, Jinja की अनुमति; notify कॉल के data में मिलाया जाता है (आपकी कुंजियाँ प्राथमिक)। वेरिएबल: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message। परीक्षण भेजें बटन से जाँचें।",
|
||||
"card_saved_view": "सहेजा गया व्यू",
|
||||
"card_saved_view_none": "कोई नहीं",
|
||||
"card_saved_view_help": "ऊपर दिए फ़िल्टरों के अतिरिक्त व्यू के स्थिति, उपयोगकर्ता और लेबल फ़िल्टर लागू करता है। व्यू का क्रम और समूहन पैनल की प्रदर्शन सेटिंग हैं और कार्ड पर लागू नहीं होते।",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "सभी बदली हुई चिह्नित करें",
|
||||
"battery_fleet_mark_one": "इस बैटरी को बदली हुई चिह्नित करें",
|
||||
"battery_fleet_offline": "ऑफ़लाइन",
|
||||
"battery_fleet_no_sensor": "कोई सेंसर नहीं",
|
||||
"battery_fleet_trigger_lost": "इस कार्य का सेंसर ट्रिगर खो गया है — यह सक्रिय नहीं होगा और स्वतः पूर्ण नहीं होगा।",
|
||||
"battery_fleet_repair": "मरम्मत करें",
|
||||
"battery_fleet_exclude": "फ्लीट से बाहर करें",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "स्वचालित खोज से छूटा बैटरी सेंसर चुनें — यह तुरंत सूची में जुड़ जाएगा।",
|
||||
"battery_fleet_track_self": "स्वयं चार्ज होने वाली बैटरियों को ट्रैक करें",
|
||||
"battery_fleet_track_self_hint": "फ़ोन, वैक्यूम और अन्य स्वयं चार्ज होने वाले उपकरण रिचार्जेबल के रूप में दिखते हैं — कम होने पर चार्ज करने को कहा जाता है, नई बैटरी कभी नहीं।",
|
||||
"battery_fleet_due_without_sensor": "जब बैटरी में सेंसर न हो तो बीत चुके पूर्वानुमान को देय मानें",
|
||||
"battery_fleet_due_without_sensor_hint": "कुछ डिवाइस बैटरी स्तर बिल्कुल नहीं बताते — Battery Notes को केवल प्रकार और पिछला बदलाव पता होता है। सामान्य आयु बीत जाने पर ऐसी बैटरी कम मानी जाती है और फ़्लीट कार्य आपको याद दिलाता है; बदली हुई चिह्नित करने से पूर्वानुमान रीसेट हो जाता है।",
|
||||
"battery_fleet_status_low": "कम",
|
||||
"battery_fleet_status_soon": "जल्द",
|
||||
"battery_fleet_status_ok": "ठीक",
|
||||
"battery_fleet_status_due": "देय",
|
||||
"battery_fleet_predicted_on": "{date} के आसपास अपेक्षित",
|
||||
"battery_fleet_predicted_trend": "इस बैटरी के डिस्चार्ज रुझान से अनुमानित: लगभग {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "रिचार्जेबल: बदलने के बजाय चार्ज करें — खरीदारी सूची में कभी नहीं",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "सेवा रिकॉर्ड (PDF)",
|
||||
"date": "दिनांक",
|
||||
"service_record_entries": "प्रविष्टियाँ",
|
||||
"ref_number": "संदर्भ संख्या",
|
||||
"print_options_title": "सेवा रिकॉर्ड प्रिंट करें",
|
||||
"print_layout": "लेआउट",
|
||||
"print_layout_chronological": "कालानुक्रमिक",
|
||||
"print_layout_by_task": "कार्य के अनुसार",
|
||||
"print_include": "शामिल करें",
|
||||
"print_inc_readings": "रीडिंग",
|
||||
"print_inc_parts": "प्रयुक्त पुर्जे",
|
||||
"print_inc_photos": "फ़ोटो",
|
||||
"print_inc_documents": "लिंक किए गए दस्तावेज़",
|
||||
"print_inc_checklist": "चेकलिस्ट",
|
||||
"print_inc_notes": "नोट्स",
|
||||
"print_inc_costs": "लागत और अवधि",
|
||||
"print_inc_person": "पूर्ण किया",
|
||||
"print_inc_refs": "संदर्भ संख्याएँ",
|
||||
"print_inc_qr": "प्रति कार्य QR कोड",
|
||||
"print_button": "प्रिंट",
|
||||
"report_scan_hint": "कार्य खोलने के लिए स्कैन करें",
|
||||
"completed_by": "द्वारा पूर्ण",
|
||||
"date_from": "से",
|
||||
"date_to": "तक",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: érvénytelen érték",
|
||||
"feat_schedule_time": "Napszakhoz kötött ütemezés",
|
||||
"feat_schedule_time_desc": "A feladatok nem éjfélkor, hanem egy megadott napszakban válnak lejárttá.",
|
||||
"schedule_time_optional": "Esedékesség időpontja (opcionális)",
|
||||
"schedule_time_toggle": "Adott időpontban esedékes",
|
||||
"schedule_time_help": "Üres = éjfél (alapértelmezés). HA időzóna.",
|
||||
"at_time": "ekkor:",
|
||||
"notes_optional": "Megjegyzések (opcionális)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Nincs címke —",
|
||||
"dashboard": "Irányítópult",
|
||||
"tab_today": "Ma",
|
||||
"palette_placeholder": "Objektumok és feladatok keresése…",
|
||||
"palette_placeholder": "Tárgyak, feladatok, alkatrészek, dokumentumok, jegyzetek keresése…",
|
||||
"palette_no_results": "Nincs találat",
|
||||
"palette_hint": "↑↓ navigálás · Enter megnyitás · Esc bezárás",
|
||||
"palette_hint": "↑↓ lépkedés · Enter megnyitás · Esc bezárás · / bárhol megnyitja a keresést",
|
||||
"search_open": "Keresés",
|
||||
"search_group_parts": "Alkatrészek",
|
||||
"search_group_content": "Dokumentumokban",
|
||||
"search_group_history": "Előzmény-jegyzetek",
|
||||
"search_page": "{page}. oldal",
|
||||
"search_empty_hint": "Írjon a tárgyak, feladatok, alkatrészek, dokumentumok és jegyzetek kereséséhez",
|
||||
"search_searching": "Keresés a dokumentumokban és jegyzetekben…",
|
||||
"search_index_status": "Teljes szöveges keresés: {indexed} / {total} fájl indexelve · {no_text} szövegréteg nélkül · {pending} függőben",
|
||||
"today_all_caught_up": "Minden rendben! Ezen a héten nincs esedékes feladat.",
|
||||
"today_overdue": "Lejárt",
|
||||
"today_due_today": "Ma esedékes",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "„Fogyóeszköz alacsony” küszöb (%)",
|
||||
"settings_battery_low_percent": "„Alacsony akkumulátor” küszöb (%)",
|
||||
"settings_thresholds_hint": "Alapértékek az új Javasolt beállításokhoz (százalékos maradék) és a saját Battery Notes-küszöb nélküli flottaelemekhez. A meglévő feladatok megtartják az értéküket.",
|
||||
"settings_value_out_of_range": "Az értéknek {min} és {max} között kell lennie",
|
||||
"bn_summary": "{name}: {pct} % {n} elemhez",
|
||||
"bn_floor_decides": "A te {floor} %-os küszöböd dönt mindegyiknél; elemenként a két küszöb közül a magasabb számít.",
|
||||
"bn_above_floor": "Ez a te {floor} %-os küszöböd felett van, így minden feljegyzett elemnél a {name} dönt.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Tesztértesítés elküldve",
|
||||
"test_notification_failed": "A tesztértesítés nem sikerült",
|
||||
"notify_per_person": "Kézbesítés személyenként",
|
||||
"member_avatars": "Tagok avatárjai",
|
||||
"member_avatars_hint": "A kiosztott feladatok mellett jelenik meg — monogram a tag színében. Alapértelmezés a névből; adjon meg saját monogramot vagy válasszon színt a tagok megkülönböztetéséhez.",
|
||||
"member_initials": "Monogram",
|
||||
"member_color": "Szín",
|
||||
"member_avatar_reset": "Visszaállítás",
|
||||
"notify_no_own_device": "Nincs saját eszköz — a háztartási szolgáltatást használja",
|
||||
"settings_notify_due_soon": "Értesítés, ha hamarosan esedékes",
|
||||
"settings_notify_overdue": "Értesítés lejáratkor",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Csoportosítás",
|
||||
"state_value_help": "A HA állapotértékét használja (általában kisbetűs, pl. „on”/„off”). A kis- és nagybetűk mentéskor normalizálódnak.",
|
||||
"target_changes_help": "Az egyező átmenetek száma a kiváltás előtt (alapértelmezés: 1).",
|
||||
"state_latch_help": "1 átmenetnél a trigger reteszként működik: helyreáll, amikor az entitás elhagyja a cél-állapotot — vagy, ha csak kiinduló állapot van megadva, amikor visszatér abba.",
|
||||
"for_minutes_state_help": "A 0 minden változást azonnal számol. Percekkel az új állapotnak előbb fenn kell maradnia — a rövid villanás ekkor se nem indít, se nem számít.",
|
||||
"qr_print_title": "QR-kódok nyomtatása",
|
||||
"qr_print_desc": "Nyomtatható oldal QR-kódokkal — vágja ki és ragassza fel a berendezésekre.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Törli a(z) „{name}” elemet?",
|
||||
"doc_too_large": "A fájl túl nagy (max. 25 MB).",
|
||||
"doc_upload_failed": "A feltöltés nem sikerült.",
|
||||
"completion_photo_optional": "Fotó az elvégzésről (opcionális)",
|
||||
"add_photo": "Fotó hozzáadása",
|
||||
"completion_photos_optional": "Fotók az elvégzésről (nem kötelező)",
|
||||
"completion_photos": "Fotók az elvégzésről",
|
||||
"choose_photos": "Fotók kiválasztása",
|
||||
"choose_photo": "Fotó kiválasztása",
|
||||
"photos_android_hint": "Az Android-alkalmazás választásonként egy fotót ad át — minden választás hozzáadódik.",
|
||||
"add_photos": "Fotók hozzáadása",
|
||||
"photos_limit": "Legfeljebb {max} fotó elvégzésenként",
|
||||
"history_edit_photos_hint": "Az itt eltávolított fotó megmarad az objektum dokumentumai között.",
|
||||
"uploading": "Feltöltés…",
|
||||
"remove": "Eltávolítás",
|
||||
"doc_deduped": "Már máshol is tárolva — megosztott, nem foglal extra helyet.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "A rögzített érték mellett jelenik meg a feladat elvégzésekor.",
|
||||
"reading_value_label": "Leolvasott érték",
|
||||
"reading_label": "Leolvasás",
|
||||
"readings_section": "Leolvasások",
|
||||
"readings_hint": "Több nevesített érték elvégzésenként — mérőnként egy sor. Egyetlen értékhez hagyja üresen.",
|
||||
"reading_name_label": "Leolvasás neve",
|
||||
"reading_unit_short": "Mértékegység",
|
||||
"reading_add": "Leolvasás hozzáadása",
|
||||
"reading_duplicate_name": "A név már foglalt",
|
||||
"reading_last": "legutóbb: {value}",
|
||||
"reading_below_last": "Kisebb az előző leolvasásnál ({value})",
|
||||
"settings_templates_label": "Sablongaléria",
|
||||
"settings_templates_hint": "Vegye ki a pipát a sosem használt sablonoknál — így eltűnnek a „Sablonból” választókból (panel és konfigurációs folyamat). Semmi más nem változik; bármikor újra engedélyezheti őket.",
|
||||
"worksheet": "Munkalap",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Saját értesítési szabály",
|
||||
"settings_notify_rule_hint": "Minden értesítés kiváltja a maintenance_supporter_notification eseményt is (típus, állapot, tárgy, feladat, hivatkozási számok, prioritás, mélylink, adatok). Irányítsa automatizálással — pl. a Tickerbe, Telegramba vagy Pushoverbe.",
|
||||
"settings_notify_event_only": "Csak az esemény, saját küldés nélkül",
|
||||
"settings_notify_event_only_hint": "A kézbesítést teljes egészében az automatizálás végzi. Az ismétlési időközök, a csendes órák és a napi korlát továbbra is meghatározzák, mikor váltódik ki az esemény.",
|
||||
"settings_notify_extra_data": "További értesítési adatok (sablon)",
|
||||
"settings_notify_extra_data_hint": "JSON vagy YAML, Jinja engedélyezett; beolvad a notify hívás data mezőjébe (a saját kulcsai nyernek). Változók: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Ellenőrizze a Teszt küldése gombbal.",
|
||||
"card_saved_view": "Mentett nézet",
|
||||
"card_saved_view_none": "Nincs",
|
||||
"card_saved_view_help": "A nézet állapot-, felhasználó- és címkeszűrőit a fenti szűrőkön felül alkalmazza. A nézet rendezése és csoportosítása a panel megjelenítési beállítása, a kártyán nem érvényesül.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Mind cseréltként jelölése",
|
||||
"battery_fleet_mark_one": "Elem cseréltként jelölése",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Nincs érzékelő",
|
||||
"battery_fleet_trigger_lost": "A feladat érzékelős kiváltója elveszett — nem fog aktiválódni és automatikusan lezáródni sem.",
|
||||
"battery_fleet_repair": "Javítás",
|
||||
"battery_fleet_exclude": "Kizárás a flottából",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Válassz egy elem-érzékelőt, amelyet az automatikus felismerés kihagyott — azonnal felkerül a listára.",
|
||||
"battery_fleet_track_self": "Öntöltő akkumulátorok követése",
|
||||
"battery_fleet_track_self_hint": "A telefonok, porszívók és más öntöltő eszközök akkumulátorként jelennek meg — alacsony töltöttségnél töltést kérnek, sosem új elemet.",
|
||||
"battery_fleet_due_without_sensor": "Lejárt előrejelzés esedékesnek számít, ha az elemnek nincs érzékelője",
|
||||
"battery_fleet_due_without_sensor_hint": "Egyes eszközök egyáltalán nem jelentenek töltöttséget — a Battery Notes csak a típust és az utolsó cserét ismeri. A szokásos élettartam letelte után az ilyen elem lemerültnek számít, és a flottafeladat emlékeztet; a cserélve jelölés visszaállítja az előrejelzést.",
|
||||
"battery_fleet_status_low": "Alacsony",
|
||||
"battery_fleet_status_soon": "Hamarosan",
|
||||
"battery_fleet_status_ok": "Rendben",
|
||||
"battery_fleet_status_due": "Esedékes",
|
||||
"battery_fleet_predicted_on": "Várhatóan {date} körül",
|
||||
"battery_fleet_predicted_trend": "Az elem merülési trendjéből előrejelezve: kb. {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Újratölthető: cserélés helyett töltse fel — soha nem kerül a bevásárlólistára",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Szervizkönyv (PDF)",
|
||||
"date": "Dátum",
|
||||
"service_record_entries": "bejegyzés",
|
||||
"ref_number": "Hivatkozási szám",
|
||||
"print_options_title": "Szervizkönyv nyomtatása",
|
||||
"print_layout": "Elrendezés",
|
||||
"print_layout_chronological": "Időrendben",
|
||||
"print_layout_by_task": "Feladatonként",
|
||||
"print_include": "Tartalmazza",
|
||||
"print_inc_readings": "Mérőállások",
|
||||
"print_inc_parts": "Felhasznált alkatrészek",
|
||||
"print_inc_photos": "Fotók",
|
||||
"print_inc_documents": "Csatolt dokumentumok",
|
||||
"print_inc_checklist": "Ellenőrzőlista",
|
||||
"print_inc_notes": "Jegyzetek",
|
||||
"print_inc_costs": "Költség és időtartam",
|
||||
"print_inc_person": "Elvégezte",
|
||||
"print_inc_refs": "Hivatkozási számok",
|
||||
"print_inc_qr": "QR-kód feladatonként",
|
||||
"print_button": "Nyomtatás",
|
||||
"report_scan_hint": "Olvassa be a feladat megnyitásához",
|
||||
"completed_by": "Elvégezte",
|
||||
"date_from": "Ettől",
|
||||
"date_to": "Eddig",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valore non valido",
|
||||
"feat_schedule_time": "Pianificazione oraria",
|
||||
"feat_schedule_time_desc": "Le attività scadono a un'ora specifica anziché a mezzanotte.",
|
||||
"schedule_time_optional": "Scadenza all'ora (opzionale)",
|
||||
"schedule_time_toggle": "Scadenza a un orario preciso",
|
||||
"schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.",
|
||||
"at_time": "alle",
|
||||
"notes_optional": "Note (opzionale)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Nessun tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Oggi",
|
||||
"palette_placeholder": "Cerca oggetti e attività…",
|
||||
"palette_placeholder": "Cerca oggetti, attività, ricambi, documenti, note…",
|
||||
"palette_no_results": "Nessun risultato",
|
||||
"palette_hint": "↑↓ naviga · Invio apri · Esc chiudi",
|
||||
"palette_hint": "↑↓ navigare · Invio aprire · Esc chiudere · / apre la ricerca ovunque",
|
||||
"search_open": "Cerca",
|
||||
"search_group_parts": "Ricambi",
|
||||
"search_group_content": "Nei documenti",
|
||||
"search_group_history": "Note della cronologia",
|
||||
"search_page": "Pagina {page}",
|
||||
"search_empty_hint": "Digita per cercare tra oggetti, attività, ricambi, documenti e note",
|
||||
"search_searching": "Ricerca in documenti e note…",
|
||||
"search_index_status": "Ricerca full-text: {indexed} di {total} file indicizzati · {no_text} senza livello testo · {pending} in attesa",
|
||||
"today_all_caught_up": "Tutto fatto! Niente in scadenza questa settimana.",
|
||||
"today_overdue": "Scadute",
|
||||
"today_due_today": "In scadenza oggi",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Soglia „consumabile basso“ (%)",
|
||||
"settings_battery_low_percent": "Soglia „batteria scarica“ (%)",
|
||||
"settings_thresholds_hint": "Valori predefiniti per le nuove Configurazioni suggerite (percentuale residua) e per le batterie della flotta senza soglia Battery Notes propria. Le attività esistenti mantengono il loro valore.",
|
||||
"settings_value_out_of_range": "Il valore deve essere compreso tra {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % per {n} batterie",
|
||||
"bn_floor_decides": "La tua soglia del {floor} % decide per tutte; per ogni batteria conta la più alta delle due soglie.",
|
||||
"bn_above_floor": "È sopra la tua soglia del {floor} %, quindi {name} decide per tutte le batterie annotate.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notifica di test inviata",
|
||||
"test_notification_failed": "Notifica di test non riuscita",
|
||||
"notify_per_person": "Consegna per persona",
|
||||
"member_avatars": "Avatar dei membri",
|
||||
"member_avatars_hint": "Mostrato accanto alle attività assegnate — iniziali nel colore del membro. Predefinito dal nome; imposta iniziali proprie o scegli un colore per distinguere i membri.",
|
||||
"member_initials": "Iniziali",
|
||||
"member_color": "Colore",
|
||||
"member_avatar_reset": "Ripristina",
|
||||
"notify_no_own_device": "Nessun dispositivo proprio — usa il servizio della casa",
|
||||
"settings_notify_due_soon": "Notifica quando in scadenza",
|
||||
"settings_notify_overdue": "Notifica quando scaduto",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Raggruppa per",
|
||||
"state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.",
|
||||
"target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).",
|
||||
"state_latch_help": "Con 1 transizione il trigger è un latch: si ripristina quando l'entità lascia lo stato di destinazione — oppure, se è impostato solo lo stato di partenza, quando vi ritorna.",
|
||||
"for_minutes_state_help": "0 conta ogni cambiamento subito. Con i minuti, il nuovo stato deve prima mantenersi — un breve sfarfallio allora non attiva né conta.",
|
||||
"qr_print_title": "Stampa codici QR",
|
||||
"qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Eliminare «{name}»?",
|
||||
"doc_too_large": "File troppo grande (max 25 MB).",
|
||||
"doc_upload_failed": "Caricamento non riuscito.",
|
||||
"completion_photo_optional": "Foto di completamento (facoltativa)",
|
||||
"add_photo": "Aggiungi foto",
|
||||
"completion_photos_optional": "Foto di completamento (facoltativo)",
|
||||
"completion_photos": "Foto di completamento",
|
||||
"choose_photos": "Scegli foto",
|
||||
"choose_photo": "Scegli foto",
|
||||
"photos_android_hint": "L'app Android passa una foto per selezione — ogni selezione viene aggiunta.",
|
||||
"add_photos": "Aggiungi foto",
|
||||
"photos_limit": "Fino a {max} foto per completamento",
|
||||
"history_edit_photos_hint": "Una foto rimossa qui resta nei documenti dell'oggetto.",
|
||||
"uploading": "Caricamento…",
|
||||
"remove": "Rimuovi",
|
||||
"doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Mostrata accanto al valore registrato al completamento di questa attività.",
|
||||
"reading_value_label": "Valore letto",
|
||||
"reading_label": "Lettura",
|
||||
"readings_section": "Letture",
|
||||
"readings_hint": "Più valori denominati per completamento — una riga per contatore. Lascia vuoto per un valore singolo.",
|
||||
"reading_name_label": "Nome lettura",
|
||||
"reading_unit_short": "Unità",
|
||||
"reading_add": "Aggiungi lettura",
|
||||
"reading_duplicate_name": "Nome già in uso",
|
||||
"reading_last": "ultimo: {value}",
|
||||
"reading_below_last": "Inferiore all'ultima lettura ({value})",
|
||||
"settings_templates_label": "Galleria modelli",
|
||||
"settings_templates_hint": "Deseleziona i modelli che non ti serviranno mai: spariscono dai selettori \"Da modello\" (pannello e config flow). Nient'altro cambia; riattivabili in qualsiasi momento.",
|
||||
"worksheet": "Scheda di lavoro",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "La tua regola di notifica",
|
||||
"settings_notify_rule_hint": "Ogni notifica genera anche l'evento maintenance_supporter_notification (tipo, stato, oggetto, attività, numeri di riferimento, priorità, link, payload). Instradalo con un'automazione — ad es. verso Ticker, Telegram o Pushover.",
|
||||
"settings_notify_event_only": "Genera solo l'evento, non inviare nulla",
|
||||
"settings_notify_event_only_hint": "La tua automazione gestisce l'intera consegna. Intervalli di ripetizione, ore di silenzio e limite giornaliero valgono ancora per quando scatta l'evento.",
|
||||
"settings_notify_extra_data": "Dati di notifica aggiuntivi (template)",
|
||||
"settings_notify_extra_data_hint": "JSON o YAML, Jinja consentito; unito ai data della chiamata notify (le tue chiavi vincono). Variabili: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Prova con il pulsante Invia test.",
|
||||
"card_saved_view": "Vista salvata",
|
||||
"card_saved_view_none": "Nessuna",
|
||||
"card_saved_view_help": "Applica i filtri stato, utente ed etichetta della vista in aggiunta ai filtri sopra. Ordinamento e raggruppamento della vista sono impostazioni di visualizzazione del pannello e non si applicano alla scheda.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Segna tutte sostituite",
|
||||
"battery_fleet_mark_one": "Segna questa batteria sostituita",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Nessun sensore",
|
||||
"battery_fleet_trigger_lost": "Il trigger del sensore di questa attività è andato perso — non si attiverà né si completerà automaticamente.",
|
||||
"battery_fleet_repair": "Ripara",
|
||||
"battery_fleet_exclude": "Escludi dal parco",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Scegli un sensore batteria sfuggito al rilevamento automatico — entra subito nell'elenco.",
|
||||
"battery_fleet_track_self": "Traccia le batterie autoricaricabili",
|
||||
"battery_fleet_track_self_hint": "Telefoni, aspirapolvere e altri dispositivi che si ricaricano da soli appaiono come ricaricabili — se scarichi chiedono una ricarica, mai batterie nuove.",
|
||||
"battery_fleet_due_without_sensor": "Considerare una previsione scaduta come in scadenza se una batteria non ha sensore",
|
||||
"battery_fleet_due_without_sensor_hint": "Alcuni dispositivi non riportano alcun livello di batteria — Battery Notes conosce solo il tipo e l'ultima sostituzione. Trascorsa la durata tipica, la batteria conta come scarica e l'attività del parco te lo ricorda; segnarla come sostituita azzera la previsione.",
|
||||
"battery_fleet_status_low": "Scarica",
|
||||
"battery_fleet_status_soon": "A breve",
|
||||
"battery_fleet_status_ok": "A posto",
|
||||
"battery_fleet_status_due": "In scadenza",
|
||||
"battery_fleet_predicted_on": "Previsto intorno al {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto dalla tendenza di scarica di questa batteria: intorno al {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Ricaricabile: da ricaricare anziché sostituire — mai nella lista della spesa",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Libretto di manutenzione (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "voci",
|
||||
"ref_number": "Numero di riferimento",
|
||||
"print_options_title": "Stampa il libretto di servizio",
|
||||
"print_layout": "Impaginazione",
|
||||
"print_layout_chronological": "Cronologico",
|
||||
"print_layout_by_task": "Per attività",
|
||||
"print_include": "Includi",
|
||||
"print_inc_readings": "Letture",
|
||||
"print_inc_parts": "Ricambi usati",
|
||||
"print_inc_photos": "Foto",
|
||||
"print_inc_documents": "Documenti collegati",
|
||||
"print_inc_checklist": "Lista di controllo",
|
||||
"print_inc_notes": "Note",
|
||||
"print_inc_costs": "Costo e durata",
|
||||
"print_inc_person": "Eseguito da",
|
||||
"print_inc_refs": "Numeri di riferimento",
|
||||
"print_inc_qr": "Codice QR per attività",
|
||||
"print_button": "Stampa",
|
||||
"report_scan_hint": "Scansiona per aprire l'attività",
|
||||
"completed_by": "Completato da",
|
||||
"date_from": "Dal",
|
||||
"date_to": "Al",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 無効な値です",
|
||||
"feat_schedule_time": "時刻指定スケジュール",
|
||||
"feat_schedule_time_desc": "タスクが深夜0時ではなく指定した時刻に期限超過となります。",
|
||||
"schedule_time_optional": "期限の時刻(任意)",
|
||||
"schedule_time_toggle": "特定の時刻に期限",
|
||||
"schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。",
|
||||
"at_time": "時刻",
|
||||
"notes_optional": "メモ(任意)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— タグなし —",
|
||||
"dashboard": "ダッシュボード",
|
||||
"tab_today": "今日",
|
||||
"palette_placeholder": "オブジェクトとタスクを検索…",
|
||||
"palette_placeholder": "対象・タスク・部品・文書・メモを検索…",
|
||||
"palette_no_results": "一致なし",
|
||||
"palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる",
|
||||
"palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる · / でどこからでも検索",
|
||||
"search_open": "検索",
|
||||
"search_group_parts": "部品",
|
||||
"search_group_content": "文書内",
|
||||
"search_group_history": "履歴メモ",
|
||||
"search_page": "{page} ページ",
|
||||
"search_empty_hint": "入力して対象・タスク・部品・文書・メモを検索",
|
||||
"search_searching": "文書とメモを検索中…",
|
||||
"search_index_status": "全文検索: {total} 件中 {indexed} 件をインデックス済み · テキスト層なし {no_text} 件 · 保留 {pending} 件",
|
||||
"today_all_caught_up": "すべて完了!今週の予定はありません。",
|
||||
"today_overdue": "期限切れ",
|
||||
"today_due_today": "今日期限",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "消耗品「残量少」しきい値(%)",
|
||||
"settings_battery_low_percent": "電池「残量少」しきい値(%)",
|
||||
"settings_thresholds_hint": "新しい「推奨セットアップ」(残量パーセント)と、独自の Battery Notes しきい値を持たないバッテリー一覧の電池に適用される既定値です。既存のタスクは値を保持します。",
|
||||
"settings_value_out_of_range": "値は {min} から {max} の間で指定してください",
|
||||
"bn_summary": "{name}:{n} 個の電池に {pct} %",
|
||||
"bn_floor_decides": "あなたの {floor} % のしきい値がすべてに適用されます。電池ごとに高い方のしきい値が有効です。",
|
||||
"bn_above_floor": "これはあなたの {floor} % のしきい値を上回るため、記録済みのすべての電池は {name} が決定します。",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "テスト通知を送信しました",
|
||||
"test_notification_failed": "テスト通知に失敗しました",
|
||||
"notify_per_person": "メンバーごとの配信",
|
||||
"member_avatars": "メンバーのアバター",
|
||||
"member_avatars_hint": "割り当てられたタスクの横に表示されます — メンバーの色で頭文字を表示。既定は名前から生成。頭文字や色を設定してメンバーを区別できます。",
|
||||
"member_initials": "頭文字",
|
||||
"member_color": "色",
|
||||
"member_avatar_reset": "リセット",
|
||||
"notify_no_own_device": "個人の端末なし — 家庭用サービスを使用",
|
||||
"settings_notify_due_soon": "期限間近で通知",
|
||||
"settings_notify_overdue": "期限超過で通知",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "グループ化",
|
||||
"state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。",
|
||||
"target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。",
|
||||
"state_latch_help": "遷移回数が1の場合トリガーはラッチになります。エンティティが「後」の状態を離れると復帰し、「前」の状態のみ設定時はその状態に戻ると復帰します。",
|
||||
"for_minutes_state_help": "0 はすべての変化を即座にカウントします。分を設定すると新しい状態がその時間続く必要があり、一瞬の変動はトリガーもカウントもされません。",
|
||||
"qr_print_title": "QRコードを印刷",
|
||||
"qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "「{name}」を削除しますか?",
|
||||
"doc_too_large": "ファイルが大きすぎます(最大 25 MB)。",
|
||||
"doc_upload_failed": "アップロードに失敗しました。",
|
||||
"completion_photo_optional": "完了写真(任意)",
|
||||
"add_photo": "写真を追加",
|
||||
"completion_photos_optional": "完了写真(任意)",
|
||||
"completion_photos": "完了写真",
|
||||
"choose_photos": "写真を選択",
|
||||
"choose_photo": "写真を選択",
|
||||
"photos_android_hint": "Android アプリは1回の選択につき写真1枚を渡します。選択するたびに追加されます。",
|
||||
"add_photos": "写真を追加",
|
||||
"photos_limit": "1回の完了につき最大{max}枚",
|
||||
"history_edit_photos_hint": "ここで外した写真は対象のドキュメントに残ります。",
|
||||
"uploading": "アップロード中…",
|
||||
"remove": "削除",
|
||||
"doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "このタスクの完了時に記録値の横に表示されます。",
|
||||
"reading_value_label": "検針値",
|
||||
"reading_label": "検針",
|
||||
"readings_section": "検針項目",
|
||||
"readings_hint": "1回の完了で複数の名前付き値を記録 — メーターごとに1行。単一の値だけなら空のままにします。",
|
||||
"reading_name_label": "検針項目名",
|
||||
"reading_unit_short": "単位",
|
||||
"reading_add": "検針項目を追加",
|
||||
"reading_duplicate_name": "この名前は既に使われています",
|
||||
"reading_last": "前回: {value}",
|
||||
"reading_below_last": "前回の値({value})より小さい",
|
||||
"settings_templates_label": "テンプレートギャラリー",
|
||||
"settings_templates_hint": "不要なテンプレートのチェックを外すと、「テンプレートから」の選択肢(パネルと設定フロー)から消えます。それ以外は何も変わらず、いつでも再有効化できます。",
|
||||
"worksheet": "作業シート",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "このビューのみ通知",
|
||||
"settings_notify_scope_all": "すべてのタスク",
|
||||
"settings_notify_scope_hint": "選択した保存済みビューのラベル/ユーザーフィルターに一致するタスクのみがリマインダーを送信します。ビューのステータス・並び順・グループ化はここでは無視されます。",
|
||||
"settings_notify_rule": "独自の通知ルール",
|
||||
"settings_notify_rule_hint": "各通知はイベント maintenance_supporter_notification(種類、状態、対象、タスク、参照番号、優先度、ディープリンク、ペイロード)も発火します。オートメーションで Ticker、Telegram、Pushover などへ転送してください。",
|
||||
"settings_notify_event_only": "イベントのみ発火し、自身では送信しない",
|
||||
"settings_notify_event_only_hint": "配信はすべてオートメーション側で行います。繰り返し間隔、静音時間、1日の上限は引き続きイベント発火の条件になります。",
|
||||
"settings_notify_extra_data": "追加の通知データ(テンプレート)",
|
||||
"settings_notify_extra_data_hint": "JSON または YAML、Jinja 可。notify 呼び出しの data にマージされます(あなたのキーが優先)。変数: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message。「テスト送信」ボタンで確認できます。",
|
||||
"card_saved_view": "保存済みビュー",
|
||||
"card_saved_view_none": "なし",
|
||||
"card_saved_view_help": "上記のフィルターに加えて、ビューのステータス・ユーザー・ラベルのフィルターを適用します。ビューの並び順とグループ化はパネルの表示設定であり、カードには適用されません。",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "すべて交換済みにする",
|
||||
"battery_fleet_mark_one": "この電池を交換済みにする",
|
||||
"battery_fleet_offline": "オフライン",
|
||||
"battery_fleet_no_sensor": "センサーなし",
|
||||
"battery_fleet_trigger_lost": "このタスクのセンサートリガーが失われました — 発動も自動完了もしません。",
|
||||
"battery_fleet_repair": "修復",
|
||||
"battery_fleet_exclude": "フリートから除外",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "自動検出が見逃した電池センサーを選択すると、すぐに一覧に追加されます。",
|
||||
"battery_fleet_track_self": "自己充電式バッテリーを追跡",
|
||||
"battery_fleet_track_self_hint": "スマートフォンや掃除機など自己充電するデバイスは充電式として表示されます。残量が少ないときは充電を促し、電池交換は求めません。",
|
||||
"battery_fleet_due_without_sensor": "センサーのない電池は、予測日を過ぎたら交換時期として扱う",
|
||||
"battery_fleet_due_without_sensor_hint": "電池残量をまったく報告しないデバイスもあります。Battery Notes は種類と最終交換日しか知りません。一般的な寿命を過ぎると、その電池は残量低下として扱われ、一括タスクが通知します。交換済みにすると予測がリセットされます。",
|
||||
"battery_fleet_status_low": "残量低下",
|
||||
"battery_fleet_status_soon": "まもなく",
|
||||
"battery_fleet_status_ok": "良好",
|
||||
"battery_fleet_status_due": "交換時期",
|
||||
"battery_fleet_predicted_on": "{date} 頃の見込み",
|
||||
"battery_fleet_predicted_trend": "この電池の放電傾向から予測: {date} 頃 ({confidence})",
|
||||
"battery_fleet_rechargeable": "充電式:交換ではなく充電——買い物リストには載りません",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "整備記録(PDF)",
|
||||
"date": "日付",
|
||||
"service_record_entries": "件",
|
||||
"ref_number": "参照番号",
|
||||
"print_options_title": "整備記録を印刷",
|
||||
"print_layout": "レイアウト",
|
||||
"print_layout_chronological": "時系列",
|
||||
"print_layout_by_task": "タスク別",
|
||||
"print_include": "含める",
|
||||
"print_inc_readings": "検針値",
|
||||
"print_inc_parts": "使用部品",
|
||||
"print_inc_photos": "写真",
|
||||
"print_inc_documents": "関連文書",
|
||||
"print_inc_checklist": "チェックリスト",
|
||||
"print_inc_notes": "メモ",
|
||||
"print_inc_costs": "費用と所要時間",
|
||||
"print_inc_person": "実施者",
|
||||
"print_inc_refs": "参照番号",
|
||||
"print_inc_qr": "タスクごとのQRコード",
|
||||
"print_button": "印刷",
|
||||
"report_scan_hint": "スキャンしてタスクを開く",
|
||||
"completed_by": "実施者",
|
||||
"date_from": "開始",
|
||||
"date_to": "終了",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 유효하지 않은 값입니다",
|
||||
"feat_schedule_time": "시각 지정 일정",
|
||||
"feat_schedule_time_desc": "작업이 자정이 아닌 지정한 시각에 기한 초과로 전환됩니다.",
|
||||
"schedule_time_optional": "기한 시각 (선택)",
|
||||
"schedule_time_toggle": "특정 시각에 기한",
|
||||
"schedule_time_help": "비워두면 자정(기본값)입니다. HA 시간대 기준.",
|
||||
"at_time": "시각",
|
||||
"notes_optional": "메모 (선택)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— 태그 없음 —",
|
||||
"dashboard": "대시보드",
|
||||
"tab_today": "오늘",
|
||||
"palette_placeholder": "객체와 작업 검색…",
|
||||
"palette_placeholder": "대상, 작업, 부품, 문서, 메모 검색…",
|
||||
"palette_no_results": "일치 항목 없음",
|
||||
"palette_hint": "↑↓ 이동 · Enter 열기 · Esc 닫기",
|
||||
"palette_hint": "↑↓ 이동 · Enter 열기 · Esc 닫기 · / 어디서든 검색 열기",
|
||||
"search_open": "검색",
|
||||
"search_group_parts": "부품",
|
||||
"search_group_content": "문서 내용",
|
||||
"search_group_history": "이력 메모",
|
||||
"search_page": "{page}페이지",
|
||||
"search_empty_hint": "대상, 작업, 부품, 문서, 메모를 검색하려면 입력하세요",
|
||||
"search_searching": "문서와 메모 검색 중…",
|
||||
"search_index_status": "전체 텍스트 검색: {total}개 중 {indexed}개 색인됨 · 텍스트 레이어 없음 {no_text}개 · 대기 {pending}개",
|
||||
"today_all_caught_up": "모두 처리했습니다! 이번 주에 예정된 작업이 없습니다.",
|
||||
"today_overdue": "기한 초과",
|
||||
"today_due_today": "오늘 예정",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "소모품 „부족“ 임계값 (%)",
|
||||
"settings_battery_low_percent": "배터리 „부족“ 임계값 (%)",
|
||||
"settings_thresholds_hint": "새 추천 설정(남은 비율)과 자체 Battery Notes 임계값이 없는 배터리 목록 배터리의 기본값입니다. 기존 작업은 값을 유지합니다.",
|
||||
"settings_value_out_of_range": "값은 {min}에서 {max} 사이여야 합니다",
|
||||
"bn_summary": "{name}: 배터리 {n}개에 {pct} %",
|
||||
"bn_floor_decides": "당신의 {floor} % 임계값이 모두에 적용됩니다. 배터리마다 두 임계값 중 높은 쪽이 적용됩니다.",
|
||||
"bn_above_floor": "이는 당신의 {floor} % 임계값보다 높으므로, 기록된 모든 배터리는 {name}이(가) 결정합니다.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "테스트 알림을 보냈습니다",
|
||||
"test_notification_failed": "테스트 알림 전송 실패",
|
||||
"notify_per_person": "사용자별 전송",
|
||||
"member_avatars": "구성원 아바타",
|
||||
"member_avatars_hint": "할당된 작업 옆에 표시됩니다 — 구성원 색상의 이니셜. 기본값은 이름에서 가져오며, 구성원을 구분하려면 이니셜을 직접 설정하거나 색상을 선택하세요.",
|
||||
"member_initials": "이니셜",
|
||||
"member_color": "색상",
|
||||
"member_avatar_reset": "초기화",
|
||||
"notify_no_own_device": "개인 기기 없음 — 가정 서비스 사용",
|
||||
"settings_notify_due_soon": "기한 임박 시 알림",
|
||||
"settings_notify_overdue": "기한 초과 시 알림",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "그룹화",
|
||||
"state_value_help": "HA 상태 값을 사용하세요(보통 소문자, 예: \"on\"/\"off\"). 대소문자는 저장 시 정규화됩니다.",
|
||||
"target_changes_help": "트리거가 발동하기까지 필요한 일치 전환 횟수입니다 (기본값: 1).",
|
||||
"state_latch_help": "전환 1회로 설정하면 트리거는 래치가 됩니다. 엔티티가 '이후' 상태를 벗어나면 복구되고, '이전' 상태만 설정한 경우 그 상태로 돌아오면 복구됩니다.",
|
||||
"for_minutes_state_help": "0은 모든 변화를 즉시 집계합니다. 분을 설정하면 새 상태가 먼저 그 시간 동안 유지되어야 하며, 짧은 깜빡임은 트리거되지도 집계되지도 않습니다.",
|
||||
"qr_print_title": "QR 코드 인쇄",
|
||||
"qr_print_desc": "잘라서 장비에 붙일 수 있는 QR 코드 인쇄 페이지를 생성합니다.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "\"{name}\"을(를) 삭제할까요?",
|
||||
"doc_too_large": "파일이 너무 큽니다 (최대 25 MB).",
|
||||
"doc_upload_failed": "업로드에 실패했습니다.",
|
||||
"completion_photo_optional": "완료 사진 (선택)",
|
||||
"add_photo": "사진 추가",
|
||||
"completion_photos_optional": "완료 사진 (선택)",
|
||||
"completion_photos": "완료 사진",
|
||||
"choose_photos": "사진 선택",
|
||||
"choose_photo": "사진 선택",
|
||||
"photos_android_hint": "Android 앱은 선택할 때마다 사진 한 장씩 전달합니다. 선택할 때마다 추가됩니다.",
|
||||
"add_photos": "사진 추가",
|
||||
"photos_limit": "완료당 최대 {max}장",
|
||||
"history_edit_photos_hint": "여기서 제거한 사진은 대상의 문서에 그대로 남습니다.",
|
||||
"uploading": "업로드 중…",
|
||||
"remove": "제거",
|
||||
"doc_deduped": "이미 다른 곳에 저장됨 — 공유되어 추가 공간을 쓰지 않습니다.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "이 작업을 완료할 때 기록 값 옆에 표시됩니다.",
|
||||
"reading_value_label": "검침 값",
|
||||
"reading_label": "검침",
|
||||
"readings_section": "검침 항목",
|
||||
"readings_hint": "완료당 여러 개의 이름 있는 값 — 계량기마다 한 줄. 단일 값만 기록하려면 비워 두세요.",
|
||||
"reading_name_label": "검침 항목 이름",
|
||||
"reading_unit_short": "단위",
|
||||
"reading_add": "검침 항목 추가",
|
||||
"reading_duplicate_name": "이미 사용 중인 이름입니다",
|
||||
"reading_last": "지난번: {value}",
|
||||
"reading_below_last": "지난 검침값({value})보다 작습니다",
|
||||
"settings_templates_label": "템플릿 갤러리",
|
||||
"settings_templates_hint": "필요 없는 템플릿의 체크를 해제하세요 — \"템플릿에서\" 선택 목록(패널 및 설정 흐름)에서 사라집니다. 다른 변화는 없으며 언제든 다시 활성화할 수 있습니다.",
|
||||
"worksheet": "작업 시트",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "특정 보기만 알림",
|
||||
"settings_notify_scope_all": "모든 작업",
|
||||
"settings_notify_scope_hint": "선택한 저장된 보기의 라벨/사용자 필터와 일치하는 작업만 알림을 보냅니다. 보기의 상태·정렬·그룹화는 여기서 무시됩니다.",
|
||||
"settings_notify_rule": "나만의 알림 규칙",
|
||||
"settings_notify_rule_hint": "모든 알림은 maintenance_supporter_notification 이벤트도 발생시킵니다(종류, 상태, 대상, 작업, 참조 번호, 우선순위, 딥 링크, 페이로드). 자동화로 라우팅하세요 — 예: Ticker, Telegram, Pushover.",
|
||||
"settings_notify_event_only": "이벤트만 발생시키고 직접 보내지 않음",
|
||||
"settings_notify_event_only_hint": "전달은 전적으로 자동화가 담당합니다. 반복 간격, 방해 금지 시간, 일일 한도는 여전히 이벤트 발생 시점에 적용됩니다.",
|
||||
"settings_notify_extra_data": "추가 알림 데이터(템플릿)",
|
||||
"settings_notify_extra_data_hint": "JSON 또는 YAML, Jinja 사용 가능. notify 호출의 data에 병합됩니다(사용자 키 우선). 변수: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. 테스트 보내기 버튼으로 확인하세요.",
|
||||
"card_saved_view": "저장된 보기",
|
||||
"card_saved_view_none": "없음",
|
||||
"card_saved_view_help": "위 필터에 더해 보기의 상태·사용자·라벨 필터를 적용합니다. 보기의 정렬과 그룹화는 패널 표시 설정이며 카드에는 적용되지 않습니다.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "모두 교체됨으로 표시",
|
||||
"battery_fleet_mark_one": "이 배터리를 교체됨으로 표시",
|
||||
"battery_fleet_offline": "오프라인",
|
||||
"battery_fleet_no_sensor": "센서 없음",
|
||||
"battery_fleet_trigger_lost": "이 작업의 센서 트리거가 사라졌습니다 — 트리거도 자동 완료도 되지 않습니다.",
|
||||
"battery_fleet_repair": "복구",
|
||||
"battery_fleet_exclude": "플릿에서 제외",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "자동 검색이 놓친 배터리 센서를 선택하면 즉시 목록에 추가됩니다.",
|
||||
"battery_fleet_track_self": "자체 충전 배터리 추적",
|
||||
"battery_fleet_track_self_hint": "휴대폰, 청소기 등 스스로 충전되는 기기는 충전식으로 표시됩니다. 잔량이 낮으면 충전을 요청하며 새 전지를 요구하지 않습니다.",
|
||||
"battery_fleet_due_without_sensor": "센서가 없는 배터리는 예측일이 지나면 교체 시기로 간주",
|
||||
"battery_fleet_due_without_sensor_hint": "일부 기기는 배터리 잔량을 전혀 보고하지 않습니다. Battery Notes는 종류와 마지막 교체일만 알고 있습니다. 일반적인 수명이 지나면 그 배터리는 부족한 것으로 간주되어 배터리 관리 작업이 알려 줍니다. 교체됨으로 표시하면 예측이 초기화됩니다.",
|
||||
"battery_fleet_status_low": "부족",
|
||||
"battery_fleet_status_soon": "곧",
|
||||
"battery_fleet_status_ok": "정상",
|
||||
"battery_fleet_status_due": "교체 시기",
|
||||
"battery_fleet_predicted_on": "{date}쯤 예상",
|
||||
"battery_fleet_predicted_trend": "이 배터리의 방전 추세로 예측: 약 {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "충전식: 교체 대신 충전 — 쇼핑 목록에 오르지 않습니다",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "정비 기록(PDF)",
|
||||
"date": "날짜",
|
||||
"service_record_entries": "건",
|
||||
"ref_number": "참조 번호",
|
||||
"print_options_title": "정비 기록 인쇄",
|
||||
"print_layout": "레이아웃",
|
||||
"print_layout_chronological": "시간순",
|
||||
"print_layout_by_task": "작업별",
|
||||
"print_include": "포함",
|
||||
"print_inc_readings": "측정값",
|
||||
"print_inc_parts": "사용 부품",
|
||||
"print_inc_photos": "사진",
|
||||
"print_inc_documents": "연결된 문서",
|
||||
"print_inc_checklist": "체크리스트",
|
||||
"print_inc_notes": "메모",
|
||||
"print_inc_costs": "비용 및 소요 시간",
|
||||
"print_inc_person": "완료자",
|
||||
"print_inc_refs": "참조 번호",
|
||||
"print_inc_qr": "작업별 QR 코드",
|
||||
"print_button": "인쇄",
|
||||
"report_scan_hint": "스캔하여 작업 열기",
|
||||
"completed_by": "수행자",
|
||||
"date_from": "시작",
|
||||
"date_to": "종료",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: ugyldig verdi",
|
||||
"feat_schedule_time": "Planlegging med klokkeslett",
|
||||
"feat_schedule_time_desc": "Oppgaver forfaller på et bestemt klokkeslett i stedet for ved midnatt.",
|
||||
"schedule_time_optional": "Forfaller kl. (valgfritt)",
|
||||
"schedule_time_toggle": "Forfaller på et bestemt klokkeslett",
|
||||
"schedule_time_help": "Tom = midnatt (standard). HA-tidssone.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Notater (valgfritt)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Ingen brikke —",
|
||||
"dashboard": "Dashbord",
|
||||
"tab_today": "I dag",
|
||||
"palette_placeholder": "Søk objekter og oppgaver…",
|
||||
"palette_placeholder": "Søk objekter, oppgaver, reservedeler, dokumenter, notater…",
|
||||
"palette_no_results": "Ingen treff",
|
||||
"palette_hint": "↑↓ naviger · Enter åpne · Esc lukk",
|
||||
"palette_hint": "↑↓ naviger · Enter åpne · Esc lukk · / åpner søket overalt",
|
||||
"search_open": "Søk",
|
||||
"search_group_parts": "Reservedeler",
|
||||
"search_group_content": "I dokumenter",
|
||||
"search_group_history": "Historikknotater",
|
||||
"search_page": "Side {page}",
|
||||
"search_empty_hint": "Skriv for å søke i objekter, oppgaver, reservedeler, dokumenter og notater",
|
||||
"search_searching": "Søker i dokumenter og notater…",
|
||||
"search_index_status": "Fulltekstsøk: {indexed} av {total} filer indeksert · {no_text} uten tekstlag · {pending} venter",
|
||||
"today_all_caught_up": "Alt er ajour! Ingenting forfaller denne uken.",
|
||||
"today_overdue": "Forfalt",
|
||||
"today_due_today": "Forfaller i dag",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Terskel „forbruksvare lav“ (%)",
|
||||
"settings_battery_low_percent": "Terskel „batteri lavt“ (%)",
|
||||
"settings_thresholds_hint": "Standardverdier for nye Foreslåtte oppsett (gjenværende prosent) og for flåtebatterier uten egen Battery Notes-terskel. Eksisterende oppgaver beholder verdien sin.",
|
||||
"settings_value_out_of_range": "Verdien må være mellom {min} og {max}",
|
||||
"bn_summary": "{name}: {pct} % for {n} batterier",
|
||||
"bn_floor_decides": "Din terskel på {floor} % avgjør for alle; per batteri gjelder den høyeste av de to tersklene.",
|
||||
"bn_above_floor": "Det er over din terskel på {floor} %, så {name} avgjør for alle noterte batterier.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Testvarsel sendt",
|
||||
"test_notification_failed": "Testvarsel mislyktes",
|
||||
"notify_per_person": "Levering per person",
|
||||
"member_avatars": "Medlemmenes avatarer",
|
||||
"member_avatars_hint": "Vises ved siden av tildelte oppgaver — initialer i medlemmets farge. Standard kommer fra navnet; angi egne initialer eller velg en farge for å skille medlemmer.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Farge",
|
||||
"member_avatar_reset": "Tilbakestill",
|
||||
"notify_no_own_device": "Ingen egen enhet — bruker husstandens tjeneste",
|
||||
"settings_notify_due_soon": "Varsle når noe forfaller snart",
|
||||
"settings_notify_overdue": "Varsle når noe er forfalt",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "Grupper etter",
|
||||
"state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.",
|
||||
"target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).",
|
||||
"state_latch_help": "Med 1 overgang er utløseren en lås: den tilbakestilles når entiteten forlater Til-tilstanden — eller, hvis bare en Fra-tilstand er satt, når den går tilbake til den.",
|
||||
"for_minutes_state_help": "0 teller hver endring umiddelbart. Med minutter må den nye tilstanden først holde seg — korte blink verken utløser eller telles da.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Slette \"{name}\"?",
|
||||
"doc_too_large": "Filen er for stor (maks. 25 MB).",
|
||||
"doc_upload_failed": "Opplasting mislyktes.",
|
||||
"completion_photo_optional": "Fullføringsbilde (valgfritt)",
|
||||
"add_photo": "Legg til bilde",
|
||||
"completion_photos_optional": "Bilder ved fullføring (valgfritt)",
|
||||
"completion_photos": "Bilder ved fullføring",
|
||||
"choose_photos": "Velg bilder",
|
||||
"choose_photo": "Velg bilde",
|
||||
"photos_android_hint": "Android-appen leverer ett bilde per valg — hvert valg legges til.",
|
||||
"add_photos": "Legg til bilder",
|
||||
"photos_limit": "Inntil {max} bilder per fullføring",
|
||||
"history_edit_photos_hint": "Et bilde som fjernes her, blir liggende i objektets dokumenter.",
|
||||
"uploading": "Laster opp…",
|
||||
"remove": "Fjern",
|
||||
"doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Vises ved siden av den registrerte verdien når oppgaven fullføres.",
|
||||
"reading_value_label": "Avlest verdi",
|
||||
"reading_label": "Avlesning",
|
||||
"readings_section": "Avlesninger",
|
||||
"readings_hint": "Flere navngitte verdier per fullføring — én rad per måler. La stå tomt for én enkelt verdi.",
|
||||
"reading_name_label": "Avlesningens navn",
|
||||
"reading_unit_short": "Enhet",
|
||||
"reading_add": "Legg til avlesning",
|
||||
"reading_duplicate_name": "Navnet er allerede i bruk",
|
||||
"reading_last": "sist: {value}",
|
||||
"reading_below_last": "Lavere enn forrige avlesning ({value})",
|
||||
"settings_templates_label": "Malgalleri",
|
||||
"settings_templates_hint": "Fjern haken for maler du aldri trenger — de forsvinner fra \"Fra mal\"-velgerne (panel og config flow). Ellers endres ingenting; kan aktiveres igjen når som helst.",
|
||||
"worksheet": "Arbeidsark",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen varslingsregel",
|
||||
"settings_notify_rule_hint": "Hvert varsel utløser også hendelsen maintenance_supporter_notification (type, status, objekt, oppgave, referansenumre, prioritet, lenke, nyttelast). Rut den med en automasjon — f.eks. til Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Bare utløs hendelsen, send ingenting selv",
|
||||
"settings_notify_event_only_hint": "Automasjonen din tar hele leveransen. Gjentakelsesintervaller, stille timer og dagstaket avgjør fortsatt når hendelsen utløses.",
|
||||
"settings_notify_extra_data": "Ekstra varslingsdata (mal)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tillatt; flettes inn i notify-kallets data (dine nøkler vinner). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test med knappen Send test.",
|
||||
"card_saved_view": "Lagret visning",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Bruker visningens status-, bruker- og etikettfiltre i tillegg til filtrene over. Visningens sortering og gruppering er panelvisningsinnstillinger og brukes ikke på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Merk alle byttet",
|
||||
"battery_fleet_mark_one": "Merk dette batteriet byttet",
|
||||
"battery_fleet_offline": "frakoblet",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Oppgavens sensorutløser er tapt — den utløses ikke og fullføres ikke automatisk.",
|
||||
"battery_fleet_repair": "Reparer",
|
||||
"battery_fleet_exclude": "Utelukk fra flåten",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Velg en batterisensor som den automatiske gjenkjenningen overså — den blir med i listen umiddelbart.",
|
||||
"battery_fleet_track_self": "Spor selvladende batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, støvsugere og andre selvladende enheter vises som oppladbare — ved lavt nivå bes det om lading, aldri om nye batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandle en passert prognose som forfalt når et batteri mangler sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Noen enheter rapporterer ikke batterinivå i det hele tatt — Battery Notes kjenner bare typen og siste bytte. Når den typiske levetiden er passert, regnes et slikt batteri som lavt og flåteoppgaven minner deg på det; å merke det som byttet nullstiller prognosen.",
|
||||
"battery_fleet_status_low": "Lav",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I orden",
|
||||
"battery_fleet_status_due": "Forfalt",
|
||||
"battery_fleet_predicted_on": "Forventes rundt {date}",
|
||||
"battery_fleet_predicted_trend": "Forutsagt ut fra batteriets utladingstrend: rundt {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oppladbart: lad i stedet for å bytte — aldri på handlelisten",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicehefte (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "oppføringer",
|
||||
"ref_number": "Referansenummer",
|
||||
"print_options_title": "Skriv ut servicehefte",
|
||||
"print_layout": "Oppsett",
|
||||
"print_layout_chronological": "Kronologisk",
|
||||
"print_layout_by_task": "Per oppgave",
|
||||
"print_include": "Ta med",
|
||||
"print_inc_readings": "Avlesninger",
|
||||
"print_inc_parts": "Brukte deler",
|
||||
"print_inc_photos": "Bilder",
|
||||
"print_inc_documents": "Tilknyttede dokumenter",
|
||||
"print_inc_checklist": "Sjekkliste",
|
||||
"print_inc_notes": "Notater",
|
||||
"print_inc_costs": "Kostnad og varighet",
|
||||
"print_inc_person": "Utført av",
|
||||
"print_inc_refs": "Referansenumre",
|
||||
"print_inc_qr": "QR-kode per oppgave",
|
||||
"print_button": "Skriv ut",
|
||||
"report_scan_hint": "Skann for å åpne oppgaven",
|
||||
"completed_by": "Utført av",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: ongeldige waarde",
|
||||
"feat_schedule_time": "Tijd-van-dag-planning",
|
||||
"feat_schedule_time_desc": "Taken vervallen op een specifieke tijd in plaats van middernacht.",
|
||||
"schedule_time_optional": "Vervaldagstijd (optioneel)",
|
||||
"schedule_time_toggle": "Vervalt op een vast tijdstip",
|
||||
"schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.",
|
||||
"at_time": "om",
|
||||
"notes_optional": "Notities (optioneel)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Geen tag —",
|
||||
"dashboard": "Dashboard",
|
||||
"tab_today": "Vandaag",
|
||||
"palette_placeholder": "Objecten en taken zoeken…",
|
||||
"palette_placeholder": "Objecten, taken, onderdelen, documenten, notities zoeken…",
|
||||
"palette_no_results": "Geen resultaten",
|
||||
"palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten",
|
||||
"palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten · / opent de zoekfunctie overal",
|
||||
"search_open": "Zoeken",
|
||||
"search_group_parts": "Onderdelen",
|
||||
"search_group_content": "In documenten",
|
||||
"search_group_history": "Notities in historie",
|
||||
"search_page": "Pagina {page}",
|
||||
"search_empty_hint": "Typ om objecten, taken, onderdelen, documenten en notities te doorzoeken",
|
||||
"search_searching": "Documenten en notities doorzoeken…",
|
||||
"search_index_status": "Volledige tekst zoeken: {indexed} van {total} bestanden geïndexeerd · {no_text} zonder tekstlaag · {pending} in wachtrij",
|
||||
"today_all_caught_up": "Helemaal bij! Niets te doen deze week.",
|
||||
"today_overdue": "Achterstallig",
|
||||
"today_due_today": "Vandaag te doen",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Drempel „verbruiksartikel laag“ (%)",
|
||||
"settings_battery_low_percent": "Drempel „batterij laag“ (%)",
|
||||
"settings_thresholds_hint": "Standaardwaarden voor nieuwe Voorgestelde installaties (resterend percentage) en voor vlootbatterijen zonder eigen Battery Notes-drempel. Bestaande taken behouden hun waarde.",
|
||||
"settings_value_out_of_range": "De waarde moet tussen {min} en {max} liggen",
|
||||
"bn_summary": "{name}: {pct} % voor {n} batterijen",
|
||||
"bn_floor_decides": "Jouw drempel van {floor} % beslist voor allemaal; per batterij telt de hoogste van de twee drempels.",
|
||||
"bn_above_floor": "Dat ligt boven jouw drempel van {floor} %, dus {name} beslist voor alle genoteerde batterijen.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testmelding verzonden",
|
||||
"test_notification_failed": "Testmelding mislukt",
|
||||
"notify_per_person": "Bezorging per persoon",
|
||||
"member_avatars": "Avatars van leden",
|
||||
"member_avatars_hint": "Wordt naast toegewezen taken getoond — initialen in de kleur van het lid. Standaard uit de naam; kies eigen initialen of een kleur om leden uit elkaar te houden.",
|
||||
"member_initials": "Initialen",
|
||||
"member_color": "Kleur",
|
||||
"member_avatar_reset": "Herstellen",
|
||||
"notify_no_own_device": "Geen eigen apparaat — gebruikt de huishoudelijke dienst",
|
||||
"settings_notify_due_soon": "Melding bij bijna verlopen",
|
||||
"settings_notify_overdue": "Melding bij achterstallig",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Groeperen op",
|
||||
"state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.",
|
||||
"target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).",
|
||||
"state_latch_help": "Bij 1 overgang werkt de trigger als vergrendeling: hij herstelt zodra de entiteit de Naar-status verlaat — of, als alleen een Van-status is ingesteld, zodra ze daarnaar terugkeert.",
|
||||
"for_minutes_state_help": "0 telt elke wijziging direct. Met minuten moet de nieuwe status eerst zo lang aanhouden — kort flikkeren triggert dan niet en telt niet mee.",
|
||||
"qr_print_title": "QR-codes afdrukken",
|
||||
"qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" verwijderen?",
|
||||
"doc_too_large": "Bestand is te groot (max. 25 MB).",
|
||||
"doc_upload_failed": "Uploaden mislukt.",
|
||||
"completion_photo_optional": "Voltooiingsfoto (optioneel)",
|
||||
"add_photo": "Foto toevoegen",
|
||||
"completion_photos_optional": "Foto's bij afronding (optioneel)",
|
||||
"completion_photos": "Foto's bij afronding",
|
||||
"choose_photos": "Foto's kiezen",
|
||||
"choose_photo": "Foto kiezen",
|
||||
"photos_android_hint": "De Android-app geeft één foto per keuze door — elke keuze wordt toegevoegd.",
|
||||
"add_photos": "Foto's toevoegen",
|
||||
"photos_limit": "Maximaal {max} foto's per afronding",
|
||||
"history_edit_photos_hint": "Een hier verwijderde foto blijft bewaard in de documenten van het object.",
|
||||
"uploading": "Uploaden…",
|
||||
"remove": "Weghalen",
|
||||
"doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wordt naast de vastgelegde waarde getoond bij het afronden van deze taak.",
|
||||
"reading_value_label": "Meterstand",
|
||||
"reading_label": "Meting",
|
||||
"readings_section": "Meterstanden",
|
||||
"readings_hint": "Meerdere benoemde waarden per afronding — één regel per meter. Leeg laten voor één enkele waarde.",
|
||||
"reading_name_label": "Naam meterstand",
|
||||
"reading_unit_short": "Eenheid",
|
||||
"reading_add": "Meterstand toevoegen",
|
||||
"reading_duplicate_name": "Naam al in gebruik",
|
||||
"reading_last": "laatst: {value}",
|
||||
"reading_below_last": "Lager dan de vorige meterstand ({value})",
|
||||
"settings_templates_label": "Sjabloongalerij",
|
||||
"settings_templates_hint": "Vink sjablonen uit die je nooit nodig hebt — ze verdwijnen uit de \"Vanuit sjabloon\"-keuzes (paneel en config flow). Verder verandert er niets; altijd weer in te schakelen.",
|
||||
"worksheet": "Werkblad",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Eigen meldingsregel",
|
||||
"settings_notify_rule_hint": "Elke melding vuurt ook het event maintenance_supporter_notification af (soort, status, object, taak, referentienummers, prioriteit, deep link, payload). Stuur het door met een automatisering — bijv. naar Ticker, Telegram of Pushover.",
|
||||
"settings_notify_event_only": "Alleen het event afvuren, zelf niets verzenden",
|
||||
"settings_notify_event_only_hint": "Je automatisering verzorgt de volledige bezorging. Herhaalintervallen, stille uren en het daglimiet bepalen nog steeds wanneer het event afgaat.",
|
||||
"settings_notify_extra_data": "Extra meldingsgegevens (sjabloon)",
|
||||
"settings_notify_extra_data_hint": "JSON of YAML, Jinja toegestaan; wordt samengevoegd met de data van de notify-aanroep (jouw sleutels winnen). Variabelen: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test met de knop Test verzenden.",
|
||||
"card_saved_view": "Opgeslagen weergave",
|
||||
"card_saved_view_none": "Geen",
|
||||
"card_saved_view_help": "Past de status-, gebruikers- en labelfilters van de weergave toe bovenop de bovenstaande filters. Sortering en groepering van de weergave zijn paneelweergave-instellingen en gelden niet voor de kaart.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Alles gemarkeerd als vervangen",
|
||||
"battery_fleet_mark_one": "Deze batterij als vervangen markeren",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Geen sensor",
|
||||
"battery_fleet_trigger_lost": "De sensortrigger van deze taak is verloren gegaan — deze wordt niet geactiveerd of automatisch voltooid.",
|
||||
"battery_fleet_repair": "Repareren",
|
||||
"battery_fleet_exclude": "Uitsluiten van de vloot",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Kies een batterijsensor die de automatische detectie heeft gemist — hij verschijnt direct in de lijst.",
|
||||
"battery_fleet_track_self": "Zelfopladende batterijen volgen",
|
||||
"battery_fleet_track_self_hint": "Telefoons, stofzuigers en andere zelfopladende apparaten verschijnen als oplaadbaar — bij een lage stand wordt om opladen gevraagd, nooit om nieuwe batterijen.",
|
||||
"battery_fleet_due_without_sensor": "Een verstreken prognose als aan de beurt beschouwen als een batterij geen sensor heeft",
|
||||
"battery_fleet_due_without_sensor_hint": "Sommige apparaten melden helemaal geen batterijniveau — Battery Notes kent alleen het type en de laatste vervanging. Zodra de gebruikelijke levensduur is verstreken, telt zo'n batterij als leeg en herinnert de vloottaak je eraan; als vervangen markeren zet de prognose terug.",
|
||||
"battery_fleet_status_low": "Laag",
|
||||
"battery_fleet_status_soon": "Binnenkort",
|
||||
"battery_fleet_status_ok": "In orde",
|
||||
"battery_fleet_status_due": "Aan de beurt",
|
||||
"battery_fleet_predicted_on": "Verwacht rond {date}",
|
||||
"battery_fleet_predicted_trend": "Voorspeld uit de ontladingstrend van deze batterij: rond {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Oplaadbaar: opladen in plaats van vervangen — nooit op het boodschappenlijstje",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Onderhoudsboekje (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "items",
|
||||
"ref_number": "Referentienummer",
|
||||
"print_options_title": "Serviceoverzicht afdrukken",
|
||||
"print_layout": "Indeling",
|
||||
"print_layout_chronological": "Chronologisch",
|
||||
"print_layout_by_task": "Per taak",
|
||||
"print_include": "Opnemen",
|
||||
"print_inc_readings": "Meterstanden",
|
||||
"print_inc_parts": "Gebruikte onderdelen",
|
||||
"print_inc_photos": "Foto's",
|
||||
"print_inc_documents": "Gekoppelde documenten",
|
||||
"print_inc_checklist": "Controlelijst",
|
||||
"print_inc_notes": "Notities",
|
||||
"print_inc_costs": "Kosten en duur",
|
||||
"print_inc_person": "Uitgevoerd door",
|
||||
"print_inc_refs": "Referentienummers",
|
||||
"print_inc_qr": "QR-code per taak",
|
||||
"print_button": "Afdrukken",
|
||||
"report_scan_hint": "Scan om de taak te openen",
|
||||
"completed_by": "Voltooid door",
|
||||
"date_from": "Van",
|
||||
"date_to": "Tot",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: nieprawidłowa wartość",
|
||||
"feat_schedule_time": "Harmonogram według pory dnia",
|
||||
"feat_schedule_time_desc": "Zadania stają się zaległe o określonej porze dnia zamiast o północy.",
|
||||
"schedule_time_optional": "Termin o godzinie (opcjonalne)",
|
||||
"schedule_time_toggle": "Termin o określonej godzinie",
|
||||
"schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.",
|
||||
"at_time": "o",
|
||||
"notes_optional": "Notatki (opcjonalne)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Brak tagu —",
|
||||
"dashboard": "Pulpit",
|
||||
"tab_today": "Dziś",
|
||||
"palette_placeholder": "Szukaj obiektów i zadań…",
|
||||
"palette_placeholder": "Szukaj obiektów, zadań, części, dokumentów, notatek…",
|
||||
"palette_no_results": "Brak wyników",
|
||||
"palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij",
|
||||
"palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij · / otwiera wyszukiwanie wszędzie",
|
||||
"search_open": "Szukaj",
|
||||
"search_group_parts": "Części",
|
||||
"search_group_content": "W dokumentach",
|
||||
"search_group_history": "Notatki z historii",
|
||||
"search_page": "Strona {page}",
|
||||
"search_empty_hint": "Wpisz tekst, aby przeszukać obiekty, zadania, części, dokumenty i notatki",
|
||||
"search_searching": "Przeszukiwanie dokumentów i notatek…",
|
||||
"search_index_status": "Wyszukiwanie pełnotekstowe: {indexed} z {total} plików zindeksowanych · {no_text} bez warstwy tekstu · {pending} oczekujących",
|
||||
"today_all_caught_up": "Wszystko zrobione! Nic w tym tygodniu.",
|
||||
"today_overdue": "Zaległe",
|
||||
"today_due_today": "Na dziś",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Próg „materiał eksploatacyjny niski” (%)",
|
||||
"settings_battery_low_percent": "Próg „bateria słaba” (%)",
|
||||
"settings_thresholds_hint": "Wartości domyślne dla nowych Sugerowanych konfiguracji (procent pozostały) i dla baterii floty bez własnego progu Battery Notes. Istniejące zadania zachowują swoją wartość.",
|
||||
"settings_value_out_of_range": "Wartość musi mieścić się w zakresie od {min} do {max}",
|
||||
"bn_summary": "{name}: {pct} % dla {n} baterii",
|
||||
"bn_floor_decides": "Twój próg {floor} % decyduje dla wszystkich; dla każdej baterii liczy się wyższy z dwóch progów.",
|
||||
"bn_above_floor": "To powyżej twojego progu {floor} %, więc o wszystkich odnotowanych bateriach decyduje {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Powiadomienie testowe wysłane",
|
||||
"test_notification_failed": "Powiadomienie testowe nie powiodło się",
|
||||
"notify_per_person": "Dostarczanie dla każdej osoby",
|
||||
"member_avatars": "Awatary domowników",
|
||||
"member_avatars_hint": "Wyświetlane obok przypisanych zadań — inicjały w kolorze domownika. Domyślnie z imienia; ustaw własne inicjały lub wybierz kolor, aby odróżnić domowników.",
|
||||
"member_initials": "Inicjały",
|
||||
"member_color": "Kolor",
|
||||
"member_avatar_reset": "Resetuj",
|
||||
"notify_no_own_device": "Brak własnego urządzenia — używa usługi domowej",
|
||||
"settings_notify_due_soon": "Powiadom gdy wkrótce",
|
||||
"settings_notify_overdue": "Powiadom gdy zaległe",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Grupuj wg",
|
||||
"state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.",
|
||||
"target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).",
|
||||
"state_latch_help": "Przy 1 przejściu wyzwalacz działa jak zatrzask: resetuje się, gdy encja opuści stan docelowy — lub, gdy ustawiono tylko stan początkowy, gdy do niego wróci.",
|
||||
"for_minutes_state_help": "0 liczy każdą zmianę od razu. Z minutami nowy stan musi się najpierw utrzymać — krótkie mignięcia wtedy nie wyzwalają i nie liczą się.",
|
||||
"qr_print_title": "Drukuj kody QR",
|
||||
"qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Usunąć „{name}”?",
|
||||
"doc_too_large": "Plik jest za duży (maks. 25 MB).",
|
||||
"doc_upload_failed": "Przesyłanie nie powiodło się.",
|
||||
"completion_photo_optional": "Zdjęcie ukończenia (opcjonalnie)",
|
||||
"add_photo": "Dodaj zdjęcie",
|
||||
"completion_photos_optional": "Zdjęcia z wykonania (opcjonalnie)",
|
||||
"completion_photos": "Zdjęcia z wykonania",
|
||||
"choose_photos": "Wybierz zdjęcia",
|
||||
"choose_photo": "Wybierz zdjęcie",
|
||||
"photos_android_hint": "Aplikacja na Androida przekazuje jedno zdjęcie na wybór — każdy wybór jest dodawany.",
|
||||
"add_photos": "Dodaj zdjęcia",
|
||||
"photos_limit": "Maksymalnie {max} zdjęć na wykonanie",
|
||||
"history_edit_photos_hint": "Zdjęcie usunięte tutaj pozostaje w dokumentach obiektu.",
|
||||
"uploading": "Przesyłanie…",
|
||||
"remove": "Usuń",
|
||||
"doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Wyświetlana obok zarejestrowanej wartości przy ukończeniu tego zadania.",
|
||||
"reading_value_label": "Wartość odczytu",
|
||||
"reading_label": "Odczyt",
|
||||
"readings_section": "Odczyty",
|
||||
"readings_hint": "Kilka nazwanych wartości na wykonanie — jeden wiersz na licznik. Zostaw puste, aby zapisać jedną wartość.",
|
||||
"reading_name_label": "Nazwa odczytu",
|
||||
"reading_unit_short": "Jednostka",
|
||||
"reading_add": "Dodaj odczyt",
|
||||
"reading_duplicate_name": "Nazwa już użyta",
|
||||
"reading_last": "ostatnio: {value}",
|
||||
"reading_below_last": "Mniej niż ostatni odczyt ({value})",
|
||||
"settings_templates_label": "Galeria szablonów",
|
||||
"settings_templates_hint": "Odznacz szablony, których nigdy nie użyjesz — znikną z wyboru „Z szablonu\" (panel i config flow). Nic więcej się nie zmienia; można je włączyć ponownie w każdej chwili.",
|
||||
"worksheet": "Karta pracy",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Własna reguła powiadomień",
|
||||
"settings_notify_rule_hint": "Każde powiadomienie wyzwala dodatkowo zdarzenie maintenance_supporter_notification (rodzaj, status, obiekt, zadanie, numery referencyjne, priorytet, link, dane). Przekieruj je automatyzacją — np. do Tickera, Telegrama lub Pushover.",
|
||||
"settings_notify_event_only": "Tylko zdarzenie, nic nie wysyłaj",
|
||||
"settings_notify_event_only_hint": "Twoja automatyzacja odpowiada za całe dostarczanie. Odstępy powtórzeń, ciche godziny i dzienny limit nadal decydują, kiedy zdarzenie się wyzwala.",
|
||||
"settings_notify_extra_data": "Dodatkowe dane powiadomienia (szablon)",
|
||||
"settings_notify_extra_data_hint": "JSON lub YAML, Jinja dozwolone; scalane z data wywołania notify (Twoje klucze wygrywają). Zmienne: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Sprawdź przyciskiem Wyślij test.",
|
||||
"card_saved_view": "Zapisany widok",
|
||||
"card_saved_view_none": "Brak",
|
||||
"card_saved_view_help": "Stosuje filtry statusu, użytkownika i etykiety widoku oprócz powyższych filtrów. Sortowanie i grupowanie widoku to ustawienia wyświetlania panelu i nie są stosowane na karcie.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Oznacz wszystkie jako wymienione",
|
||||
"battery_fleet_mark_one": "Oznacz tę baterię jako wymienioną",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Brak czujnika",
|
||||
"battery_fleet_trigger_lost": "Wyzwalacz czujnika tego zadania został utracony — nie uruchomi się ani nie zakończy automatycznie.",
|
||||
"battery_fleet_repair": "Napraw",
|
||||
"battery_fleet_exclude": "Wyklucz z floty",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Wybierz czujnik baterii pominięty przez automatyczne wykrywanie — od razu trafi na listę.",
|
||||
"battery_fleet_track_self": "Śledź baterie samoładujące",
|
||||
"battery_fleet_track_self_hint": "Telefony, odkurzacze i inne samoładujące się urządzenia pojawiają się jako akumulatory — przy niskim poziomie proszą o ładowanie, nigdy o nowe baterie.",
|
||||
"battery_fleet_due_without_sensor": "Traktuj przekroczoną prognozę jako termin wymiany, gdy bateria nie ma czujnika",
|
||||
"battery_fleet_due_without_sensor_hint": "Niektóre urządzenia w ogóle nie zgłaszają poziomu baterii — Battery Notes zna tylko typ i ostatnią wymianę. Po upływie typowej żywotności taka bateria liczy się jako słaba i zadanie floty przypomni ci o tym; oznaczenie jako wymienionej resetuje prognozę.",
|
||||
"battery_fleet_status_low": "Niski",
|
||||
"battery_fleet_status_soon": "Wkrótce",
|
||||
"battery_fleet_status_ok": "W porządku",
|
||||
"battery_fleet_status_due": "Do wymiany",
|
||||
"battery_fleet_predicted_on": "Przewidywane około {date}",
|
||||
"battery_fleet_predicted_trend": "Prognoza na podstawie trendu rozładowania tej baterii: około {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Akumulator: ładowanie zamiast wymiany — nigdy na liście zakupów",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Książka serwisowa (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "wpisów",
|
||||
"ref_number": "Numer referencyjny",
|
||||
"print_options_title": "Drukuj książkę serwisową",
|
||||
"print_layout": "Układ",
|
||||
"print_layout_chronological": "Chronologicznie",
|
||||
"print_layout_by_task": "Według zadania",
|
||||
"print_include": "Uwzględnij",
|
||||
"print_inc_readings": "Odczyty",
|
||||
"print_inc_parts": "Zużyte części",
|
||||
"print_inc_photos": "Zdjęcia",
|
||||
"print_inc_documents": "Powiązane dokumenty",
|
||||
"print_inc_checklist": "Lista kontrolna",
|
||||
"print_inc_notes": "Notatki",
|
||||
"print_inc_costs": "Koszt i czas",
|
||||
"print_inc_person": "Wykonał",
|
||||
"print_inc_refs": "Numery referencyjne",
|
||||
"print_inc_qr": "Kod QR dla każdego zadania",
|
||||
"print_button": "Drukuj",
|
||||
"report_scan_hint": "Zeskanuj, aby otworzyć zadanie",
|
||||
"completed_by": "Wykonane przez",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Agendamento por horário",
|
||||
"feat_schedule_time_desc": "As tarefas ficam atrasadas em um horário específico do dia, em vez de à meia-noite.",
|
||||
"schedule_time_optional": "Vence às (opcional)",
|
||||
"schedule_time_toggle": "Vence em um horário específico",
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário do HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Observações (opcional)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Sem tag —",
|
||||
"dashboard": "Painel",
|
||||
"tab_today": "Hoje",
|
||||
"palette_placeholder": "Pesquisar objetos e tarefas…",
|
||||
"palette_placeholder": "Pesquisar objetos, tarefas, peças, documentos, notas…",
|
||||
"palette_no_results": "Nenhum resultado",
|
||||
"palette_hint": "↑↓ para navegar · Enter para abrir · Esc para fechar",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar · / abre a pesquisa em qualquer lugar",
|
||||
"search_open": "Pesquisar",
|
||||
"search_group_parts": "Peças",
|
||||
"search_group_content": "Nos documentos",
|
||||
"search_group_history": "Notas do histórico",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Digite para pesquisar objetos, tarefas, peças, documentos e notas",
|
||||
"search_searching": "Pesquisando documentos e notas…",
|
||||
"search_index_status": "Busca de texto completo: {indexed} de {total} arquivos indexados · {no_text} sem camada de texto · {pending} pendentes",
|
||||
"today_all_caught_up": "Tudo em dia! Nada vence esta semana.",
|
||||
"today_overdue": "Atrasadas",
|
||||
"today_due_today": "Vencem hoje",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "Limite „consumível baixo“ (%)",
|
||||
"settings_battery_low_percent": "Limite „pilha fraca“ (%)",
|
||||
"settings_thresholds_hint": "Padrões para novas Configurações sugeridas (porcentagem restante) e para pilhas da frota sem limite próprio do Battery Notes. As tarefas existentes mantêm seu valor.",
|
||||
"settings_value_out_of_range": "O valor deve estar entre {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilhas",
|
||||
"bn_floor_decides": "Seu limite de {floor} % decide para todas; por pilha vale o maior dos dois limites.",
|
||||
"bn_above_floor": "Está acima do seu limite de {floor} %, então o {name} decide para todas as pilhas anotadas.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"member_avatars": "Avatares dos membros",
|
||||
"member_avatars_hint": "Exibido ao lado das tarefas atribuídas — iniciais na cor do membro. O padrão vem do nome; defina iniciais próprias ou escolha uma cor para distinguir os membros.",
|
||||
"member_initials": "Iniciais",
|
||||
"member_color": "Cor",
|
||||
"member_avatar_reset": "Redefinir",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando vencer em breve",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado do HA (geralmente minúsculo, ex.: \"on\"/\"off\"). Maiúsculas e minúsculas são normalizadas ao salvar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes de o gatilho disparar (padrão: 1).",
|
||||
"state_latch_help": "Com 1 transição o gatilho é uma trava: ele se recupera quando a entidade sai do estado de destino — ou, se houver apenas um estado de origem, quando volta a ele.",
|
||||
"for_minutes_state_help": "0 conta cada mudança imediatamente. Com minutos, o novo estado deve se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gere uma página imprimível de códigos QR para recortar e colar nos seus equipamentos.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "Excluir \"{name}\"?",
|
||||
"doc_too_large": "O arquivo é muito grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Falha no envio.",
|
||||
"completion_photo_optional": "Foto da conclusão (opcional)",
|
||||
"add_photo": "Adicionar foto",
|
||||
"completion_photos_optional": "Fotos de conclusão (opcional)",
|
||||
"completion_photos": "Fotos de conclusão",
|
||||
"choose_photos": "Escolher fotos",
|
||||
"choose_photo": "Escolher foto",
|
||||
"photos_android_hint": "O app Android entrega uma foto por seleção — cada seleção é adicionada.",
|
||||
"add_photos": "Adicionar fotos",
|
||||
"photos_limit": "Até {max} fotos por conclusão",
|
||||
"history_edit_photos_hint": "Uma foto removida aqui continua nos documentos do objeto.",
|
||||
"uploading": "Enviando…",
|
||||
"remove": "Remover",
|
||||
"doc_deduped": "Já armazenado em outro lugar — compartilhado, sem uso de espaço extra.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Exibida ao lado do valor registrado ao concluir esta tarefa.",
|
||||
"reading_value_label": "Valor da leitura",
|
||||
"reading_label": "Leitura",
|
||||
"readings_section": "Leituras",
|
||||
"readings_hint": "Vários valores nomeados por conclusão — uma linha por medidor. Deixe vazio para um único valor.",
|
||||
"reading_name_label": "Nome da leitura",
|
||||
"reading_unit_short": "Unidade",
|
||||
"reading_add": "Adicionar leitura",
|
||||
"reading_duplicate_name": "Nome já em uso",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Menor que a última leitura ({value})",
|
||||
"settings_templates_label": "Galeria de modelos",
|
||||
"settings_templates_hint": "Desmarque os modelos de que você nunca precisará — eles somem dos seletores \"A partir de modelo\" (painel e fluxo de configuração). Nada mais muda; você pode reativá-los a qualquer momento.",
|
||||
"worksheet": "Folha de trabalho",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Sua regra de notificação",
|
||||
"settings_notify_rule_hint": "Cada notificação também dispara o evento maintenance_supporter_notification (tipo, status, objeto, tarefa, números de referência, prioridade, link, payload). Roteie-o com uma automação — por ex. para Ticker, Telegram ou Pushover.",
|
||||
"settings_notify_event_only": "Apenas disparar o evento, não enviar nada",
|
||||
"settings_notify_event_only_hint": "Sua automação cuida de toda a entrega. Intervalos de repetição, horário silencioso e o limite diário continuam valendo para quando o evento dispara.",
|
||||
"settings_notify_extra_data": "Dados extras de notificação (modelo)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja permitido; mesclado nos data da chamada notify (suas chaves vencem). Variáveis: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Teste com o botão Enviar teste.",
|
||||
"card_saved_view": "Visualização salva",
|
||||
"card_saved_view_none": "Nenhuma",
|
||||
"card_saved_view_help": "Aplica os filtros de status, usuário e etiqueta da visualização por cima dos filtros acima. A ordenação e o agrupamento da visualização são configurações de exibição do painel e não se aplicam ao cartão.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas como trocadas",
|
||||
"battery_fleet_mark_one": "Marcar esta bateria como trocada",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Sem sensor",
|
||||
"battery_fleet_trigger_lost": "O gatilho do sensor desta tarefa foi perdido — ela não será acionada nem concluída automaticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Remover da frota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a detecção automática não encontrou — ele entra na lista imediatamente.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem uma recarga, nunca pilhas novas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar uma previsão vencida como pendente quando uma bateria não tem sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Alguns dispositivos não informam nenhum nível de bateria — o Battery Notes só conhece o tipo e a última troca. Passada a vida útil típica, essa bateria conta como fraca e a tarefa da frota lembra você; marcá-la como trocada reinicia a previsão.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_status_due": "Vencida",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta bateria: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: recarregue em vez de substituir — nunca na lista de compras",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registro de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registros",
|
||||
"ref_number": "Número de referência",
|
||||
"print_options_title": "Imprimir o registro de serviço",
|
||||
"print_layout": "Disposição",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarefa",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Leituras",
|
||||
"print_inc_parts": "Peças usadas",
|
||||
"print_inc_photos": "Fotos",
|
||||
"print_inc_documents": "Documentos vinculados",
|
||||
"print_inc_checklist": "Lista de verificação",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Custo e duração",
|
||||
"print_inc_person": "Concluído por",
|
||||
"print_inc_refs": "Números de referência",
|
||||
"print_inc_qr": "Código QR por tarefa",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Escaneie para abrir a tarefa",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: valor inválido",
|
||||
"feat_schedule_time": "Agendamento por hora",
|
||||
"feat_schedule_time_desc": "Tarefas vencem em um horário específico em vez de meia-noite.",
|
||||
"schedule_time_optional": "Vence às (opcional)",
|
||||
"schedule_time_toggle": "Vence a uma hora específica",
|
||||
"schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.",
|
||||
"at_time": "às",
|
||||
"notes_optional": "Notas (opcional)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Sem etiqueta —",
|
||||
"dashboard": "Painel",
|
||||
"tab_today": "Hoje",
|
||||
"palette_placeholder": "Pesquisar objetos e tarefas…",
|
||||
"palette_placeholder": "Pesquisar objetos, tarefas, peças, documentos, notas…",
|
||||
"palette_no_results": "Sem resultados",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar",
|
||||
"palette_hint": "↑↓ navegar · Enter abrir · Esc fechar · / abre a pesquisa em qualquer lado",
|
||||
"search_open": "Pesquisar",
|
||||
"search_group_parts": "Peças",
|
||||
"search_group_content": "Nos documentos",
|
||||
"search_group_history": "Notas do histórico",
|
||||
"search_page": "Página {page}",
|
||||
"search_empty_hint": "Escreva para pesquisar objetos, tarefas, peças, documentos e notas",
|
||||
"search_searching": "A pesquisar documentos e notas…",
|
||||
"search_index_status": "Pesquisa de texto integral: {indexed} de {total} ficheiros indexados · {no_text} sem camada de texto · {pending} pendentes",
|
||||
"today_all_caught_up": "Tudo em dia! Nada para esta semana.",
|
||||
"today_overdue": "Atrasadas",
|
||||
"today_due_today": "Vence hoje",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Limiar „consumível baixo“ (%)",
|
||||
"settings_battery_low_percent": "Limiar „pilha fraca“ (%)",
|
||||
"settings_thresholds_hint": "Predefinições para novas Configurações sugeridas (percentagem restante) e para pilhas da frota sem limiar Battery Notes próprio. As tarefas existentes mantêm o seu valor.",
|
||||
"settings_value_out_of_range": "O valor tem de estar entre {min} e {max}",
|
||||
"bn_summary": "{name}: {pct} % para {n} pilhas",
|
||||
"bn_floor_decides": "O teu limiar de {floor} % decide para todas; por pilha conta o mais alto dos dois limiares.",
|
||||
"bn_above_floor": "Está acima do teu limiar de {floor} %, por isso o {name} decide para todas as pilhas anotadas.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Notificação de teste enviada",
|
||||
"test_notification_failed": "Falha na notificação de teste",
|
||||
"notify_per_person": "Entrega por pessoa",
|
||||
"member_avatars": "Avatares dos membros",
|
||||
"member_avatars_hint": "Mostrado junto às tarefas atribuídas — iniciais na cor do membro. Por defeito vêm do nome; defina iniciais próprias ou escolha uma cor para distinguir os membros.",
|
||||
"member_initials": "Iniciais",
|
||||
"member_color": "Cor",
|
||||
"member_avatar_reset": "Repor",
|
||||
"notify_no_own_device": "Sem dispositivo próprio — usa o serviço da casa",
|
||||
"settings_notify_due_soon": "Notificar quando próxima",
|
||||
"settings_notify_overdue": "Notificar quando atrasada",
|
||||
@@ -533,6 +547,7 @@
|
||||
"group_by_label": "Agrupar por",
|
||||
"state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.",
|
||||
"target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).",
|
||||
"state_latch_help": "Com 1 transição o gatilho é um trinco: recupera quando a entidade sai do estado de destino — ou, se só houver um estado de origem, quando volta a ele.",
|
||||
"for_minutes_state_help": "0 conta cada mudança de imediato. Com minutos, o novo estado tem de se manter primeiro — uma oscilação breve então não dispara nem conta.",
|
||||
"qr_print_title": "Imprimir códigos QR",
|
||||
"qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Eliminar «{name}»?",
|
||||
"doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).",
|
||||
"doc_upload_failed": "Falha no carregamento.",
|
||||
"completion_photo_optional": "Foto de conclusão (opcional)",
|
||||
"add_photo": "Adicionar foto",
|
||||
"completion_photos_optional": "Fotos de conclusão (opcional)",
|
||||
"completion_photos": "Fotos de conclusão",
|
||||
"choose_photos": "Escolher fotos",
|
||||
"choose_photo": "Escolher foto",
|
||||
"photos_android_hint": "A app Android entrega uma foto por seleção — cada seleção é adicionada.",
|
||||
"add_photos": "Adicionar fotos",
|
||||
"photos_limit": "Até {max} fotos por conclusão",
|
||||
"history_edit_photos_hint": "Uma foto removida aqui continua nos documentos do objeto.",
|
||||
"uploading": "A enviar…",
|
||||
"remove": "Remover",
|
||||
"doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Mostrada ao lado do valor registado ao concluir esta tarefa.",
|
||||
"reading_value_label": "Valor de leitura",
|
||||
"reading_label": "Leitura",
|
||||
"readings_section": "Leituras",
|
||||
"readings_hint": "Vários valores nomeados por conclusão — uma linha por contador. Deixe vazio para um único valor.",
|
||||
"reading_name_label": "Nome da leitura",
|
||||
"reading_unit_short": "Unidade",
|
||||
"reading_add": "Adicionar leitura",
|
||||
"reading_duplicate_name": "Nome já utilizado",
|
||||
"reading_last": "último: {value}",
|
||||
"reading_below_last": "Inferior à última leitura ({value})",
|
||||
"settings_templates_label": "Galeria de modelos",
|
||||
"settings_templates_hint": "Desmarque modelos de que nunca precisará — desaparecem dos seletores \"A partir de modelo\" (painel e config flow). Nada mais muda; reativáveis a qualquer momento.",
|
||||
"worksheet": "Folha de trabalho",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "A sua regra de notificação",
|
||||
"settings_notify_rule_hint": "Cada notificação dispara também o evento maintenance_supporter_notification (tipo, estado, objeto, tarefa, números de referência, prioridade, ligação, conteúdo). Encaminhe-o com uma automação — p. ex. para o Ticker, Telegram ou Pushover.",
|
||||
"settings_notify_event_only": "Apenas disparar o evento, não enviar nada",
|
||||
"settings_notify_event_only_hint": "A sua automação trata de toda a entrega. Intervalos de repetição, horas de silêncio e o limite diário continuam a decidir quando o evento dispara.",
|
||||
"settings_notify_extra_data": "Dados de notificação adicionais (modelo)",
|
||||
"settings_notify_extra_data_hint": "JSON ou YAML, Jinja permitido; é fundido nos data da chamada notify (as suas chaves ganham). Variáveis: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Teste com o botão Enviar teste.",
|
||||
"card_saved_view": "Vista guardada",
|
||||
"card_saved_view_none": "Nenhuma",
|
||||
"card_saved_view_help": "Aplica os filtros de estado, utilizador e etiqueta da vista além dos filtros acima. A ordenação e o agrupamento da vista são definições de apresentação do painel e não se aplicam ao cartão.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Marcar todas substituídas",
|
||||
"battery_fleet_mark_one": "Marcar esta bateria substituída",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Sem sensor",
|
||||
"battery_fleet_trigger_lost": "O gatilho de sensor desta tarefa foi perdido — ela não será acionada nem concluída automaticamente.",
|
||||
"battery_fleet_repair": "Reparar",
|
||||
"battery_fleet_exclude": "Excluir da frota",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Escolha um sensor de bateria que a deteção automática não encontrou — entra de imediato na lista.",
|
||||
"battery_fleet_track_self": "Acompanhar baterias de carregamento automático",
|
||||
"battery_fleet_track_self_hint": "Telefones, aspiradores e outros dispositivos que se recarregam sozinhos aparecem como recarregáveis — com carga baixa pedem um carregamento, nunca pilhas novas.",
|
||||
"battery_fleet_due_without_sensor": "Tratar uma previsão ultrapassada como a substituir quando uma pilha não tem sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Alguns dispositivos não reportam qualquer nível de pilha — o Battery Notes só conhece o tipo e a última substituição. Passada a vida útil típica, essa pilha conta como fraca e a tarefa da frota lembra-o; marcá-la como substituída repõe a previsão.",
|
||||
"battery_fleet_status_low": "Fraca",
|
||||
"battery_fleet_status_soon": "Em breve",
|
||||
"battery_fleet_status_ok": "Em bom estado",
|
||||
"battery_fleet_status_due": "A substituir",
|
||||
"battery_fleet_predicted_on": "Previsto por volta de {date}",
|
||||
"battery_fleet_predicted_trend": "Previsto pela tendência de descarga desta pilha: por volta de {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Recarregável: carrega-se em vez de se substituir — nunca na lista de compras",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Registo de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registos",
|
||||
"ref_number": "Número de referência",
|
||||
"print_options_title": "Imprimir o registo de serviço",
|
||||
"print_layout": "Disposição",
|
||||
"print_layout_chronological": "Cronológico",
|
||||
"print_layout_by_task": "Por tarefa",
|
||||
"print_include": "Incluir",
|
||||
"print_inc_readings": "Leituras",
|
||||
"print_inc_parts": "Peças usadas",
|
||||
"print_inc_photos": "Fotografias",
|
||||
"print_inc_documents": "Documentos associados",
|
||||
"print_inc_checklist": "Lista de verificação",
|
||||
"print_inc_notes": "Notas",
|
||||
"print_inc_costs": "Custo e duração",
|
||||
"print_inc_person": "Concluído por",
|
||||
"print_inc_refs": "Números de referência",
|
||||
"print_inc_qr": "Código QR por tarefa",
|
||||
"print_button": "Imprimir",
|
||||
"report_scan_hint": "Digitalize para abrir a tarefa",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: неверное значение",
|
||||
"feat_schedule_time": "Планирование по времени дня",
|
||||
"feat_schedule_time_desc": "Задачи становятся просроченными в определённое время дня, а не в полночь.",
|
||||
"schedule_time_optional": "Срок (необязательно)",
|
||||
"schedule_time_toggle": "Срок в определённое время",
|
||||
"schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.",
|
||||
"at_time": "в",
|
||||
"notes_optional": "Примечания (опционально)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Нет метки —",
|
||||
"dashboard": "Панель",
|
||||
"tab_today": "Сегодня",
|
||||
"palette_placeholder": "Поиск объектов и задач…",
|
||||
"palette_placeholder": "Поиск объектов, задач, запчастей, документов, заметок…",
|
||||
"palette_no_results": "Нет совпадений",
|
||||
"palette_hint": "↑↓ выбор · Enter открыть · Esc закрыть",
|
||||
"palette_hint": "↑↓ навигация · Enter открыть · Esc закрыть · / открывает поиск где угодно",
|
||||
"search_open": "Поиск",
|
||||
"search_group_parts": "Запчасти",
|
||||
"search_group_content": "В документах",
|
||||
"search_group_history": "Заметки в истории",
|
||||
"search_page": "Стр. {page}",
|
||||
"search_empty_hint": "Введите текст для поиска по объектам, задачам, запчастям, документам и заметкам",
|
||||
"search_searching": "Поиск по документам и заметкам…",
|
||||
"search_index_status": "Полнотекстовый поиск: проиндексировано {indexed} из {total} файлов · {no_text} без текстового слоя · {pending} в очереди",
|
||||
"today_all_caught_up": "Всё сделано! На этой неделе ничего нет.",
|
||||
"today_overdue": "Просрочено",
|
||||
"today_due_today": "Сегодня",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Порог «расходник заканчивается» (%)",
|
||||
"settings_battery_low_percent": "Порог «батарея разряжена» (%)",
|
||||
"settings_thresholds_hint": "Значения по умолчанию для новых «Предлагаемых настроек» (остаток в процентах) и для батарей парка без собственного порога Battery Notes. Существующие задачи сохраняют своё значение.",
|
||||
"settings_value_out_of_range": "Значение должно быть от {min} до {max}",
|
||||
"bn_summary": "{name}: {pct} % для {n} батарей",
|
||||
"bn_floor_decides": "Ваш порог {floor} % действует для всех; для каждой батареи учитывается больший из двух порогов.",
|
||||
"bn_above_floor": "Это выше вашего порога {floor} %, поэтому для всех отмеченных батарей решает {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Тестовое уведомление отправлено",
|
||||
"test_notification_failed": "Не удалось отправить тестовое уведомление",
|
||||
"notify_per_person": "Доставка по пользователям",
|
||||
"member_avatars": "Аватары участников",
|
||||
"member_avatars_hint": "Показывается рядом с назначенными задачами — инициалы в цвете участника. По умолчанию берутся из имени; задайте свои инициалы или выберите цвет, чтобы различать участников.",
|
||||
"member_initials": "Инициалы",
|
||||
"member_color": "Цвет",
|
||||
"member_avatar_reset": "Сбросить",
|
||||
"notify_no_own_device": "Нет своего устройства — используется общий сервис",
|
||||
"settings_notify_due_soon": "Уведомлять, когда срок скоро истекает",
|
||||
"settings_notify_overdue": "Уведомлять при просрочке",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Группировать по",
|
||||
"state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.",
|
||||
"target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).",
|
||||
"state_latch_help": "При 1 переходе триггер работает как защёлка: он сбрасывается, когда объект покидает состояние «в» — или, если задано только состояние «из», когда он в него возвращается.",
|
||||
"for_minutes_state_help": "0 — каждое изменение засчитывается сразу. С минутами новое состояние сначала должно удержаться — короткие сбои тогда не срабатывают и не считаются.",
|
||||
"qr_print_title": "Печать QR-кодов",
|
||||
"qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Удалить «{name}»?",
|
||||
"doc_too_large": "Файл слишком большой (макс. 25 МБ).",
|
||||
"doc_upload_failed": "Не удалось загрузить.",
|
||||
"completion_photo_optional": "Фото выполнения (необязательно)",
|
||||
"add_photo": "Добавить фото",
|
||||
"completion_photos_optional": "Фото выполнения (необязательно)",
|
||||
"completion_photos": "Фото выполнения",
|
||||
"choose_photos": "Выбрать фото",
|
||||
"choose_photo": "Выбрать фото",
|
||||
"photos_android_hint": "Приложение для Android передаёт по одному фото за выбор — каждый выбор добавляется.",
|
||||
"add_photos": "Добавить фото",
|
||||
"photos_limit": "До {max} фото на одно выполнение",
|
||||
"history_edit_photos_hint": "Удалённое здесь фото остаётся в документах объекта.",
|
||||
"uploading": "Загрузка…",
|
||||
"remove": "Убрать",
|
||||
"doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Отображается рядом с записанным значением при выполнении этой задачи.",
|
||||
"reading_value_label": "Показание",
|
||||
"reading_label": "Показание",
|
||||
"readings_section": "Показания",
|
||||
"readings_hint": "Несколько именованных значений на одно выполнение — по строке на счётчик. Оставьте пустым для одного значения.",
|
||||
"reading_name_label": "Название показания",
|
||||
"reading_unit_short": "Единица",
|
||||
"reading_add": "Добавить показание",
|
||||
"reading_duplicate_name": "Название уже используется",
|
||||
"reading_last": "последнее: {value}",
|
||||
"reading_below_last": "Меньше предыдущего показания ({value})",
|
||||
"settings_templates_label": "Галерея шаблонов",
|
||||
"settings_templates_hint": "Снимите отметку с шаблонов, которые вам не нужны — они исчезнут из выбора «Из шаблона» (панель и config flow). Больше ничего не меняется; можно включить обратно в любой момент.",
|
||||
"worksheet": "Рабочий лист",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "Уведомлять только для представления",
|
||||
"settings_notify_scope_all": "Все задачи",
|
||||
"settings_notify_scope_hint": "Напоминания отправляют только задачи, совпадающие с фильтрами метки/пользователя выбранного представления. Статус, сортировка и группировка представления здесь игнорируются.",
|
||||
"settings_notify_rule": "Своё правило уведомлений",
|
||||
"settings_notify_rule_hint": "Каждое уведомление дополнительно вызывает событие maintenance_supporter_notification (вид, статус, объект, задача, номера ссылок, приоритет, ссылка, данные). Направьте его автоматизацией — например в Ticker, Telegram или Pushover.",
|
||||
"settings_notify_event_only": "Только событие, ничего не отправлять",
|
||||
"settings_notify_event_only_hint": "Доставку целиком выполняет ваша автоматизация. Интервалы повтора, тихие часы и дневной лимит по-прежнему определяют, когда срабатывает событие.",
|
||||
"settings_notify_extra_data": "Дополнительные данные уведомления (шаблон)",
|
||||
"settings_notify_extra_data_hint": "JSON или YAML, Jinja разрешён; добавляется в data вызова notify (ваши ключи важнее). Переменные: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Проверьте кнопкой «Отправить тест».",
|
||||
"card_saved_view": "Сохранённое представление",
|
||||
"card_saved_view_none": "Нет",
|
||||
"card_saved_view_help": "Применяет фильтры статуса, пользователя и метки представления в дополнение к фильтрам выше. Сортировка и группировка представления — настройки отображения панели и на карточке не применяются.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Отметить все заменёнными",
|
||||
"battery_fleet_mark_one": "Отметить эту батарею заменённой",
|
||||
"battery_fleet_offline": "не в сети",
|
||||
"battery_fleet_no_sensor": "Без датчика",
|
||||
"battery_fleet_trigger_lost": "Сенсорный триггер этой задачи утерян — она не сработает и не завершится автоматически.",
|
||||
"battery_fleet_repair": "Восстановить",
|
||||
"battery_fleet_exclude": "Исключить из парка",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Выберите датчик батареи, который автоматическое обнаружение пропустило — он сразу появится в списке.",
|
||||
"battery_fleet_track_self": "Отслеживать самозаряжающиеся батареи",
|
||||
"battery_fleet_track_self_hint": "Телефоны, пылесосы и другие самозаряжающиеся устройства отображаются как аккумуляторы — при низком заряде нужна зарядка, а не новые батарейки.",
|
||||
"battery_fleet_due_without_sensor": "Считать истёкший прогноз поводом для замены, если у батареи нет датчика",
|
||||
"battery_fleet_due_without_sensor_hint": "Некоторые устройства вообще не сообщают уровень заряда — Battery Notes знает только тип и дату последней замены. Когда типичный срок службы истёк, такая батарея считается разряженной, и задача парка напомнит; отметка «заменена» сбрасывает прогноз.",
|
||||
"battery_fleet_status_low": "Низкий",
|
||||
"battery_fleet_status_soon": "Скоро",
|
||||
"battery_fleet_status_ok": "В норме",
|
||||
"battery_fleet_status_due": "Пора менять",
|
||||
"battery_fleet_predicted_on": "Ожидается примерно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз по тренду разряда этой батареи: примерно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Аккумулятор: заряжается, а не заменяется — никогда не попадает в список покупок",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Сервисная книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записей",
|
||||
"ref_number": "Номер ссылки",
|
||||
"print_options_title": "Печать сервисной книжки",
|
||||
"print_layout": "Структура",
|
||||
"print_layout_chronological": "Хронологически",
|
||||
"print_layout_by_task": "По задачам",
|
||||
"print_include": "Включить",
|
||||
"print_inc_readings": "Показания",
|
||||
"print_inc_parts": "Использованные детали",
|
||||
"print_inc_photos": "Фото",
|
||||
"print_inc_documents": "Связанные документы",
|
||||
"print_inc_checklist": "Чек-лист",
|
||||
"print_inc_notes": "Заметки",
|
||||
"print_inc_costs": "Стоимость и длительность",
|
||||
"print_inc_person": "Выполнил",
|
||||
"print_inc_refs": "Номера ссылок",
|
||||
"print_inc_qr": "QR-код для каждой задачи",
|
||||
"print_button": "Печать",
|
||||
"report_scan_hint": "Отсканируйте, чтобы открыть задачу",
|
||||
"completed_by": "Выполнил",
|
||||
"date_from": "С",
|
||||
"date_to": "По",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: ogiltigt värde",
|
||||
"feat_schedule_time": "Schemaläggning per tid på dygnet",
|
||||
"feat_schedule_time_desc": "Uppgifter blir försenade vid en specifik tid på dygnet istället för midnatt.",
|
||||
"schedule_time_optional": "Förfaller kl. (valfritt)",
|
||||
"schedule_time_toggle": "Förfaller vid en viss tid",
|
||||
"schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.",
|
||||
"at_time": "kl.",
|
||||
"notes_optional": "Anteckningar (valfritt)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Ingen tagg —",
|
||||
"dashboard": "Översikt",
|
||||
"tab_today": "Idag",
|
||||
"palette_placeholder": "Sök objekt och uppgifter…",
|
||||
"palette_placeholder": "Sök objekt, uppgifter, reservdelar, dokument, anteckningar…",
|
||||
"palette_no_results": "Inga träffar",
|
||||
"palette_hint": "↑↓ navigera · Enter öppna · Esc stäng",
|
||||
"palette_hint": "↑↓ navigera · Enter öppna · Esc stäng · / öppnar sökningen överallt",
|
||||
"search_open": "Sök",
|
||||
"search_group_parts": "Reservdelar",
|
||||
"search_group_content": "I dokument",
|
||||
"search_group_history": "Historikanteckningar",
|
||||
"search_page": "Sida {page}",
|
||||
"search_empty_hint": "Skriv för att söka bland objekt, uppgifter, reservdelar, dokument och anteckningar",
|
||||
"search_searching": "Söker i dokument och anteckningar…",
|
||||
"search_index_status": "Fulltextsökning: {indexed} av {total} filer indexerade · {no_text} utan textlager · {pending} väntar",
|
||||
"today_all_caught_up": "Allt klart! Inget den här veckan.",
|
||||
"today_overdue": "Försenade",
|
||||
"today_due_today": "Förfaller idag",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Tröskel „förbrukningsartikel låg“ (%)",
|
||||
"settings_battery_low_percent": "Tröskel „batteri lågt“ (%)",
|
||||
"settings_thresholds_hint": "Standardvärden för nya Föreslagna inställningar (återstående procent) och för flottans batterier utan egen Battery Notes-tröskel. Befintliga uppgifter behåller sitt värde.",
|
||||
"settings_value_out_of_range": "Värdet måste vara mellan {min} och {max}",
|
||||
"bn_summary": "{name}: {pct} % för {n} batterier",
|
||||
"bn_floor_decides": "Din tröskel på {floor} % avgör för alla; per batteri gäller den högre av de två trösklarna.",
|
||||
"bn_above_floor": "Det är över din tröskel på {floor} %, så {name} avgör för alla noterade batterier.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Testnotifikation skickad",
|
||||
"test_notification_failed": "Testnotifikation misslyckades",
|
||||
"notify_per_person": "Leverans per person",
|
||||
"member_avatars": "Medlemmarnas avatarer",
|
||||
"member_avatars_hint": "Visas bredvid tilldelade uppgifter — initialer i medlemmens färg. Standard hämtas från namnet; ange egna initialer eller välj en färg för att skilja medlemmar åt.",
|
||||
"member_initials": "Initialer",
|
||||
"member_color": "Färg",
|
||||
"member_avatar_reset": "Återställ",
|
||||
"notify_no_own_device": "Ingen egen enhet — använder hushållets tjänst",
|
||||
"settings_notify_due_soon": "Notifiera när snart förfallande",
|
||||
"settings_notify_overdue": "Notifiera när försenad",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Gruppera efter",
|
||||
"state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.",
|
||||
"target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).",
|
||||
"state_latch_help": "Med 1 övergång är utlösaren en spärr: den återställs när entiteten lämnar Till-tillståndet — eller, om bara ett Från-tillstånd är satt, när den återgår till det.",
|
||||
"for_minutes_state_help": "0 räknar varje ändring direkt. Med minuter måste det nya tillståndet först hålla i sig — korta blinkningar varken utlöser eller räknas då.",
|
||||
"qr_print_title": "Skriv ut QR-koder",
|
||||
"qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Ta bort \"{name}\"?",
|
||||
"doc_too_large": "Filen är för stor (max 25 MB).",
|
||||
"doc_upload_failed": "Uppladdningen misslyckades.",
|
||||
"completion_photo_optional": "Slutförandefoto (valfritt)",
|
||||
"add_photo": "Lägg till foto",
|
||||
"completion_photos_optional": "Foton vid slutförande (valfritt)",
|
||||
"completion_photos": "Foton vid slutförande",
|
||||
"choose_photos": "Välj foton",
|
||||
"choose_photo": "Välj foto",
|
||||
"photos_android_hint": "Android-appen lämnar över ett foto per val — varje val läggs till.",
|
||||
"add_photos": "Lägg till foton",
|
||||
"photos_limit": "Högst {max} foton per slutförande",
|
||||
"history_edit_photos_hint": "Ett foto som tas bort här finns kvar bland objektets dokument.",
|
||||
"uploading": "Laddar upp…",
|
||||
"remove": "Ta bort",
|
||||
"doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Visas bredvid det registrerade värdet när uppgiften slutförs.",
|
||||
"reading_value_label": "Avläst värde",
|
||||
"reading_label": "Avläsning",
|
||||
"readings_section": "Avläsningar",
|
||||
"readings_hint": "Flera namngivna värden per slutförande — en rad per mätare. Lämna tomt för ett enda värde.",
|
||||
"reading_name_label": "Avläsningens namn",
|
||||
"reading_unit_short": "Enhet",
|
||||
"reading_add": "Lägg till avläsning",
|
||||
"reading_duplicate_name": "Namnet används redan",
|
||||
"reading_last": "senast: {value}",
|
||||
"reading_below_last": "Lägre än senaste avläsningen ({value})",
|
||||
"settings_templates_label": "Mallgalleri",
|
||||
"settings_templates_hint": "Avmarkera mallar du aldrig behöver — de försvinner från \"Från mall\"-väljarna (panel och config flow). Inget annat ändras; kan återaktiveras när som helst.",
|
||||
"worksheet": "Arbetsblad",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Din egen aviseringsregel",
|
||||
"settings_notify_rule_hint": "Varje avisering utlöser även händelsen maintenance_supporter_notification (typ, status, objekt, uppgift, referensnummer, prioritet, länk, nyttolast). Styr den med en automation — t.ex. till Ticker, Telegram eller Pushover.",
|
||||
"settings_notify_event_only": "Utlös bara händelsen, skicka inget själv",
|
||||
"settings_notify_event_only_hint": "Din automation sköter hela leveransen. Upprepningsintervall, tysta timmar och dagstaket avgör fortfarande när händelsen utlöses.",
|
||||
"settings_notify_extra_data": "Extra aviseringsdata (mall)",
|
||||
"settings_notify_extra_data_hint": "JSON eller YAML, Jinja tillåtet; slås ihop med notify-anropets data (dina nycklar vinner). Variabler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Testa med knappen Skicka test.",
|
||||
"card_saved_view": "Sparad vy",
|
||||
"card_saved_view_none": "Ingen",
|
||||
"card_saved_view_help": "Tillämpar vyns status-, användar- och etikettfilter utöver filtren ovan. Vyns sortering och gruppering är panelvisningsinställningar och tillämpas inte på kortet.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Markera alla bytta",
|
||||
"battery_fleet_mark_one": "Markera detta batteri bytt",
|
||||
"battery_fleet_offline": "offline",
|
||||
"battery_fleet_no_sensor": "Ingen sensor",
|
||||
"battery_fleet_trigger_lost": "Uppgiftens sensorutlösare har gått förlorad — den utlöses inte och slutförs inte automatiskt.",
|
||||
"battery_fleet_repair": "Reparera",
|
||||
"battery_fleet_exclude": "Uteslut från flottan",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Välj en batterisensor som den automatiska identifieringen missade — den läggs till i listan direkt.",
|
||||
"battery_fleet_track_self": "Spåra självladdande batterier",
|
||||
"battery_fleet_track_self_hint": "Telefoner, dammsugare och andra självladdande enheter visas som laddningsbara — vid låg nivå begärs laddning, aldrig nya batterier.",
|
||||
"battery_fleet_due_without_sensor": "Behandla en passerad prognos som dags när ett batteri saknar sensor",
|
||||
"battery_fleet_due_without_sensor_hint": "Vissa enheter rapporterar ingen batterinivå alls — Battery Notes känner bara till typ och senaste byte. När den typiska livslängden har passerat räknas ett sådant batteri som lågt och flottuppgiften påminner dig; att markera det som bytt nollställer prognosen.",
|
||||
"battery_fleet_status_low": "Låg",
|
||||
"battery_fleet_status_soon": "Snart",
|
||||
"battery_fleet_status_ok": "I ordning",
|
||||
"battery_fleet_status_due": "Dags",
|
||||
"battery_fleet_predicted_on": "Väntas omkring {date}",
|
||||
"battery_fleet_predicted_trend": "Förutspått utifrån batteriets urladdningstrend: omkring {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Uppladdningsbart: ladda i stället för att byta — aldrig på inköpslistan",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servicebok (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "poster",
|
||||
"ref_number": "Referensnummer",
|
||||
"print_options_title": "Skriv ut servicebok",
|
||||
"print_layout": "Upplägg",
|
||||
"print_layout_chronological": "Kronologiskt",
|
||||
"print_layout_by_task": "Per uppgift",
|
||||
"print_include": "Ta med",
|
||||
"print_inc_readings": "Avläsningar",
|
||||
"print_inc_parts": "Använda delar",
|
||||
"print_inc_photos": "Foton",
|
||||
"print_inc_documents": "Länkade dokument",
|
||||
"print_inc_checklist": "Checklista",
|
||||
"print_inc_notes": "Anteckningar",
|
||||
"print_inc_costs": "Kostnad och tid",
|
||||
"print_inc_person": "Utförd av",
|
||||
"print_inc_refs": "Referensnummer",
|
||||
"print_inc_qr": "QR-kod per uppgift",
|
||||
"print_button": "Skriv ut",
|
||||
"report_scan_hint": "Skanna för att öppna uppgiften",
|
||||
"completed_by": "Utförd av",
|
||||
"date_from": "Från",
|
||||
"date_to": "Till",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: geçersiz değer",
|
||||
"feat_schedule_time": "Gün içi saat zamanlaması",
|
||||
"feat_schedule_time_desc": "Görevler gece yarısı yerine günün belirli bir saatinde gecikmiş olur.",
|
||||
"schedule_time_optional": "Vade saati (isteğe bağlı)",
|
||||
"schedule_time_toggle": "Belirli bir saatte vadesi dolar",
|
||||
"schedule_time_help": "Boş = gece yarısı (varsayılan). HA saat dilimi.",
|
||||
"at_time": "saat",
|
||||
"notes_optional": "Notlar (isteğe bağlı)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— Etiket yok —",
|
||||
"dashboard": "Gösterge Paneli",
|
||||
"tab_today": "Bugün",
|
||||
"palette_placeholder": "Nesne ve görevlerde ara…",
|
||||
"palette_placeholder": "Nesne, görev, yedek parça, belge, not ara…",
|
||||
"palette_no_results": "Eşleşme yok",
|
||||
"palette_hint": "↑↓ gezinme · Enter açma · Esc kapatma",
|
||||
"palette_hint": "↑↓ gezin · Enter aç · Esc kapat · / her yerde aramayı açar",
|
||||
"search_open": "Ara",
|
||||
"search_group_parts": "Yedek parçalar",
|
||||
"search_group_content": "Belgelerde",
|
||||
"search_group_history": "Geçmiş notları",
|
||||
"search_page": "Sayfa {page}",
|
||||
"search_empty_hint": "Nesneler, görevler, yedek parçalar, belgeler ve notlarda aramak için yazın",
|
||||
"search_searching": "Belgeler ve notlar aranıyor…",
|
||||
"search_index_status": "Tam metin arama: {total} dosyadan {indexed} tanesi dizinlendi · {no_text} metin katmanı yok · {pending} bekliyor",
|
||||
"today_all_caught_up": "Her şey tamam! Bu hafta vadesi gelen yok.",
|
||||
"today_overdue": "Gecikmiş",
|
||||
"today_due_today": "Bugün vadeli",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "„Sarf malzemesi düşük“ eşiği (%)",
|
||||
"settings_battery_low_percent": "„Pil zayıf“ eşiği (%)",
|
||||
"settings_thresholds_hint": "Yeni Önerilen kurulumlar (kalan yüzde) ve kendi Battery Notes eşiği olmayan filo pilleri için varsayılanlar. Mevcut görevler değerlerini korur.",
|
||||
"settings_value_out_of_range": "Değer {min} ile {max} arasında olmalıdır",
|
||||
"bn_summary": "{name}: {n} pil için % {pct}",
|
||||
"bn_floor_decides": "Senin % {floor} eşiğin hepsi için belirleyici; her pil için iki eşikten yüksek olanı geçerli.",
|
||||
"bn_above_floor": "Bu, senin % {floor} eşiğinin üzerinde; bu yüzden kayıtlı tüm piller için {name} belirleyici.",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "Test bildirimi gönderildi",
|
||||
"test_notification_failed": "Test bildirimi başarısız oldu",
|
||||
"notify_per_person": "Kişi bazında iletim",
|
||||
"member_avatars": "Üye avatarları",
|
||||
"member_avatars_hint": "Atanmış görevlerin yanında gösterilir — üyenin renginde baş harfler. Varsayılan addan gelir; üyeleri ayırt etmek için kendi baş harflerinizi belirleyin veya bir renk seçin.",
|
||||
"member_initials": "Baş harfler",
|
||||
"member_color": "Renk",
|
||||
"member_avatar_reset": "Sıfırla",
|
||||
"notify_no_own_device": "Kendi cihazı yok — ev servisini kullanır",
|
||||
"settings_notify_due_soon": "Vade yaklaştığında bildir",
|
||||
"settings_notify_overdue": "Geciktiğinde bildir",
|
||||
@@ -546,6 +560,7 @@
|
||||
"group_by_label": "Grupla",
|
||||
"state_value_help": "HA durum değerini kullanın (genellikle küçük harf, örn. \"on\"/\"off\"). Büyük/küçük harf kaydederken normalleştirilir.",
|
||||
"target_changes_help": "Tetikleyici çalışmadan önceki eşleşen geçiş sayısı (varsayılan: 1).",
|
||||
"state_latch_help": "1 geçişte tetikleyici bir mandal gibi çalışır: varlık Hedef durumdan çıkınca sıfırlanır — ya da yalnızca Kaynak durum ayarlıysa ona geri döndüğünde.",
|
||||
"for_minutes_state_help": "0 her değişikliği hemen sayar. Dakika ayarlanırsa yeni durumun önce o kadar sürmesi gerekir — kısa titremeler o zaman ne tetikler ne de sayılır.",
|
||||
"qr_print_title": "QR kodlarını yazdır",
|
||||
"qr_print_desc": "Kesip ekipmanınıza yapıştırmak için yazdırılabilir bir QR kodu sayfası oluşturun.",
|
||||
@@ -683,8 +698,14 @@
|
||||
"doc_delete_confirm": "\"{name}\" silinsin mi?",
|
||||
"doc_too_large": "Dosya çok büyük (en fazla 25 MB).",
|
||||
"doc_upload_failed": "Yükleme başarısız oldu.",
|
||||
"completion_photo_optional": "Tamamlama fotoğrafı (isteğe bağlı)",
|
||||
"add_photo": "Fotoğraf ekle",
|
||||
"completion_photos_optional": "Tamamlama fotoğrafları (isteğe bağlı)",
|
||||
"completion_photos": "Tamamlama fotoğrafları",
|
||||
"choose_photos": "Fotoğraf seç",
|
||||
"choose_photo": "Fotoğraf seç",
|
||||
"photos_android_hint": "Android uygulaması her seçimde bir fotoğraf iletir — her seçim eklenir.",
|
||||
"add_photos": "Fotoğraf ekle",
|
||||
"photos_limit": "Tamamlama başına en fazla {max} fotoğraf",
|
||||
"history_edit_photos_hint": "Burada kaldırılan fotoğraf nesnenin belgelerinde kalır.",
|
||||
"uploading": "Yükleniyor…",
|
||||
"remove": "Kaldır",
|
||||
"doc_deduped": "Zaten başka bir yerde saklı — paylaşıldı, ek alan kullanılmadı.",
|
||||
@@ -739,6 +760,14 @@
|
||||
"reading_unit_help": "Bu görevi tamamlarken kaydedilen değerin yanında gösterilir.",
|
||||
"reading_value_label": "Okuma değeri",
|
||||
"reading_label": "Okuma",
|
||||
"readings_section": "Sayaç okumaları",
|
||||
"readings_hint": "Tamamlama başına birden fazla adlandırılmış değer — her sayaç için bir satır. Tek bir değer için boş bırakın.",
|
||||
"reading_name_label": "Okuma adı",
|
||||
"reading_unit_short": "Birim",
|
||||
"reading_add": "Okuma ekle",
|
||||
"reading_duplicate_name": "Bu ad zaten kullanılıyor",
|
||||
"reading_last": "son: {value}",
|
||||
"reading_below_last": "Son okumadan ({value}) düşük",
|
||||
"settings_templates_label": "Şablon galerisi",
|
||||
"settings_templates_hint": "Asla ihtiyaç duymayacağınız şablonların işaretini kaldırın — \"Şablondan\" seçicilerinden (panel ve yapılandırma akışı) kaybolurlar. Başka hiçbir şey değişmez; istediğiniz zaman yeniden etkinleştirebilirsiniz.",
|
||||
"worksheet": "Çalışma sayfası",
|
||||
@@ -818,6 +847,12 @@
|
||||
"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.",
|
||||
"settings_notify_rule": "Kendi bildirim kuralınız",
|
||||
"settings_notify_rule_hint": "Her bildirim ayrıca maintenance_supporter_notification olayını tetikler (tür, durum, nesne, görev, referans numaraları, öncelik, derin bağlantı, yük). Bir otomasyonla yönlendirin — ör. Ticker, Telegram veya Pushover'a.",
|
||||
"settings_notify_event_only": "Yalnızca olayı tetikle, kendin gönderme",
|
||||
"settings_notify_event_only_hint": "Teslimatın tamamını otomasyonunuz yapar. Tekrar aralıkları, sessiz saatler ve günlük sınır, olayın ne zaman tetikleneceği için geçerli kalır.",
|
||||
"settings_notify_extra_data": "Ek bildirim verileri (şablon)",
|
||||
"settings_notify_extra_data_hint": "JSON veya YAML, Jinja kullanılabilir; notify çağrısının data alanına birleştirilir (sizin anahtarlarınız öncelikli). Değişkenler: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Test gönder düğmesiyle deneyin.",
|
||||
"card_saved_view": "Kayıtlı görünüm",
|
||||
"card_saved_view_none": "Yok",
|
||||
"card_saved_view_help": "Görünümün durum, kullanıcı ve etiket filtrelerini yukarıdaki filtrelerin üzerine uygular. Görünümün sıralaması ve gruplaması panel görüntüleme ayarlarıdır ve kartta uygulanmaz.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Tümünü değiştirildi olarak işaretle",
|
||||
"battery_fleet_mark_one": "Bu pili değiştirildi olarak işaretle",
|
||||
"battery_fleet_offline": "çevrimdışı",
|
||||
"battery_fleet_no_sensor": "Sensör yok",
|
||||
"battery_fleet_trigger_lost": "Bu görevin sensör tetikleyicisi kayboldu — tetiklenmeyecek ve otomatik tamamlanmayacak.",
|
||||
"battery_fleet_repair": "Onar",
|
||||
"battery_fleet_exclude": "Filodan hariç tut",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Otomatik keşfin atladığı bir pil sensörü seçin — hemen listeye katılır.",
|
||||
"battery_fleet_track_self": "Kendi kendine şarj olan pilleri izle",
|
||||
"battery_fleet_track_self_hint": "Telefonlar, süpürgeler ve kendi kendine şarj olan diğer cihazlar şarj edilebilir olarak görünür — seviye düşükken şarj istenir, asla yeni pil istenmez.",
|
||||
"battery_fleet_due_without_sensor": "Sensörü olmayan bir pilde geçmiş tahmini süresi dolmuş say",
|
||||
"battery_fleet_due_without_sensor_hint": "Bazı cihazlar hiç pil seviyesi bildirmez — Battery Notes yalnızca türü ve son değişimi bilir. Tipik ömür geçtiğinde böyle bir pil zayıf sayılır ve filo görevi size hatırlatır; değiştirildi olarak işaretlemek tahmini sıfırlar.",
|
||||
"battery_fleet_status_low": "Düşük",
|
||||
"battery_fleet_status_soon": "Yakında",
|
||||
"battery_fleet_status_ok": "İyi durumda",
|
||||
"battery_fleet_status_due": "Süresi doldu",
|
||||
"battery_fleet_predicted_on": "Yaklaşık {date} tarihinde bekleniyor",
|
||||
"battery_fleet_predicted_trend": "Bu pilin deşarj eğiliminden tahmin edildi: yaklaşık {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Şarj edilebilir: değiştirmek yerine şarj edin — alışveriş listesine asla girmez",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Servis kaydı (PDF)",
|
||||
"date": "Tarih",
|
||||
"service_record_entries": "kayıt",
|
||||
"ref_number": "Referans numarası",
|
||||
"print_options_title": "Servis defterini yazdır",
|
||||
"print_layout": "Düzen",
|
||||
"print_layout_chronological": "Kronolojik",
|
||||
"print_layout_by_task": "Göreve göre",
|
||||
"print_include": "Dahil et",
|
||||
"print_inc_readings": "Okumalar",
|
||||
"print_inc_parts": "Kullanılan parçalar",
|
||||
"print_inc_photos": "Fotoğraflar",
|
||||
"print_inc_documents": "Bağlı belgeler",
|
||||
"print_inc_checklist": "Kontrol listesi",
|
||||
"print_inc_notes": "Notlar",
|
||||
"print_inc_costs": "Maliyet ve süre",
|
||||
"print_inc_person": "Tamamlayan",
|
||||
"print_inc_refs": "Referans numaraları",
|
||||
"print_inc_qr": "Görev başına QR kodu",
|
||||
"print_button": "Yazdır",
|
||||
"report_scan_hint": "Görevi açmak için tarayın",
|
||||
"completed_by": "Tamamlayan",
|
||||
"date_from": "Başlangıç",
|
||||
"date_to": "Bitiş",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"err_invalid_value": "{field}: невірне значення",
|
||||
"feat_schedule_time": "Планування за часом доби",
|
||||
"feat_schedule_time_desc": "Задачі стають простроченими у певний час доби, а не опівночі.",
|
||||
"schedule_time_optional": "Час прострочення (необов'язково)",
|
||||
"schedule_time_toggle": "Термін у визначений час",
|
||||
"schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.",
|
||||
"at_time": "о",
|
||||
"notes_optional": "Примітки (необов'язково)",
|
||||
@@ -425,9 +425,17 @@
|
||||
"no_nfc_tag": "— Без тега —",
|
||||
"dashboard": "Дашборд",
|
||||
"tab_today": "Сьогодні",
|
||||
"palette_placeholder": "Пошук об'єктів і завдань…",
|
||||
"palette_placeholder": "Пошук об'єктів, завдань, запчастин, документів, нотаток…",
|
||||
"palette_no_results": "Немає збігів",
|
||||
"palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити",
|
||||
"palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити · / відкриває пошук будь-де",
|
||||
"search_open": "Пошук",
|
||||
"search_group_parts": "Запчастини",
|
||||
"search_group_content": "У документах",
|
||||
"search_group_history": "Нотатки в історії",
|
||||
"search_page": "Стор. {page}",
|
||||
"search_empty_hint": "Введіть текст для пошуку об'єктів, завдань, запчастин, документів і нотаток",
|
||||
"search_searching": "Пошук у документах і нотатках…",
|
||||
"search_index_status": "Повнотекстовий пошук: проіндексовано {indexed} із {total} файлів · {no_text} без текстового шару · {pending} у черзі",
|
||||
"today_all_caught_up": "Усе зроблено! Цього тижня нічого немає.",
|
||||
"today_overdue": "Прострочені",
|
||||
"today_due_today": "Сьогодні",
|
||||
@@ -454,6 +462,7 @@
|
||||
"settings_consumable_threshold": "Поріг «витратний матеріал закінчується» (%)",
|
||||
"settings_battery_low_percent": "Поріг «батарея розряджена» (%)",
|
||||
"settings_thresholds_hint": "Значення за замовчуванням для нових «Запропонованих налаштувань» (залишок у відсотках) і для батарей парку без власного порогу Battery Notes. Наявні завдання зберігають своє значення.",
|
||||
"settings_value_out_of_range": "Значення має бути від {min} до {max}",
|
||||
"bn_summary": "{name}: {pct} % для {n} батарей",
|
||||
"bn_floor_decides": "Ваш поріг {floor} % діє для всіх; для кожної батареї враховується більший із двох порогів.",
|
||||
"bn_above_floor": "Це вище за ваш поріг {floor} %, тому для всіх занотованих батарей вирішує {name}.",
|
||||
@@ -479,6 +488,11 @@
|
||||
"test_notification_success": "Тестове сповіщення надіслано",
|
||||
"test_notification_failed": "Не вдалося надіслати тестове сповіщення",
|
||||
"notify_per_person": "Доставка за особами",
|
||||
"member_avatars": "Аватари учасників",
|
||||
"member_avatars_hint": "Показується поруч із призначеними завданнями — ініціали в кольорі учасника. Типово беруться з імені; задайте власні ініціали або оберіть колір, щоб розрізняти учасників.",
|
||||
"member_initials": "Ініціали",
|
||||
"member_color": "Колір",
|
||||
"member_avatar_reset": "Скинути",
|
||||
"notify_no_own_device": "Немає власного пристрою — використовується загальна служба",
|
||||
"settings_notify_due_soon": "Сповіщати, коли термін наближається",
|
||||
"settings_notify_overdue": "Сповіщати про прострочення",
|
||||
@@ -538,6 +552,7 @@
|
||||
"group_by_label": "Групувати за",
|
||||
"state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.",
|
||||
"target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).",
|
||||
"state_latch_help": "При 1 переході тригер працює як засувка: він скидається, коли сутність залишає стан «до» — або, якщо задано лише стан «з», коли вона до нього повертається.",
|
||||
"for_minutes_state_help": "0 — кожна зміна зараховується одразу. З хвилинами новий стан спершу має утриматися — короткі збої тоді не спрацьовують і не рахуються.",
|
||||
"qr_print_title": "Друкувати QR-коди",
|
||||
"qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "Видалити «{name}»?",
|
||||
"doc_too_large": "Файл завеликий (макс. 25 МБ).",
|
||||
"doc_upload_failed": "Не вдалося завантажити.",
|
||||
"completion_photo_optional": "Фото виконання (необов'язково)",
|
||||
"add_photo": "Додати фото",
|
||||
"completion_photos_optional": "Фото виконання (необов'язково)",
|
||||
"completion_photos": "Фото виконання",
|
||||
"choose_photos": "Вибрати фото",
|
||||
"choose_photo": "Вибрати фото",
|
||||
"photos_android_hint": "Застосунок для Android передає по одному фото за вибір — кожен вибір додається.",
|
||||
"add_photos": "Додати фото",
|
||||
"photos_limit": "До {max} фото на одне виконання",
|
||||
"history_edit_photos_hint": "Видалене тут фото залишається в документах об'єкта.",
|
||||
"uploading": "Завантаження…",
|
||||
"remove": "Прибрати",
|
||||
"doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "Показується поруч із записаним значенням під час виконання цієї задачі.",
|
||||
"reading_value_label": "Показання",
|
||||
"reading_label": "Показання",
|
||||
"readings_section": "Показання",
|
||||
"readings_hint": "Кілька іменованих значень на одне виконання — по рядку на лічильник. Залиште порожнім для одного значення.",
|
||||
"reading_name_label": "Назва показання",
|
||||
"reading_unit_short": "Одиниця",
|
||||
"reading_add": "Додати показання",
|
||||
"reading_duplicate_name": "Назва вже використовується",
|
||||
"reading_last": "останнє: {value}",
|
||||
"reading_below_last": "Менше за попереднє показання ({value})",
|
||||
"settings_templates_label": "Галерея шаблонів",
|
||||
"settings_templates_hint": "Зніміть позначку з шаблонів, які вам не потрібні — вони зникнуть із вибору «З шаблону» (панель і config flow). Більше нічого не змінюється; можна ввімкнути знову будь-коли.",
|
||||
"worksheet": "Робочий аркуш",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "Сповіщати лише для подання",
|
||||
"settings_notify_scope_all": "Усі завдання",
|
||||
"settings_notify_scope_hint": "Нагадування надсилають лише завдання, що відповідають фільтрам мітки/користувача обраного подання. Статус, сортування та групування подання тут ігноруються.",
|
||||
"settings_notify_rule": "Власне правило сповіщень",
|
||||
"settings_notify_rule_hint": "Кожне сповіщення додатково викликає подію maintenance_supporter_notification (вид, статус, об'єкт, завдання, номери посилань, пріоритет, посилання, дані). Спрямуйте її автоматизацією — наприклад у Ticker, Telegram чи Pushover.",
|
||||
"settings_notify_event_only": "Лише подія, нічого не надсилати",
|
||||
"settings_notify_event_only_hint": "Доставку повністю виконує ваша автоматизація. Інтервали повтору, тихі години та денний ліміт і далі визначають, коли спрацьовує подія.",
|
||||
"settings_notify_extra_data": "Додаткові дані сповіщення (шаблон)",
|
||||
"settings_notify_extra_data_hint": "JSON або YAML, Jinja дозволено; додається до data виклику notify (ваші ключі мають перевагу). Змінні: kind, status, priority, object_name, task_name, object_ref, task_ref, days_until_due, next_due, responsible_user_id, url, target, title, message. Перевірте кнопкою «Надіслати тест».",
|
||||
"card_saved_view": "Збережене подання",
|
||||
"card_saved_view_none": "Немає",
|
||||
"card_saved_view_help": "Застосовує фільтри статусу, користувача та мітки подання додатково до фільтрів вище. Сортування та групування подання — налаштування відображення панелі й на картці не застосовуються.",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "Позначити всі заміненими",
|
||||
"battery_fleet_mark_one": "Позначити цю батарею заміненою",
|
||||
"battery_fleet_offline": "не в мережі",
|
||||
"battery_fleet_no_sensor": "Без датчика",
|
||||
"battery_fleet_trigger_lost": "Сенсорний тригер цього завдання втрачено — воно не спрацює і не завершиться автоматично.",
|
||||
"battery_fleet_repair": "Відновити",
|
||||
"battery_fleet_exclude": "Виключити з парку",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "Виберіть датчик батареї, який автоматичне виявлення пропустило — він одразу з'явиться у списку.",
|
||||
"battery_fleet_track_self": "Відстежувати самозарядні батареї",
|
||||
"battery_fleet_track_self_hint": "Телефони, пилососи та інші самозарядні пристрої відображаються як акумулятори — за низького заряду потрібна зарядка, а не нові батарейки.",
|
||||
"battery_fleet_due_without_sensor": "Вважати прострочений прогноз приводом для заміни, якщо батарея не має датчика",
|
||||
"battery_fleet_due_without_sensor_hint": "Деякі пристрої взагалі не повідомляють рівень заряду — Battery Notes знає лише тип і дату останньої заміни. Коли типовий термін служби минув, така батарея вважається розрядженою, і завдання парку нагадає; позначка «замінено» скидає прогноз.",
|
||||
"battery_fleet_status_low": "Низький",
|
||||
"battery_fleet_status_soon": "Незабаром",
|
||||
"battery_fleet_status_ok": "У нормі",
|
||||
"battery_fleet_status_due": "Час замінити",
|
||||
"battery_fleet_predicted_on": "Очікується приблизно {date}",
|
||||
"battery_fleet_predicted_trend": "Прогноз за трендом розряду цієї батареї: приблизно {date} ({confidence})",
|
||||
"battery_fleet_rechargeable": "Акумулятор: заряджається, а не замінюється — ніколи не потрапляє до списку покупок",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "Сервісна книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записів",
|
||||
"ref_number": "Номер посилання",
|
||||
"print_options_title": "Друк сервісної книжки",
|
||||
"print_layout": "Структура",
|
||||
"print_layout_chronological": "Хронологічно",
|
||||
"print_layout_by_task": "За завданнями",
|
||||
"print_include": "Включити",
|
||||
"print_inc_readings": "Показники",
|
||||
"print_inc_parts": "Використані деталі",
|
||||
"print_inc_photos": "Фото",
|
||||
"print_inc_documents": "Пов'язані документи",
|
||||
"print_inc_checklist": "Чек-лист",
|
||||
"print_inc_notes": "Нотатки",
|
||||
"print_inc_costs": "Вартість і тривалість",
|
||||
"print_inc_person": "Виконав",
|
||||
"print_inc_refs": "Номери посилань",
|
||||
"print_inc_qr": "QR-код для кожного завдання",
|
||||
"print_button": "Друк",
|
||||
"report_scan_hint": "Скануйте, щоб відкрити завдання",
|
||||
"completed_by": "Виконав",
|
||||
"date_from": "Від",
|
||||
"date_to": "До",
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"err_invalid_value": "{field}: 无效数值",
|
||||
"feat_schedule_time": "具体时间调度",
|
||||
"feat_schedule_time_desc": "任务将在特定时间点变为逾期,而非默认的午夜。",
|
||||
"schedule_time_optional": "到期时间 (可选)",
|
||||
"schedule_time_toggle": "在特定时间到期",
|
||||
"schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。",
|
||||
"at_time": "于",
|
||||
"notes_optional": "备注 (可选)",
|
||||
@@ -427,9 +427,17 @@
|
||||
"no_nfc_tag": "— 无标签 —",
|
||||
"dashboard": "仪表盘",
|
||||
"tab_today": "今天",
|
||||
"palette_placeholder": "搜索对象和任务…",
|
||||
"palette_placeholder": "搜索对象、任务、备件、文档、备注…",
|
||||
"palette_no_results": "无匹配",
|
||||
"palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭",
|
||||
"palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭 · / 在任意位置打开搜索",
|
||||
"search_open": "搜索",
|
||||
"search_group_parts": "备件",
|
||||
"search_group_content": "文档内容",
|
||||
"search_group_history": "历史备注",
|
||||
"search_page": "第 {page} 页",
|
||||
"search_empty_hint": "输入以搜索对象、任务、备件、文档和备注",
|
||||
"search_searching": "正在搜索文档和备注…",
|
||||
"search_index_status": "全文搜索:已索引 {indexed}/{total} 个文件 · {no_text} 个无文本层 · {pending} 个待处理",
|
||||
"today_all_caught_up": "全部完成!本周没有待办。",
|
||||
"today_overdue": "逾期",
|
||||
"today_due_today": "今天到期",
|
||||
@@ -456,6 +464,7 @@
|
||||
"settings_consumable_threshold": "耗材“不足”阈值(%)",
|
||||
"settings_battery_low_percent": "电池“电量低”阈值(%)",
|
||||
"settings_thresholds_hint": "用于新的“建议设置”(剩余百分比)以及没有自身 Battery Notes 阈值的电池组电池。现有任务保留其值。",
|
||||
"settings_value_out_of_range": "数值必须介于 {min} 和 {max} 之间",
|
||||
"bn_summary": "{name}:{n} 块电池为 {pct}%",
|
||||
"bn_floor_decides": "你的 {floor}% 阈值对所有电池生效;每块电池取两个阈值中较高者。",
|
||||
"bn_above_floor": "这高于你的 {floor}% 阈值,因此所有已记录的电池由 {name} 决定。",
|
||||
@@ -481,6 +490,11 @@
|
||||
"test_notification_success": "测试通知已发送",
|
||||
"test_notification_failed": "测试通知发送失败",
|
||||
"notify_per_person": "按成员分别投递",
|
||||
"member_avatars": "成员头像",
|
||||
"member_avatars_hint": "显示在已分配任务旁——以成员颜色显示首字母。默认取自姓名;可自定义首字母或选择颜色以区分成员。",
|
||||
"member_initials": "首字母",
|
||||
"member_color": "颜色",
|
||||
"member_avatar_reset": "重置",
|
||||
"notify_no_own_device": "无个人设备 — 使用家庭通知服务",
|
||||
"settings_notify_due_soon": "到期前通知提醒",
|
||||
"settings_notify_overdue": "超期后通知提醒",
|
||||
@@ -540,6 +554,7 @@
|
||||
"group_by_label": "分组依据",
|
||||
"state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。",
|
||||
"target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。",
|
||||
"state_latch_help": "设为 1 次转换时触发器为锁存:实体离开“到”状态时恢复;若只设置了“从”状态,则在实体回到该状态时恢复。",
|
||||
"for_minutes_state_help": "0 表示每次变化立即计数。设置分钟后,新状态需先保持该时长——短暂闪变既不触发也不计数。",
|
||||
"qr_print_title": "打印二维码",
|
||||
"qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。",
|
||||
@@ -677,8 +692,14 @@
|
||||
"doc_delete_confirm": "删除“{name}”?",
|
||||
"doc_too_large": "文件过大(最大 25 MB)。",
|
||||
"doc_upload_failed": "上传失败。",
|
||||
"completion_photo_optional": "完成照片(可选)",
|
||||
"add_photo": "添加照片",
|
||||
"completion_photos_optional": "完成照片(可选)",
|
||||
"completion_photos": "完成照片",
|
||||
"choose_photos": "选择照片",
|
||||
"choose_photo": "选择照片",
|
||||
"photos_android_hint": "Android 应用每次只传一张照片——每次选择都会添加。",
|
||||
"add_photos": "添加照片",
|
||||
"photos_limit": "每次完成最多 {max} 张照片",
|
||||
"history_edit_photos_hint": "在此移除的照片仍保留在对象的文档中。",
|
||||
"uploading": "上传中…",
|
||||
"remove": "移除",
|
||||
"doc_deduped": "已在别处存储——共享,不占用额外空间。",
|
||||
@@ -733,6 +754,14 @@
|
||||
"reading_unit_help": "完成此任务时显示在记录值旁边。",
|
||||
"reading_value_label": "读数值",
|
||||
"reading_label": "读数",
|
||||
"readings_section": "读数",
|
||||
"readings_hint": "每次完成可记录多个命名值——每个表计一行。留空则只记录单个值。",
|
||||
"reading_name_label": "读数名称",
|
||||
"reading_unit_short": "单位",
|
||||
"reading_add": "添加读数",
|
||||
"reading_duplicate_name": "名称已被使用",
|
||||
"reading_last": "上次:{value}",
|
||||
"reading_below_last": "低于上次读数({value})",
|
||||
"settings_templates_label": "模板库",
|
||||
"settings_templates_hint": "取消勾选你永远不需要的模板——它们会从\"从模板\"选择器(面板和配置流程)中消失。其他一切不变;可随时重新启用。",
|
||||
"worksheet": "工作单",
|
||||
@@ -818,6 +847,12 @@
|
||||
"settings_notify_scope": "仅为该视图发送通知",
|
||||
"settings_notify_scope_all": "所有任务",
|
||||
"settings_notify_scope_hint": "只有匹配所选视图的标签/用户筛选的任务才会发送提醒。视图的状态、排序和分组在此处被忽略。",
|
||||
"settings_notify_rule": "自定义通知规则",
|
||||
"settings_notify_rule_hint": "每条通知还会触发事件 maintenance_supporter_notification(类型、状态、对象、任务、参考编号、优先级、深层链接、载荷)。用自动化转发它——例如到 Ticker、Telegram 或 Pushover。",
|
||||
"settings_notify_event_only": "仅触发事件,自身不发送",
|
||||
"settings_notify_event_only_hint": "由你的自动化完成全部发送。重复间隔、免打扰时段和每日上限仍决定事件何时触发。",
|
||||
"settings_notify_extra_data": "附加通知数据(模板)",
|
||||
"settings_notify_extra_data_hint": "JSON 或 YAML,可用 Jinja;合并进 notify 调用的 data(你的键优先)。变量:kind、status、priority、object_name、task_name、object_ref、task_ref、days_until_due、next_due、responsible_user_id、url、target、title、message。用“发送测试”按钮验证。",
|
||||
"card_saved_view": "已保存的视图",
|
||||
"card_saved_view_none": "无",
|
||||
"card_saved_view_help": "在上述筛选之外再应用该视图的状态、用户和标签筛选。视图的排序和分组属于面板显示设置,不会应用到卡片上。",
|
||||
@@ -859,6 +894,7 @@
|
||||
"battery_fleet_mark_all": "全部标记为已更换",
|
||||
"battery_fleet_mark_one": "标记此电池为已更换",
|
||||
"battery_fleet_offline": "离线",
|
||||
"battery_fleet_no_sensor": "无传感器",
|
||||
"battery_fleet_trigger_lost": "此任务的传感器触发器已丢失——它不会触发,也不会自动完成。",
|
||||
"battery_fleet_repair": "修复",
|
||||
"battery_fleet_exclude": "从电池群中排除",
|
||||
@@ -870,9 +906,12 @@
|
||||
"battery_fleet_add_hint": "选择自动发现遗漏的电池传感器——它会立即加入列表。",
|
||||
"battery_fleet_track_self": "跟踪自充电电池",
|
||||
"battery_fleet_track_self_hint": "手机、吸尘器等自充电设备显示为可充电电池——电量低时提示充电,而不是更换电池。",
|
||||
"battery_fleet_due_without_sensor": "电池没有传感器时,将已过期的预测视为已到期",
|
||||
"battery_fleet_due_without_sensor_hint": "有些设备完全不报告电池电量——Battery Notes 只知道类型和上次更换时间。一旦超过典型寿命,这样的电池就会被视为电量低,电池群任务会提醒你;标记为已更换会重置预测。",
|
||||
"battery_fleet_status_low": "电量低",
|
||||
"battery_fleet_status_soon": "即将",
|
||||
"battery_fleet_status_ok": "正常",
|
||||
"battery_fleet_status_due": "已到期",
|
||||
"battery_fleet_predicted_on": "预计在 {date} 前后",
|
||||
"battery_fleet_predicted_trend": "根据此电池的放电趋势预测:约 {date}({confidence})",
|
||||
"battery_fleet_rechargeable": "可充电电池:充电即可,无需更换——不会出现在购物清单中",
|
||||
@@ -917,6 +956,24 @@
|
||||
"service_record_print": "维护记录(PDF)",
|
||||
"date": "日期",
|
||||
"service_record_entries": "条记录",
|
||||
"ref_number": "参考编号",
|
||||
"print_options_title": "打印保养记录",
|
||||
"print_layout": "版式",
|
||||
"print_layout_chronological": "按时间顺序",
|
||||
"print_layout_by_task": "按任务",
|
||||
"print_include": "包含",
|
||||
"print_inc_readings": "读数",
|
||||
"print_inc_parts": "使用的零件",
|
||||
"print_inc_photos": "照片",
|
||||
"print_inc_documents": "关联文档",
|
||||
"print_inc_checklist": "检查清单",
|
||||
"print_inc_notes": "备注",
|
||||
"print_inc_costs": "费用和用时",
|
||||
"print_inc_person": "完成者",
|
||||
"print_inc_refs": "参考编号",
|
||||
"print_inc_qr": "每个任务的二维码",
|
||||
"print_button": "打印",
|
||||
"report_scan_hint": "扫描以打开任务",
|
||||
"completed_by": "完成人",
|
||||
"date_from": "从",
|
||||
"date_to": "至",
|
||||
|
||||
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
+250
@@ -0,0 +1,250 @@
|
||||
/*! maintenance_supporter frontend 2.80.0 */
|
||||
import{e as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-PRQXFMNO.js";import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3IZWSKBJ.js";import{a as o,b as _,c as i,f as l,h,l as g,m as n,q as t,s as u,u as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NAI3MD3R.js";var r=class extends h{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=s=>{let a=new Set(this._selected);a.has(s)?a.delete(s):a.add(s),this._selected=a};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(s=>s.entity_id))};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let s=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:e.suggested_entry_id??void 0,object_name:e.suggested_object_name,device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),a=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/adopt",selections:s});this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:a})),this._open=!1}catch(s){this._error=p(s,this._lang)}finally{this._adopting=!1}}}}get _lang(){return u(this.hass)}updated(s){s.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,v(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._responsible="",this._forMinutes="0";try{this._userService?this._userService.updateHass(this.hass):this._userService=new f(this.hass);let[s,a]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/problem_sensors/discover"}),this._userService.getUsers().catch(()=>[])]);this._sensors=s.sensors||[],this._selected=new Set(this._sensors.map(e=>e.entity_id)),this._users=a}catch(s){this._error=p(s,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return i``;let s=this._lang,a=this._sensors.length>0&&this._selected.size===this._sensors.length;return i`
|
||||
<div class="overlay" @click=${this._close}>
|
||||
<div class="card" @click=${e=>e.stopPropagation()}>
|
||||
<div class="title">${t("adopt_problem_title",s)}</div>
|
||||
<div class="hint">${t("adopt_problem_hint",s)}</div>
|
||||
${this._error?i`<div class="error">${this._error}</div>`:l}
|
||||
|
||||
${this._loading?i`<div class="loading">…</div>`:this._sensors.length===0?i`<div class="empty">${t("adopt_problem_none",s)}</div>`:i`
|
||||
<label class="select-all">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${a}
|
||||
@change=${this._toggleAll}
|
||||
/>
|
||||
<span>${t("selected",s)}: ${this._selected.size} / ${this._sensors.length}</span>
|
||||
</label>
|
||||
<div class="list">
|
||||
${this._sensors.map(e=>{let m=this._selected.has(e.entity_id),d=e.state==="on",c=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return i`
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${m}
|
||||
@change=${()=>this._toggle(e.entity_id)}
|
||||
/>
|
||||
<div class="row-main">
|
||||
<div class="row-top">
|
||||
<span class="row-name">${e.name}</span>
|
||||
<span class="chip ${d?"chip-active":"chip-ok"}">
|
||||
${d?t("adopt_problem_active",s):t("adopt_problem_ok",s)}
|
||||
</span>
|
||||
</div>
|
||||
${c?i`<div class="row-sub">${c}</div>`:l}
|
||||
<div class="row-target">
|
||||
→ ${e.suggested_object_name}${e.suggested_entry_id?l:i` <span class="new-tag">${t("adopt_problem_new_object",s)}</span>`}
|
||||
</div>
|
||||
${e.suggested_part_name?i`<div class="row-part">
|
||||
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
|
||||
${t("adopt_problem_part",s).replace("{name}",e.suggested_part_name)}
|
||||
</div>`:l}
|
||||
</div>
|
||||
</label>
|
||||
`})}
|
||||
</div>
|
||||
`}
|
||||
|
||||
${!this._loading&&this._sensors.length>0?i`
|
||||
<label class="responsible">
|
||||
<span>${t("for_at_least_minutes",s)}</span>
|
||||
<input
|
||||
class="for-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1440"
|
||||
.value=${this._forMinutes}
|
||||
@input=${e=>this._forMinutes=e.target.value}
|
||||
/>
|
||||
</label>
|
||||
<div class="for-hint">${t("adopt_for_minutes_hint",s)}</div>
|
||||
`:l}
|
||||
|
||||
${!this._loading&&this._sensors.length>0&&this._users.length>0?i`
|
||||
<label class="responsible">
|
||||
<span>${t("adopt_problem_responsible",s)}</span>
|
||||
<select
|
||||
.value=${this._responsible}
|
||||
@change=${e=>{this._responsible=e.target.value}}
|
||||
>
|
||||
<option value="" ?selected=${!this._responsible}>${t("no_user_assigned",s)}</option>
|
||||
${this._users.map(e=>i`<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}>
|
||||
${t("cancel",s)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._adopt}
|
||||
.disabled=${this._selected.size===0||this._adopting}
|
||||
>
|
||||
${t("adopt_problem_adopt",s)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}};r.styles=_`
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 560px;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.hint {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
.loading,
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-all input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row input {
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.row-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
.row-sub {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-target {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-part {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.row-part ha-icon {
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
.new-tag {
|
||||
font-style: italic;
|
||||
}
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip-active {
|
||||
background: var(--error-color, #f44336);
|
||||
color: #fff;
|
||||
}
|
||||
.chip-ok {
|
||||
background: var(--divider-color);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.responsible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.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})],r.prototype,"hass",2),o([n()],r.prototype,"_open",2),o([n()],r.prototype,"_loading",2),o([n()],r.prototype,"_adopting",2),o([n()],r.prototype,"_error",2),o([n()],r.prototype,"_sensors",2),o([n()],r.prototype,"_selected",2),o([n()],r.prototype,"_users",2),o([n()],r.prototype,"_responsible",2),o([n()],r.prototype,"_forMinutes",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",r);export{r as MaintenanceAdoptProblemSensorsDialog};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*! maintenance_supporter frontend 2.80.0 */
|
||||
import{a as r,b as d,c as a,f as l,h as p,l as s,q as c}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NAI3MD3R.js";function h(o,e){if(e)switch(o){case"date":return e.split("T")[0];case"time":return e.length===5?`${e}:00`:e;case"datetime":{let[i,n="00:00:00"]=e.split("T");return`${i} ${n.length===5?`${n}:00`:n}`}}}function u(o,e){if(typeof e!="string"||!e)return"";switch(o){case"date":return e.slice(0,10);case"time":return e.slice(0,5);case"datetime":{let[i,n="00:00:00"]=e.split(" ");return`${i}T${n.length===5?`${n}:00`:n}`}}}var t=class extends p{constructor(){super(...arguments);this.kind="date";this.label="";this.value="";this.clearable=!1;this.disabled=!1;this.required=!1;this.lang="en"}_selector(){switch(this.kind){case"date":return{date:{}};case"time":return{time:{no_second:!0}};case"datetime":return{datetime:{}}}}_onSelectorChange(i){i.stopPropagation(),this._emit(u(this.kind,i.detail?.value))}_clear(){this._emit("")}_emit(i){i!==this.value&&(this.value=i,this.dispatchEvent(new CustomEvent("value-changed",{bubbles:!0,composed:!0,detail:{value:i}})))}render(){return a`
|
||||
<div class="field">
|
||||
${this.label?a`<span class="label">${this.label}${this.required?a`<span class="req">*</span>`:l}</span>`:l}
|
||||
<div class="row">
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._selector()}
|
||||
.value=${h(this.kind,this.value)}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._onSelectorChange}
|
||||
></ha-selector>
|
||||
${this.clearable&&this.value&&!this.disabled?a`<button type="button" class="clear" title=${c("clear",this.lang)} aria-label=${c("clear",this.lang)} @click=${this._clear}>
|
||||
<ha-icon icon="mdi:close"></ha-icon>
|
||||
</button>`:l}
|
||||
</div>
|
||||
${this.helper?a`<span class="helper">${this.helper}</span>`:l}
|
||||
</div>
|
||||
`}};t.styles=d`
|
||||
: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 {
|
||||
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 { background: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.08); }
|
||||
.clear:focus-visible { outline: 2px solid var(--primary-color); }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
`,r([s({attribute:!1})],t.prototype,"hass",2),r([s()],t.prototype,"kind",2),r([s()],t.prototype,"label",2),r([s()],t.prototype,"value",2),r([s()],t.prototype,"helper",2),r([s({type:Boolean})],t.prototype,"clearable",2),r([s({type:Boolean})],t.prototype,"disabled",2),r([s({type:Boolean})],t.prototype,"required",2),r([s()],t.prototype,"lang",2);customElements.get("ms-date-field")||customElements.define("ms-date-field",t);
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.80.0 */
|
||||
import{q as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NAI3MD3R.js";var s={entry_id:"object",name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_equals:"trigger_equals",trigger_not_equals:"trigger_not_equals",trigger_for_minutes:"trigger_for_minutes"};function c(e,o){let r=s[e];if(!r)return e;let t=a(r,o);return t&&t!==r?t:e}function d(e){let r=e.match(/data\['([^']+)'\]/)?.[1],t;return(t=e.match(/length of value must be at most (\d+)/))?{field:r,rule:"too_long",param:t[1]}:(t=e.match(/length of value must be at least (\d+)/))?{field:r,rule:"too_short",param:t[1]}:(t=e.match(/value must be at most (\S+)/))?{field:r,rule:"value_too_high",param:t[1]}:(t=e.match(/value must be at least (\S+)/))?{field:r,rule:"value_too_low",param:t[1]}:/required key not provided/.test(e)?{field:r,rule:"required"}:(t=e.match(/expected (\w+)/))?{field:r,rule:"wrong_type",param:t[1]}:/value must be one of/.test(e)?{field:r,rule:"invalid_choice"}:/not a valid value/.test(e)?{field:r,rule:"invalid_value"}:{field:r,rule:"unknown"}}function g(e,o,r){if(r=r??a("action_error",o),typeof e=="string")return e;if(typeof e!="object"||e===null)return r;let t=e,_=t.message||t.error?.message||"";if(!_)return r;let i=d(_),l=i.field?c(i.field,o):"",n=u=>a(u,o).replace("{field}",l).replace("{n}",i.param??"");switch(i.rule){case"too_long":return n("err_too_long");case"too_short":return n("err_too_short");case"value_too_high":return n("err_value_too_high");case"value_too_low":return n("err_value_too_low");case"required":return n("err_required");case"wrong_type":return n("err_wrong_type").replace("{type}",i.param??"");case"invalid_choice":return n("err_invalid_choice");case"invalid_value":return n("err_invalid_value");default:return _||r}}export{g as a};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*! maintenance_supporter frontend 2.80.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-NAI3MD3R.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.80.0 */
|
||||
var l=[{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"}],i=l.map(a=>a.key),r=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function o(a){if(!Array.isArray(a))return[...r];let t=new Set,e=[];for(let n of a)typeof n=="string"&&i.includes(n)&&!t.has(n)&&(t.add(n),e.push(n));return e.length?(e.includes("name")||e.unshift("name"),e):[...r]}export{l as a,r as b,o as c};
|
||||
@@ -0,0 +1,153 @@
|
||||
/*! maintenance_supporter frontend 2.80.0 */
|
||||
import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-3IZWSKBJ.js";import{a as r,b as _,c as l,f as o,h,l as d,m as s,q as i,s as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NAI3MD3R.js";var a=class extends h{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return p(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._error="",this._open=!0}openEdit(e,n){this._entryId=e,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(e){this._error=u(e,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let e=this._lang,n=this._entryId?i("edit_object",e):i("new_object",e);return l`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${n}</div>
|
||||
<div class="content">
|
||||
${this._error?l`<div class="error">${this._error}</div>`:o}
|
||||
<ms-textfield
|
||||
label="${i("name",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>
|
||||
<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>
|
||||
`}};a.styles=_`
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
ms-textfield {
|
||||
display: block;
|
||||
}
|
||||
.textarea-field {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.textarea-label {
|
||||
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
|
||||
}
|
||||
.textarea-field textarea {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
.textarea-field textarea:focus {
|
||||
outline: none; border-color: var(--primary-color);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
`,r([d({attribute:!1})],a.prototype,"hass",2),r([d({attribute:!1})],a.prototype,"objects",2),r([s()],a.prototype,"_open",2),r([s()],a.prototype,"_loading",2),r([s()],a.prototype,"_error",2),r([s()],a.prototype,"_name",2),r([s()],a.prototype,"_manufacturer",2),r([s()],a.prototype,"_model",2),r([s()],a.prototype,"_serialNumber",2),r([s()],a.prototype,"_areaId",2),r([s()],a.prototype,"_installationDate",2),r([s()],a.prototype,"_warrantyExpiry",2),r([s()],a.prototype,"_documentationUrl",2),r([s()],a.prototype,"_notes",2),r([s()],a.prototype,"_haDeviceId",2),r([s()],a.prototype,"_parentEntryId",2),r([s()],a.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",a);export{a};
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user