147 files
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
{"pid": 71, "version": 1, "ha_version": "2026.8.3", "start_ts": 1787776418.1303492}
|
{"pid": 72, "version": 1, "ha_version": "2026.8.3", "start_ts": 1787864016.8899596}
|
||||||
+25
-25
@@ -81,117 +81,117 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "88b90f2b1d6b45f0b44bee786ee1a525",
|
"id": "88b90f2b1d6b45f0b44bee786ee1a525",
|
||||||
"url": "/taskmate/taskmate-attr-resolver.js?v=5.3.0",
|
"url": "/taskmate/taskmate-attr-resolver.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0796c92931dd458e81dc9c2677248274",
|
"id": "0796c92931dd458e81dc9c2677248274",
|
||||||
"url": "/taskmate/taskmate-localize.js?v=5.3.0",
|
"url": "/taskmate/taskmate-localize.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "d2e281920ac7409583eafff1f1db69bd",
|
"id": "d2e281920ac7409583eafff1f1db69bd",
|
||||||
"url": "/taskmate/taskmate-design.js?v=5.3.0",
|
"url": "/taskmate/taskmate-design.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9eae9632f0734480be5edd784b030673",
|
"id": "9eae9632f0734480be5edd784b030673",
|
||||||
"url": "/taskmate/taskmate-badges-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-badges-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "5df49cd8a4eb420497b5df22cb31362c",
|
"id": "5df49cd8a4eb420497b5df22cb31362c",
|
||||||
"url": "/taskmate/taskmate-child-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-child-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b44c87d0d74f468cb96c5eabc340f9ee",
|
"id": "b44c87d0d74f468cb96c5eabc340f9ee",
|
||||||
"url": "/taskmate/taskmate-rewards-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-rewards-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "787a96f9c30548708165a48457c99feb",
|
"id": "787a96f9c30548708165a48457c99feb",
|
||||||
"url": "/taskmate/taskmate-approvals-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-approvals-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "84031597de424ad68e7c2bc66d06b72e",
|
"id": "84031597de424ad68e7c2bc66d06b72e",
|
||||||
"url": "/taskmate/taskmate-points-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-points-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "02613225844746b699bfbfac9642eca2",
|
"id": "02613225844746b699bfbfac9642eca2",
|
||||||
"url": "/taskmate/taskmate-reorder-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-reorder-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7a06a07e17ba4b31b712bc91b77360b4",
|
"id": "7a06a07e17ba4b31b712bc91b77360b4",
|
||||||
"url": "/taskmate/taskmate-overview-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-overview-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "08b42b53b33b4fa88db932455cf8d5aa",
|
"id": "08b42b53b33b4fa88db932455cf8d5aa",
|
||||||
"url": "/taskmate/taskmate-activity-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-activity-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "15907eff51784acdba88ff694b574509",
|
"id": "15907eff51784acdba88ff694b574509",
|
||||||
"url": "/taskmate/taskmate-streak-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-streak-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39",
|
"id": "1bd6bb5c0a6d4f0d916e3d82cb7fac39",
|
||||||
"url": "/taskmate/taskmate-weekly-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-weekly-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "289ce5943f5a4972a593697db4c96ece",
|
"id": "289ce5943f5a4972a593697db4c96ece",
|
||||||
"url": "/taskmate/taskmate-graph-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-graph-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "d2904acee4c74a398dcc7b996c23e7e7",
|
"id": "d2904acee4c74a398dcc7b996c23e7e7",
|
||||||
"url": "/taskmate/taskmate-reward-progress-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-reward-progress-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "46d378736fd54a26be253f2dfb27202e",
|
"id": "46d378736fd54a26be253f2dfb27202e",
|
||||||
"url": "/taskmate/taskmate-leaderboard-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-leaderboard-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "db11ae36457842fa826c7991e1d9c6f4",
|
"id": "db11ae36457842fa826c7991e1d9c6f4",
|
||||||
"url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-parent-dashboard-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ce85f301b59a412e9abfe5ffd51cb9a2",
|
"id": "ce85f301b59a412e9abfe5ffd51cb9a2",
|
||||||
"url": "/taskmate/taskmate-penalties-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-penalties-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1959e1629de2422daa8adaf87069c288",
|
"id": "1959e1629de2422daa8adaf87069c288",
|
||||||
"url": "/taskmate/taskmate-bonuses-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-bonuses-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2548524b269c4959bc7e0e967df943af",
|
"id": "2548524b269c4959bc7e0e967df943af",
|
||||||
"url": "/taskmate/taskmate-points-display-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-points-display-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "36ada49368db4187a283fbbbef45454f",
|
"id": "36ada49368db4187a283fbbbef45454f",
|
||||||
"url": "/taskmate/taskmate-calendar-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-calendar-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "67c32687acd2497b90030ef141f4b6db",
|
"id": "67c32687acd2497b90030ef141f4b6db",
|
||||||
"url": "/taskmate/taskmate-photo-gallery-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-photo-gallery-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6f0c8348a91f4b74a9b846ab3f62621f",
|
"id": "6f0c8348a91f4b74a9b846ab3f62621f",
|
||||||
"url": "/taskmate/taskmate-family-goal-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-family-goal-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "73dbd2de03de4530b451a55bfe6c46cf",
|
"id": "73dbd2de03de4530b451a55bfe6c46cf",
|
||||||
"url": "/taskmate/taskmate-routine-card.js?v=5.3.0",
|
"url": "/taskmate/taskmate-routine-card.js?v=5.4.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9227b25fc2ea4cb5be22e4436912268a",
|
"id": "17e81c8801114b2d941a7d5f22eb47e6",
|
||||||
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
|
"url": "/climate_scheduler/static/climate-scheduler-card.js?v=1.15.1",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -564,6 +564,12 @@ async def async_setup_entry(
|
|||||||
_LOGGER.debug("Maintenance calendar entity created")
|
_LOGGER.debug("Maintenance calendar entity created")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _task_label(task: MaintenanceTask) -> str:
|
||||||
|
"""Task name, with the due cycle phase appended for phased tasks (#139)."""
|
||||||
|
phase = task.current_phase_name
|
||||||
|
return f"{task.name} · {phase}" if phase else task.name
|
||||||
|
|
||||||
class MaintenanceCalendar(CalendarEntity):
|
class MaintenanceCalendar(CalendarEntity):
|
||||||
"""Calendar entity aggregating all maintenance tasks."""
|
"""Calendar entity aggregating all maintenance tasks."""
|
||||||
|
|
||||||
@@ -713,7 +719,7 @@ class MaintenanceCalendar(CalendarEntity):
|
|||||||
today = dt_util.now().date()
|
today = dt_util.now().date()
|
||||||
if start_d <= today <= end_d:
|
if start_d <= today <= end_d:
|
||||||
return CalendarEvent(
|
return CalendarEvent(
|
||||||
summary=f"{STATUS_PREFIX.get(MaintenanceStatus.TRIGGERED, '🔔')} {task.name} ({object_name})",
|
summary=f"{STATUS_PREFIX.get(MaintenanceStatus.TRIGGERED, '🔔')} {_task_label(task)} ({object_name})",
|
||||||
start=today,
|
start=today,
|
||||||
end=today + timedelta(days=1),
|
end=today + timedelta(days=1),
|
||||||
description=_cal_t("manually_triggered", lang),
|
description=_cal_t("manually_triggered", lang),
|
||||||
@@ -728,7 +734,7 @@ class MaintenanceCalendar(CalendarEntity):
|
|||||||
today = dt_util.now().date()
|
today = dt_util.now().date()
|
||||||
if start_d <= today <= end_d:
|
if start_d <= today <= end_d:
|
||||||
return CalendarEvent(
|
return CalendarEvent(
|
||||||
summary=f"{STATUS_PREFIX.get(MaintenanceStatus.TRIGGERED, '🔔')} {task.name} ({object_name})",
|
summary=f"{STATUS_PREFIX.get(MaintenanceStatus.TRIGGERED, '🔔')} {_task_label(task)} ({object_name})",
|
||||||
start=today,
|
start=today,
|
||||||
end=today + timedelta(days=1),
|
end=today + timedelta(days=1),
|
||||||
description=_cal_t("sensor_triggered", lang, name=task.name),
|
description=_cal_t("sensor_triggered", lang, name=task.name),
|
||||||
@@ -770,7 +776,7 @@ class MaintenanceCalendar(CalendarEntity):
|
|||||||
pass # malformed schedule_time → fall back to all-day
|
pass # malformed schedule_time → fall back to all-day
|
||||||
|
|
||||||
return CalendarEvent(
|
return CalendarEvent(
|
||||||
summary=f"{prefix} {task.name} ({object_name})",
|
summary=f"{prefix} {_task_label(task)} ({object_name})",
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
description=(
|
description=(
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ class _OptionsFlowBase(TriggerConfigMixin, OptionsFlow):
|
|||||||
menu.append("remove_trigger")
|
menu.append("remove_trigger")
|
||||||
if global_opts.get(CONF_ADVANCED_CHECKLISTS, False):
|
if global_opts.get(CONF_ADVANCED_CHECKLISTS, False):
|
||||||
menu.append("edit_checklist")
|
menu.append("edit_checklist")
|
||||||
|
# #139: cycle phases are not feature-gated (the panel editor isn't
|
||||||
|
# either) — the minimal flow editor is always reachable.
|
||||||
|
menu.append("edit_phases")
|
||||||
if global_opts.get(CONF_ADVANCED_ADAPTIVE, False):
|
if global_opts.get(CONF_ADVANCED_ADAPTIVE, False):
|
||||||
menu.append("adaptive_scheduling")
|
menu.append("adaptive_scheduling")
|
||||||
menu.extend(["delete_task", "manage_tasks"])
|
menu.extend(["delete_task", "manage_tasks"])
|
||||||
|
|||||||
@@ -539,6 +539,73 @@ class TaskCrudMixin:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_edit_phases(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
|
"""Minimal cycle-phase editor (#139): one textarea, one line per
|
||||||
|
sequence step, optional per-phase checklist as "Name: item; item".
|
||||||
|
|
||||||
|
Names matching existing phases keep their ids (history phase_id and
|
||||||
|
the Store cursor stay stable) AND their panel-only overrides (parts /
|
||||||
|
required fields). Sanitizing + storage go through the SAME
|
||||||
|
``_apply_phase_fields`` as the WS write path, and the Store cursor is
|
||||||
|
clamped the same way ws_update_task clamps it.
|
||||||
|
"""
|
||||||
|
from .helpers.phases import clamp_phase_cursor, parse_phases_text, phases_to_text
|
||||||
|
from .websocket.tasks_crud import _apply_phase_fields
|
||||||
|
|
||||||
|
tasks_data = self.config_entry.data.get(CONF_TASKS, {})
|
||||||
|
task = tasks_data.get(self._selected_task_id or "", {})
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
if user_input.get("go_back"):
|
||||||
|
return self._show_task_action_menu()
|
||||||
|
|
||||||
|
raw_defs, raw_seq = parse_phases_text(
|
||||||
|
user_input.get("phases_text", ""), task.get("phases") or {}
|
||||||
|
)
|
||||||
|
new_data = dict(self.config_entry.data)
|
||||||
|
new_tasks = dict(new_data.get(CONF_TASKS, {}))
|
||||||
|
updated_task = dict(new_tasks.get(self._selected_task_id or "", {}))
|
||||||
|
_apply_phase_fields(self.hass, self.config_entry, updated_task, raw_defs, raw_seq)
|
||||||
|
new_tasks[self._selected_task_id or ""] = updated_task
|
||||||
|
new_data[CONF_TASKS] = new_tasks
|
||||||
|
self._update_config_entry(new_data)
|
||||||
|
|
||||||
|
# Clamp/clear the Store cursor against the edited sequence —
|
||||||
|
# the Store wins on restore, so an out-of-range cursor would
|
||||||
|
# otherwise survive this edit forever (same block as ws_update).
|
||||||
|
rd = getattr(self.config_entry, "runtime_data", None)
|
||||||
|
store = getattr(rd, "store", None)
|
||||||
|
if store is not None and self._selected_task_id:
|
||||||
|
seq = updated_task.get("phase_sequence") or []
|
||||||
|
if seq:
|
||||||
|
cur = store.get_task_state(self._selected_task_id).get("phase_cursor", 0)
|
||||||
|
store.set_phase_cursor(self._selected_task_id, clamp_phase_cursor(cur, len(seq)))
|
||||||
|
else:
|
||||||
|
store.get_task_state(self._selected_task_id).pop("phase_cursor", None)
|
||||||
|
store.async_delay_save()
|
||||||
|
|
||||||
|
return self._show_task_action_menu()
|
||||||
|
|
||||||
|
default_text = phases_to_text(task.get("phases") or {}, task.get("phase_sequence") or [])
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="edit_phases",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("phases_text", default=default_text): selector.TextSelector(
|
||||||
|
selector.TextSelectorConfig(
|
||||||
|
type=selector.TextSelectorType.TEXT,
|
||||||
|
multiline=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Optional("go_back", default=False): selector.BooleanSelector(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={
|
||||||
|
"task_name": task.get("name", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_delete_task(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
async def async_step_delete_task(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
"""Confirm and delete a task."""
|
"""Confirm and delete a task."""
|
||||||
tasks_data = self.config_entry.data.get(CONF_TASKS, {})
|
tasks_data = self.config_entry.data.get(CONF_TASKS, {})
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ from .storage import MaintenanceStore
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_task_label(task: dict[str, Any]) -> str:
|
||||||
|
"""Task name for notifications — phased tasks (#139) name the due step,
|
||||||
|
so "Mower blades · Replace blades" tells the user what the work IS."""
|
||||||
|
from .helpers.phases import current_phase
|
||||||
|
|
||||||
|
name = task.get("name", "")
|
||||||
|
phase = current_phase(task)
|
||||||
|
return f"{name} · {phase['name']}" if phase else name
|
||||||
|
|
||||||
|
|
||||||
def _inert_task_result(task: MaintenanceTask, status: str, **extra: Any) -> dict[str, Any]:
|
def _inert_task_result(task: MaintenanceTask, status: str, **extra: Any) -> dict[str, Any]:
|
||||||
"""Coordinator payload for a task that gets NO live evaluation (archived /
|
"""Coordinator payload for a task that gets NO live evaluation (archived /
|
||||||
paused): due fields nulled, trigger off, only cost/history-derived fields
|
paused): due fields nulled, trigger off, only cost/history-derived fields
|
||||||
@@ -744,7 +754,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
tasks=[
|
tasks=[
|
||||||
{
|
{
|
||||||
"task_id": tid,
|
"task_id": tid,
|
||||||
"task_name": tr.get("name", ""),
|
"task_name": _notify_task_label(tr),
|
||||||
"status": status,
|
"status": status,
|
||||||
"days_until_due": tr.get("_days_until_due"),
|
"days_until_due": tr.get("_days_until_due"),
|
||||||
}
|
}
|
||||||
@@ -756,7 +766,7 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
await nm.async_task_status_changed(
|
await nm.async_task_status_changed(
|
||||||
entry_id=self.entry.entry_id,
|
entry_id=self.entry.entry_id,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
task_name=task_result.get("name", ""),
|
task_name=_notify_task_label(task_result),
|
||||||
object_name=obj_name,
|
object_name=obj_name,
|
||||||
new_status=new_status,
|
new_status=new_status,
|
||||||
days_until_due=task_result.get("_days_until_due"),
|
days_until_due=task_result.get("_days_until_due"),
|
||||||
@@ -862,6 +872,10 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
state["due_override"] = do
|
state["due_override"] = do
|
||||||
elif "due_override" in state:
|
elif "due_override" in state:
|
||||||
del state["due_override"]
|
del state["due_override"]
|
||||||
|
# Phase cursor (#139): to_dict emits it only for phase-carrying tasks.
|
||||||
|
pc = td.get("phase_cursor")
|
||||||
|
if pc is not None:
|
||||||
|
self._store.set_phase_cursor(task_id, pc)
|
||||||
self._store.set_history(task_id, td.get("history", []))
|
self._store.set_history(task_id, td.get("history", []))
|
||||||
if task.adaptive_config:
|
if task.adaptive_config:
|
||||||
self._store.set_adaptive_config(task_id, task.adaptive_config)
|
self._store.set_adaptive_config(task_id, task.adaptive_config)
|
||||||
@@ -1061,8 +1075,10 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
# will consume below — go on the record, so completions from the
|
# will consume below — go on the record, so completions from the
|
||||||
# no-dialog surfaces (service, button, QR, to-do, voice, recovery)
|
# no-dialog surfaces (service, button, QR, to-do, voice, recovery)
|
||||||
# stay correctable through the history editor like dialog ones.
|
# stay correctable through the history editor like dialog ones.
|
||||||
|
from .helpers.phases import effective_field as _phase_effective
|
||||||
|
|
||||||
record_links = (
|
record_links = (
|
||||||
used_parts if used_parts is not None else (merged[task_id].get("consumes_parts") or [])
|
used_parts if used_parts is not None else (_phase_effective(merged[task_id], "consumes_parts") or [])
|
||||||
)
|
)
|
||||||
enriched_used: list[dict[str, Any]] | None = None
|
enriched_used: list[dict[str, Any]] | None = None
|
||||||
if used_parts is not None or record_links:
|
if used_parts is not None or record_links:
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ def _build_export_object(
|
|||||||
"required_completion_fields": tdata.get("required_completion_fields"),
|
"required_completion_fields": tdata.get("required_completion_fields"),
|
||||||
"rotation_strategy": tdata.get("rotation_strategy"),
|
"rotation_strategy": tdata.get("rotation_strategy"),
|
||||||
"reading_unit": tdata.get("reading_unit"),
|
"reading_unit": tdata.get("reading_unit"),
|
||||||
|
# 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.
|
||||||
|
"phases": tdata.get("phases"),
|
||||||
|
"phase_sequence": tdata.get("phase_sequence"),
|
||||||
|
"phase_cursor": tdata.get("phase_cursor"),
|
||||||
# Spare parts: consumption links + the auto-buy-task marker.
|
# Spare parts: consumption links + the auto-buy-task marker.
|
||||||
"consumes_parts": tdata.get("consumes_parts"),
|
"consumes_parts": tdata.get("consumes_parts"),
|
||||||
"part_ref": tdata.get("part_ref"),
|
"part_ref": tdata.get("part_ref"),
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export class MaintenanceCompleteDialog extends LitElement {
|
|||||||
* enforces the same list at every completion surface; blocking Save here
|
* enforces the same list at every completion surface; blocking Save here
|
||||||
* just means the user never has to meet that rejection. */
|
* just means the user never has to meet that rejection. */
|
||||||
@property({ type: Array }) public requiredFields: string[] = [];
|
@property({ type: Array }) public requiredFields: string[] = [];
|
||||||
|
/** #139: "2/4 · Flip blades" — which phase of the cycle this completion
|
||||||
|
* records. Purely informative; the backend advances the cursor itself. */
|
||||||
|
@property() public phaseLabel = "";
|
||||||
@state() private _open = false;
|
@state() private _open = false;
|
||||||
@state() private _notes = "";
|
@state() private _notes = "";
|
||||||
@state() private _cost = "";
|
@state() private _cost = "";
|
||||||
@@ -286,6 +289,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<ha-dialog open @closed=${this._close}>
|
<ha-dialog open @closed=${this._close}>
|
||||||
<div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
|
<div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
|
||||||
|
${this.phaseLabel ? html`<div class="phase-line">${t("phase_current", L)}: ${this.phaseLabel}</div>` : nothing}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||||
${this.checklist.length > 0 ? html`
|
${this.checklist.length > 0 ? html`
|
||||||
@@ -473,6 +477,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.phase-line {
|
||||||
|
margin-top: -8px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -260,6 +260,18 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
|
|
||||||
// Checklist (newline-separated steps, one per line)
|
// Checklist (newline-separated steps, one per line)
|
||||||
@state() private _checklistText = "";
|
@state() private _checklistText = "";
|
||||||
|
// Task phases (#139): editable defs + the cycle order. The v1 editor keeps
|
||||||
|
// each phase to name + checklist + at most ONE own-object part (the mower
|
||||||
|
// "14 new blades" case); richer per-phase config exists at the WS level.
|
||||||
|
// `carry` holds def fields the editor has no form for (notes,
|
||||||
|
// required_completion_fields, future keys) and `extraParts` any links
|
||||||
|
// beyond the first — both re-merged on save so a no-op edit never wipes
|
||||||
|
// them (#106 pattern; same passthrough idea as trigger_config's carry).
|
||||||
|
@state() private _phaseDefs: Array<{
|
||||||
|
id: string; name: string; checklistText: string; partId: string; partQty: string;
|
||||||
|
extraParts: TaskPartLink[]; carry: Record<string, unknown>;
|
||||||
|
}> = [];
|
||||||
|
@state() private _phaseSeq: string[] = [];
|
||||||
/** Details this task demands on completion — enforced by the backend on
|
/** Details this task demands on completion — enforced by the backend on
|
||||||
* every surface, so a button press or voice command cannot bypass it. */
|
* every surface, so a button press or voice command cannot bypass it. */
|
||||||
@state() private _requiredCompletion: string[] = [];
|
@state() private _requiredCompletion: string[] = [];
|
||||||
@@ -388,6 +400,19 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
this._rotationStrategy = task.rotation_strategy || "";
|
this._rotationStrategy = task.rotation_strategy || "";
|
||||||
|
|
||||||
this._checklistText = (task.checklist || []).join("\n");
|
this._checklistText = (task.checklist || []).join("\n");
|
||||||
|
this._phaseDefs = Object.entries(task.phases || {}).map(([id, def]) => {
|
||||||
|
const { name: _n, checklist: _c, consumes_parts: _p, ...carry } = def as unknown as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: def.name || id,
|
||||||
|
checklistText: (def.checklist || []).join("\n"),
|
||||||
|
partId: def.consumes_parts?.[0]?.part_id || "",
|
||||||
|
partQty: def.consumes_parts?.[0]?.quantity != null ? String(def.consumes_parts[0].quantity) : "",
|
||||||
|
extraParts: (def.consumes_parts || []).slice(1),
|
||||||
|
carry,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this._phaseSeq = [...(task.phase_sequence || [])];
|
||||||
this._requiredCompletion = [...(task.required_completion_fields || [])];
|
this._requiredCompletion = [...(task.required_completion_fields || [])];
|
||||||
this._scheduleTime = task.schedule_time || "";
|
this._scheduleTime = task.schedule_time || "";
|
||||||
|
|
||||||
@@ -506,6 +531,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
this._assigneePool = [];
|
this._assigneePool = [];
|
||||||
this._rotationStrategy = "";
|
this._rotationStrategy = "";
|
||||||
this._checklistText = "";
|
this._checklistText = "";
|
||||||
|
this._phaseDefs = [];
|
||||||
|
this._phaseSeq = [];
|
||||||
this._requiredCompletion = [];
|
this._requiredCompletion = [];
|
||||||
this._scheduleTime = "";
|
this._scheduleTime = "";
|
||||||
this._environmentalEntity = "";
|
this._environmentalEntity = "";
|
||||||
@@ -993,6 +1020,103 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
this._requiredCompletion = [...next];
|
this._requiredCompletion = [...next];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Task phases (#139) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private _phaseSlug(name: string): string {
|
||||||
|
const base = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "phase";
|
||||||
|
let slug = base;
|
||||||
|
let n = 2;
|
||||||
|
while (this._phaseDefs.some((d) => d.id === slug)) slug = `${base}-${n++}`;
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addPhaseDef(): void {
|
||||||
|
const id = this._phaseSlug(`phase-${this._phaseDefs.length + 1}`);
|
||||||
|
this._phaseDefs = [...this._phaseDefs, { id, name: "", checklistText: "", partId: "", partQty: "", extraParts: [], carry: {} }];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removePhaseDef(id: string): void {
|
||||||
|
this._phaseDefs = this._phaseDefs.filter((d) => d.id !== id);
|
||||||
|
this._phaseSeq = this._phaseSeq.filter((s) => s !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _patchPhaseDef(id: string, patch: Partial<{ name: string; checklistText: string; partId: string; partQty: string }>): void {
|
||||||
|
this._phaseDefs = this._phaseDefs.map((d) => (d.id === id ? { ...d, ...patch } : d));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderPhasesEditor(L: string) {
|
||||||
|
const defName = (id: string) => this._phaseDefs.find((d) => d.id === id)?.name || id;
|
||||||
|
return html`
|
||||||
|
<h3>${t("phases_section", L)}</h3>
|
||||||
|
<div class="field-help">${t("phases_hint", L)}</div>
|
||||||
|
${this._phaseDefs.map((d) => html`
|
||||||
|
<div class="phase-def">
|
||||||
|
<div class="phase-def-head">
|
||||||
|
<ms-textfield
|
||||||
|
label="${t("phase_name", L)}"
|
||||||
|
.value=${d.name}
|
||||||
|
@input=${(e: Event) => this._patchPhaseDef(d.id, { name: (e.target as HTMLInputElement).value })}
|
||||||
|
></ms-textfield>
|
||||||
|
${this.parts.length ? html`
|
||||||
|
<select
|
||||||
|
class="phase-part"
|
||||||
|
.value=${d.partId}
|
||||||
|
@change=${(e: Event) => this._patchPhaseDef(d.id, { partId: (e.target as HTMLSelectElement).value })}
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
${this.parts.map((p) => html`<option value=${p.id} ?selected=${p.id === d.partId}>${p.name}</option>`)}
|
||||||
|
</select>
|
||||||
|
${d.partId ? html`
|
||||||
|
<input class="phase-qty" type="number" min="0.01" step="0.01" .value=${d.partQty || "1"}
|
||||||
|
@input=${(e: Event) => this._patchPhaseDef(d.id, { partQty: (e.target as HTMLInputElement).value })} />
|
||||||
|
` : nothing}
|
||||||
|
` : nothing}
|
||||||
|
<mwc-icon-button class="phase-remove" @click=${() => this._removePhaseDef(d.id)}>
|
||||||
|
<ha-icon icon="mdi:delete-outline"></ha-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</div>
|
||||||
|
${this.checklistsEnabled ? html`
|
||||||
|
<textarea
|
||||||
|
class="checklist-textarea phase-checklist"
|
||||||
|
rows="2"
|
||||||
|
placeholder="${t("checklist_placeholder", L)}"
|
||||||
|
.value=${d.checklistText}
|
||||||
|
@input=${(e: Event) => this._patchPhaseDef(d.id, { checklistText: (e.target as HTMLTextAreaElement).value })}
|
||||||
|
></textarea>
|
||||||
|
` : nothing}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
<ha-button appearance="plain" @click=${this._addPhaseDef}>
|
||||||
|
<ha-icon icon="mdi:plus"></ha-icon> ${t("phase_add", L)}
|
||||||
|
</ha-button>
|
||||||
|
${this._phaseDefs.some((d) => d.name.trim()) ? html`
|
||||||
|
<div class="phase-seq-label">${t("phase_sequence_label", L)}</div>
|
||||||
|
<div class="phase-seq">
|
||||||
|
${this._phaseSeq.map((id, i) => html`
|
||||||
|
<span class="phase-chip">
|
||||||
|
${i + 1}. ${defName(id)}
|
||||||
|
<button class="phase-chip-x" @click=${() => {
|
||||||
|
this._phaseSeq = this._phaseSeq.filter((_, idx) => idx !== i);
|
||||||
|
}}>✕</button>
|
||||||
|
</span>
|
||||||
|
`)}
|
||||||
|
<select
|
||||||
|
class="phase-seq-add"
|
||||||
|
.value=${""}
|
||||||
|
@change=${(e: Event) => {
|
||||||
|
const id = (e.target as HTMLSelectElement).value;
|
||||||
|
if (id) this._phaseSeq = [...this._phaseSeq, id];
|
||||||
|
(e.target as HTMLSelectElement).value = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">+ ${t("phase_sequence_add_step", L)}</option>
|
||||||
|
${this._phaseDefs.filter((d) => d.name.trim()).map((d) => html`<option value=${d.id}>${d.name}</option>`)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
` : nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private async _save(): Promise<void> {
|
private async _save(): Promise<void> {
|
||||||
if (this._loading) return; // synchronous re-entry guard (double-click)
|
if (this._loading) return; // synchronous re-entry guard (double-click)
|
||||||
if (!this._name.trim()) return;
|
if (!this._name.trim()) return;
|
||||||
@@ -1073,6 +1197,27 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
data.last_performed = this._lastPerformed || null;
|
data.last_performed = this._lastPerformed || null;
|
||||||
data.nfc_tag_id = this._nfcTagId || null;
|
data.nfc_tag_id = this._nfcTagId || null;
|
||||||
data.reading_unit = this._readingUnit.trim() || null;
|
data.reading_unit = this._readingUnit.trim() || null;
|
||||||
|
// Task phases (#139): always sent — null clears a removed cycle.
|
||||||
|
{
|
||||||
|
const defs: Record<string, unknown> = {};
|
||||||
|
for (const d of this._phaseDefs) {
|
||||||
|
if (!d.name.trim()) continue;
|
||||||
|
const def: Record<string, unknown> = { ...d.carry, name: d.name.trim() };
|
||||||
|
const items = d.checklistText.split("\n").map((s) => s.trim()).filter(Boolean);
|
||||||
|
if (items.length) def.checklist = items;
|
||||||
|
const links: TaskPartLink[] = [];
|
||||||
|
if (d.partId) {
|
||||||
|
const qty = parseFloat(d.partQty);
|
||||||
|
links.push({ part_id: d.partId, quantity: Number.isFinite(qty) && qty > 0 ? qty : 1 });
|
||||||
|
}
|
||||||
|
links.push(...d.extraParts);
|
||||||
|
if (links.length) def.consumes_parts = links;
|
||||||
|
defs[d.id] = def;
|
||||||
|
}
|
||||||
|
const seq = this._phaseSeq.filter((id) => id in defs);
|
||||||
|
data.phases = Object.keys(defs).length && seq.length ? defs : null;
|
||||||
|
data.phase_sequence = data.phases ? seq : null;
|
||||||
|
}
|
||||||
// Only send when a picker was actually rendered. A failed parts load
|
// Only send when a picker was actually rendered. A failed parts load
|
||||||
// leaves both lists empty, and sending [] then would silently wipe links
|
// leaves both lists empty, and sending [] then would silently wipe links
|
||||||
// the user never saw. entry_id is written ONLY for a foreign pick, so an
|
// the user never saw. entry_id is written ONLY for a foreign pick, so an
|
||||||
@@ -2463,6 +2608,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
></textarea>
|
></textarea>
|
||||||
<div class="field-help">${t("checklist_help", L)}</div>
|
<div class="field-help">${t("checklist_help", L)}</div>
|
||||||
` : nothing}
|
` : nothing}
|
||||||
|
${this._renderPhasesEditor(L)}
|
||||||
<h3>${t("require_on_completion", L)}</h3>
|
<h3>${t("require_on_completion", L)}</h3>
|
||||||
<div class="required-completion">
|
<div class="required-completion">
|
||||||
${REQUIRED_COMPLETION_KEYS.map((field) => html`
|
${REQUIRED_COMPLETION_KEYS.map((field) => html`
|
||||||
@@ -2742,6 +2888,82 @@ export class MaintenanceTaskDialog extends LitElement {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
.phase-def {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
.phase-def-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.phase-def-head ms-textfield {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.phase-part {
|
||||||
|
max-width: 160px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.phase-qty {
|
||||||
|
width: 64px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.phase-checklist {
|
||||||
|
min-height: 56px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.phase-remove {
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.phase-seq-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.phase-seq {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.phase-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.phase-chip-x {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.phase-seq-add {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px dashed var(--divider-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.consumes-check {
|
.consumes-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+13
-4
@@ -18,6 +18,7 @@ import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInter
|
|||||||
import { describeWsError } from "../ws-errors";
|
import { describeWsError } from "../ws-errors";
|
||||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||||
import { partsForCompletion } from "../helpers/shared-parts";
|
import { partsForCompletion } from "../helpers/shared-parts";
|
||||||
|
import { effectivePhase, phaseLabel } from "../helpers/phases";
|
||||||
import { renderWeibullSection } from "../renderers/weibull";
|
import { renderWeibullSection } from "../renderers/weibull";
|
||||||
import { renderPredictionSection } from "../renderers/prediction";
|
import { renderPredictionSection } from "../renderers/prediction";
|
||||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||||
@@ -158,13 +159,17 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
|||||||
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
|
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
|
||||||
const task = this._task!;
|
const task = this._task!;
|
||||||
const isBuy = !!(task as { part_ref?: string }).part_ref;
|
const isBuy = !!(task as { part_ref?: string }).part_ref;
|
||||||
|
// #139: a phased task completes the phase currently due, so the phase's
|
||||||
|
// checklist/parts/required fields replace the task-level ones here.
|
||||||
|
const phase = effectivePhase(task);
|
||||||
|
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||||
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
|
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
|
||||||
if (!isBuy) {
|
if (!isBuy) {
|
||||||
try {
|
try {
|
||||||
const r = await this.hass.connection.sendMessagePromise<{
|
const r = await this.hass.connection.sendMessagePromise<{
|
||||||
objects: MaintenanceObjectResponse[];
|
objects: MaintenanceObjectResponse[];
|
||||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||||
parts = partsForCompletion(task, this._entryId!, r.objects || [], this._lang);
|
parts = partsForCompletion({ consumes_parts: links }, this._entryId!, r.objects || [], this._lang);
|
||||||
} catch {
|
} catch {
|
||||||
// Parts stay empty — the dialog still completes without them.
|
// Parts stay empty — the dialog still completes without them.
|
||||||
}
|
}
|
||||||
@@ -173,13 +178,14 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
|||||||
entry_id: this._entryId!,
|
entry_id: this._entryId!,
|
||||||
task_id: this._taskId!,
|
task_id: this._taskId!,
|
||||||
task_name: task.name,
|
task_name: task.name,
|
||||||
checklist: task.checklist || [],
|
checklist: phase ? phase.checklist : (task.checklist || []),
|
||||||
adaptive_enabled: !!task.adaptive_config?.enabled,
|
adaptive_enabled: !!task.adaptive_config?.enabled,
|
||||||
required_completion_fields: task.required_completion_fields || [],
|
required_completion_fields: phase ? phase.requiredFields : (task.required_completion_fields || []),
|
||||||
task_type: task.type || "",
|
task_type: task.type || "",
|
||||||
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
|
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
|
||||||
parts,
|
parts,
|
||||||
consumes_parts: isBuy ? [] : (task.consumes_parts || []),
|
consumes_parts: isBuy ? [] : links,
|
||||||
|
phase_label: phaseLabel(task),
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this._notifyChanged("complete");
|
this._notifyChanged("complete");
|
||||||
@@ -542,6 +548,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
|||||||
${(task.schedule?.kind && !["manual", "one_time"].includes(task.schedule.kind)) || task.interval_days != null
|
${(task.schedule?.kind && !["manual", "one_time"].includes(task.schedule.kind)) || task.interval_days != null
|
||||||
? html`<span><strong>${t("interval", L) || "Interval"}:</strong> ${formatRecurrence(task, L)}</span>`
|
? html`<span><strong>${t("interval", L) || "Interval"}:</strong> ${formatRecurrence(task, L)}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${phaseLabel(task)
|
||||||
|
? html`<span><strong>${t("phase_current", L)}:</strong> ${phaseLabel(task)}</span>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ export function openCompleteDialog(args: {
|
|||||||
/** #99/#111: per-completion parts selection incl. shared pools. */
|
/** #99/#111: per-completion parts selection incl. shared pools. */
|
||||||
parts?: MaintenanceCompleteDialog["parts"];
|
parts?: MaintenanceCompleteDialog["parts"];
|
||||||
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
|
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
|
||||||
|
/** #139: "2/4 · Flip blades" — the phase this completion records. */
|
||||||
|
phase_label?: string;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
||||||
if (!syncHass(dlg)) return false;
|
if (!syncHass(dlg)) return false;
|
||||||
@@ -304,6 +306,7 @@ export function openCompleteDialog(args: {
|
|||||||
dlg.readingUnit = args.reading_unit ?? "";
|
dlg.readingUnit = args.reading_unit ?? "";
|
||||||
dlg.parts = args.parts ?? [];
|
dlg.parts = args.parts ?? [];
|
||||||
dlg.consumesParts = args.consumes_parts ?? [];
|
dlg.consumesParts = args.consumes_parts ?? [];
|
||||||
|
dlg.phaseLabel = args.phase_label ?? "";
|
||||||
dlg.lang = (getHass()?.language) || "en";
|
dlg.lang = (getHass()?.language) || "en";
|
||||||
dlg.open();
|
dlg.open();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "záznamů",
|
"service_record_entries": "záznamů",
|
||||||
"completed_by": "Dokončil",
|
"completed_by": "Dokončil",
|
||||||
"date_from": "Od",
|
"date_from": "Od",
|
||||||
"date_to": "Do"
|
"date_to": "Do",
|
||||||
|
"phases_section": "Fáze cyklu (volitelné)",
|
||||||
|
"phases_hint": "Různé práce ve stejném rytmu — každé dokončení posune na další krok (např. malý, malý, velký servis).",
|
||||||
|
"phase_add": "Přidat fázi",
|
||||||
|
"phase_name": "Název fáze",
|
||||||
|
"phase_sequence_label": "Pořadí cyklu",
|
||||||
|
"phase_sequence_add_step": "Přidat krok",
|
||||||
|
"phase_current": "Aktuální fáze",
|
||||||
|
"phase_set": "Nastavit jako aktuální",
|
||||||
|
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "poster",
|
"service_record_entries": "poster",
|
||||||
"completed_by": "Udført af",
|
"completed_by": "Udført af",
|
||||||
"date_from": "Fra",
|
"date_from": "Fra",
|
||||||
"date_to": "Til"
|
"date_to": "Til",
|
||||||
|
"phases_section": "Cyklusfaser (valgfrit)",
|
||||||
|
"phases_hint": "Forskelligt arbejde i samme rytme — hver udførelse går videre til næste trin (f.eks. lille, lille, stor service).",
|
||||||
|
"phase_add": "Tilføj fase",
|
||||||
|
"phase_name": "Fasenavn",
|
||||||
|
"phase_sequence_label": "Cyklusrækkefølge",
|
||||||
|
"phase_sequence_add_step": "Tilføj trin",
|
||||||
|
"phase_current": "Aktuel fase",
|
||||||
|
"phase_set": "Sæt som aktuel",
|
||||||
|
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "Einträge",
|
"service_record_entries": "Einträge",
|
||||||
"completed_by": "Erledigt von",
|
"completed_by": "Erledigt von",
|
||||||
"date_from": "Von",
|
"date_from": "Von",
|
||||||
"date_to": "Bis"
|
"date_to": "Bis",
|
||||||
|
"phases_section": "Zyklus-Phasen (optional)",
|
||||||
|
"phases_hint": "Unterschiedliche Arbeiten im gleichen Takt — jede Erledigung schaltet zum nächsten Schritt (z. B. klein, klein, groß).",
|
||||||
|
"phase_add": "Phase hinzufügen",
|
||||||
|
"phase_name": "Phasenname",
|
||||||
|
"phase_sequence_label": "Zyklus-Reihenfolge",
|
||||||
|
"phase_sequence_add_step": "Schritt anhängen",
|
||||||
|
"phase_current": "Aktuelle Phase",
|
||||||
|
"phase_set": "Als aktuell setzen",
|
||||||
|
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entries",
|
"service_record_entries": "entries",
|
||||||
"completed_by": "Completed by",
|
"completed_by": "Completed by",
|
||||||
"date_from": "From",
|
"date_from": "From",
|
||||||
"date_to": "To"
|
"date_to": "To",
|
||||||
|
"phases_section": "Cycle phases (optional)",
|
||||||
|
"phases_hint": "Different work on one shared schedule — each completion moves to the next step (e.g. small service, small service, big service).",
|
||||||
|
"phase_add": "Add phase",
|
||||||
|
"phase_name": "Phase name",
|
||||||
|
"phase_sequence_label": "Cycle order",
|
||||||
|
"phase_sequence_add_step": "Add step",
|
||||||
|
"phase_current": "Current phase",
|
||||||
|
"phase_set": "Set as current",
|
||||||
|
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entradas",
|
"service_record_entries": "entradas",
|
||||||
"completed_by": "Realizado por",
|
"completed_by": "Realizado por",
|
||||||
"date_from": "Desde",
|
"date_from": "Desde",
|
||||||
"date_to": "Hasta"
|
"date_to": "Hasta",
|
||||||
|
"phases_section": "Fases del ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabajos distintos con un mismo ritmo: cada finalización pasa al siguiente paso (p. ej. pequeño, pequeño, grande).",
|
||||||
|
"phase_add": "Añadir fase",
|
||||||
|
"phase_name": "Nombre de la fase",
|
||||||
|
"phase_sequence_label": "Orden del ciclo",
|
||||||
|
"phase_sequence_add_step": "Añadir paso",
|
||||||
|
"phase_current": "Fase actual",
|
||||||
|
"phase_set": "Establecer como actual",
|
||||||
|
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "merkintää",
|
"service_record_entries": "merkintää",
|
||||||
"completed_by": "Suorittanut",
|
"completed_by": "Suorittanut",
|
||||||
"date_from": "Alkaen",
|
"date_from": "Alkaen",
|
||||||
"date_to": "Asti"
|
"date_to": "Asti",
|
||||||
|
"phases_section": "Syklin vaiheet (valinnainen)",
|
||||||
|
"phases_hint": "Eri työt samassa tahdissa — jokainen suoritus siirtyy seuraavaan vaiheeseen (esim. pieni, pieni, iso huolto).",
|
||||||
|
"phase_add": "Lisää vaihe",
|
||||||
|
"phase_name": "Vaiheen nimi",
|
||||||
|
"phase_sequence_label": "Syklin järjestys",
|
||||||
|
"phase_sequence_add_step": "Lisää askel",
|
||||||
|
"phase_current": "Nykyinen vaihe",
|
||||||
|
"phase_set": "Aseta nykyiseksi",
|
||||||
|
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entrées",
|
"service_record_entries": "entrées",
|
||||||
"completed_by": "Réalisé par",
|
"completed_by": "Réalisé par",
|
||||||
"date_from": "Du",
|
"date_from": "Du",
|
||||||
"date_to": "Au"
|
"date_to": "Au",
|
||||||
|
"phases_section": "Phases du cycle (optionnel)",
|
||||||
|
"phases_hint": "Des travaux différents sur un même rythme — chaque réalisation passe à l'étape suivante (p. ex. petit, petit, grand entretien).",
|
||||||
|
"phase_add": "Ajouter une phase",
|
||||||
|
"phase_name": "Nom de la phase",
|
||||||
|
"phase_sequence_label": "Ordre du cycle",
|
||||||
|
"phase_sequence_add_step": "Ajouter une étape",
|
||||||
|
"phase_current": "Phase actuelle",
|
||||||
|
"phase_set": "Définir comme actuelle",
|
||||||
|
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "प्रविष्टियाँ",
|
"service_record_entries": "प्रविष्टियाँ",
|
||||||
"completed_by": "द्वारा पूर्ण",
|
"completed_by": "द्वारा पूर्ण",
|
||||||
"date_from": "से",
|
"date_from": "से",
|
||||||
"date_to": "तक"
|
"date_to": "तक",
|
||||||
|
"phases_section": "चक्र चरण (वैकल्पिक)",
|
||||||
|
"phases_hint": "एक ही लय में अलग-अलग कार्य — प्रत्येक पूर्णता अगले चरण पर ले जाती है (जैसे छोटा, छोटा, बड़ा)।",
|
||||||
|
"phase_add": "चरण जोड़ें",
|
||||||
|
"phase_name": "चरण का नाम",
|
||||||
|
"phase_sequence_label": "चक्र क्रम",
|
||||||
|
"phase_sequence_add_step": "कदम जोड़ें",
|
||||||
|
"phase_current": "वर्तमान चरण",
|
||||||
|
"phase_set": "वर्तमान बनाएँ",
|
||||||
|
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "bejegyzés",
|
"service_record_entries": "bejegyzés",
|
||||||
"completed_by": "Elvégezte",
|
"completed_by": "Elvégezte",
|
||||||
"date_from": "Ettől",
|
"date_from": "Ettől",
|
||||||
"date_to": "Eddig"
|
"date_to": "Eddig",
|
||||||
|
"phases_section": "Ciklusfázisok (opcionális)",
|
||||||
|
"phases_hint": "Különböző munkák azonos ütemben — minden elvégzés a következő lépésre vált (pl. kis, kis, nagy szerviz).",
|
||||||
|
"phase_add": "Fázis hozzáadása",
|
||||||
|
"phase_name": "Fázis neve",
|
||||||
|
"phase_sequence_label": "Ciklus sorrendje",
|
||||||
|
"phase_sequence_add_step": "Lépés hozzáadása",
|
||||||
|
"phase_current": "Aktuális fázis",
|
||||||
|
"phase_set": "Beállítás aktuálisként",
|
||||||
|
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "voci",
|
"service_record_entries": "voci",
|
||||||
"completed_by": "Completato da",
|
"completed_by": "Completato da",
|
||||||
"date_from": "Dal",
|
"date_from": "Dal",
|
||||||
"date_to": "Al"
|
"date_to": "Al",
|
||||||
|
"phases_section": "Fasi del ciclo (opzionale)",
|
||||||
|
"phases_hint": "Lavori diversi sullo stesso ritmo — ogni completamento passa al passo successivo (es. piccolo, piccolo, grande).",
|
||||||
|
"phase_add": "Aggiungi fase",
|
||||||
|
"phase_name": "Nome della fase",
|
||||||
|
"phase_sequence_label": "Ordine del ciclo",
|
||||||
|
"phase_sequence_add_step": "Aggiungi passo",
|
||||||
|
"phase_current": "Fase attuale",
|
||||||
|
"phase_set": "Imposta come attuale",
|
||||||
|
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "件",
|
"service_record_entries": "件",
|
||||||
"completed_by": "実施者",
|
"completed_by": "実施者",
|
||||||
"date_from": "開始",
|
"date_from": "開始",
|
||||||
"date_to": "終了"
|
"date_to": "終了",
|
||||||
|
"phases_section": "サイクルフェーズ(任意)",
|
||||||
|
"phases_hint": "同じ周期で異なる作業を行います。完了するたびに次のステップへ進みます(例:小・小・大整備)。",
|
||||||
|
"phase_add": "フェーズを追加",
|
||||||
|
"phase_name": "フェーズ名",
|
||||||
|
"phase_sequence_label": "サイクルの順序",
|
||||||
|
"phase_sequence_add_step": "ステップを追加",
|
||||||
|
"phase_current": "現在のフェーズ",
|
||||||
|
"phase_set": "現在に設定",
|
||||||
|
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "건",
|
"service_record_entries": "건",
|
||||||
"completed_by": "수행자",
|
"completed_by": "수행자",
|
||||||
"date_from": "시작",
|
"date_from": "시작",
|
||||||
"date_to": "종료"
|
"date_to": "종료",
|
||||||
|
"phases_section": "주기 단계(선택)",
|
||||||
|
"phases_hint": "같은 주기에 다른 작업을 수행합니다. 완료할 때마다 다음 단계로 넘어갑니다(예: 소·소·대 정비).",
|
||||||
|
"phase_add": "단계 추가",
|
||||||
|
"phase_name": "단계 이름",
|
||||||
|
"phase_sequence_label": "주기 순서",
|
||||||
|
"phase_sequence_add_step": "스텝 추가",
|
||||||
|
"phase_current": "현재 단계",
|
||||||
|
"phase_set": "현재로 설정",
|
||||||
|
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "oppføringer",
|
"service_record_entries": "oppføringer",
|
||||||
"completed_by": "Utført av",
|
"completed_by": "Utført av",
|
||||||
"date_from": "Fra",
|
"date_from": "Fra",
|
||||||
"date_to": "Til"
|
"date_to": "Til",
|
||||||
|
"phases_section": "Syklusfaser (valgfritt)",
|
||||||
|
"phases_hint": "Ulikt arbeid i samme rytme — hver fullføring går videre til neste trinn (f.eks. liten, liten, stor service).",
|
||||||
|
"phase_add": "Legg til fase",
|
||||||
|
"phase_name": "Fasenavn",
|
||||||
|
"phase_sequence_label": "Syklusrekkefølge",
|
||||||
|
"phase_sequence_add_step": "Legg til trinn",
|
||||||
|
"phase_current": "Gjeldende fase",
|
||||||
|
"phase_set": "Sett som gjeldende",
|
||||||
|
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "items",
|
"service_record_entries": "items",
|
||||||
"completed_by": "Voltooid door",
|
"completed_by": "Voltooid door",
|
||||||
"date_from": "Van",
|
"date_from": "Van",
|
||||||
"date_to": "Tot"
|
"date_to": "Tot",
|
||||||
|
"phases_section": "Cyclusfasen (optioneel)",
|
||||||
|
"phases_hint": "Verschillend werk op één ritme — elke afronding schuift naar de volgende stap (bijv. klein, klein, groot).",
|
||||||
|
"phase_add": "Fase toevoegen",
|
||||||
|
"phase_name": "Fasenaam",
|
||||||
|
"phase_sequence_label": "Cyclusvolgorde",
|
||||||
|
"phase_sequence_add_step": "Stap toevoegen",
|
||||||
|
"phase_current": "Huidige fase",
|
||||||
|
"phase_set": "Instellen als huidige",
|
||||||
|
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "wpisów",
|
"service_record_entries": "wpisów",
|
||||||
"completed_by": "Wykonane przez",
|
"completed_by": "Wykonane przez",
|
||||||
"date_from": "Od",
|
"date_from": "Od",
|
||||||
"date_to": "Do"
|
"date_to": "Do",
|
||||||
|
"phases_section": "Fazy cyklu (opcjonalnie)",
|
||||||
|
"phases_hint": "Różne prace w jednym rytmie — każde ukończenie przechodzi do następnego kroku (np. mały, mały, duży).",
|
||||||
|
"phase_add": "Dodaj fazę",
|
||||||
|
"phase_name": "Nazwa fazy",
|
||||||
|
"phase_sequence_label": "Kolejność cyklu",
|
||||||
|
"phase_sequence_add_step": "Dodaj krok",
|
||||||
|
"phase_current": "Bieżąca faza",
|
||||||
|
"phase_set": "Ustaw jako bieżącą",
|
||||||
|
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "registros",
|
"service_record_entries": "registros",
|
||||||
"completed_by": "Concluído por",
|
"completed_by": "Concluído por",
|
||||||
"date_from": "De",
|
"date_from": "De",
|
||||||
"date_to": "Até"
|
"date_to": "Até",
|
||||||
|
"phases_section": "Fases do ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabalhos diferentes no mesmo ritmo — cada conclusão avança para o próximo passo (ex.: pequeno, pequeno, grande).",
|
||||||
|
"phase_add": "Adicionar fase",
|
||||||
|
"phase_name": "Nome da fase",
|
||||||
|
"phase_sequence_label": "Ordem do ciclo",
|
||||||
|
"phase_sequence_add_step": "Adicionar passo",
|
||||||
|
"phase_current": "Fase atual",
|
||||||
|
"phase_set": "Definir como atual",
|
||||||
|
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "registos",
|
"service_record_entries": "registos",
|
||||||
"completed_by": "Concluído por",
|
"completed_by": "Concluído por",
|
||||||
"date_from": "De",
|
"date_from": "De",
|
||||||
"date_to": "Até"
|
"date_to": "Até",
|
||||||
|
"phases_section": "Fases do ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabalhos diferentes no mesmo ritmo — cada conclusão avança para o passo seguinte (ex.: pequeno, pequeno, grande).",
|
||||||
|
"phase_add": "Adicionar fase",
|
||||||
|
"phase_name": "Nome da fase",
|
||||||
|
"phase_sequence_label": "Ordem do ciclo",
|
||||||
|
"phase_sequence_add_step": "Adicionar passo",
|
||||||
|
"phase_current": "Fase atual",
|
||||||
|
"phase_set": "Definir como atual",
|
||||||
|
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "записей",
|
"service_record_entries": "записей",
|
||||||
"completed_by": "Выполнил",
|
"completed_by": "Выполнил",
|
||||||
"date_from": "С",
|
"date_from": "С",
|
||||||
"date_to": "По"
|
"date_to": "По",
|
||||||
|
"phases_section": "Фазы цикла (необязательно)",
|
||||||
|
"phases_hint": "Разные работы в одном ритме — каждое выполнение переходит к следующему шагу (напр., малое, малое, большое ТО).",
|
||||||
|
"phase_add": "Добавить фазу",
|
||||||
|
"phase_name": "Название фазы",
|
||||||
|
"phase_sequence_label": "Порядок цикла",
|
||||||
|
"phase_sequence_add_step": "Добавить шаг",
|
||||||
|
"phase_current": "Текущая фаза",
|
||||||
|
"phase_set": "Сделать текущей",
|
||||||
|
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "poster",
|
"service_record_entries": "poster",
|
||||||
"completed_by": "Utförd av",
|
"completed_by": "Utförd av",
|
||||||
"date_from": "Från",
|
"date_from": "Från",
|
||||||
"date_to": "Till"
|
"date_to": "Till",
|
||||||
|
"phases_section": "Cykelfaser (valfritt)",
|
||||||
|
"phases_hint": "Olika arbeten i samma rytm — varje slutförande går vidare till nästa steg (t.ex. liten, liten, stor service).",
|
||||||
|
"phase_add": "Lägg till fas",
|
||||||
|
"phase_name": "Fasnamn",
|
||||||
|
"phase_sequence_label": "Cykelordning",
|
||||||
|
"phase_sequence_add_step": "Lägg till steg",
|
||||||
|
"phase_current": "Aktuell fas",
|
||||||
|
"phase_set": "Ange som aktuell",
|
||||||
|
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "kayıt",
|
"service_record_entries": "kayıt",
|
||||||
"completed_by": "Tamamlayan",
|
"completed_by": "Tamamlayan",
|
||||||
"date_from": "Başlangıç",
|
"date_from": "Başlangıç",
|
||||||
"date_to": "Bitiş"
|
"date_to": "Bitiş",
|
||||||
|
"phases_section": "Döngü aşamaları (isteğe bağlı)",
|
||||||
|
"phases_hint": "Aynı ritimde farklı işler — her tamamlama bir sonraki adıma geçer (örn. küçük, küçük, büyük bakım).",
|
||||||
|
"phase_add": "Aşama ekle",
|
||||||
|
"phase_name": "Aşama adı",
|
||||||
|
"phase_sequence_label": "Döngü sırası",
|
||||||
|
"phase_sequence_add_step": "Adım ekle",
|
||||||
|
"phase_current": "Geçerli aşama",
|
||||||
|
"phase_set": "Geçerli olarak ayarla",
|
||||||
|
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "записів",
|
"service_record_entries": "записів",
|
||||||
"completed_by": "Виконав",
|
"completed_by": "Виконав",
|
||||||
"date_from": "Від",
|
"date_from": "Від",
|
||||||
"date_to": "До"
|
"date_to": "До",
|
||||||
|
"phases_section": "Фази циклу (необов'язково)",
|
||||||
|
"phases_hint": "Різні роботи в одному ритмі — кожне виконання переходить до наступного кроку (напр., мале, мале, велике ТО).",
|
||||||
|
"phase_add": "Додати фазу",
|
||||||
|
"phase_name": "Назва фази",
|
||||||
|
"phase_sequence_label": "Порядок циклу",
|
||||||
|
"phase_sequence_add_step": "Додати крок",
|
||||||
|
"phase_current": "Поточна фаза",
|
||||||
|
"phase_set": "Зробити поточною",
|
||||||
|
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "条记录",
|
"service_record_entries": "条记录",
|
||||||
"completed_by": "完成人",
|
"completed_by": "完成人",
|
||||||
"date_from": "从",
|
"date_from": "从",
|
||||||
"date_to": "至"
|
"date_to": "至",
|
||||||
|
"phases_section": "循环阶段(可选)",
|
||||||
|
"phases_hint": "同一节奏下的不同工作——每次完成后进入下一步(例如:小保养、小保养、大保养)。",
|
||||||
|
"phase_add": "添加阶段",
|
||||||
|
"phase_name": "阶段名称",
|
||||||
|
"phase_sequence_label": "循环顺序",
|
||||||
|
"phase_sequence_add_step": "添加步骤",
|
||||||
|
"phase_current": "当前阶段",
|
||||||
|
"phase_set": "设为当前",
|
||||||
|
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { UserService } from "./user-service";
|
import { UserService } from "./user-service";
|
||||||
import { partsForCompletion } from "./helpers/shared-parts";
|
import { partsForCompletion } from "./helpers/shared-parts";
|
||||||
|
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||||
import "./maintenance-card-editor";
|
import "./maintenance-card-editor";
|
||||||
import "./components/complete-dialog";
|
import "./components/complete-dialog";
|
||||||
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
||||||
@@ -469,7 +470,7 @@ export class MaintenanceSupporterCard extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${!compact
|
${!compact
|
||||||
? html`<div class="task-meta">
|
? html`<div class="task-meta">
|
||||||
${object_name} · ${t(task.type, L)}${this._assigneeName(task)
|
${object_name} · ${t(task.type, L)}${phaseLabel(task) ? html` · ${phaseLabel(task)}` : nothing}${this._assigneeName(task)
|
||||||
? html` · <span class="assignee"
|
? html` · <span class="assignee"
|
||||||
><ha-icon icon="mdi:account"></ha-icon>${this._assigneeName(task)}</span
|
><ha-icon icon="mdi:account"></ha-icon>${this._assigneeName(task)}</span
|
||||||
>`
|
>`
|
||||||
@@ -514,14 +515,19 @@ export class MaintenanceSupporterCard extends LitElement {
|
|||||||
// Stop the row's open-task handler from also firing
|
// Stop the row's open-task handler from also firing
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any;
|
const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any;
|
||||||
|
// #139: a phased task completes the phase
|
||||||
|
// currently due — its fields override the
|
||||||
|
// task-level ones (fallthrough included).
|
||||||
|
const phase = effectivePhase(task);
|
||||||
dlg.entryId = entry_id;
|
dlg.entryId = entry_id;
|
||||||
dlg.taskId = task.id;
|
dlg.taskId = task.id;
|
||||||
dlg.taskName = task.name;
|
dlg.taskName = task.name;
|
||||||
dlg.checklist = task.checklist || [];
|
dlg.checklist = phase ? phase.checklist : (task.checklist || []);
|
||||||
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
|
dlg.adaptiveEnabled = !!task.adaptive_config?.enabled;
|
||||||
dlg.taskType = task.type || "";
|
dlg.taskType = task.type || "";
|
||||||
dlg.readingUnit = (task as any).reading_unit || "";
|
dlg.readingUnit = (task as any).reading_unit || "";
|
||||||
dlg.requiredFields = task.required_completion_fields || [];
|
dlg.requiredFields = phase ? phase.requiredFields : (task.required_completion_fields || []);
|
||||||
|
dlg.phaseLabel = phaseLabel(task);
|
||||||
dlg.lang = L;
|
dlg.lang = L;
|
||||||
// #99: editable per-completion parts selection
|
// #99: editable per-completion parts selection
|
||||||
// (skip on buy tasks — those restock instead).
|
// (skip on buy tasks — those restock instead).
|
||||||
@@ -530,8 +536,9 @@ export class MaintenanceSupporterCard extends LitElement {
|
|||||||
// owner — resolving against the object's own
|
// owner — resolving against the object's own
|
||||||
// parts alone left a foreign link invisible.
|
// parts alone left a foreign link invisible.
|
||||||
const isBuy = !!(task as any).part_ref;
|
const isBuy = !!(task as any).part_ref;
|
||||||
dlg.parts = isBuy ? [] : partsForCompletion(task, entry_id, this._objects, L);
|
const links = phase ? phase.consumesParts : (task.consumes_parts || []);
|
||||||
dlg.consumesParts = isBuy ? [] : (task.consumes_parts || []);
|
dlg.parts = isBuy ? [] : partsForCompletion({ consumes_parts: links }, entry_id, this._objects, L);
|
||||||
|
dlg.consumesParts = isBuy ? [] : links;
|
||||||
dlg.open();
|
dlg.open();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "
|
|||||||
import { downloadTextFile } from "./helpers/download";
|
import { downloadTextFile } from "./helpers/download";
|
||||||
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
|
import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet";
|
||||||
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
|
import { describePartLink, partsForCompletion } from "./helpers/shared-parts";
|
||||||
|
import { effectivePhase, phaseLabel } from "./helpers/phases";
|
||||||
import { describeWsError } from "./ws-errors";
|
import { describeWsError } from "./ws-errors";
|
||||||
import { panelStyles } from "./panel-styles";
|
import { panelStyles } from "./panel-styles";
|
||||||
import type {
|
import type {
|
||||||
@@ -1861,7 +1862,9 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
const task = obj?.tasks.find((x) => x.id === taskId);
|
const task = obj?.tasks.find((x) => x.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
const state: Record<string, boolean> = {};
|
const state: Record<string, boolean> = {};
|
||||||
for (const step of task.checklist || []) {
|
// #139: a phased task ticks the checklist of the phase currently due.
|
||||||
|
const steps = effectivePhase(task)?.checklist ?? (task.checklist || []);
|
||||||
|
for (const step of steps) {
|
||||||
const current = task.checklist_progress?.[step] ?? false;
|
const current = task.checklist_progress?.[step] ?? false;
|
||||||
state[step] = step === item ? done : current;
|
state[step] = step === item ? done : current;
|
||||||
}
|
}
|
||||||
@@ -1888,11 +1891,20 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
const tk = this._objects
|
const tk = this._objects
|
||||||
.find((o) => o.entry_id === entryId)
|
.find((o) => o.entry_id === entryId)
|
||||||
?.tasks.find((tsk) => tsk.id === taskId);
|
?.tasks.find((tsk) => tsk.id === taskId);
|
||||||
|
// #139: a phased task completes the phase currently due — its checklist,
|
||||||
|
// parts and required fields override the task-level values (fallthrough
|
||||||
|
// is already baked into effectivePhase). Resolved HERE so every call
|
||||||
|
// site of _openCompleteDialog rotates for free.
|
||||||
|
const phase = tk ? effectivePhase(tk) : null;
|
||||||
|
dlg.phaseLabel = phase ? phaseLabel(tk) : "";
|
||||||
dlg.taskType = tk?.type || "";
|
dlg.taskType = tk?.type || "";
|
||||||
dlg.readingUnit = tk?.reading_unit || "";
|
dlg.readingUnit = tk?.reading_unit || "";
|
||||||
// #73: ticks recorded during the cycle prefill the dialog's checklist.
|
// #73: ticks recorded during the cycle prefill the dialog's checklist.
|
||||||
dlg.checklistPrefill = tk?.checklist_progress || {};
|
dlg.checklistPrefill = tk?.checklist_progress || {};
|
||||||
dlg.requiredFields = tk?.required_completion_fields || [];
|
dlg.requiredFields = phase ? phase.requiredFields : (tk?.required_completion_fields || []);
|
||||||
|
if (phase) {
|
||||||
|
dlg.checklist = this._features.checklists ? phase.checklist : [];
|
||||||
|
}
|
||||||
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
// Spare parts: a buy task gets an editable restock-qty field; a consuming
|
||||||
// task shows what it will decrement (incl. the storage location).
|
// task shows what it will decrement (incl. the storage location).
|
||||||
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
|
const objParts = this._objects.find((o) => o.entry_id === entryId)?.parts || [];
|
||||||
@@ -1904,15 +1916,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
dlg.currencySymbol = this._currencySymbol;
|
dlg.currencySymbol = this._currencySymbol;
|
||||||
// #111: a link may point at another object's pool — name that object, and
|
// #111: a link may point at another object's pool — name that object, and
|
||||||
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
|
// never drop a line that fails to resolve (the old .filter(Boolean) hid it).
|
||||||
dlg.consumesInfo = (tk?.consumes_parts || []).map((link) =>
|
const links = phase ? phase.consumesParts : (tk?.consumes_parts || []);
|
||||||
|
dlg.consumesInfo = links.map((link) =>
|
||||||
describePartLink(link, entryId, this._objects, this._lang),
|
describePartLink(link, entryId, this._objects, this._lang),
|
||||||
);
|
);
|
||||||
// #99: editable per-completion parts selection (not on buy tasks — those
|
// #99: editable per-completion parts selection (not on buy tasks — those
|
||||||
// RESTOCK via the qty field instead of consuming). The list carries the
|
// RESTOCK via the qty field instead of consuming). The list carries the
|
||||||
// object's own parts plus the shared pools this task draws on, so a foreign
|
// object's own parts plus the shared pools this task draws on, so a foreign
|
||||||
// link is visible and untickable rather than silently absent.
|
// link is visible and untickable rather than silently absent.
|
||||||
dlg.parts = tk?.part_ref ? [] : partsForCompletion(tk, entryId, this._objects, this._lang);
|
dlg.parts = tk?.part_ref ? [] : partsForCompletion({ consumes_parts: links }, entryId, this._objects, this._lang);
|
||||||
dlg.consumesParts = tk?.part_ref ? [] : (tk?.consumes_parts || []);
|
dlg.consumesParts = tk?.part_ref ? [] : links;
|
||||||
dlg.open();
|
dlg.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3535,6 +3548,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
lang: this._lang,
|
lang: this._lang,
|
||||||
detailStatsData: this._detailStatsData,
|
detailStatsData: this._detailStatsData,
|
||||||
hasStatsService: !!this._statsService,
|
hasStatsService: !!this._statsService,
|
||||||
|
historyFallbackIds: this._statsService?.historyFallbackIds,
|
||||||
isCounterEntity: (tc) => this._isCounterEntity(tc),
|
isCounterEntity: (tc) => this._isCounterEntity(tc),
|
||||||
rangeDays: this._chartRangeDays,
|
rangeDays: this._chartRangeDays,
|
||||||
setRangeDays: (days) => this._setChartRange(days),
|
setRangeDays: (days) => this._setChartRange(days),
|
||||||
@@ -3579,6 +3593,10 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
setSearch: (s) => { this._historySearch = s; },
|
setSearch: (s) => { this._historySearch = s; },
|
||||||
openEdit: (entry) => this._openHistoryEdit(entry),
|
openEdit: (entry) => this._openHistoryEdit(entry),
|
||||||
readingUnit: task?.reading_unit ?? null,
|
readingUnit: task?.reading_unit ?? null,
|
||||||
|
// #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]),
|
||||||
|
),
|
||||||
readingDelta: (entry) => {
|
readingDelta: (entry) => {
|
||||||
const idx = readings.findIndex((h) => h.timestamp === entry.timestamp);
|
const idx = readings.findIndex((h) => h.timestamp === entry.timestamp);
|
||||||
if (idx <= 0) return null;
|
if (idx <= 0) return null;
|
||||||
@@ -3604,6 +3622,12 @@ export class MaintenanceSupporterPanel extends LitElement {
|
|||||||
objectManualDocs: obj?.object?.manual_docs ?? [],
|
objectManualDocs: obj?.object?.manual_docs ?? [],
|
||||||
openManualDoc: (doc) => this._openManualDoc(doc),
|
openManualDoc: (doc) => this._openManualDoc(doc),
|
||||||
setChecklistItem: (item, done) => this._setChecklistItem(entryId, taskId, item, done),
|
setChecklistItem: (item, done) => this._setChecklistItem(entryId, taskId, item, done),
|
||||||
|
setPhaseCursor: (cursor) => {
|
||||||
|
void this._runAction({
|
||||||
|
type: "maintenance_supporter/task/set_phase",
|
||||||
|
entry_id: entryId, task_id: taskId, cursor,
|
||||||
|
});
|
||||||
|
},
|
||||||
isOperator: this._isOperator,
|
isOperator: this._isOperator,
|
||||||
actionLoading: this._actionLoading,
|
actionLoading: this._actionLoading,
|
||||||
moreMenuOpen: this._moreMenuOpen,
|
moreMenuOpen: this._moreMenuOpen,
|
||||||
|
|||||||
@@ -12,12 +12,16 @@
|
|||||||
"playwright": "1.62"
|
"playwright": "1.62"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
"@open-wc/testing": "^5.0.0",
|
"@open-wc/testing": "^5.0.0",
|
||||||
|
"@types/react": "^19.2.18",
|
||||||
"@web/dev-server-esbuild": "^2.0.0",
|
"@web/dev-server-esbuild": "^2.0.0",
|
||||||
"@web/test-runner": "^1.0.0",
|
"@web/test-runner": "^1.0.0",
|
||||||
"@web/test-runner-playwright": "^1.0.0",
|
"@web/test-runner-playwright": "^1.0.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
"lit": "^3.3.3",
|
"lit": "^3.3.3",
|
||||||
|
"react": "^19.2.8",
|
||||||
|
"react-dom": "^19.2.8",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -550,6 +554,13 @@
|
|||||||
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdi/js": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@mdn/browser-compat-data": {
|
"node_modules/@mdn/browser-compat-data": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz",
|
||||||
@@ -1586,6 +1597,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.2.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
|
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -2921,6 +2942,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||||
@@ -4981,6 +5009,29 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "19.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "19.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"scheduler": "^0.27.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -5221,6 +5272,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
|||||||
@@ -9,12 +9,16 @@
|
|||||||
"test:watch": "web-test-runner --config web-test-runner.config.mjs --watch"
|
"test:watch": "web-test-runner --config web-test-runner.config.mjs --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
"@open-wc/testing": "^5.0.0",
|
"@open-wc/testing": "^5.0.0",
|
||||||
|
"@types/react": "^19.2.18",
|
||||||
"@web/dev-server-esbuild": "^2.0.0",
|
"@web/dev-server-esbuild": "^2.0.0",
|
||||||
"@web/test-runner": "^1.0.0",
|
"@web/test-runner": "^1.0.0",
|
||||||
"@web/test-runner-playwright": "^1.0.0",
|
"@web/test-runner-playwright": "^1.0.0",
|
||||||
"esbuild": "^0.28.1",
|
"esbuild": "^0.28.1",
|
||||||
"lit": "^3.3.3",
|
"lit": "^3.3.3",
|
||||||
|
"react": "^19.2.8",
|
||||||
|
"react-dom": "^19.2.8",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1053,6 +1053,53 @@ export const panelStyles = css`
|
|||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #139: cycle-phase strip in the task overview */
|
||||||
|
.phases-card {
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.phases-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.phases-card-header ha-icon {
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.phases-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.phase-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.phase-step.current {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: color-mix(in srgb, var(--primary-color) 12%, transparent);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.phase-step-last {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
/* Checklist preview card (read-only display in task overview) */
|
/* Checklist preview card (read-only display in task overview) */
|
||||||
.checklist-preview-card {
|
.checklist-preview-card {
|
||||||
background: var(--card-background-color, #fff);
|
background: var(--card-background-color, #fff);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface HistoryContext {
|
|||||||
/** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */
|
/** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */
|
||||||
readingUnit?: string | null;
|
readingUnit?: string | null;
|
||||||
readingDelta?: (entry: HistoryEntry) => number | null;
|
readingDelta?: (entry: HistoryEntry) => number | null;
|
||||||
|
/** #139: phase id → display name, for entries stamped with a phase_id. */
|
||||||
|
phaseNames?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every HistoryEntryType is filterable (drift audit 2026-08: the repair-flow
|
// Every HistoryEntryType is filterable (drift audit 2026-08: the repair-flow
|
||||||
@@ -96,6 +98,7 @@ export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) {
|
|||||||
<div class="history-content">
|
<div class="history-content">
|
||||||
<div class="history-row">
|
<div class="history-row">
|
||||||
<strong>${t(entry.type, L)}</strong>
|
<strong>${t(entry.type, L)}</strong>
|
||||||
|
${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}
|
${entry.auto ? html`<span class="history-auto-badge">${t("history_auto", L)}</span>` : nothing}
|
||||||
${editable
|
${editable
|
||||||
? html`<button class="history-edit-btn"
|
? html`<button class="history-edit-btn"
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export interface SparklineContext {
|
|||||||
lang: string;
|
lang: string;
|
||||||
detailStatsData: Map<string, StatisticsPoint[]>;
|
detailStatsData: Map<string, StatisticsPoint[]>;
|
||||||
hasStatsService: boolean;
|
hasStatsService: boolean;
|
||||||
|
/** #141: entities whose series is recorder STATE HISTORY (no long-term
|
||||||
|
* statistics exist — binary sensors etc.). Changes the footnote and
|
||||||
|
* suppresses the trigger_current_value tail point (for a state_change
|
||||||
|
* trigger that is the change COUNTER, not the plotted state). */
|
||||||
|
historyFallbackIds?: Set<string>;
|
||||||
isCounterEntity: (tc: TriggerConfig) => boolean;
|
isCounterEntity: (tc: TriggerConfig) => boolean;
|
||||||
rangeDays: number;
|
rangeDays: number;
|
||||||
setRangeDays: (days: number) => void;
|
setRangeDays: (days: number) => void;
|
||||||
@@ -252,7 +257,11 @@ function rawStatsPoints(task: MaintenanceTask, ctx: SparklineContext): ChartPoin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (task.trigger_current_value != null) {
|
// #141: a history-fallback series already runs to "now" and plots the
|
||||||
|
// STATE — appending trigger_current_value (a change counter on
|
||||||
|
// state_change triggers) would graft a wrong-dimension point onto it.
|
||||||
|
const fromHistory = !!entityId && !!ctx.historyFallbackIds?.has(entityId) && statsPoints.length >= 2;
|
||||||
|
if (task.trigger_current_value != null && !fromHistory) {
|
||||||
points.push({ ts: Date.now(), val: task.trigger_current_value });
|
points.push({ ts: Date.now(), val: task.trigger_current_value });
|
||||||
}
|
}
|
||||||
points.sort((a, b) => a.ts - b.ts);
|
points.sort((a, b) => a.ts - b.ts);
|
||||||
@@ -364,11 +373,23 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
|||||||
@range-change=${(e: CustomEvent<{ days: number }>) => ctx.setRangeDays(e.detail.days)}
|
@range-change=${(e: CustomEvent<{ days: number }>) => ctx.setRangeDays(e.detail.days)}
|
||||||
@outlier-toggle=${(e: CustomEvent<{ hide: boolean }>) => ctx.setHideOutliers(e.detail.hide)}
|
@outlier-toggle=${(e: CustomEvent<{ hide: boolean }>) => ctx.setHideOutliers(e.detail.hide)}
|
||||||
></maintenance-trigger-chart>
|
></maintenance-trigger-chart>
|
||||||
${statsFetchedEmpty && !loading
|
${(() => {
|
||||||
? html`<div class="chart-note">
|
if (loading) return nothing;
|
||||||
|
// #141: the series came from recorder state history (binary sensors
|
||||||
|
// never have long-term statistics) — say so, incl. the shorter window.
|
||||||
|
if (entityId && ctx.historyFallbackIds?.has(entityId) && !statsFetchedEmpty) {
|
||||||
|
return html`<div class="chart-note">
|
||||||
|
<ha-icon icon="mdi:information-outline"></ha-icon>
|
||||||
|
${t("chart_history_fallback", ctx.lang)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (statsFetchedEmpty) {
|
||||||
|
return html`<div class="chart-note">
|
||||||
<ha-icon icon="mdi:information-outline"></ha-icon>
|
<ha-icon icon="mdi:information-outline"></ha-icon>
|
||||||
${t("chart_no_stats", ctx.lang)}
|
${t("chart_no_stats", ctx.lang)}
|
||||||
</div>`
|
</div>`;
|
||||||
: nothing}
|
}
|
||||||
|
return nothing;
|
||||||
|
})()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { renderSeasonalCardCompact, renderSeasonalCardExpanded } from "./seasona
|
|||||||
import { renderCostDurationCard } from "./charts";
|
import { renderCostDurationCard } from "./charts";
|
||||||
import { renderDaysProgress } from "./progress";
|
import { renderDaysProgress } from "./progress";
|
||||||
import { renderHistoryFilters, renderHistoryList, type HistoryContext } from "./history";
|
import { renderHistoryFilters, renderHistoryList, type HistoryContext } from "./history";
|
||||||
|
import { clampPhaseCursor, effectivePhase, hasPhases } from "../helpers/phases";
|
||||||
import "../components/task-documents";
|
import "../components/task-documents";
|
||||||
|
|
||||||
export interface TaskDetailContext {
|
export interface TaskDetailContext {
|
||||||
@@ -39,6 +40,8 @@ export interface TaskDetailContext {
|
|||||||
openManualDoc: (doc: ManualDocRef) => void;
|
openManualDoc: (doc: ManualDocRef) => void;
|
||||||
/** #73: persist one checklist tick (panel sends the full state via WS). */
|
/** #73: persist one checklist tick (panel sends the full state via WS). */
|
||||||
setChecklistItem: (item: string, done: boolean) => void;
|
setChecklistItem: (item: string, done: boolean) => void;
|
||||||
|
/** #139: re-point the phase cursor (operator repair / mid-cycle adoption). */
|
||||||
|
setPhaseCursor: (cursor: number) => void;
|
||||||
isOperator: boolean;
|
isOperator: boolean;
|
||||||
actionLoading: boolean;
|
actionLoading: boolean;
|
||||||
moreMenuOpen: boolean;
|
moreMenuOpen: boolean;
|
||||||
@@ -190,9 +193,51 @@ function collapsible(key: string, titleKey: string, body: unknown, ctx: TaskDeta
|
|||||||
* reloads, prefills the complete dialog) and resets when the task is
|
* reloads, prefills the complete dialog) and resets when the task is
|
||||||
* completed or skipped. Only rendered when the Checklists feature is
|
* completed or skipped. Only rendered when the Checklists feature is
|
||||||
* enabled and steps are set. */
|
* enabled and steps are set. */
|
||||||
|
/** #139: the cycle strip — every step of the phase sequence as a chip, the
|
||||||
|
* step currently due highlighted, each phase's last completion underneath.
|
||||||
|
* Operators can re-point the cursor (mis-click repair, mid-cycle adoption). */
|
||||||
|
function renderPhasesCard(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||||
|
if (!hasPhases(task)) return nothing;
|
||||||
|
const L = ctx.lang;
|
||||||
|
const seq = task.phase_sequence!;
|
||||||
|
const cursor = clampPhaseCursor(task.phase_cursor, seq.length);
|
||||||
|
// Last completion per phase id — history is append-ordered, so walk it
|
||||||
|
// backwards and keep the first hit.
|
||||||
|
const lastByPhase = new Map<string, string>();
|
||||||
|
for (let i = task.history.length - 1; i >= 0; i--) {
|
||||||
|
const h = task.history[i];
|
||||||
|
if (h.phase_id && h.type === "completed" && !lastByPhase.has(h.phase_id)) {
|
||||||
|
lastByPhase.set(h.phase_id, h.timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="phases-card">
|
||||||
|
<div class="phases-card-header">
|
||||||
|
<ha-icon icon="mdi:rotate-right"></ha-icon>
|
||||||
|
<span>${t("phase_sequence_label", L)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="phases-strip">
|
||||||
|
${seq.map((id, i) => {
|
||||||
|
const name = task.phases?.[id]?.name || id;
|
||||||
|
const last = lastByPhase.get(id);
|
||||||
|
return html`
|
||||||
|
<div class="phase-step ${i === cursor ? "current" : ""}"
|
||||||
|
title=${i === cursor ? t("phase_current", L) : (ctx.isOperator ? t("phase_set", L) : "")}
|
||||||
|
@click=${() => { if (ctx.isOperator && i !== cursor) ctx.setPhaseCursor(i); }}>
|
||||||
|
<span class="phase-step-name">${i + 1}. ${name}</span>
|
||||||
|
${last ? html`<span class="phase-step-last">${formatDate(last, L)}</span>` : nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) {
|
function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) {
|
||||||
if (!ctx.features.checklists) return nothing;
|
if (!ctx.features.checklists) return nothing;
|
||||||
const items = task.checklist || [];
|
// #139: a phased task shows the checklist of the phase currently due.
|
||||||
|
const items = effectivePhase(task)?.checklist ?? (task.checklist || []);
|
||||||
if (items.length === 0) return nothing;
|
if (items.length === 0) return nothing;
|
||||||
const L = ctx.lang;
|
const L = ctx.lang;
|
||||||
const progress = task.checklist_progress || {};
|
const progress = task.checklist_progress || {};
|
||||||
@@ -439,6 +484,7 @@ export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext)
|
|||||||
</div>
|
</div>
|
||||||
`, ctx)
|
`, ctx)
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${renderPhasesCard(task, ctx)}
|
||||||
${renderChecklistCard(task, ctx)}
|
${renderChecklistCard(task, ctx)}
|
||||||
${renderRecentActivities(task, ctx)}
|
${renderRecentActivities(task, ctx)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,27 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|||||||
const DETAIL_DAYS = 30;
|
const DETAIL_DAYS = 30;
|
||||||
const MINI_DAYS = 14;
|
const MINI_DAYS = 14;
|
||||||
|
|
||||||
|
/** history/history_during_period rows — compressed keys over WS ({s, lu});
|
||||||
|
* the uncompressed REST shape is tolerated defensively. */
|
||||||
|
interface HAHistoryRow {
|
||||||
|
s?: string;
|
||||||
|
lu?: number;
|
||||||
|
state?: string;
|
||||||
|
last_updated?: string | number;
|
||||||
|
last_changed?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
export class StatisticsService {
|
export class StatisticsService {
|
||||||
private _hass: HomeAssistant;
|
private _hass: HomeAssistant;
|
||||||
private _cache = new Map<string, EntityStatisticsCache>();
|
private _cache = new Map<string, EntityStatisticsCache>();
|
||||||
private _pending = new Map<string, Promise<StatisticsPoint[]>>();
|
private _pending = new Map<string, Promise<StatisticsPoint[]>>();
|
||||||
|
|
||||||
|
/** #141: entities whose DETAIL series came from recorder state history
|
||||||
|
* instead of long-term statistics (binary sensors and other entities
|
||||||
|
* without a state_class never have statistics) — the chart footnote
|
||||||
|
* tells the two apart. */
|
||||||
|
public readonly historyFallbackIds = new Set<string>();
|
||||||
|
|
||||||
constructor(hass: HomeAssistant) {
|
constructor(hass: HomeAssistant) {
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
}
|
}
|
||||||
@@ -141,7 +157,23 @@ export class StatisticsService {
|
|||||||
})) as Record<string, HAStatisticsRow[]>;
|
})) as Record<string, HAStatisticsRow[]>;
|
||||||
|
|
||||||
const rows = result[entityId] || [];
|
const rows = result[entityId] || [];
|
||||||
const points = this._normalizeRows(rows, isCounter);
|
let points = this._normalizeRows(rows, isCounter);
|
||||||
|
|
||||||
|
// #141: entities without long-term statistics (binary sensors, sensors
|
||||||
|
// without a state_class) always come back empty here — fall back to
|
||||||
|
// recorder STATE HISTORY (typically ~10 days) so a problem sensor's
|
||||||
|
// on/off timeline draws as a 0/1 step line instead of a flat value.
|
||||||
|
if (points.length < 2) {
|
||||||
|
const historyPoints = await this._fetchHistoryFallback(entityId, startTime);
|
||||||
|
if (historyPoints.length >= 2) {
|
||||||
|
points = historyPoints;
|
||||||
|
this.historyFallbackIds.add(entityId);
|
||||||
|
} else {
|
||||||
|
this.historyFallbackIds.delete(entityId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.historyFallbackIds.delete(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
this._cache.set(cacheKey, {
|
this._cache.set(cacheKey, {
|
||||||
entityId,
|
entityId,
|
||||||
@@ -157,6 +189,60 @@ export class StatisticsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recorder state history as chart points. Binary states map to 0/1, and
|
||||||
|
* every value CHANGE gets a doubled point ((t, old), (t, new)) so the
|
||||||
|
* line renderer draws a step instead of a slope between samples. */
|
||||||
|
private async _fetchHistoryFallback(entityId: string, startTime: string): Promise<StatisticsPoint[]> {
|
||||||
|
try {
|
||||||
|
const result = (await this._hass.connection.sendMessagePromise({
|
||||||
|
type: "history/history_during_period",
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: new Date().toISOString(),
|
||||||
|
entity_ids: [entityId],
|
||||||
|
minimal_response: true,
|
||||||
|
no_attributes: true,
|
||||||
|
})) as Record<string, HAHistoryRow[]>;
|
||||||
|
|
||||||
|
let rows = result?.[entityId] || [];
|
||||||
|
// A chatty numeric entity can return thousands of rows — stride-sample
|
||||||
|
// BEFORE the step expansion (the expansion preserves the edges that
|
||||||
|
// survive sampling).
|
||||||
|
if (rows.length > 1000) {
|
||||||
|
const stride = Math.ceil(rows.length / 500);
|
||||||
|
rows = rows.filter((_, i) => i % stride === 0 || i === rows.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const points: StatisticsPoint[] = [];
|
||||||
|
let prev: number | null = null;
|
||||||
|
for (const row of rows) {
|
||||||
|
const state = row.s ?? row.state;
|
||||||
|
if (state == null || state === "unknown" || state === "unavailable") continue;
|
||||||
|
let val: number;
|
||||||
|
if (state === "on" || state === "open" || state === "true") val = 1;
|
||||||
|
else if (state === "off" || state === "closed" || state === "false") val = 0;
|
||||||
|
else {
|
||||||
|
val = parseFloat(state);
|
||||||
|
if (!Number.isFinite(val)) continue;
|
||||||
|
}
|
||||||
|
const raw = row.lu ?? row.last_updated ?? row.last_changed;
|
||||||
|
const ts = typeof raw === "number" ? raw * 1000 : raw != null ? Date.parse(raw) : NaN;
|
||||||
|
if (!Number.isFinite(ts)) continue;
|
||||||
|
if (prev != null && prev !== val) points.push({ ts, val: prev });
|
||||||
|
points.push({ ts, val });
|
||||||
|
prev = val;
|
||||||
|
}
|
||||||
|
points.sort((a, b) => a.ts - b.ts);
|
||||||
|
// Extend the last known state to "now" so the line reaches the right
|
||||||
|
// edge (the caller must NOT append trigger_current_value here — for a
|
||||||
|
// state_change trigger that is the change COUNTER, not the state).
|
||||||
|
if (points.length && prev != null) points.push({ ts: Date.now(), val: prev });
|
||||||
|
return points;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[maintenance-supporter] History fallback failed for ${entityId}:`, err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _fetchBatch(
|
private async _fetchBatch(
|
||||||
entityIds: string[],
|
entityIds: string[],
|
||||||
period: "hour" | "day",
|
period: "hour" | "day",
|
||||||
|
|||||||
@@ -604,6 +604,21 @@ export const sharedStyles = css`
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
/* #139 — names the cycle phase a completion recorded. Shares the auto-badge
|
||||||
|
look; carries the left-pinning margin itself unless the auto badge (which
|
||||||
|
already has it) follows. */
|
||||||
|
.history-phase-badge {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.history-phase-badge:has(+ .history-auto-badge) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
.history-edit-btn {
|
.history-edit-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ export interface HistoryEntry {
|
|||||||
/** v2.37: completion recorded by the system itself (trigger recovered),
|
/** v2.37: completion recorded by the system itself (trigger recovered),
|
||||||
* not performed by a user in the UI. */
|
* not performed by a user in the UI. */
|
||||||
auto?: boolean;
|
auto?: boolean;
|
||||||
|
/** #139: which cycle phase this LATEST completion recorded (backfills
|
||||||
|
* carry none — they never advanced the cursor). */
|
||||||
|
phase_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdaptiveConfig {
|
export interface AdaptiveConfig {
|
||||||
@@ -269,6 +272,22 @@ export interface MaintenanceTask {
|
|||||||
consumes_parts?: TaskPartLink[] | null;
|
consumes_parts?: TaskPartLink[] | null;
|
||||||
/** Present on an auto-created "buy" reminder: the owning part. */
|
/** Present on an auto-created "buy" reminder: the owning part. */
|
||||||
part_ref?: { part_id: string } | null;
|
part_ref?: { part_id: string } | null;
|
||||||
|
/** Task phases (#139): cyclic content rotation on one shared cadence. */
|
||||||
|
phases?: Record<string, TaskPhaseDef> | null;
|
||||||
|
phase_sequence?: string[] | null;
|
||||||
|
phase_cursor?: number;
|
||||||
|
/** Backend-resolved phase currently due (null for phase-less tasks). */
|
||||||
|
current_phase?: { id: string; name: string; index: number; count: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One phase definition (#139). A set field OVERRIDES the task-level field
|
||||||
|
* for completions performed while this phase is due; unset falls through. */
|
||||||
|
export interface TaskPhaseDef {
|
||||||
|
name: string;
|
||||||
|
notes?: string;
|
||||||
|
checklist?: string[];
|
||||||
|
consumes_parts?: TaskPartLink[];
|
||||||
|
required_completion_fields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One entry of a task's `consumes_parts`.
|
/** One entry of a task's `consumes_parts`.
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "záznamů",
|
"service_record_entries": "záznamů",
|
||||||
"completed_by": "Dokončil",
|
"completed_by": "Dokončil",
|
||||||
"date_from": "Od",
|
"date_from": "Od",
|
||||||
"date_to": "Do"
|
"date_to": "Do",
|
||||||
|
"phases_section": "Fáze cyklu (volitelné)",
|
||||||
|
"phases_hint": "Různé práce ve stejném rytmu — každé dokončení posune na další krok (např. malý, malý, velký servis).",
|
||||||
|
"phase_add": "Přidat fázi",
|
||||||
|
"phase_name": "Název fáze",
|
||||||
|
"phase_sequence_label": "Pořadí cyklu",
|
||||||
|
"phase_sequence_add_step": "Přidat krok",
|
||||||
|
"phase_current": "Aktuální fáze",
|
||||||
|
"phase_set": "Nastavit jako aktuální",
|
||||||
|
"chart_history_fallback": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazuje se historie stavů recorderu (obvykle posledních ~10 dní)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "poster",
|
"service_record_entries": "poster",
|
||||||
"completed_by": "Udført af",
|
"completed_by": "Udført af",
|
||||||
"date_from": "Fra",
|
"date_from": "Fra",
|
||||||
"date_to": "Til"
|
"date_to": "Til",
|
||||||
|
"phases_section": "Cyklusfaser (valgfrit)",
|
||||||
|
"phases_hint": "Forskelligt arbejde i samme rytme — hver udførelse går videre til næste trin (f.eks. lille, lille, stor service).",
|
||||||
|
"phase_add": "Tilføj fase",
|
||||||
|
"phase_name": "Fasenavn",
|
||||||
|
"phase_sequence_label": "Cyklusrækkefølge",
|
||||||
|
"phase_sequence_add_step": "Tilføj trin",
|
||||||
|
"phase_current": "Aktuel fase",
|
||||||
|
"phase_set": "Sæt som aktuel",
|
||||||
|
"chart_history_fallback": "Ingen langtidsstatistik for denne enhed — recorderens tilstandshistorik vises (typisk de seneste ~10 dage)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "Einträge",
|
"service_record_entries": "Einträge",
|
||||||
"completed_by": "Erledigt von",
|
"completed_by": "Erledigt von",
|
||||||
"date_from": "Von",
|
"date_from": "Von",
|
||||||
"date_to": "Bis"
|
"date_to": "Bis",
|
||||||
|
"phases_section": "Zyklus-Phasen (optional)",
|
||||||
|
"phases_hint": "Unterschiedliche Arbeiten im gleichen Takt — jede Erledigung schaltet zum nächsten Schritt (z. B. klein, klein, groß).",
|
||||||
|
"phase_add": "Phase hinzufügen",
|
||||||
|
"phase_name": "Phasenname",
|
||||||
|
"phase_sequence_label": "Zyklus-Reihenfolge",
|
||||||
|
"phase_sequence_add_step": "Schritt anhängen",
|
||||||
|
"phase_current": "Aktuelle Phase",
|
||||||
|
"phase_set": "Als aktuell setzen",
|
||||||
|
"chart_history_fallback": "Keine Langzeitstatistik für diese Entität — angezeigt wird der Recorder-Zustandsverlauf (üblicherweise die letzten ~10 Tage)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entries",
|
"service_record_entries": "entries",
|
||||||
"completed_by": "Completed by",
|
"completed_by": "Completed by",
|
||||||
"date_from": "From",
|
"date_from": "From",
|
||||||
"date_to": "To"
|
"date_to": "To",
|
||||||
|
"phases_section": "Cycle phases (optional)",
|
||||||
|
"phases_hint": "Different work on one shared schedule — each completion moves to the next step (e.g. small service, small service, big service).",
|
||||||
|
"phase_add": "Add phase",
|
||||||
|
"phase_name": "Phase name",
|
||||||
|
"phase_sequence_label": "Cycle order",
|
||||||
|
"phase_sequence_add_step": "Add step",
|
||||||
|
"phase_current": "Current phase",
|
||||||
|
"phase_set": "Set as current",
|
||||||
|
"chart_history_fallback": "No long-term statistics for this entity — showing recorder state history (typically the last ~10 days)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entradas",
|
"service_record_entries": "entradas",
|
||||||
"completed_by": "Realizado por",
|
"completed_by": "Realizado por",
|
||||||
"date_from": "Desde",
|
"date_from": "Desde",
|
||||||
"date_to": "Hasta"
|
"date_to": "Hasta",
|
||||||
|
"phases_section": "Fases del ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabajos distintos con un mismo ritmo: cada finalización pasa al siguiente paso (p. ej. pequeño, pequeño, grande).",
|
||||||
|
"phase_add": "Añadir fase",
|
||||||
|
"phase_name": "Nombre de la fase",
|
||||||
|
"phase_sequence_label": "Orden del ciclo",
|
||||||
|
"phase_sequence_add_step": "Añadir paso",
|
||||||
|
"phase_current": "Fase actual",
|
||||||
|
"phase_set": "Establecer como actual",
|
||||||
|
"chart_history_fallback": "Sin estadísticas a largo plazo para esta entidad: se muestra el historial de estados del recorder (normalmente los últimos ~10 días)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "merkintää",
|
"service_record_entries": "merkintää",
|
||||||
"completed_by": "Suorittanut",
|
"completed_by": "Suorittanut",
|
||||||
"date_from": "Alkaen",
|
"date_from": "Alkaen",
|
||||||
"date_to": "Asti"
|
"date_to": "Asti",
|
||||||
|
"phases_section": "Syklin vaiheet (valinnainen)",
|
||||||
|
"phases_hint": "Eri työt samassa tahdissa — jokainen suoritus siirtyy seuraavaan vaiheeseen (esim. pieni, pieni, iso huolto).",
|
||||||
|
"phase_add": "Lisää vaihe",
|
||||||
|
"phase_name": "Vaiheen nimi",
|
||||||
|
"phase_sequence_label": "Syklin järjestys",
|
||||||
|
"phase_sequence_add_step": "Lisää askel",
|
||||||
|
"phase_current": "Nykyinen vaihe",
|
||||||
|
"phase_set": "Aseta nykyiseksi",
|
||||||
|
"chart_history_fallback": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään recorderin tilahistoria (yleensä viimeiset ~10 päivää)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "entrées",
|
"service_record_entries": "entrées",
|
||||||
"completed_by": "Réalisé par",
|
"completed_by": "Réalisé par",
|
||||||
"date_from": "Du",
|
"date_from": "Du",
|
||||||
"date_to": "Au"
|
"date_to": "Au",
|
||||||
|
"phases_section": "Phases du cycle (optionnel)",
|
||||||
|
"phases_hint": "Des travaux différents sur un même rythme — chaque réalisation passe à l'étape suivante (p. ex. petit, petit, grand entretien).",
|
||||||
|
"phase_add": "Ajouter une phase",
|
||||||
|
"phase_name": "Nom de la phase",
|
||||||
|
"phase_sequence_label": "Ordre du cycle",
|
||||||
|
"phase_sequence_add_step": "Ajouter une étape",
|
||||||
|
"phase_current": "Phase actuelle",
|
||||||
|
"phase_set": "Définir comme actuelle",
|
||||||
|
"chart_history_fallback": "Pas de statistiques à long terme pour cette entité — affichage de l'historique d'états du recorder (généralement les ~10 derniers jours)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "प्रविष्टियाँ",
|
"service_record_entries": "प्रविष्टियाँ",
|
||||||
"completed_by": "द्वारा पूर्ण",
|
"completed_by": "द्वारा पूर्ण",
|
||||||
"date_from": "से",
|
"date_from": "से",
|
||||||
"date_to": "तक"
|
"date_to": "तक",
|
||||||
|
"phases_section": "चक्र चरण (वैकल्पिक)",
|
||||||
|
"phases_hint": "एक ही लय में अलग-अलग कार्य — प्रत्येक पूर्णता अगले चरण पर ले जाती है (जैसे छोटा, छोटा, बड़ा)।",
|
||||||
|
"phase_add": "चरण जोड़ें",
|
||||||
|
"phase_name": "चरण का नाम",
|
||||||
|
"phase_sequence_label": "चक्र क्रम",
|
||||||
|
"phase_sequence_add_step": "कदम जोड़ें",
|
||||||
|
"phase_current": "वर्तमान चरण",
|
||||||
|
"phase_set": "वर्तमान बनाएँ",
|
||||||
|
"chart_history_fallback": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — recorder की स्थिति का इतिहास दिखाया जा रहा है (आमतौर पर पिछले ~10 दिन)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "bejegyzés",
|
"service_record_entries": "bejegyzés",
|
||||||
"completed_by": "Elvégezte",
|
"completed_by": "Elvégezte",
|
||||||
"date_from": "Ettől",
|
"date_from": "Ettől",
|
||||||
"date_to": "Eddig"
|
"date_to": "Eddig",
|
||||||
|
"phases_section": "Ciklusfázisok (opcionális)",
|
||||||
|
"phases_hint": "Különböző munkák azonos ütemben — minden elvégzés a következő lépésre vált (pl. kis, kis, nagy szerviz).",
|
||||||
|
"phase_add": "Fázis hozzáadása",
|
||||||
|
"phase_name": "Fázis neve",
|
||||||
|
"phase_sequence_label": "Ciklus sorrendje",
|
||||||
|
"phase_sequence_add_step": "Lépés hozzáadása",
|
||||||
|
"phase_current": "Aktuális fázis",
|
||||||
|
"phase_set": "Beállítás aktuálisként",
|
||||||
|
"chart_history_fallback": "Ehhez az entitáshoz nincs hosszú távú statisztika — a recorder állapotelőzményei jelennek meg (jellemzően az utolsó ~10 nap)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "voci",
|
"service_record_entries": "voci",
|
||||||
"completed_by": "Completato da",
|
"completed_by": "Completato da",
|
||||||
"date_from": "Dal",
|
"date_from": "Dal",
|
||||||
"date_to": "Al"
|
"date_to": "Al",
|
||||||
|
"phases_section": "Fasi del ciclo (opzionale)",
|
||||||
|
"phases_hint": "Lavori diversi sullo stesso ritmo — ogni completamento passa al passo successivo (es. piccolo, piccolo, grande).",
|
||||||
|
"phase_add": "Aggiungi fase",
|
||||||
|
"phase_name": "Nome della fase",
|
||||||
|
"phase_sequence_label": "Ordine del ciclo",
|
||||||
|
"phase_sequence_add_step": "Aggiungi passo",
|
||||||
|
"phase_current": "Fase attuale",
|
||||||
|
"phase_set": "Imposta come attuale",
|
||||||
|
"chart_history_fallback": "Nessuna statistica a lungo termine per questa entità — viene mostrata la cronologia degli stati del recorder (di solito gli ultimi ~10 giorni)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "件",
|
"service_record_entries": "件",
|
||||||
"completed_by": "実施者",
|
"completed_by": "実施者",
|
||||||
"date_from": "開始",
|
"date_from": "開始",
|
||||||
"date_to": "終了"
|
"date_to": "終了",
|
||||||
|
"phases_section": "サイクルフェーズ(任意)",
|
||||||
|
"phases_hint": "同じ周期で異なる作業を行います。完了するたびに次のステップへ進みます(例:小・小・大整備)。",
|
||||||
|
"phase_add": "フェーズを追加",
|
||||||
|
"phase_name": "フェーズ名",
|
||||||
|
"phase_sequence_label": "サイクルの順序",
|
||||||
|
"phase_sequence_add_step": "ステップを追加",
|
||||||
|
"phase_current": "現在のフェーズ",
|
||||||
|
"phase_set": "現在に設定",
|
||||||
|
"chart_history_fallback": "このエンティティには長期統計がありません — レコーダーの状態履歴を表示しています(通常は直近約10日分)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "건",
|
"service_record_entries": "건",
|
||||||
"completed_by": "수행자",
|
"completed_by": "수행자",
|
||||||
"date_from": "시작",
|
"date_from": "시작",
|
||||||
"date_to": "종료"
|
"date_to": "종료",
|
||||||
|
"phases_section": "주기 단계(선택)",
|
||||||
|
"phases_hint": "같은 주기에 다른 작업을 수행합니다. 완료할 때마다 다음 단계로 넘어갑니다(예: 소·소·대 정비).",
|
||||||
|
"phase_add": "단계 추가",
|
||||||
|
"phase_name": "단계 이름",
|
||||||
|
"phase_sequence_label": "주기 순서",
|
||||||
|
"phase_sequence_add_step": "스텝 추가",
|
||||||
|
"phase_current": "현재 단계",
|
||||||
|
"phase_set": "현재로 설정",
|
||||||
|
"chart_history_fallback": "이 엔티티에는 장기 통계가 없습니다 — 레코더 상태 기록을 표시합니다(보통 최근 약 10일)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "oppføringer",
|
"service_record_entries": "oppføringer",
|
||||||
"completed_by": "Utført av",
|
"completed_by": "Utført av",
|
||||||
"date_from": "Fra",
|
"date_from": "Fra",
|
||||||
"date_to": "Til"
|
"date_to": "Til",
|
||||||
|
"phases_section": "Syklusfaser (valgfritt)",
|
||||||
|
"phases_hint": "Ulikt arbeid i samme rytme — hver fullføring går videre til neste trinn (f.eks. liten, liten, stor service).",
|
||||||
|
"phase_add": "Legg til fase",
|
||||||
|
"phase_name": "Fasenavn",
|
||||||
|
"phase_sequence_label": "Syklusrekkefølge",
|
||||||
|
"phase_sequence_add_step": "Legg til trinn",
|
||||||
|
"phase_current": "Gjeldende fase",
|
||||||
|
"phase_set": "Sett som gjeldende",
|
||||||
|
"chart_history_fallback": "Ingen langtidsstatistikk for denne enheten — recorderens tilstandshistorikk vises (vanligvis de siste ~10 dagene)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "items",
|
"service_record_entries": "items",
|
||||||
"completed_by": "Voltooid door",
|
"completed_by": "Voltooid door",
|
||||||
"date_from": "Van",
|
"date_from": "Van",
|
||||||
"date_to": "Tot"
|
"date_to": "Tot",
|
||||||
|
"phases_section": "Cyclusfasen (optioneel)",
|
||||||
|
"phases_hint": "Verschillend werk op één ritme — elke afronding schuift naar de volgende stap (bijv. klein, klein, groot).",
|
||||||
|
"phase_add": "Fase toevoegen",
|
||||||
|
"phase_name": "Fasenaam",
|
||||||
|
"phase_sequence_label": "Cyclusvolgorde",
|
||||||
|
"phase_sequence_add_step": "Stap toevoegen",
|
||||||
|
"phase_current": "Huidige fase",
|
||||||
|
"phase_set": "Instellen als huidige",
|
||||||
|
"chart_history_fallback": "Geen langetermijnstatistieken voor deze entiteit — de recorder-statusgeschiedenis wordt getoond (meestal de laatste ~10 dagen)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "wpisów",
|
"service_record_entries": "wpisów",
|
||||||
"completed_by": "Wykonane przez",
|
"completed_by": "Wykonane przez",
|
||||||
"date_from": "Od",
|
"date_from": "Od",
|
||||||
"date_to": "Do"
|
"date_to": "Do",
|
||||||
|
"phases_section": "Fazy cyklu (opcjonalnie)",
|
||||||
|
"phases_hint": "Różne prace w jednym rytmie — każde ukończenie przechodzi do następnego kroku (np. mały, mały, duży).",
|
||||||
|
"phase_add": "Dodaj fazę",
|
||||||
|
"phase_name": "Nazwa fazy",
|
||||||
|
"phase_sequence_label": "Kolejność cyklu",
|
||||||
|
"phase_sequence_add_step": "Dodaj krok",
|
||||||
|
"phase_current": "Bieżąca faza",
|
||||||
|
"phase_set": "Ustaw jako bieżącą",
|
||||||
|
"chart_history_fallback": "Brak statystyk długoterminowych dla tej encji — wyświetlana jest historia stanów recordera (zwykle ostatnie ~10 dni)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "registros",
|
"service_record_entries": "registros",
|
||||||
"completed_by": "Concluído por",
|
"completed_by": "Concluído por",
|
||||||
"date_from": "De",
|
"date_from": "De",
|
||||||
"date_to": "Até"
|
"date_to": "Até",
|
||||||
|
"phases_section": "Fases do ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabalhos diferentes no mesmo ritmo — cada conclusão avança para o próximo passo (ex.: pequeno, pequeno, grande).",
|
||||||
|
"phase_add": "Adicionar fase",
|
||||||
|
"phase_name": "Nome da fase",
|
||||||
|
"phase_sequence_label": "Ordem do ciclo",
|
||||||
|
"phase_sequence_add_step": "Adicionar passo",
|
||||||
|
"phase_current": "Fase atual",
|
||||||
|
"phase_set": "Definir como atual",
|
||||||
|
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — mostrando o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "registos",
|
"service_record_entries": "registos",
|
||||||
"completed_by": "Concluído por",
|
"completed_by": "Concluído por",
|
||||||
"date_from": "De",
|
"date_from": "De",
|
||||||
"date_to": "Até"
|
"date_to": "Até",
|
||||||
|
"phases_section": "Fases do ciclo (opcional)",
|
||||||
|
"phases_hint": "Trabalhos diferentes no mesmo ritmo — cada conclusão avança para o passo seguinte (ex.: pequeno, pequeno, grande).",
|
||||||
|
"phase_add": "Adicionar fase",
|
||||||
|
"phase_name": "Nome da fase",
|
||||||
|
"phase_sequence_label": "Ordem do ciclo",
|
||||||
|
"phase_sequence_add_step": "Adicionar passo",
|
||||||
|
"phase_current": "Fase atual",
|
||||||
|
"phase_set": "Definir como atual",
|
||||||
|
"chart_history_fallback": "Sem estatísticas de longo prazo para esta entidade — a mostrar o histórico de estados do recorder (normalmente os últimos ~10 dias)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "записей",
|
"service_record_entries": "записей",
|
||||||
"completed_by": "Выполнил",
|
"completed_by": "Выполнил",
|
||||||
"date_from": "С",
|
"date_from": "С",
|
||||||
"date_to": "По"
|
"date_to": "По",
|
||||||
|
"phases_section": "Фазы цикла (необязательно)",
|
||||||
|
"phases_hint": "Разные работы в одном ритме — каждое выполнение переходит к следующему шагу (напр., малое, малое, большое ТО).",
|
||||||
|
"phase_add": "Добавить фазу",
|
||||||
|
"phase_name": "Название фазы",
|
||||||
|
"phase_sequence_label": "Порядок цикла",
|
||||||
|
"phase_sequence_add_step": "Добавить шаг",
|
||||||
|
"phase_current": "Текущая фаза",
|
||||||
|
"phase_set": "Сделать текущей",
|
||||||
|
"chart_history_fallback": "Для этой сущности нет долгосрочной статистики — показана история состояний recorder (обычно последние ~10 дней)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "poster",
|
"service_record_entries": "poster",
|
||||||
"completed_by": "Utförd av",
|
"completed_by": "Utförd av",
|
||||||
"date_from": "Från",
|
"date_from": "Från",
|
||||||
"date_to": "Till"
|
"date_to": "Till",
|
||||||
|
"phases_section": "Cykelfaser (valfritt)",
|
||||||
|
"phases_hint": "Olika arbeten i samma rytm — varje slutförande går vidare till nästa steg (t.ex. liten, liten, stor service).",
|
||||||
|
"phase_add": "Lägg till fas",
|
||||||
|
"phase_name": "Fasnamn",
|
||||||
|
"phase_sequence_label": "Cykelordning",
|
||||||
|
"phase_sequence_add_step": "Lägg till steg",
|
||||||
|
"phase_current": "Aktuell fas",
|
||||||
|
"phase_set": "Ange som aktuell",
|
||||||
|
"chart_history_fallback": "Ingen långtidsstatistik för denna entitet — recorderns tillståndshistorik visas (vanligtvis de senaste ~10 dagarna)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "kayıt",
|
"service_record_entries": "kayıt",
|
||||||
"completed_by": "Tamamlayan",
|
"completed_by": "Tamamlayan",
|
||||||
"date_from": "Başlangıç",
|
"date_from": "Başlangıç",
|
||||||
"date_to": "Bitiş"
|
"date_to": "Bitiş",
|
||||||
|
"phases_section": "Döngü aşamaları (isteğe bağlı)",
|
||||||
|
"phases_hint": "Aynı ritimde farklı işler — her tamamlama bir sonraki adıma geçer (örn. küçük, küçük, büyük bakım).",
|
||||||
|
"phase_add": "Aşama ekle",
|
||||||
|
"phase_name": "Aşama adı",
|
||||||
|
"phase_sequence_label": "Döngü sırası",
|
||||||
|
"phase_sequence_add_step": "Adım ekle",
|
||||||
|
"phase_current": "Geçerli aşama",
|
||||||
|
"phase_set": "Geçerli olarak ayarla",
|
||||||
|
"chart_history_fallback": "Bu varlık için uzun dönem istatistik yok — recorder durum geçmişi gösteriliyor (genellikle son ~10 gün)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "записів",
|
"service_record_entries": "записів",
|
||||||
"completed_by": "Виконав",
|
"completed_by": "Виконав",
|
||||||
"date_from": "Від",
|
"date_from": "Від",
|
||||||
"date_to": "До"
|
"date_to": "До",
|
||||||
|
"phases_section": "Фази циклу (необов'язково)",
|
||||||
|
"phases_hint": "Різні роботи в одному ритмі — кожне виконання переходить до наступного кроку (напр., мале, мале, велике ТО).",
|
||||||
|
"phase_add": "Додати фазу",
|
||||||
|
"phase_name": "Назва фази",
|
||||||
|
"phase_sequence_label": "Порядок циклу",
|
||||||
|
"phase_sequence_add_step": "Додати крок",
|
||||||
|
"phase_current": "Поточна фаза",
|
||||||
|
"phase_set": "Зробити поточною",
|
||||||
|
"chart_history_fallback": "Для цієї сутності немає довгострокової статистики — показано історію станів recorder (зазвичай останні ~10 днів)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -889,5 +889,14 @@
|
|||||||
"service_record_entries": "条记录",
|
"service_record_entries": "条记录",
|
||||||
"completed_by": "完成人",
|
"completed_by": "完成人",
|
||||||
"date_from": "从",
|
"date_from": "从",
|
||||||
"date_to": "至"
|
"date_to": "至",
|
||||||
|
"phases_section": "循环阶段(可选)",
|
||||||
|
"phases_hint": "同一节奏下的不同工作——每次完成后进入下一步(例如:小保养、小保养、大保养)。",
|
||||||
|
"phase_add": "添加阶段",
|
||||||
|
"phase_name": "阶段名称",
|
||||||
|
"phase_sequence_label": "循环顺序",
|
||||||
|
"phase_sequence_add_step": "添加步骤",
|
||||||
|
"phase_current": "当前阶段",
|
||||||
|
"phase_set": "设为当前",
|
||||||
|
"chart_history_fallback": "该实体没有长期统计数据——显示 recorder 状态历史(通常为最近约 10 天)"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
|||||||
/*! maintenance_supporter frontend 2.64.0 */
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
var S="2.64.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
|
var S="2.65.0";var l="maintenance-supporter",T=`ll-strategy-dashboard-${l}`,D="hui-maintenance-supporter-strategy-editor",C=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${S}`,m=null;function v(){return m||(m=import(C)),m}async function I(){let r=await v();if(!r.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return r.MaintenanceDashboardStrategy}var p=class extends HTMLElement{static getCreateSuggestions(c){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await v(),document.createElement(D)}static async generate(c,f){return(await I()).generate(c,f)}};function M(){try{customElements.define(T,p)}catch{}}M();var w=window;w.customStrategies=w.customStrategies||[];w.customStrategies.some(r=>r.type===l&&r.strategyType==="dashboard")||w.customStrategies.push({type:l,strategyType:"dashboard",name:"Maintenance Supporter",description:"Auto-generated dashboard. Group views by area, status, floor, or due date \u2014 picked from the strategy editor or YAML.",documentationURL:"https://github.com/iluebbe/maintenance_supporter#dashboard-strategy"});(()=>{let r=window;if(r.__msStrategyHealActive)return;r.__msStrategyHealActive=!0;let c=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,f=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,g=`custom:${l}`;function R(a){let t=[document.documentElement],n=0;for(;t.length&&n<9e3;){let o=t.pop();if(n++,!o)continue;let e=o;if(e.nodeType===1&&e.tagName&&e.tagName.toLowerCase()===a)return e;e.shadowRoot&&t.push(e.shadowRoot);let i=o.children;if(i)for(let d of Array.from(i))t.push(d)}return null}function k(a){let t=a?.views;if(!Array.isArray(t)||!t.length)return null;let n=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(i=>i?.path===n);if(o)return o;let e=Number(n);return Number.isInteger(e)&&t[e]?t[e]:t[0]}function b(){try{let t=R("ha-panel-lovelace")?.lovelace;if(!t)return!1;let n=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(n(o.strategy)===g)return!0;let e=k(o);if(e&&n(e.strategy)===g)return!0}return!1}catch{return!1}}function A(){let a=!1,t=0,n=!1,o=!1,e=[document.documentElement],i=0;for(;e.length&&i<9e3;){let d=e.pop();if(i++,!d)continue;let u=d;if(u.nodeType===1&&u.tagName){let s=u.tagName.toLowerCase();(s==="hui-view"||s==="hui-sections-view")&&(a=!0),(s==="ha-card"||s==="hui-card")&&t++,s==="hui-empty-state-card"&&(o=!0),s==="hui-error-card"&&f.test(u.textContent||"")&&(n=!0)}u.shadowRoot&&e.push(u.shadowRoot);let E=d.children;if(E)for(let s of Array.from(E))e.push(s)}return n?!0:o?!1:a&&t<3&&b()}let N="/maintenance_supporter_strategy_shim.js",y=0,_=0;function L(){let a=Date.now();a-_<5e3||y>=3||(_=a,y+=1,import(`${N}?heal=${a}`).catch(()=>{}).finally(()=>{let t=window.location.pathname+window.location.search;history.pushState(null,"","/lovelace"),window.dispatchEvent(new CustomEvent("location-changed")),window.setTimeout(()=>{history.pushState(null,"",t),window.dispatchEvent(new CustomEvent("location-changed"))},200)}))}function h(){if(c.test(window.location.pathname))return;let a=0,t=Date.now(),n=window.setInterval(()=>{a++;try{if(Date.now()-t<6e3)return;if(c.test(window.location.pathname)){window.clearInterval(n);return}A()?L():window.clearInterval(n),a>=30&&window.clearInterval(n)}catch{window.clearInterval(n)}},500)}try{document.readyState==="loading"?window.addEventListener("DOMContentLoaded",h):h(),window.addEventListener("location-changed",()=>{c.test(window.location.pathname)||h()})}catch{}})();
|
||||||
|
|||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-IAYX7ZWS.js";import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import{a as o,b as _,c as i,f as l,g as h,k as g,l as n,p as t,r as u,t as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.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,306 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as _,e as k}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-UBKG7VLY.js";import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import{B as m,a as s,b,c as a,f as d,g as v,k as n,l,p as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";var i=class extends v{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.restockDefault=null;this.restockUnitCost=null;this.currencySymbol="";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this.phaseLabel="";this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue="";this._restockQty="";this._completedAt="";this._usedParts={};this.checklistPrefill={}}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((e,t)=>[String(t),!!this.checklistPrefill[e]]).filter(([,e])=>e)),this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="",this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._completedAt="",this._usedParts=Object.fromEntries(this.consumesParts.map(e=>[_(e),{...e}])))}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,o=t.files?.[0];if(t.value="",!!o){this._photoUploading=!0,this._error="";try{let c=new FormData;c.append("entry_id",this.entryId),c.append("tags","photo"),c.append("file",o,o.name);let p=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:c});if(!p.ok){this._error=p.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let u=await p.json();u.id&&(this._photoDocId=u.id,this._photoPreview=URL.createObjectURL(o))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._completedAt){if(new Date(this._completedAt).getTime()>Date.now()){this._error=r("completed_at_future_error",this.lang),this._loading=!1;return}e.completed_at=this._completedAt.length===16?`${this._completedAt}:00`:this._completedAt}if(this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}if(this.restockDefault!==null&&this._restockQty!==""){let t=parseFloat(this._restockQty);!isNaN(t)&&t>=1&&(e.restock_quantity=t)}this.parts.length>0&&(e.used_parts=Object.values(this._usedParts).filter(t=>Number.isFinite(t.quantity)&&t.quantity>0).map(t=>t.entry_id?{part_id:t.part_id,quantity:t.quantity,entry_id:t.entry_id}:{part_id:t.part_id,quantity:t.quantity})),await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=g(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photoDocId!=="",user:!!this.hass?.user};return this.requiredFields.filter(t=>!e[t])}_req(e){return this.requiredFields.includes(e)?a`<span class="req-mark" aria-hidden="true">*</span>`:d}_partsCostSuggestion(){if(this.restockDefault!==null){let o=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(o)||o<=0?null:Math.round(this.restockUnitCost*o*100)/100}if(!this.parts.length)return null;let e=0,t=!1;for(let o of Object.values(this._usedParts)){let c=this.parts.find(p=>_({part_id:p.id,entry_id:p.entry_id})===_(o));c?.cost!=null&&(e+=c.cost*(o.quantity||1),t=!0)}return t?Math.round(e*100)/100:null}_renderCostSuggestion(e){if(this._cost.trim()!=="")return d;let t=this._partsCostSuggestion();if(t==null||t<=0)return d;let o=`${t.toFixed(2)}${this.currencySymbol?` ${this.currencySymbol}`:""}`;return a`<button
|
||||||
|
type="button"
|
||||||
|
class="cost-suggestion"
|
||||||
|
@click=${()=>this._cost=t.toFixed(2)}
|
||||||
|
>${r("cost_from_parts",e).replace("{amount}",o)}</button>`}_close(){this._open=!1}render(){if(!this._open)return a``;let e=this.lang||this.hass?.language||"en";return a`
|
||||||
|
<ha-dialog open @closed=${this._close}>
|
||||||
|
<div class="dialog-title">${r("complete_title",e)}${this.taskName}</div>
|
||||||
|
${this.phaseLabel?a`<div class="phase-line">${r("phase_current",e)}: ${this.phaseLabel}</div>`:d}
|
||||||
|
<div class="content">
|
||||||
|
${this._error?a`<div class="error">${this._error}</div>`:d}
|
||||||
|
${this.checklist.length>0?a`
|
||||||
|
<div class="checklist-section">
|
||||||
|
<label class="checklist-label">${r("checklist",e)}</label>
|
||||||
|
${this.checklist.map((t,o)=>a`
|
||||||
|
<label class="checklist-item" @click=${()=>this._toggleCheck(o)}>
|
||||||
|
<input type="checkbox" .checked=${!!this._checklistState[String(o)]} />
|
||||||
|
<span>${t}</span>
|
||||||
|
</label>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`:d}
|
||||||
|
${this.taskType==="reading"?a`
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("reading_value_label",e)}${this.readingUnit?` (${this.readingUnit})`:""}</span>
|
||||||
|
<input type="number" step="any" class="field-input"
|
||||||
|
.value=${this._readingValue}
|
||||||
|
@input=${t=>this._readingValue=t.target.value} />
|
||||||
|
</label>`:d}
|
||||||
|
${this.parts.length?a`<div class="used-parts">
|
||||||
|
<span class="field-label">${r("complete_parts_used",e)}</span>
|
||||||
|
${this.parts.map(t=>{let o=_({part_id:t.id,entry_id:t.entry_id}),c=this._usedParts[o],p=c!==void 0,u=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return a`<div class="used-part-row">
|
||||||
|
<label class="used-part-check">
|
||||||
|
<input type="checkbox" .checked=${p}
|
||||||
|
@change=${f=>{let h={...this._usedParts};f.target.checked?h[o]=h[o]||u:delete h[o],this._usedParts=h}} />
|
||||||
|
<span
|
||||||
|
>${t.name}${t.owner_name?a`<span class="used-part-owner"> (${t.owner_name})</span>`:d}${t.stock!==null&&t.stock!==void 0?` (${t.stock}${t.unit?" "+t.unit:""})`:""}</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
${p?a`<input class="used-part-qty" type="number" min="0.01" max="999" step="0.01"
|
||||||
|
.value=${String(c.quantity)}
|
||||||
|
@input=${f=>{let h=parseFloat(f.target.value);this._usedParts={...this._usedParts,[o]:{...u,quantity:Number.isFinite(h)&&h>=.01?h:1}}}} />`:d}
|
||||||
|
</div>`})}
|
||||||
|
</div>`:this.consumesInfo.length?a`<div class="consumes-hint">
|
||||||
|
${this.consumesInfo.map(t=>a`<div>${t}</div>`)}
|
||||||
|
</div>`:d}
|
||||||
|
${this.restockDefault!==null?a`
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("restock_quantity_label",e)}</span>
|
||||||
|
<input type="number" step="0.01" min="0.01" class="field-input"
|
||||||
|
.value=${this._restockQty}
|
||||||
|
@input=${t=>this._restockQty=t.target.value} />
|
||||||
|
</label>`:d}
|
||||||
|
<!-- Native <input>s rather than <ha-textfield>: when this dialog
|
||||||
|
is opened from a Lovelace card via dialog-mount, ha-textfield
|
||||||
|
isn't yet registered (HA loads it lazily when its own panels
|
||||||
|
need it) so the elements render with zero height and the user
|
||||||
|
only sees the title + Cancel/Complete buttons — the original
|
||||||
|
bug report. Native inputs always render. -->
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("notes_optional",e)}${this._req("notes")}</span>
|
||||||
|
<input type="text" class="field-input"
|
||||||
|
.value=${this._notes}
|
||||||
|
@input=${t=>this._notes=t.target.value} />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("cost_optional",e)}${this._req("cost")}</span>
|
||||||
|
<input type="number" step="0.01" min="0" class="field-input"
|
||||||
|
.value=${this._cost}
|
||||||
|
@input=${t=>this._cost=t.target.value} />
|
||||||
|
${this._renderCostSuggestion(e)}
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("duration_minutes",e)}${this._req("duration")}</span>
|
||||||
|
<input type="number" step="0.01" min="0" class="field-input"
|
||||||
|
.value=${this._duration}
|
||||||
|
@input=${t=>this._duration=t.target.value} />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">${r("completed_at_optional",e)}</span>
|
||||||
|
<input type="datetime-local" class="field-input"
|
||||||
|
max=${new Date(Date.now()-new Date().getTimezoneOffset()*6e4).toISOString().slice(0,16)}
|
||||||
|
.value=${this._completedAt}
|
||||||
|
@change=${t=>this._completedAt=t.target.value} />
|
||||||
|
</label>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">${r("completion_photo_optional",e)}${this._req("photo")}</span>
|
||||||
|
${this._photoPreview?a`
|
||||||
|
<div class="photo-preview">
|
||||||
|
<img src=${this._photoPreview} alt="" />
|
||||||
|
<button type="button" class="photo-remove" @click=${this._removePhoto}
|
||||||
|
title="${r("remove",e)}">✕</button>
|
||||||
|
</div>`:a`
|
||||||
|
<label class="photo-pick">
|
||||||
|
<ha-icon icon="mdi:camera"></ha-icon>
|
||||||
|
<span>${this._photoUploading?r("uploading",e):r("add_photo",e)}</span>
|
||||||
|
<input type="file" accept="image/*" capture="environment"
|
||||||
|
?disabled=${this._photoUploading}
|
||||||
|
@change=${this._onPhotoInput} />
|
||||||
|
</label>`}
|
||||||
|
</div>
|
||||||
|
${this.adaptiveEnabled?a`
|
||||||
|
<div class="feedback-section">
|
||||||
|
<label class="feedback-label">${r("was_maintenance_needed",e)}</label>
|
||||||
|
<div class="feedback-buttons">
|
||||||
|
<button
|
||||||
|
class="feedback-btn ${this._feedback==="needed"?"selected":""}"
|
||||||
|
@click=${()=>this._setFeedback("needed")}
|
||||||
|
>${r("feedback_needed",e)}</button>
|
||||||
|
<button
|
||||||
|
class="feedback-btn ${this._feedback==="not_needed"?"selected":""}"
|
||||||
|
@click=${()=>this._setFeedback("not_needed")}
|
||||||
|
>${r("feedback_not_needed",e)}</button>
|
||||||
|
<button
|
||||||
|
class="feedback-btn ${this._feedback==="not_sure"?"selected":""}"
|
||||||
|
@click=${()=>this._setFeedback("not_sure")}
|
||||||
|
>${r("feedback_not_sure",e)}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`:d}
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<ha-button appearance="plain" @click=${this._close}>
|
||||||
|
${r("cancel",e)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._complete}
|
||||||
|
.disabled=${this._loading||this._missingRequired.length>0}
|
||||||
|
title=${this._missingRequired.length?this._missingRequired.map(t=>r("err_required",e).replace("{field}",r(k[t]??t,e))).join(" \xB7 "):""}
|
||||||
|
>
|
||||||
|
${this._loading?r("completing",e):r("complete",e)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`}};i.styles=[m,b`
|
||||||
|
.req-mark {
|
||||||
|
color: var(--error-color, #f44336);
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
|
||||||
|
.cost-suggestion {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 12.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.phase-line {
|
||||||
|
margin-top: -8px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.consumes-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
border-left: 3px solid var(--primary-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
}
|
||||||
|
/* #99: editable per-completion parts selection */
|
||||||
|
.used-parts { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.used-part-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.used-part-check {
|
||||||
|
display: flex; align-items: center; gap: 6px; flex: 1;
|
||||||
|
font-size: 13px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.used-part-check input { cursor: pointer; }
|
||||||
|
/* #111: whose stock this row draws on. Muted but never omitted — an
|
||||||
|
unlabelled foreign pool is indistinguishable from an own part. */
|
||||||
|
.used-part-owner { color: var(--secondary-text-color); }
|
||||||
|
.used-part-qty {
|
||||||
|
width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: var(--error-color, #f44336);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
/* .field/.field-label/.field-input come from nativeFieldStyles */
|
||||||
|
.photo-pick {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px dashed var(--divider-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.photo-pick:hover { border-color: var(--primary-color); }
|
||||||
|
.photo-pick input[type="file"] { display: none; }
|
||||||
|
.photo-preview {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.photo-preview img {
|
||||||
|
max-width: 160px;
|
||||||
|
max-height: 160px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.photo-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: var(--error-color, #db4437);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.checklist-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.checklist-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.checklist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.checklist-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.feedback-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
.feedback-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.feedback-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.feedback-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.feedback-btn:hover {
|
||||||
|
background: var(--secondary-background-color, #f5f5f5);
|
||||||
|
}
|
||||||
|
.feedback-btn.selected {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`],s([n({attribute:!1})],i.prototype,"hass",2),s([n()],i.prototype,"entryId",2),s([n()],i.prototype,"taskId",2),s([n()],i.prototype,"taskName",2),s([n()],i.prototype,"lang",2),s([n({type:Array})],i.prototype,"checklist",2),s([n({type:Boolean})],i.prototype,"adaptiveEnabled",2),s([n()],i.prototype,"taskType",2),s([n()],i.prototype,"readingUnit",2),s([n({attribute:!1})],i.prototype,"restockDefault",2),s([n({attribute:!1})],i.prototype,"restockUnitCost",2),s([n()],i.prototype,"currencySymbol",2),s([n({attribute:!1})],i.prototype,"parts",2),s([n({attribute:!1})],i.prototype,"consumesParts",2),s([n({type:Array})],i.prototype,"consumesInfo",2),s([n({type:Array})],i.prototype,"requiredFields",2),s([n()],i.prototype,"phaseLabel",2),s([l()],i.prototype,"_open",2),s([l()],i.prototype,"_notes",2),s([l()],i.prototype,"_cost",2),s([l()],i.prototype,"_duration",2),s([l()],i.prototype,"_loading",2),s([l()],i.prototype,"_error",2),s([l()],i.prototype,"_checklistState",2),s([l()],i.prototype,"_feedback",2),s([l()],i.prototype,"_photoDocId",2),s([l()],i.prototype,"_photoPreview",2),s([l()],i.prototype,"_photoUploading",2),s([l()],i.prototype,"_readingValue",2),s([l()],i.prototype,"_restockQty",2),s([l()],i.prototype,"_completedAt",2),s([l()],i.prototype,"_usedParts",2),s([n({attribute:!1})],i.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",i);export{i as a};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as t,b as a,c as i,f as l,g as p,k as r}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";var e=class extends p{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(n){let o=n.target.value;this.value=o,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:o}}))}render(){return i`
|
||||||
|
<label class="field">
|
||||||
|
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:l}</span>`:l}
|
||||||
|
<input
|
||||||
|
.value=${this.value??""}
|
||||||
|
.type=${this.type}
|
||||||
|
?required=${this.required}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
placeholder=${this.placeholder}
|
||||||
|
step=${this.step??l}
|
||||||
|
min=${this.min??l}
|
||||||
|
max=${this.max??l}
|
||||||
|
pattern=${this.pattern??l}
|
||||||
|
@input=${this._onInput}
|
||||||
|
@change=${this._onInput}
|
||||||
|
/>
|
||||||
|
${this.helper?i`<span class="helper">${this.helper}</span>`:l}
|
||||||
|
</label>
|
||||||
|
`}};e.styles=a`
|
||||||
|
:host { display: block; }
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.req { color: var(--error-color, #f44336); margin-left: 2px; }
|
||||||
|
input {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.helper {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
`,t([r()],e.prototype,"label",2),t([r()],e.prototype,"value",2),t([r()],e.prototype,"placeholder",2),t([r()],e.prototype,"type",2),t([r({type:Boolean})],e.prototype,"required",2),t([r({type:Boolean})],e.prototype,"disabled",2),t([r()],e.prototype,"step",2),t([r()],e.prototype,"min",2),t([r()],e.prototype,"max",2),t([r()],e.prototype,"pattern",2),t([r()],e.prototype,"helper",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e);
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as x}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GSH25Q5Z.js";import{a,b as v,c as n,f as p,g as b,k as h,l as c,p as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";function d(l){return l.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function f(l){return!l.startsWith("data:image/svg+xml,")&&!l.startsWith("data:image/png;base64,")?"":d(l)}function q(l){return l.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var o=class extends b{constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,i){this._entryId=e,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,i,r,s){this._entryId=e,this._taskId=i,this._objectName=r,this._taskName=s,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let r=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&r.push(this.hass.connection.sendMessagePromise({...i,action:"complete"}));let s=await Promise.all(r);if(e!==this._generateSeq)return;this._viewResult=s[0],s.length>1&&(this._completeResult=s[1])}catch(i){if(e!==this._generateSeq)return;let r=i?.code,s=i?.message;this._error=r==="no_url"||typeof s=="string"&&s.includes("No Home Assistant URL")?t("qr_error_no_url",this.lang):t("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,i=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,r=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),s=window.open("","_blank","width=600,height=500");if(!s)return;let g=this.lang||"en",u=d(i),m=d(r),_=!!this._completeResult,w=d(t("qr_action_view",g)),$=d(t("qr_action_complete",g));s.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||||
|
<meta name="color-scheme" content="light">
|
||||||
|
<title>${u}</title>
|
||||||
|
<style>
|
||||||
|
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
|
||||||
|
carry their own white quiet zone and stay scannable either way, but the
|
||||||
|
labels below are explicit dark greys and would vanish on a WebView's dark
|
||||||
|
canvas. Same reasoning as helpers/report.ts. */
|
||||||
|
:root{color-scheme:light}
|
||||||
|
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
|
||||||
|
h2{margin:0 0 4px}
|
||||||
|
.sub{color:#666;font-size:14px;margin-bottom:16px}
|
||||||
|
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
|
||||||
|
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||||
|
.qr-col img{width:${_?"200px":"280px"}}
|
||||||
|
.qr-label{font-size:13px;font-weight:500;color:#333}
|
||||||
|
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
|
||||||
|
</style></head><body>
|
||||||
|
<h2>${u}</h2>
|
||||||
|
${m?`<div class="sub">${m}</div>`:""}
|
||||||
|
<div class="qr-row">
|
||||||
|
<div class="qr-col">
|
||||||
|
<img src="${f(this._viewResult.svg_data_uri)}" alt="QR Info" />
|
||||||
|
<div class="qr-label">${w}</div>
|
||||||
|
</div>
|
||||||
|
${_?`<div class="qr-col">
|
||||||
|
<img src="${f(this._completeResult.svg_data_uri)}" alt="QR Complete" />
|
||||||
|
<div class="qr-label">${$}</div>
|
||||||
|
</div>`:""}
|
||||||
|
</div>
|
||||||
|
<div class="url">${d(this._viewResult.url)}</div>
|
||||||
|
<script>setTimeout(()=>window.print(),300)<\/script>
|
||||||
|
</body></html>`),s.document.close()}_downloadSvg(e,i){let r=decodeURIComponent(e.svg_data_uri.replace("data:image/svg+xml,","")),s=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;x(r,`qr-${q(s)}-${i}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en",i=this._taskName?`${t("qr_code",e)}: ${this._objectName} \u2014 ${this._taskName}`:`${t("qr_code",e)}: ${this._objectName}`,r=!!this._viewResult;return n`
|
||||||
|
<ha-dialog open @closed=${this._close}>
|
||||||
|
<div class="dialog-title">${i}</div>
|
||||||
|
<div class="content">
|
||||||
|
${this._loading?n`<div class="loading">${t("qr_generating",e)}</div>`:this._error?n`<div class="error">${this._error}</div>`:r?n`
|
||||||
|
<div class="qr-pair">
|
||||||
|
<div class="qr-item">
|
||||||
|
<img
|
||||||
|
class="qr-image ${this._completeResult?"small":""}"
|
||||||
|
src="${this._viewResult.svg_data_uri}"
|
||||||
|
alt="QR Info"
|
||||||
|
/>
|
||||||
|
<div class="qr-item-label">${t("qr_action_view",e)}</div>
|
||||||
|
<button class="dl-btn"
|
||||||
|
@click=${()=>this._downloadSvg(this._viewResult,"info")}>
|
||||||
|
<ha-icon icon="mdi:download"></ha-icon>
|
||||||
|
${t("qr_download",e)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this._completeResult?n`
|
||||||
|
<div class="qr-item">
|
||||||
|
<img
|
||||||
|
class="qr-image small"
|
||||||
|
src="${this._completeResult.svg_data_uri}"
|
||||||
|
alt="QR Complete"
|
||||||
|
/>
|
||||||
|
<div class="qr-item-label">${t("qr_action_complete",e)}</div>
|
||||||
|
<button class="dl-btn"
|
||||||
|
@click=${()=>this._downloadSvg(this._completeResult,"complete")}>
|
||||||
|
<ha-icon icon="mdi:download"></ha-icon>
|
||||||
|
${t("qr_download",e)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`:p}
|
||||||
|
</div>
|
||||||
|
<div class="url-display">${this._viewResult.url}</div>
|
||||||
|
`:p}
|
||||||
|
<div class="action-row">
|
||||||
|
<label>${t("qr_url_mode",e)}</label>
|
||||||
|
<div class="action-toggle">
|
||||||
|
<button class="toggle-btn ${this._urlMode==="companion"?"active":""}"
|
||||||
|
@click=${()=>this._setUrlMode("companion")}>${t("qr_mode_companion",e)}</button>
|
||||||
|
<button class="toggle-btn ${this._urlMode==="local"?"active":""}"
|
||||||
|
@click=${()=>this._setUrlMode("local")}>${t("qr_mode_local",e)}</button>
|
||||||
|
<button class="toggle-btn ${this._urlMode==="server"?"active":""}"
|
||||||
|
@click=${()=>this._setUrlMode("server")}>${t("qr_mode_server",e)}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<ha-button appearance="plain" @click=${this._close}>
|
||||||
|
${t("cancel",e)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._print}
|
||||||
|
.disabled=${!r}
|
||||||
|
>
|
||||||
|
${t("qr_print",e)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`}};o.styles=v`
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.qr-pair {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.qr-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.qr-image {
|
||||||
|
width: 240px;
|
||||||
|
height: 240px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
.qr-image.small {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
.qr-item-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dl-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--divider-color, #e0e0e0);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.dl-btn:hover {
|
||||||
|
background: var(--secondary-background-color, #f5f5f5);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.dl-btn ha-icon {
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
}
|
||||||
|
.url-display {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
padding: 40px 0;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
padding: 20px 0;
|
||||||
|
color: var(--error-color, #f44336);
|
||||||
|
}
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.action-row label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.action-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--divider-color, #e0e0e0);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.toggle-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.toggle-btn.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
`,a([h({attribute:!1})],o.prototype,"hass",2),a([h()],o.prototype,"lang",2),a([c()],o.prototype,"_open",2),a([c()],o.prototype,"_loading",2),a([c()],o.prototype,"_error",2),a([c()],o.prototype,"_viewResult",2),a([c()],o.prototype,"_completeResult",2),a([c()],o.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",o);export{o as a};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GSH25Q5Z.js";async function l(e,n,t=300){return(await e.connection.sendMessagePromise({type:"auth/sign_path",path:n,expires:t})).path}async function r(e,n,t=300){return l(e,`/api/maintenance_supporter/document/${n}`,t)}async function y(e,n,t=""){let a=window.open("about:blank","_blank");try{let o=await r(e,n);a&&(a.location.href=new URL(o+t,window.location.origin).href)}catch(o){throw a&&a.close(),o}}async function p(e,n,t){i(await r(e,n,30),t)}function d(e){let n=URL.createObjectURL(new Blob([e],{type:"text/html"}));window.open(n,"_blank"),setTimeout(()=>URL.revokeObjectURL(n),6e4)}var c=[{key:"name",labelKey:"name",required:!0},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=c.map(e=>e.key),s=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function g(e){if(!Array.isArray(e))return[...s];let n=new Set,t=[];for(let a of e)typeof a=="string"&&u.includes(a)&&!n.has(a)&&(n.add(a),t.push(a));return t.length?(t.includes("name")||t.unshift("name"),t):[...s]}export{l as a,r as b,y as c,p as d,d as e,c as f,s as g,g as h};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
function c(o,t,e){let l=new Blob([o],{type:e}),d=URL.createObjectURL(l),n=document.createElement("a");n.href=d,n.download=t,n.target="_blank",n.rel="noopener",n.style.display="none",document.body.appendChild(n),n.dispatchEvent(new MouseEvent("click")),document.body.removeChild(n),setTimeout(()=>URL.revokeObjectURL(d),6e4)}function r(o,t){let e=document.createElement("a");e.href=o,e.download=t,e.target="_blank",e.rel="noopener",e.style.display="none",document.body.appendChild(e),e.dispatchEvent(new MouseEvent("click")),document.body.removeChild(e)}export{c as a,r as b};
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
var r=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=e,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,e,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:e,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{r as a};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{p as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";function u(e){return`${e.entry_id??""}\0${e.part_id}`}function l(e,r,s,c){let n=!!e.entry_id&&e.entry_id!==r,o=n?e.entry_id:r,a=s.find(p=>p.entry_id===o),t=(a?.parts||[]).find(p=>p.id===e.part_id)||null,d=n&&a?.object?.name||"",i=t?.name||_("shared_part_unknown",c);return{part:t,foreign:n,ownerName:d,label:d?`${i} (${d})`:i}}function P(e,r,s,c){let{part:n,label:o}=l(e,r,s,c),a=n&&n.stock!==null&&n.stock!==void 0?` (${n.stock}${n.unit?" "+n.unit:""})`:"",t=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${e.quantity}\xD7 ${o}${a}${t}`}function g(e,r,s,c){let o=(s.find(t=>t.entry_id===r)?.parts||[]).map(t=>({...t})),a=new Set(o.map(t=>u({part_id:t.id})));for(let t of e?.consumes_parts||[]){if(!t.entry_id||t.entry_id===r)continue;let d=u(t);if(a.has(d))continue;a.add(d);let{part:i,ownerName:p}=l(t,r,s,c);o.push({id:t.part_id,name:i?.name||_("shared_part_unknown",c),unit:i?.unit,stock:i?.stock??null,storage_location:i?.storage_location,entry_id:t.entry_id,owner_name:p})}return o}var f=["notes","cost","duration","photo","user"],m={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};export{u as a,P as b,g as c,f as d,m as e};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{p as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.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,143 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as u}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import{a as r,b as _,c as l,f as o,g as p,k as d,l as s,p as i,r as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";var e=class extends p{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return h(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(a,n){this._entryId=a,this._name=n.name||"",this._manufacturer=n.manufacturer||"",this._model=n.model||"",this._serialNumber=n.serial_number||"",this._areaId=n.area_id||"",this._installationDate=n.installation_date||"",this._warrantyExpiry=n.warranty_expiry||"",this._documentationUrl=n.documentation_url||"",this._notes=n.notes||"",this._haDeviceId=n.ha_device_id||"",this._parentEntryId=n.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(a){this._error=u(a,this._lang,i("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(a=>a.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;let a=this._lang,n=this._entryId?i("edit_object",a):i("new_object",a);return l`
|
||||||
|
<ha-dialog open @closed=${this._close}>
|
||||||
|
<div class="dialog-title">${n}</div>
|
||||||
|
<div class="content">
|
||||||
|
${this._error?l`<div class="error">${this._error}</div>`:o}
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("name",a)}"
|
||||||
|
required
|
||||||
|
.value=${this._name}
|
||||||
|
@input=${t=>this._name=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("manufacturer_optional",a)}"
|
||||||
|
.value=${this._manufacturer}
|
||||||
|
@input=${t=>this._manufacturer=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("model_optional",a)}"
|
||||||
|
.value=${this._model}
|
||||||
|
@input=${t=>this._model=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("serial_number_optional",a)}"
|
||||||
|
.value=${this._serialNumber}
|
||||||
|
@input=${t=>this._serialNumber=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("documentation_url_optional",a)}"
|
||||||
|
type="url"
|
||||||
|
.value=${this._documentationUrl}
|
||||||
|
@input=${t=>this._documentationUrl=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ha-area-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
label="${i("area_id_optional",a)}"
|
||||||
|
.value=${this._areaId}
|
||||||
|
@value-changed=${t=>this._areaId=t.detail.value||""}
|
||||||
|
></ha-area-picker>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("installation_date_optional",a)}"
|
||||||
|
type="date"
|
||||||
|
.value=${this._installationDate}
|
||||||
|
@input=${t=>this._installationDate=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ms-textfield
|
||||||
|
label="${i("warranty_expiry_optional",a)}"
|
||||||
|
type="date"
|
||||||
|
.value=${this._warrantyExpiry}
|
||||||
|
@input=${t=>this._warrantyExpiry=t.target.value}
|
||||||
|
></ms-textfield>
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${{device:this._haDeviceId||void 0}}
|
||||||
|
.schema=${[{name:"device",selector:{device:{}}}]}
|
||||||
|
.computeLabel=${()=>i("link_device_optional",a)}
|
||||||
|
@value-changed=${t=>this._haDeviceId=t.detail.value?.device||""}
|
||||||
|
></ha-form>
|
||||||
|
${this._parentChoices().length?l`<label class="textarea-field">
|
||||||
|
<span class="textarea-label">${i("parent_object_optional",a)}</span>
|
||||||
|
<select
|
||||||
|
class="parent-select"
|
||||||
|
.value=${this._parentEntryId}
|
||||||
|
@change=${t=>this._parentEntryId=t.target.value}
|
||||||
|
>
|
||||||
|
<option value="" ?selected=${!this._parentEntryId}>
|
||||||
|
${i("parent_none",a)}
|
||||||
|
</option>
|
||||||
|
${this._parentChoices().map(t=>l`<option
|
||||||
|
value=${t.entry_id}
|
||||||
|
?selected=${this._parentEntryId===t.entry_id}
|
||||||
|
>${t.object.name}</option>`)}
|
||||||
|
</select>
|
||||||
|
</label>`:o}
|
||||||
|
<label class="textarea-field">
|
||||||
|
<span class="textarea-label">${i("object_notes_optional",a)}</span>
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
.value=${this._notes}
|
||||||
|
@input=${t=>this._notes=t.target.value}
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<ha-button appearance="plain" @click=${this._close}>
|
||||||
|
${i("cancel",this._lang)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._save}
|
||||||
|
.disabled=${this._loading||!this._name.trim()}
|
||||||
|
>
|
||||||
|
${this._loading?i("saving",this._lang):i("save",this._lang)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`}};e.styles=_`
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
ms-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.textarea-field {
|
||||||
|
display: flex; flex-direction: column; gap: 4px;
|
||||||
|
}
|
||||||
|
.textarea-label {
|
||||||
|
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
|
||||||
|
}
|
||||||
|
.textarea-field textarea {
|
||||||
|
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||||
|
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.textarea-field textarea:focus {
|
||||||
|
outline: none; border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.parent-select {
|
||||||
|
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||||
|
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: var(--error-color, #f44336);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
`,r([d({attribute:!1})],e.prototype,"hass",2),r([d({attribute:!1})],e.prototype,"objects",2),r([s()],e.prototype,"_open",2),r([s()],e.prototype,"_loading",2),r([s()],e.prototype,"_error",2),r([s()],e.prototype,"_name",2),r([s()],e.prototype,"_manufacturer",2),r([s()],e.prototype,"_model",2),r([s()],e.prototype,"_serialNumber",2),r([s()],e.prototype,"_areaId",2),r([s()],e.prototype,"_installationDate",2),r([s()],e.prototype,"_warrantyExpiry",2),r([s()],e.prototype,"_documentationUrl",2),r([s()],e.prototype,"_notes",2),r([s()],e.prototype,"_haDeviceId",2),r([s()],e.prototype,"_parentEntryId",2),r([s()],e.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",e);export{e as a};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-7PCGJNSR.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-UBKG7VLY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";export{a as MaintenanceCompleteDialog};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{o as a,p as b,q as c,r as d,s as e,t as f,u as g,v as h,w as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-H5N2BEP6.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZQ7M5ZII.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-KFTLMBBT.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EUQJA76V.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-7PCGJNSR.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-UBKG7VLY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-FWFB6OZJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GSH25Q5Z.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-IAYX7ZWS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";export{f as openCompleteDialog,a as openCreateObjectDialog,c as openCreateTaskDialog,b as openEditObjectDialog,d as openEditTaskDialog,e as openHistoryEditDialog,i as openObjectQuickActions,g as openQrDialog,h as openTaskQuickActions};
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZQ7M5ZII.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EUQJA76V.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";export{a as MaintenanceObjectDialog};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FWFB6OZJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-GSH25Q5Z.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";export{a as MaintenanceQrDialog};
|
||||||
+1142
File diff suppressed because it is too large
Load Diff
+146
@@ -0,0 +1,146 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as g}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import{a as l,b as m,c as i,f as h,g as v,k as u,l as d,p,r as f,t as x}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._setups=[];this._selected=new Set;this._baselines=new Map;this._targets=new Map;this._objects=[];this._localeReady=!1;this._toggle=t=>{let e=new Set(this._selected);e.has(t)?e.delete(t):e.add(t),this._selected=e};this._adopt=async()=>{if(!(this._selected.size===0||this._adopting)){this._adopting=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/adopt",selections:[...this._selected].map(e=>{let r={device_id:e},c=this._targets.get(e);c&&(r.entry_id=c);let s=this._setups.find(n=>n.device_id===e);for(let n of s?.tasks??[]){let o=this._baselines.get(`${e} ${n.task_name}`),_=o?parseFloat(o):NaN;!isNaN(_)&&_>=0&&((r.baselines??={})[n.task_name]=_)}return r})});this.dispatchEvent(new CustomEvent("integration-setups-adopted",{bubbles:!0,composed:!0,detail:t})),this._open=!1}catch(t){this._error=g(t,this._lang)}finally{this._adopting=!1}}}}get _lang(){return f(this.hass)}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,x(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._setups=[],this._selected=new Set;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/integration_setups/discover"});this._setups=t.setups||[],this._selected=new Set(this._setups.map(e=>e.device_id)),this._baselines=new Map,this._targets=new Map;try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"});this._objects=(e.objects||[]).map(r=>({entry_id:r.entry_id,name:r.object?.name||r.entry_id})).sort((r,c)=>r.name.localeCompare(c.name))}catch{this._objects=[]}}catch(t){this._error=g(t,this._lang)}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return i``;let t=this._lang;return i`
|
||||||
|
<div class="overlay" @click=${this._close}>
|
||||||
|
<div class="card" @click=${e=>e.stopPropagation()}>
|
||||||
|
<div class="title">${p("setups_title",t)}</div>
|
||||||
|
<div class="hint">${p("setups_hint",t)}</div>
|
||||||
|
${this._error?i`<div class="error">${this._error}</div>`:h}
|
||||||
|
|
||||||
|
${this._loading?i`<div class="loading">…</div>`:this._setups.length===0?i`<div class="empty">${p("setups_none",t)}</div>`:i`
|
||||||
|
<div class="list">
|
||||||
|
${this._setups.map(e=>{let r=this._selected.has(e.device_id),c=[e.integration_name,e.area_name].filter(Boolean).join(" \xB7 ");return i`
|
||||||
|
<label class="row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
.checked=${r}
|
||||||
|
@change=${()=>this._toggle(e.device_id)}
|
||||||
|
/>
|
||||||
|
<div class="row-main">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-name">${e.device_name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row-sub">${c}</div>
|
||||||
|
<div class="row-target" @click=${s=>s.preventDefault()}>
|
||||||
|
→
|
||||||
|
${r&&this._objects.length>0?i`
|
||||||
|
<select
|
||||||
|
class="target-select"
|
||||||
|
@change=${s=>{let n=new Map(this._targets),o=s.target.value;o?n.set(e.device_id,o):n.delete(e.device_id),this._targets=n}}
|
||||||
|
>
|
||||||
|
<option value="" ?selected=${!this._targets.get(e.device_id)}>
|
||||||
|
${e.suggested_entry_id?e.suggested_object_name:p("setups_target_new",t).replace("{name}",e.suggested_object_name)}
|
||||||
|
</option>
|
||||||
|
${this._objects.filter(s=>s.entry_id!==e.suggested_entry_id).map(s=>i`<option
|
||||||
|
value=${s.entry_id}
|
||||||
|
?selected=${this._targets.get(e.device_id)===s.entry_id}
|
||||||
|
>
|
||||||
|
${s.name}
|
||||||
|
</option>`)}
|
||||||
|
</select>
|
||||||
|
`:i`${e.suggested_object_name}${e.suggested_entry_id?h:i` <span class="new-tag">${p("adopt_problem_new_object",t)}</span>`}`}
|
||||||
|
</div>
|
||||||
|
<div class="row-tasks">
|
||||||
|
${e.tasks.map(s=>i`<span class="chip" title=${s.entity_ids.join(", ")}>
|
||||||
|
<ha-icon icon="mdi:link-variant"></ha-icon>${s.task_name_localized||s.task_name}
|
||||||
|
</span>`)}
|
||||||
|
</div>
|
||||||
|
${r?e.tasks.filter(s=>s.direction==="usage_delta").map(s=>{let n=`${e.device_id} ${s.task_name}`;return i`
|
||||||
|
<div class="baseline-field" @click=${o=>o.preventDefault()}>
|
||||||
|
<span class="baseline-label"
|
||||||
|
>${s.task_name_localized||s.task_name} —
|
||||||
|
${p("setups_baseline_hint",t)}</span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
min="0"
|
||||||
|
.value=${this._baselines.get(n)??""}
|
||||||
|
@click=${o=>o.preventDefault()}
|
||||||
|
@input=${o=>{let _=new Map(this._baselines);_.set(n,o.target.value),this._baselines=_}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`}):h}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
`})}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<ha-button appearance="plain" @click=${this._close}>
|
||||||
|
${p("cancel",t)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._adopt}
|
||||||
|
.disabled=${this._selected.size===0||this._adopting}
|
||||||
|
>
|
||||||
|
${p("setups_adopt",t)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`}};a.styles=m`
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: min(360px, calc(100vw - 24px));
|
||||||
|
max-width: 560px;
|
||||||
|
width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.title { font-size: 18px; font-weight: 500; }
|
||||||
|
.hint { color: var(--secondary-text-color); font-size: 13px; }
|
||||||
|
.error { color: var(--error-color, #f44336); font-size: 13px; }
|
||||||
|
.loading, .empty { color: var(--secondary-text-color); font-size: 14px; padding: 12px 0; }
|
||||||
|
.list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 50vh; }
|
||||||
|
.row {
|
||||||
|
display: flex; align-items: flex-start; gap: 10px; padding: 8px;
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.row input { margin-top: 2px; cursor: pointer; }
|
||||||
|
.row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
|
||||||
|
.row-name { font-weight: 500; font-size: 13px; }
|
||||||
|
.row-sub, .row-target { color: var(--secondary-text-color); font-size: 12px; }
|
||||||
|
.new-tag { font-style: italic; }
|
||||||
|
.row-tasks { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
|
||||||
|
.chip {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
||||||
|
background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
|
||||||
|
color: var(--primary-text-color); white-space: nowrap;
|
||||||
|
}
|
||||||
|
.chip ha-icon { --mdc-icon-size: 12px; color: var(--primary-color); }
|
||||||
|
.target-select {
|
||||||
|
font-size: 12px; padding: 2px 4px; max-width: 100%;
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 4px;
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.baseline-field {
|
||||||
|
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||||||
|
margin-top: 4px; font-size: 12px; color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.baseline-field input {
|
||||||
|
width: 110px; padding: 3px 6px; font-size: 12px;
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 4px;
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
|
||||||
|
`,l([u({attribute:!1})],a.prototype,"hass",2),l([d()],a.prototype,"_open",2),l([d()],a.prototype,"_loading",2),l([d()],a.prototype,"_adopting",2),l([d()],a.prototype,"_error",2),l([d()],a.prototype,"_setups",2),l([d()],a.prototype,"_selected",2),l([d()],a.prototype,"_baselines",2),l([d()],a.prototype,"_targets",2),l([d()],a.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",a);export{a as MaintenanceSuggestedSetupsDialog};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KFTLMBBT.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-EUQJA76V.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-UBKG7VLY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-IAYX7ZWS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Z6JBBYEY.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-O6LFYSTY.js";export{a as MaintenanceTaskDialog};
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as m,b as f}from"./chunk-HZFXKH3I.js";import{a as u,b as l,d as h,e as b,f as y,g as o,h as g,i as a,j as _,l as v,u as p}from"./chunk-CYP6LL7W.js";import{a as n}from"./chunk-UTYWUNJT.js";var k=80,s=class extends b{constructor(){super(...arguments);this._config={type:""};this._status=null;this._busy=!1;this._error="";this._localMonthly="";this._localYearly="";this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return _(this.hass)}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),v(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/budget_status"});this._status=t,this._localMonthly=t.monthly_budget?String(t.monthly_budget):"",this._localYearly=t.yearly_budget?String(t.yearly_budget):"",this._dirty=!1}catch(t){this._error=p(t,this._lang)}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=this._localMonthly.trim()===""?0:parseFloat(this._localMonthly),r=this._localYearly.trim()===""?0:parseFloat(this._localYearly),i={};!isNaN(t)&&t>=0&&(i.budget_monthly=t),!isNaN(r)&&r>=0&&(i.budget_yearly=r),await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/global/update",settings:i}),await this._load()}catch(t){this._error=p(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_budget"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,r=this._status;if(!r)return l`<ha-card><div class="loading">${a("loading",t)||"Loading\u2026"}</div></ha-card>`;let i=r.currency_symbol||g,x=r.alert_threshold_pct??k,$=[{label:a("budget_monthly",t)||"Monthly",spent:r.monthly_spent||0,budget:r.monthly_budget||0},{label:a("budget_yearly",t)||"Yearly",spent:r.yearly_spent||0,budget:r.yearly_budget||0}];return l`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<span class="emoji">💰</span>
|
||||||
|
<span>${this._config.title||a("settings_budget",t)||"Budget"}</span>
|
||||||
|
</div>
|
||||||
|
<span class="currency">${i}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this._error?l`<div class="error">${this._error}</div>`:h}
|
||||||
|
|
||||||
|
${$.map(e=>{if(!(e.budget>0))return l`
|
||||||
|
<div class="track spent-only">
|
||||||
|
<div class="track-label-row">
|
||||||
|
<label>${e.label}</label>
|
||||||
|
<span class="track-numbers ok">${e.spent.toFixed(0)} ${i}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;let d=Math.min(100,Math.max(0,e.spent/e.budget*100)),c=d>=100?"danger":d>=x?"warning":"ok";return l`
|
||||||
|
<div class="track">
|
||||||
|
<div class="track-label-row">
|
||||||
|
<label>${e.label}</label>
|
||||||
|
<span class="track-numbers ${c}">
|
||||||
|
${e.spent.toFixed(0)} / ${e.budget.toFixed(0)} ${i}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="bar"><div class="bar-fill ${c}" style="width:${d}%"></div></div>
|
||||||
|
</div>
|
||||||
|
`})}
|
||||||
|
|
||||||
|
${this._isAdmin?l`
|
||||||
|
<div class="inputs-row">
|
||||||
|
<div class="input-field">
|
||||||
|
<label>${a("budget_monthly_set",t)||"Set monthly"}</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<input type="number" min="0" step="1"
|
||||||
|
.value=${this._localMonthly}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${e=>{this._localMonthly=e.target.value,this._dirty=!0}} />
|
||||||
|
<span class="input-suffix">${i}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>${a("budget_yearly_set",t)||"Set yearly"}</label>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<input type="number" min="0" step="1"
|
||||||
|
.value=${this._localYearly}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${e=>{this._localYearly=e.target.value,this._dirty=!0}} />
|
||||||
|
<span class="input-suffix">${i}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn ${this._dirty?"primary":"muted"}"
|
||||||
|
@click=${this._save}
|
||||||
|
?disabled=${this._busy||!this._dirty}>
|
||||||
|
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
|
||||||
|
${this._dirty?a("save",t)||"Save":a("saved",t)||"Saved"}
|
||||||
|
</button>
|
||||||
|
<button class="btn link" @click=${this._onDeepLink}>
|
||||||
|
${a("budget_advanced",t)||"Currency, alerts\u2026"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`:l`
|
||||||
|
<button class="btn link" @click=${this._onDeepLink}>
|
||||||
|
${a("budget_open_panel",t)||"Open in panel"}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`}};s.styles=[f,u`
|
||||||
|
.currency {
|
||||||
|
font-size: 14px; font-weight: 600;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
padding: 2px 10px; border-radius: 999px;
|
||||||
|
}
|
||||||
|
.track { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.track-label-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.track-label-row label {
|
||||||
|
font-size: 12px; color: var(--secondary-text-color);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.track-numbers { font-size: 13px; font-weight: 600; }
|
||||||
|
.track-numbers.ok { color: var(--primary-text-color); }
|
||||||
|
.track-numbers.warning { color: #ff9800; }
|
||||||
|
.track-numbers.danger { color: var(--error-color, #f44336); }
|
||||||
|
.bar {
|
||||||
|
height: 6px; background: var(--secondary-background-color);
|
||||||
|
border-radius: 3px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar-fill { height: 100%; transition: width 0.3s; border-radius: 3px; }
|
||||||
|
.bar-fill.ok { background: var(--primary-color); }
|
||||||
|
.bar-fill.warning { background: #ff9800; }
|
||||||
|
.bar-fill.danger { background: var(--error-color, #f44336); }
|
||||||
|
.inputs-row {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
|
||||||
|
padding-top: 4px; border-top: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
.input-field { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.input-field label {
|
||||||
|
font-size: 11px; color: var(--secondary-text-color);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.input-wrap { position: relative; display: flex; align-items: center; }
|
||||||
|
.input-wrap input {
|
||||||
|
flex: 1; padding: 6px 32px 6px 8px; font-size: 13px;
|
||||||
|
background: var(--secondary-background-color, #2c2c2c);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.input-suffix {
|
||||||
|
position: absolute; right: 8px;
|
||||||
|
color: var(--secondary-text-color); font-size: 13px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.actions { display: flex; gap: 8px; align-items: center; }
|
||||||
|
`],n([y({attribute:!1})],s.prototype,"hass",2),n([o()],s.prototype,"_config",2),n([o()],s.prototype,"_status",2),n([o()],s.prototype,"_busy",2),n([o()],s.prototype,"_error",2),n([o()],s.prototype,"_localMonthly",2),n([o()],s.prototype,"_localYearly",2),n([o()],s.prototype,"_dirty",2);customElements.get("maintenance-budget-section-card")||customElements.define("maintenance-budget-section-card",s);m({type:"maintenance-budget-section-card",name:"Maintenance Supporter \u2014 Budget",description:"Inline monthly + yearly budget editor",preview:!1});export{s as MaintenanceBudgetSectionCard};
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,60 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as t}from"./chunk-CYP6LL7W.js";function e(o){let r=window;r.customCards=r.customCards||[],r.customCards.some(a=>a.type===o.type)||r.customCards.push(o)}var d=t`
|
||||||
|
ha-card { overflow: hidden; }
|
||||||
|
.card-content {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
font-size: 16px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.emoji { font-size: 20px; }
|
||||||
|
|
||||||
|
/* Button family — primary action / muted-saved-state / link / icon-with-text */
|
||||||
|
.btn {
|
||||||
|
padding: 6px 12px; font-size: 13px;
|
||||||
|
border-radius: 6px; cursor: pointer;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
background: var(--secondary-background-color, transparent);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); }
|
||||||
|
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.btn.primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-primary-color, white);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.btn.primary[disabled] { opacity: 0.6; }
|
||||||
|
.btn.muted {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
.btn.muted[disabled] { opacity: 1; cursor: default; }
|
||||||
|
.btn.muted ha-icon, .btn.primary ha-icon { --mdc-icon-size: 14px; }
|
||||||
|
.btn.link {
|
||||||
|
background: transparent; border: none; padding: 6px 4px;
|
||||||
|
color: var(--primary-color); margin-left: auto;
|
||||||
|
}
|
||||||
|
.btn.link:hover { background: transparent; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Error + loading states */
|
||||||
|
.error {
|
||||||
|
padding: 8px; border-radius: 6px;
|
||||||
|
background: rgba(211, 47, 47, 0.1);
|
||||||
|
color: var(--error-color, #d32f2f); font-size: 13px;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
padding: 24px; text-align: center;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;export{e as a,d as b};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
var s=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var t=(a,r,c,o)=>{for(var e=o>1?void 0:o?l(r,c):r,i=a.length-1,d;i>=0;i--)(d=a[i])&&(e=(o?d(r,c,e):d(e))||e);return o&&e&&s(r,c,e),e};var m={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},v={ok:"mdi:check-circle",due_soon:"mdi:alert-circle",overdue:"mdi:alert-octagon",triggered:"mdi:bell-alert",archived:"mdi:archive-outline",paused:"mdi:pause-circle-outline",completed:"mdi:check-circle",skipped:"mdi:skip-next",missed:"mdi:calendar-remove",reset:"mdi:refresh"};export{t as a,m as b,v as c};
|
||||||
+2790
File diff suppressed because one or more lines are too long
+137
@@ -0,0 +1,137 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as v,b}from"./chunk-HZFXKH3I.js";import{a as u,b as s,d as c,e as h,f as g,g as o,i,j as _,l as m,u as p}from"./chunk-CYP6LL7W.js";import{a}from"./chunk-UTYWUNJT.js";var e=class extends h{constructor(){super(...arguments);this._config={type:""};this._groups={};this._loaded=!1;this._busy=!1;this._error="";this._newName="";this._editingId=null;this._editingName="";this._hasInitiallyLoaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return _(this.hass)}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._hasInitiallyLoaded&&(this._hasInitiallyLoaded=!0,this._load(),m(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/groups"});this._groups=t.groups||{},this._loaded=!0}catch(t){this._error=p(t,this._lang)}}async _addGroup(){if(!this._isAdmin)return;let t=this._newName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/create",name:t}),this._newName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}_startEdit(t){this._editingId=t,this._editingName=this._groups[t]?.name||""}async _saveEdit(){if(!this._isAdmin||!this._editingId)return;let t=this._editingName.trim();if(t){this._busy=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/update",group_id:this._editingId,name:t}),this._editingId=null,this._editingName="",await this._load()}catch(r){this._error=p(r,this._lang)}finally{this._busy=!1}}}async _deleteGroup(t,r){if(!this._isAdmin)return;let n=(i("group_delete_confirm",this._lang)||'Delete group "{name}"?').replace("{name}",r);if(window.confirm(n)){this._busy=!0;try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/group/delete",group_id:t}),await this._load()}catch(d){this._error=p(d,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_groups"),window.dispatchEvent(new CustomEvent("location-changed"))}_onKeyDown(t,r){t.key==="Enter"?(t.preventDefault(),r()):t.key==="Escape"&&(t.preventDefault(),this._editingId=null,this._editingName="")}render(){let t=this._lang;if(!this._loaded)return s`<ha-card><div class="loading">${i("loading",t)||"Loading\u2026"}</div></ha-card>`;let r=Object.keys(this._groups);return s`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<span class="emoji">🏷️</span>
|
||||||
|
<span>${this._config.title||i("groups",t)||"Groups"}</span>
|
||||||
|
<span class="count">${r.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this._error?s`<div class="error">${this._error}</div>`:c}
|
||||||
|
|
||||||
|
${r.length===0?s`<div class="empty">${i("groups_empty",t)||"No groups yet."}</div>`:s`
|
||||||
|
<div class="group-list">
|
||||||
|
${r.map(n=>{let d=this._groups[n],y=d.task_refs?.length??0,f=this._editingId===n;return s`
|
||||||
|
<div class="group-row">
|
||||||
|
${f?s`
|
||||||
|
<input class="edit-input" type="text"
|
||||||
|
.value=${this._editingName}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${l=>{this._editingName=l.target.value}}
|
||||||
|
@keydown=${l=>this._onKeyDown(l,this._saveEdit.bind(this))} />
|
||||||
|
<button class="btn small primary"
|
||||||
|
@click=${this._saveEdit}
|
||||||
|
?disabled=${this._busy||!this._editingName.trim()}>
|
||||||
|
${i("save",t)||"Save"}
|
||||||
|
</button>
|
||||||
|
<button class="btn small"
|
||||||
|
@click=${()=>{this._editingId=null}}>
|
||||||
|
${i("cancel",t)||"Cancel"}
|
||||||
|
</button>
|
||||||
|
`:s`
|
||||||
|
<span class="group-name">${d.name||"Unnamed"}</span>
|
||||||
|
<span class="task-count">${y}</span>
|
||||||
|
${this._isAdmin?s`
|
||||||
|
<button class="icon-btn"
|
||||||
|
title="${i("edit",t)||"Edit"}"
|
||||||
|
@click=${()=>this._startEdit(n)}
|
||||||
|
?disabled=${this._busy}>
|
||||||
|
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn danger"
|
||||||
|
title="${i("delete",t)||"Delete"}"
|
||||||
|
@click=${()=>this._deleteGroup(n,d.name||"Unnamed")}
|
||||||
|
?disabled=${this._busy}>
|
||||||
|
<ha-icon icon="mdi:delete"></ha-icon>
|
||||||
|
</button>
|
||||||
|
`:c}
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`})}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
|
${this._isAdmin?s`
|
||||||
|
<div class="add-row">
|
||||||
|
<input type="text"
|
||||||
|
placeholder="${i("group_new_placeholder",t)||"Add group\u2026"}"
|
||||||
|
.value=${this._newName}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${n=>{this._newName=n.target.value}}
|
||||||
|
@keydown=${n=>this._onKeyDown(n,this._addGroup.bind(this))} />
|
||||||
|
<button class="btn primary"
|
||||||
|
@click=${this._addGroup}
|
||||||
|
?disabled=${this._busy||!this._newName.trim()}>
|
||||||
|
<ha-icon icon="mdi:plus"></ha-icon>
|
||||||
|
${i("add",t)||"Add"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn link" @click=${this._onDeepLink}>
|
||||||
|
${i("groups_manage_tasks",t)||"Manage task assignments\u2026"}
|
||||||
|
</button>
|
||||||
|
`:s`
|
||||||
|
<button class="btn link" @click=${this._onDeepLink}>
|
||||||
|
${i("groups_open_panel",t)||"Open in panel"}
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`}};e.styles=[b,u`
|
||||||
|
.count {
|
||||||
|
font-size: 12px; color: var(--secondary-text-color);
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
padding: 2px 8px; border-radius: 999px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
padding: 16px; text-align: center;
|
||||||
|
color: var(--secondary-text-color); font-style: italic;
|
||||||
|
}
|
||||||
|
.group-list { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.group-row {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 6px 8px; border-radius: 6px;
|
||||||
|
background: var(--secondary-background-color, rgba(255,255,255,0.03));
|
||||||
|
}
|
||||||
|
.group-name { flex: 1; font-size: 14px; }
|
||||||
|
.task-count {
|
||||||
|
font-size: 11px; color: var(--secondary-text-color);
|
||||||
|
background: var(--card-background-color, rgba(0,0,0,0.2));
|
||||||
|
padding: 1px 8px; border-radius: 999px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.edit-input {
|
||||||
|
flex: 1; padding: 4px 8px; font-size: 14px;
|
||||||
|
background: var(--card-background-color, #1c1c1c);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--primary-color); border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.icon-btn {
|
||||||
|
background: transparent; border: none; cursor: pointer;
|
||||||
|
color: var(--secondary-text-color); padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: var(--state-icon-color, rgba(255,255,255,0.06));
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.icon-btn.danger:hover { color: var(--error-color); }
|
||||||
|
.icon-btn ha-icon { --mdc-icon-size: 18px; }
|
||||||
|
.add-row {
|
||||||
|
display: flex; gap: 6px;
|
||||||
|
padding-top: 8px; border-top: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
.add-row input {
|
||||||
|
flex: 1; padding: 6px 8px; font-size: 13px;
|
||||||
|
background: var(--secondary-background-color, #2c2c2c);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
/* Card-specific overrides on the shared .btn */
|
||||||
|
.btn.small { padding: 4px 8px; font-size: 12px; }
|
||||||
|
.btn ha-icon { --mdc-icon-size: 16px; }
|
||||||
|
`],a([g({attribute:!1})],e.prototype,"hass",2),a([o()],e.prototype,"_config",2),a([o()],e.prototype,"_groups",2),a([o()],e.prototype,"_loaded",2),a([o()],e.prototype,"_busy",2),a([o()],e.prototype,"_error",2),a([o()],e.prototype,"_newName",2),a([o()],e.prototype,"_editingId",2),a([o()],e.prototype,"_editingName",2);customElements.get("maintenance-groups-section-card")||customElements.define("maintenance-groups-section-card",e);v({type:"maintenance-groups-section-card",name:"Maintenance Supporter \u2014 Groups",description:"Inline group CRUD",preview:!1});export{e as MaintenanceGroupsSectionCard};
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
/*! maintenance_supporter frontend 2.65.0 */
|
||||||
|
import{a as m,b as g}from"./chunk-HZFXKH3I.js";import{a as h,b as n,d as c,e as _,f as v,g as r,i as e,j as f,l as b,u as l}from"./chunk-CYP6LL7W.js";import{a as i}from"./chunk-UTYWUNJT.js";var a=class extends _{constructor(){super(...arguments);this._config={type:""};this._state=null;this._busy=!1;this._error="";this._localStart="";this._localEnd="";this._localBuffer=7;this._dirty=!1;this._loaded=!1}setConfig(t){this._config=t}getCardSize(){return 2}get _lang(){return f(this.hass)}get _isAdmin(){return this.hass?.user?.is_admin??!0}updated(t){super.updated(t),t.has("hass")&&this.hass&&!this._loaded&&(this._loaded=!0,this._load(),b(this._lang).then(()=>this.requestUpdate()))}async _load(){try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/state"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||"",this._localBuffer=t.buffer_days??7,this._dirty=!1}catch(t){this._error=l(t,this._lang)}}async _toggleEnabled(t){this._busy=!0,this._error="";try{let s=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",enabled:t});this._state=s}catch(s){this._error=l(s,this._lang)}finally{this._busy=!1}}async _save(){if(this._isAdmin){this._busy=!0,this._error="";try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/update",start:this._localStart||null,end:this._localEnd||null,buffer_days:this._localBuffer});this._state=t,this._dirty=!1}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}async _endNow(){if(this._isAdmin&&window.confirm(e("vacation_end_now_confirm",this._lang)||"End vacation immediately?")){this._busy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/vacation/end_now"});this._state=t,this._localStart=t.start||"",this._localEnd=t.end||""}catch(t){this._error=l(t,this._lang)}finally{this._busy=!1}}}_onDeepLink(){history.pushState(null,"","/maintenance-supporter?ms_action=open_vacation"),window.dispatchEvent(new CustomEvent("location-changed"))}render(){let t=this._lang,s=this._state;if(!s)return n`<ha-card><div class="loading">${e("loading",t)||"Loading\u2026"}</div></ha-card>`;let p=s.is_active===!0,d=s.enabled===!0,u=s.exempt_task_ids?.length??0,y=p?e("vacation_status_active",t)||"Active now":d?e("vacation_status_scheduled",t)||"Scheduled":e("vacation_status_inactive",t)||"Inactive",$=p?"active":d?"scheduled":"inactive";return n`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<span class="emoji">🏖️</span>
|
||||||
|
<span>${this._config.title||e("vacation_mode",t)||"Vacation mode"}</span>
|
||||||
|
</div>
|
||||||
|
<span class="status-pill ${$}">${y}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this._error?n`<div class="error">${this._error}</div>`:c}
|
||||||
|
|
||||||
|
${this._isAdmin?n`
|
||||||
|
<div class="row toggle-row">
|
||||||
|
<label>${e("enable",t)||"Enable"}</label>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${d}
|
||||||
|
.disabled=${this._busy}
|
||||||
|
@change=${o=>this._toggleEnabled(o.target.checked)}
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dates-row">
|
||||||
|
<div class="date-field">
|
||||||
|
<label>${e("vacation_start",t)||"Start"}</label>
|
||||||
|
<input type="date" .value=${this._localStart}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${o=>{this._localStart=o.target.value,this._dirty=!0}} />
|
||||||
|
</div>
|
||||||
|
<div class="date-field">
|
||||||
|
<label>${e("vacation_end",t)||"End"}</label>
|
||||||
|
<input type="date" .value=${this._localEnd}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${o=>{this._localEnd=o.target.value,this._dirty=!0}} />
|
||||||
|
</div>
|
||||||
|
<div class="date-field buffer">
|
||||||
|
<label>${e("vacation_buffer",t)||"Buffer days"}</label>
|
||||||
|
<input type="number" min="0" max="14"
|
||||||
|
.value=${String(this._localBuffer)}
|
||||||
|
?disabled=${this._busy}
|
||||||
|
@input=${o=>{this._localBuffer=parseInt(o.target.value,10)||0,this._dirty=!0}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn ${this._dirty?"primary":"muted"}"
|
||||||
|
@click=${this._save}
|
||||||
|
?disabled=${this._busy||!this._dirty}>
|
||||||
|
<ha-icon icon="${this._dirty?"mdi:content-save":"mdi:check"}"></ha-icon>
|
||||||
|
${this._dirty?e("save",t)||"Save":e("saved",t)||"Saved"}
|
||||||
|
</button>
|
||||||
|
${p?n`<button class="btn"
|
||||||
|
@click=${this._endNow}
|
||||||
|
?disabled=${this._busy}>
|
||||||
|
${e("vacation_end_now",t)||"End now"}
|
||||||
|
</button>`:c}
|
||||||
|
${u>0?n`<button class="btn link"
|
||||||
|
@click=${this._onDeepLink}>
|
||||||
|
${u} ${e("vacation_exempt_count",t)||"exempt"}…
|
||||||
|
</button>`:n`<button class="btn link"
|
||||||
|
@click=${this._onDeepLink}>
|
||||||
|
${e("vacation_advanced",t)||"Advanced\u2026"}
|
||||||
|
</button>`}
|
||||||
|
</div>
|
||||||
|
`:n`
|
||||||
|
<div class="readonly">
|
||||||
|
${d&&s.start&&s.end?n`<div>${s.start} → ${s.end}</div>`:c}
|
||||||
|
<button class="btn link" @click=${this._onDeepLink}>
|
||||||
|
${e("vacation_open_panel",t)||"Open in panel"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`}};a.styles=[g,h`
|
||||||
|
.status-pill {
|
||||||
|
font-size: 11px; font-weight: 600;
|
||||||
|
padding: 3px 8px; border-radius: 999px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.status-pill.active {
|
||||||
|
background: rgba(76, 175, 80, 0.15);
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
.status-pill.scheduled {
|
||||||
|
background: rgba(255, 152, 0, 0.15);
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
.status-pill.inactive {
|
||||||
|
background: rgba(158, 158, 158, 0.15);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.row.toggle-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.row.toggle-row label {
|
||||||
|
font-size: 14px; color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.dates-row {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px;
|
||||||
|
}
|
||||||
|
.date-field.buffer label { white-space: nowrap; }
|
||||||
|
.date-field { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.date-field label {
|
||||||
|
font-size: 11px; color: var(--secondary-text-color);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.date-field input {
|
||||||
|
padding: 6px 8px; font-size: 13px;
|
||||||
|
background: var(--secondary-background-color, #2c2c2c);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.date-field input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.actions {
|
||||||
|
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.readonly { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
`],i([v({attribute:!1})],a.prototype,"hass",2),i([r()],a.prototype,"_config",2),i([r()],a.prototype,"_state",2),i([r()],a.prototype,"_busy",2),i([r()],a.prototype,"_error",2),i([r()],a.prototype,"_localStart",2),i([r()],a.prototype,"_localEnd",2),i([r()],a.prototype,"_localBuffer",2),i([r()],a.prototype,"_dirty",2);customElements.get("maintenance-vacation-section-card")||customElements.define("maintenance-vacation-section-card",a);m({type:"maintenance-vacation-section-card",name:"Maintenance Supporter \u2014 Vacation",description:"Inline vacation mode toggle + dates",preview:!1});export{a as MaintenanceVacationSectionCard};
|
||||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -22,5 +22,5 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"pypdf>=4.3.0"
|
"pypdf>=4.3.0"
|
||||||
],
|
],
|
||||||
"version": "2.64.0"
|
"version": "2.65.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ class MaintenanceTask:
|
|||||||
# --- Trigger ---
|
# --- Trigger ---
|
||||||
trigger_config: dict[str, Any] | None = None
|
trigger_config: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
# --- Phases (#139): a cyclic sequence of different activities on ONE
|
||||||
|
# shared cadence. Definitions + sequence are STATIC config (dict-only,
|
||||||
|
# like consumes_parts — the model carries them read-only so complete()
|
||||||
|
# can rotate); the cursor is DYNAMIC (Store, like the rotation pointer).
|
||||||
|
phases: dict[str, Any] | None = None
|
||||||
|
phase_sequence: list[str] = field(default_factory=list)
|
||||||
|
phase_cursor: int = 0
|
||||||
|
|
||||||
# --- Metadata ---
|
# --- Metadata ---
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
documentation_url: str | None = None
|
documentation_url: str | None = None
|
||||||
@@ -136,6 +144,17 @@ class MaintenanceTask:
|
|||||||
due_override=parse_iso_date(self.due_override),
|
due_override=parse_iso_date(self.due_override),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_phase_name(self) -> str | None:
|
||||||
|
"""Name of the cycle phase currently due (#139), or None."""
|
||||||
|
if not (self.phases and self.phase_sequence):
|
||||||
|
return None
|
||||||
|
from ..helpers.phases import clamp_phase_cursor
|
||||||
|
|
||||||
|
phase_id = self.phase_sequence[clamp_phase_cursor(self.phase_cursor, len(self.phase_sequence))]
|
||||||
|
definition = self.phases.get(phase_id)
|
||||||
|
return definition.get("name") if isinstance(definition, dict) else None
|
||||||
|
|
||||||
def _planned_grid_due(self) -> date | None:
|
def _planned_grid_due(self) -> date | None:
|
||||||
"""``next_due`` WITHOUT the postpone override — the drift-free grid.
|
"""``next_due`` WITHOUT the postpone override — the drift-free grid.
|
||||||
|
|
||||||
@@ -375,6 +394,17 @@ class MaintenanceTask:
|
|||||||
anchors.append(self.last_performed)
|
anchors.append(self.last_performed)
|
||||||
is_latest = ts_iso >= max(anchors, default="")
|
is_latest = ts_iso >= max(anchors, default="")
|
||||||
|
|
||||||
|
# Phases (#139): the step being completed is the one currently due.
|
||||||
|
# Resolved BEFORE the cursor advances; a pure backfill gets no phase
|
||||||
|
# attribution (we cannot know which step it was) and never rotates.
|
||||||
|
completed_phase_id: str | None = None
|
||||||
|
if is_latest and self.phases and self.phase_sequence:
|
||||||
|
from ..helpers.phases import clamp_phase_cursor
|
||||||
|
|
||||||
|
cursor = clamp_phase_cursor(self.phase_cursor, len(self.phase_sequence))
|
||||||
|
completed_phase_id = self.phase_sequence[cursor]
|
||||||
|
self.phase_cursor = (cursor + 1) % len(self.phase_sequence)
|
||||||
|
|
||||||
if is_latest:
|
if is_latest:
|
||||||
# Save the PLANNED grid date as the anchor before resetting — not
|
# Save the PLANNED grid date as the anchor before resetting — not
|
||||||
# next_due, which returns a postpone override and would shift the
|
# next_due, which returns a postpone override and would shift the
|
||||||
@@ -404,6 +434,7 @@ class MaintenanceTask:
|
|||||||
used_parts=used_parts,
|
used_parts=used_parts,
|
||||||
auto=auto,
|
auto=auto,
|
||||||
timestamp=ts_iso,
|
timestamp=ts_iso,
|
||||||
|
phase_id=completed_phase_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Shared tasks: rotate the "currently responsible" pointer to the next
|
# Shared tasks: rotate the "currently responsible" pointer to the next
|
||||||
@@ -508,6 +539,7 @@ class MaintenanceTask:
|
|||||||
used_parts: list[dict[str, Any]] | None = None,
|
used_parts: list[dict[str, Any]] | None = None,
|
||||||
auto: bool = False,
|
auto: bool = False,
|
||||||
timestamp: str | None = None,
|
timestamp: str | None = None,
|
||||||
|
phase_id: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add an entry to the maintenance history.
|
"""Add an entry to the maintenance history.
|
||||||
|
|
||||||
@@ -520,6 +552,10 @@ class MaintenanceTask:
|
|||||||
}
|
}
|
||||||
if auto:
|
if auto:
|
||||||
entry["auto"] = True
|
entry["auto"] = True
|
||||||
|
# Phases (#139): which cycle step this completion performed — the key
|
||||||
|
# per-phase statistics and "last done" lines pool by.
|
||||||
|
if phase_id is not None:
|
||||||
|
entry["phase_id"] = phase_id
|
||||||
if notes is not None:
|
if notes is not None:
|
||||||
entry["notes"] = notes
|
entry["notes"] = notes
|
||||||
if cost is not None:
|
if cost is not None:
|
||||||
@@ -630,6 +666,13 @@ class MaintenanceTask:
|
|||||||
data["checklist"] = self.checklist
|
data["checklist"] = self.checklist
|
||||||
if self.adaptive_config is not None:
|
if self.adaptive_config is not None:
|
||||||
data["adaptive_config"] = self.adaptive_config
|
data["adaptive_config"] = self.adaptive_config
|
||||||
|
# Phases (#139): defs + sequence are static config (like
|
||||||
|
# trigger_config), the cursor is the dynamic pointer — emitted only
|
||||||
|
# for phase-carrying tasks so plain tasks stay lean.
|
||||||
|
if self.phases and self.phase_sequence:
|
||||||
|
data["phases"] = self.phases
|
||||||
|
data["phase_sequence"] = self.phase_sequence
|
||||||
|
data["phase_cursor"] = self.phase_cursor
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -642,6 +685,9 @@ class MaintenanceTask:
|
|||||||
``task.interval_days`` etc. is unaffected.
|
``task.interval_days`` etc. is unaffected.
|
||||||
"""
|
"""
|
||||||
sched = read_legacy_fields(data)
|
sched = read_legacy_fields(data)
|
||||||
|
raw_phases = data.get("phases")
|
||||||
|
raw_seq = data.get("phase_sequence")
|
||||||
|
raw_cursor = data.get("phase_cursor", 0)
|
||||||
return cls(
|
return cls(
|
||||||
id=data.get("id", uuid4().hex),
|
id=data.get("id", uuid4().hex),
|
||||||
object_id=data.get("object_id", ""),
|
object_id=data.get("object_id", ""),
|
||||||
@@ -677,4 +723,7 @@ class MaintenanceTask:
|
|||||||
checklist=data.get("checklist", []),
|
checklist=data.get("checklist", []),
|
||||||
adaptive_config=data.get("adaptive_config"),
|
adaptive_config=data.get("adaptive_config"),
|
||||||
history=data.get("history", []),
|
history=data.get("history", []),
|
||||||
|
phases=raw_phases if isinstance(raw_phases, dict) else None,
|
||||||
|
phase_sequence=list(raw_seq) if isinstance(raw_seq, list) else [],
|
||||||
|
phase_cursor=raw_cursor if isinstance(raw_cursor, int) else 0,
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user