155 files
This commit is contained in:
1 parent
a212615470
commit
0ff8c287a8
155 files changed
+19264
-3148
No files matched your search
@@ -53,6 +53,7 @@ from .const import (
|
||||
CONF_BUDGET_YEARLY,
|
||||
CONF_GROUPS,
|
||||
CONF_INSTALL_ASSIST_SENTENCES,
|
||||
CONF_NOTIFY_SERVICE,
|
||||
CONF_OBJECT,
|
||||
CONF_PANEL_ENABLED,
|
||||
CONF_SHOPPING_LIST_ENTITY,
|
||||
@@ -1150,17 +1151,25 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
|
||||
from .helpers.entity_rename import rewrite_object, rewrite_store, rewrite_tasks
|
||||
|
||||
# v2.67: the global shopping-list target is an entity reference too.
|
||||
# v2.67: the global shopping-list target is an entity reference too —
|
||||
# and so is a notify ENTITY picked as the notification target (a
|
||||
# renamed one used to leave notifications to a repair message; a
|
||||
# legacy notify service is no entity and never renamed).
|
||||
gentry = get_global_entry(hass)
|
||||
if gentry is not None and gentry.options.get(CONF_SHOPPING_LIST_ENTITY) == old_eid:
|
||||
hass.config_entries.async_update_entry(
|
||||
gentry, options={**gentry.options, CONF_SHOPPING_LIST_ENTITY: new_eid}
|
||||
)
|
||||
from .shopping_sync import SHOPPING_SYNC_KEY
|
||||
if gentry is not None:
|
||||
renamed = {
|
||||
key: new_eid
|
||||
for key in (CONF_SHOPPING_LIST_ENTITY, CONF_NOTIFY_SERVICE)
|
||||
if gentry.options.get(key, gentry.data.get(key)) == old_eid
|
||||
}
|
||||
if renamed:
|
||||
hass.config_entries.async_update_entry(gentry, options={**gentry.options, **renamed})
|
||||
if CONF_SHOPPING_LIST_ENTITY in renamed:
|
||||
from .shopping_sync import SHOPPING_SYNC_KEY
|
||||
|
||||
sync = hass.data.get(DOMAIN, {}).get(SHOPPING_SYNC_KEY)
|
||||
if sync is not None:
|
||||
await sync.async_handle_rename(old_eid, new_eid)
|
||||
sync = hass.data.get(DOMAIN, {}).get(SHOPPING_SYNC_KEY)
|
||||
if sync is not None:
|
||||
await sync.async_handle_rename(old_eid, new_eid)
|
||||
|
||||
for ce in hass.config_entries.async_entries(DOMAIN):
|
||||
if ce.unique_id == GLOBAL_UNIQUE_ID:
|
||||
|
||||
@@ -1709,7 +1709,8 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
dynamic: dict[str, Any],
|
||||
static: dict[str, Any],
|
||||
) -> None:
|
||||
"""Put a task back the way it was before one voice action (undo).
|
||||
"""Put a task back the way it was before one action (undo — by voice,
|
||||
or the panel's and the card's Undo after Complete).
|
||||
|
||||
``dynamic`` maps Store fields to their earlier values (``None`` = the
|
||||
field was absent); ``static`` does the same for the entry-level
|
||||
@@ -1730,8 +1731,11 @@ class MaintenanceCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
write_task(self.hass, self.entry, task_id, current)
|
||||
# The undone completion must not swallow the real one that follows it
|
||||
# as a double tap, nor hold the triggers in their post-completion
|
||||
# cooldown.
|
||||
# cooldown. That holds for a back-dated one too: undone and sent again
|
||||
# with the same moment (to add the forgotten note), it was dropped as
|
||||
# a "double submit" for 30 s.
|
||||
self._recent_manual_completions.pop(task_id, None)
|
||||
self._recent_backfills = {key: ts for key, ts in self._recent_backfills.items() if key[0] != task_id}
|
||||
self._recently_completed.pop(task_id, None)
|
||||
self._completion_cooldown.discard(task_id)
|
||||
await self._store.async_save()
|
||||
|
||||
+3
-5
@@ -22,8 +22,8 @@ import {
|
||||
MAX_OCCURRENCES_PER_TASK,
|
||||
} from "../helpers/calendar-bucket";
|
||||
|
||||
const TODAY = new Date(2026, 4, 1); // 2026-05-01 local
|
||||
const TODAY_ISO = "2026-05-01";
|
||||
const TODAY = "2026-05-01"; // HA's today, as haToday() gives it
|
||||
const TODAY_ISO = TODAY;
|
||||
|
||||
function addDays(iso: string, days: number): string {
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
@@ -341,10 +341,8 @@ describe("buildCalendarBuckets", () => {
|
||||
// ── v2.2.0 — past-window bucketer ────────────────────────────────────────
|
||||
|
||||
function histEntry(daysAgo: number, type = "completed", over: Partial<any> = {}) {
|
||||
const d = new Date(TODAY);
|
||||
d.setDate(d.getDate() - daysAgo);
|
||||
return {
|
||||
timestamp: `${isoDateLocal(d)}T08:30:00`,
|
||||
timestamp: `${addDays(TODAY_ISO, -daysAgo)}T08:30:00`,
|
||||
type,
|
||||
...over,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { expect } from "@open-wc/testing";
|
||||
import { warrantyStatus, WARRANTY_WARN_DAYS } from "../helpers/warranty";
|
||||
|
||||
const TODAY = new Date(2026, 5, 1); // 2026-06-01 local
|
||||
const TODAY = "2026-06-01"; // HA's today, as haToday() gives it
|
||||
|
||||
describe("warrantyStatus (#67)", () => {
|
||||
it("null / undefined / empty / invalid → none", () => {
|
||||
|
||||
+9
-7
@@ -11,7 +11,7 @@ import { LS_KEYS, lsGet, lsSet } from "../helpers/storage-keys";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions";
|
||||
import { fetchSettingsOnce } from "../helpers/settings-cache";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { addDaysIso, haToday } from "../helpers/ha-time";
|
||||
import { px } from "../renderers/chart-utils";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -332,16 +332,18 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
* `days_until` comes from last-replaced + typical lifetime, so it is an
|
||||
* estimate — the tilde in the template says so. Negative values (past the
|
||||
* typical lifetime but not reported low yet) render as past dates, which
|
||||
* is honest: the battery is living on borrowed time. */
|
||||
* is honest: the battery is living on borrowed time. The server counts
|
||||
* the days from HA's today — so does the date (the browser's day plus
|
||||
* N × 24 h was a day off on a device in another zone). */
|
||||
private _predictedDate(daysUntil: number): string {
|
||||
return this._fmtDate(Date.now() + daysUntil * 864e5);
|
||||
return formatDate(addDaysIso(haToday(), daysUntil), this._lang);
|
||||
}
|
||||
|
||||
/** Local calendar date of an epoch, in the HA profile date format (#163 —
|
||||
* a direct Intl.DateTimeFormat call here ignored the profile and showed
|
||||
* 9/2/2026 to a DD/MM/YYYY user). */
|
||||
/** The calendar date of an epoch (a detected swap), in the HA profile
|
||||
* date format and time zone (#163 — a direct Intl.DateTimeFormat call
|
||||
* here ignored the profile and showed 9/2/2026 to a DD/MM/YYYY user). */
|
||||
private _fmtDate(epochMs: number): string {
|
||||
return formatDate(isoDateLocal(new Date(epochMs)), this._lang);
|
||||
return Number.isFinite(epochMs) ? formatDate(new Date(epochMs).toISOString(), this._lang) : "—";
|
||||
}
|
||||
|
||||
/** The grouped shopping quantities as CLICKABLE chips: a type filters the
|
||||
|
||||
@@ -4,9 +4,9 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { HomeAssistant, ReadingSlot, TaskPartLink } from "../types";
|
||||
import { lastReadingBefore, type ReadingHistoryEntry } from "../helpers/reading-slots";
|
||||
import { t, nativeFieldStyles, formatCost, formatNumber, formatQty } from "../styles";
|
||||
import { t, nativeFieldStyles, formatCost, formatNumber, formatQty, haTimeZoneHint } from "../styles";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { isoMinuteLocal } from "../helpers/calendar-bucket";
|
||||
import { haNowMinute, stampMs } from "../helpers/ha-time";
|
||||
import { PART_QTY_RANGE, RESTOCK_QTY_RANGE } from "../helpers/setting-ranges";
|
||||
import { partLinkKey, type LinkedPart } from "../helpers/shared-parts";
|
||||
import { unitPrice, type PartsCostMode } from "../helpers/parts-cost";
|
||||
@@ -23,6 +23,15 @@ import { photoPickerStyles } from "./ms-photo-picker";
|
||||
* One TS source (helpers/setting-ranges), pinned to Python. */
|
||||
const USED_QTY_RANGE = PART_QTY_RANGE;
|
||||
|
||||
/** `task-completed`'s detail: which task, and whether the server remembered
|
||||
* the completion so the opener may offer Undo. */
|
||||
export interface TaskCompletedDetail {
|
||||
entryId: string;
|
||||
taskId: string;
|
||||
taskName: string;
|
||||
undo: boolean;
|
||||
}
|
||||
|
||||
export class MaintenanceCompleteDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property() public entryId = "";
|
||||
@@ -97,7 +106,8 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
/** #161 phase 2: typed text per slot id (parsed on save; "" = unread). */
|
||||
@state() private _readingValues: Record<string, string> = {};
|
||||
@state() private _restockQty = "";
|
||||
/** #133: optional backdated completion moment ("YYYY-MM-DDTHH:MM:SS" local; "" = now). */
|
||||
/** #133: optional backdated completion moment ("YYYY-MM-DDTHH:MM:SS" in
|
||||
* HA local time — zone-less, the way the backend reads it; "" = now). */
|
||||
@state() private _completedAt = "";
|
||||
/** Keyed by `partLinkKey` — the (entry_id, part_id) pair — because two
|
||||
* objects can carry the same part id, so part_id alone would merge pools. */
|
||||
@@ -203,8 +213,10 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
}
|
||||
if (this._completedAt) {
|
||||
// Client-side guard mirrors the backend rule — a picked future moment
|
||||
// fails fast with a localized message instead of a WS roundtrip.
|
||||
if (new Date(this._completedAt).getTime() > Date.now()) {
|
||||
// fails fast with a localized message instead of a WS roundtrip. The
|
||||
// value is HA local time, so it is judged on HA's clock: read in the
|
||||
// browser's zone, a moment just past was "future" east of HA.
|
||||
if (stampMs(this._completedAt) > Date.now()) {
|
||||
this._error = t("completed_at_future_error", this.lang);
|
||||
return;
|
||||
}
|
||||
@@ -270,14 +282,22 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
if (res === undefined) return;
|
||||
this._photos.markAttached(); // attached now — Cancel cleanup must not touch them
|
||||
this._open = false;
|
||||
this.dispatchEvent(new CustomEvent("task-completed"));
|
||||
// The opener offers Undo when the server remembered the completion for
|
||||
// this person (task/undo, ten minutes, one level).
|
||||
const detail: TaskCompletedDetail = {
|
||||
entryId: this.entryId,
|
||||
taskId: this.taskId,
|
||||
taskName: this.taskName,
|
||||
undo: (res as { undo?: boolean } | null)?.undo === true,
|
||||
};
|
||||
this.dispatchEvent(new CustomEvent<TaskCompletedDetail>("task-completed", { detail }));
|
||||
}
|
||||
|
||||
/** #161 phase 2: one field per slot with the previous value as a hint;
|
||||
* a value below the last one gets a warning (meters count up — a
|
||||
* replaced meter or a typo), never a block. */
|
||||
private _renderReadingField(slot: ReadingSlot, L: string) {
|
||||
const at = this._completedAt ? new Date(this._completedAt).getTime() : NaN;
|
||||
const at = this._completedAt ? stampMs(this._completedAt) : NaN;
|
||||
const last = lastReadingBefore(this.readingHistory, slot.id, isNaN(at) ? undefined : at);
|
||||
const unit = slot.unit || this.readingUnit;
|
||||
const raw = (this._readingValues[slot.id] ?? "").trim();
|
||||
@@ -386,9 +406,12 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
this._photos.discardOrphans();
|
||||
}
|
||||
|
||||
/** Seed the backdate field with the current minute (local, seconds zeroed). */
|
||||
/** Seed the backdate field with HA's current minute (seconds zeroed) —
|
||||
* the field means HA local time. The browser's minute was refused as
|
||||
* "future" east of HA and stored hours early west of it (next_due a day
|
||||
* early once that crossed midnight). */
|
||||
private _pickCompletedAt(): void {
|
||||
this._completedAt = isoMinuteLocal(new Date());
|
||||
this._completedAt = `${haNowMinute()}:00`;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -525,6 +548,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.value=${this._completedAt}
|
||||
.helper=${haTimeZoneHint(L)}
|
||||
@value-changed=${(e: CustomEvent) => (this._completedAt = e.detail.value as string)}
|
||||
></ms-date-field>`
|
||||
: html`<button type="button" class="backdate-pick" @click=${this._pickCompletedAt}>
|
||||
|
||||
+15
-5
@@ -9,9 +9,10 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { t, langOf, formatNumber } from "../styles";
|
||||
import { t, langOf, formatNumber, haTimeZoneHint } from "../styles";
|
||||
import type { HomeAssistant, ReadingSlot, ReadingValue } from "../types";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { haWallTime } from "../helpers/ha-time";
|
||||
import { PART_QTY_RANGE } from "../helpers/setting-ranges";
|
||||
import { focusModalShell, modalShellStyles, renderModalShell } from "../helpers/modal-shell";
|
||||
import { PhotoUploadController } from "../helpers/photo-upload-controller";
|
||||
@@ -245,15 +246,20 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
|
||||
/** #170: remove the entry altogether (after a confirm). The backend
|
||||
* re-anchors last_performed on what remains; photos stay in the object's
|
||||
* documents, consumed parts are not restocked. */
|
||||
* documents, and the parts the entry used go back to stock — the confirm
|
||||
* names them. */
|
||||
private async _delete(): Promise<void> {
|
||||
if (!this._draft || !this._originalSnapshot) return;
|
||||
const L = this._lang;
|
||||
const used = (this._originalSnapshot.used_parts || []).filter((u) => u.quantity > 0);
|
||||
const parts = used.map((u) => `${formatNumber(u.quantity, L)}× ${u.name || u.part_id}`).join(", ");
|
||||
// The confirm dialog, not the browser's native modal — this dialog is
|
||||
// also mounted on dashboards (DRY audit 2026-09-26).
|
||||
const confirmed = await confirmAction(this.hass, {
|
||||
title: t("history_delete_entry", L),
|
||||
message: t("history_delete_confirm", L),
|
||||
message: parts
|
||||
? `${t("history_delete_confirm_entry", L)} ${t("history_delete_returns_parts", L).replace("{parts}", parts)}`
|
||||
: t("history_delete_confirm_entry", L),
|
||||
confirmText: t("delete", L),
|
||||
danger: true,
|
||||
});
|
||||
@@ -386,10 +392,14 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.label=${t("history_edit_timestamp", L)}
|
||||
.value=${d.timestamp.slice(0, 19)}
|
||||
.helper=${haTimeZoneHint(L)}
|
||||
.value=${haWallTime(d.timestamp)}
|
||||
@value-changed=${(e: CustomEvent) => {
|
||||
// Always "YYYY-MM-DDTHH:MM:SS" from the field; the naive value
|
||||
// means local time, exactly as the datetime-local input did.
|
||||
// means HA local time — the backend reads it so. The field
|
||||
// therefore shows HA's wall clock (an imported "+00:00" stamp's
|
||||
// own digits were UTC) and names HA's zone when the history
|
||||
// list shows another one (the profile's "local" zone).
|
||||
const v = e.detail.value as string;
|
||||
if (v) this._set("timestamp", v);
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { downloadTextFile } from "../helpers/download";
|
||||
import { invalidateSettingsCache, ROW_ACTION_STYLES } from "../helpers/settings-cache";
|
||||
import { SETTING_INT_RANGES, VACATION_BUFFER_DAYS_RANGE, settingIntRange } from "../helpers/setting-ranges";
|
||||
import { ToastTimer } from "../helpers/toast";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { haToday } from "../helpers/ha-time";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { DOCS_ARCHIVE_MAX_BYTES, postMultipart } from "../helpers/photo-upload";
|
||||
import { countryName, detectionReasons, dwellingLabel, type HomeProfile } from "../helpers/home-profile";
|
||||
@@ -2084,7 +2084,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
...(ids ? { entry_ids: ids } : {}),
|
||||
});
|
||||
if (!result) return;
|
||||
const ts = isoDateLocal(new Date());
|
||||
// Dated by HA's day, like every date inside the export.
|
||||
const ts = haToday();
|
||||
this._downloadFile(result.data, `maintenance_export_${ts}.json`, "application/json");
|
||||
this._showToast(t("settings_export_success", this._lang));
|
||||
}
|
||||
@@ -2095,7 +2096,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
private async _exportSettings(): Promise<void> {
|
||||
const result = await this._ws<{ data: string }>({ type: "maintenance_supporter/settings/export" });
|
||||
if (!result) return;
|
||||
const ts = isoDateLocal(new Date());
|
||||
const ts = haToday();
|
||||
this._downloadFile(result.data, `maintenance_settings_${ts}.json`, "application/json");
|
||||
this._showToast(t("settings_export_success", this._lang));
|
||||
}
|
||||
@@ -2109,7 +2110,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
...(ids ? { entry_ids: ids } : {}),
|
||||
});
|
||||
if (!result) return;
|
||||
const ts = isoDateLocal(new Date());
|
||||
const ts = haToday();
|
||||
this._downloadFile(result.data, `maintenance_export_${ts}.yaml`, "application/yaml");
|
||||
this._showToast(t("settings_export_success", this._lang));
|
||||
}
|
||||
@@ -2121,7 +2122,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
...(ids ? { entry_ids: ids } : {}),
|
||||
});
|
||||
if (!result) return;
|
||||
const ts = isoDateLocal(new Date());
|
||||
const ts = haToday();
|
||||
this._downloadFile(result.csv, `maintenance_export_${ts}.csv`, "text/csv");
|
||||
this._showToast(t("settings_export_success", this._lang));
|
||||
}
|
||||
|
||||
+6
-4
@@ -19,7 +19,7 @@ import { sharedStyles, t, formatDate, formatInterval, formatRecurrence, formatCo
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { snoozedMessage, type SnoozeResult } from "../helpers/snooze";
|
||||
import { focusModalShell, modalShellStyles, renderModalShell } from "../helpers/modal-shell";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { haToday } from "../helpers/ha-time";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
@@ -108,9 +108,11 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
this._showAdaptive = false;
|
||||
this._skipReason = "";
|
||||
this._postponeDate = "";
|
||||
// Local calendar date — toISOString() is UTC and prefills YESTERDAY for
|
||||
// users east of UTC before their morning (bug audit 2026-08-22).
|
||||
this._resetDate = isoDateLocal(new Date());
|
||||
// HA's calendar date — the server dates the reset and refuses one after
|
||||
// ITS today; the browser's date is a day off around midnight on a device
|
||||
// in another zone (and toISOString() was UTC, YESTERDAY for users east
|
||||
// of UTC before their morning — bug audit 2026-08-22).
|
||||
this._resetDate = haToday();
|
||||
this._open = true;
|
||||
await Promise.all([this._loadTask(), this._loadFeatures()]);
|
||||
}
|
||||
|
||||
@@ -102,8 +102,9 @@ function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean {
|
||||
// Same story for the profile date/time format (#163): on a strategy
|
||||
// dashboard no panel or card may ever have fed hass.locale into the prefs
|
||||
// singleton, so formatDate inside the dialog would fall back to the
|
||||
// language default and ignore a DMY/MDY/YMD profile setting.
|
||||
setProfilePrefs(hass.locale as Parameters<typeof setProfilePrefs>[0], hass.config?.country);
|
||||
// language default and ignore a DMY/MDY/YMD profile setting. Likewise
|
||||
// HA's time zone, which a back-dated completion's "now" runs on.
|
||||
setProfilePrefs(hass.locale as Parameters<typeof setProfilePrefs>[0], hass.config?.country, hass.config?.time_zone);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { HistoryEntry, MaintenanceObjectResponse } from "../types";
|
||||
import { entrySpend } from "./parts-cost";
|
||||
import { intervalSpanDays } from "./interval";
|
||||
import { statusRank } from "../status-constants";
|
||||
import { addDaysIso, stampDate } from "./ha-time";
|
||||
|
||||
export const MAX_OCCURRENCES_PER_TASK = 5;
|
||||
|
||||
@@ -64,9 +65,10 @@ export interface CalendarDayBucket {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Date as ISO YYYY-MM-DD using LOCAL time components — matches what
|
||||
* the user sees on their wall clock, not UTC. The naive `.toISOString().slice(0,10)`
|
||||
* approach silently shifts dates near midnight in non-UTC timezones.
|
||||
* Format a Date as ISO YYYY-MM-DD using LOCAL time components — the
|
||||
* BROWSER's calendar day of a local Date (the naive `.toISOString().slice(0,10)`
|
||||
* is the UTC day). Never "today": next_due counts from HA's day, which a
|
||||
* device in another zone does not share — that is helpers/ha-time haToday.
|
||||
*/
|
||||
export function isoDateLocal(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
@@ -75,34 +77,13 @@ export function isoDateLocal(d: Date): string {
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/** The local wall-clock minute as "YYYY-MM-DDTHH:MM:00" (seconds zeroed) —
|
||||
* the value shape of a datetime field; the date half is isoDateLocal. */
|
||||
export function isoMinuteLocal(d: Date): string {
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mm = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${isoDateLocal(d)}T${hh}:${mm}:00`;
|
||||
}
|
||||
|
||||
/** Build the list of N consecutive ISO dates starting today (local). */
|
||||
export function buildWindowDates(today: Date, windowDays: number): string[] {
|
||||
/** The N consecutive ISO dates of a window starting at `today` ("YYYY-MM-DD"). */
|
||||
export function buildWindowDates(today: string, windowDays: number): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < windowDays; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() + i);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
out.push(isoDateLocal(d));
|
||||
}
|
||||
for (let i = 0; i < windowDays; i++) out.push(addDaysIso(today, i));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Add `days` to an ISO date (local), return new ISO date. */
|
||||
function addDaysIso(iso: string, days: number): string {
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
const date = new Date(y, m - 1, d);
|
||||
date.setDate(date.getDate() + days);
|
||||
return isoDateLocal(date);
|
||||
}
|
||||
|
||||
/** Average recorded cost across history rows that HAVE a cost (mean of
|
||||
* non-null `cost` values). Note this differs from the panel KPI, which divides
|
||||
* total_cost by times_performed — completions without a recorded cost are
|
||||
@@ -185,6 +166,8 @@ function projectTask(input: ProjectionInput): CalendarEvent[] {
|
||||
const nextDue = task.next_due;
|
||||
if (typeof nextDue !== "string" || !nextDue) return out;
|
||||
const firstDate = nextDue.slice(0, 10); // strip time portion if any
|
||||
// The projection steps a cursor from here — only from a real date shape.
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(firstDate)) return out;
|
||||
|
||||
// First occurrence must be in window
|
||||
if (firstDate >= windowStart && firstDate <= windowEnd) {
|
||||
@@ -214,13 +197,15 @@ function projectTask(input: ProjectionInput): CalendarEvent[] {
|
||||
* Main entry point: build the day-bucketed event list for the Calendar tab.
|
||||
*
|
||||
* @param objects The panel's loaded objects (with nested tasks).
|
||||
* @param today "Today" as a Date (caller passes new Date() — easier for tests).
|
||||
* @param today HA's today, "YYYY-MM-DD" (the caller passes haToday() —
|
||||
* next_due and the overdue status count from that day,
|
||||
* not from the browser's).
|
||||
* @param windowDays Number of days to include (7 / 14 / 30).
|
||||
* @param userFilter null/empty = all; otherwise filter tasks by responsible_user_id.
|
||||
*/
|
||||
export function buildCalendarBuckets(
|
||||
objects: MaintenanceObjectResponse[],
|
||||
today: Date,
|
||||
today: string,
|
||||
windowDays: number,
|
||||
userFilter: string | null = null,
|
||||
): CalendarDayBucket[] {
|
||||
@@ -315,7 +300,7 @@ export function pastHistoryKey(entryId: string, taskId: string): string {
|
||||
* so the card refetches only then. */
|
||||
export function pastHistoryGaps(
|
||||
objects: MaintenanceObjectResponse[],
|
||||
today: Date,
|
||||
today: string,
|
||||
pastDays: number,
|
||||
): Array<{ entryId: string; taskId: string; key: string; sig: string }> {
|
||||
const windowStart = buildPastWindowDates(today, pastDays)[0];
|
||||
@@ -325,8 +310,8 @@ export function pastHistoryGaps(
|
||||
const listed = (task.history || []) as HistoryEntryShape[];
|
||||
const count = task.history_count ?? listed.length;
|
||||
if (count <= listed.length) continue;
|
||||
const stamps = listed.map((h) => (typeof h?.timestamp === "string" ? h.timestamp : "")).filter(Boolean);
|
||||
const oldest = stamps.length ? stamps.reduce((a, b) => (a < b ? a : b)).slice(0, 10) : "";
|
||||
const days = listed.map((h) => stampDate(h?.timestamp)).filter((d): d is string => !!d);
|
||||
const oldest = days.length ? days.reduce((a, b) => (a < b ? a : b)) : "";
|
||||
if (oldest && oldest < windowStart) continue; // the window is fully listed
|
||||
const key = pastHistoryKey(obj.entry_id, task.id);
|
||||
out.push({ entryId: obj.entry_id, taskId: task.id, key, sig: `${key}:${count}:${contentHash(JSON.stringify(listed))}` });
|
||||
@@ -346,15 +331,10 @@ function contentHash(text: string): string {
|
||||
return (h >>> 0).toString(36);
|
||||
}
|
||||
|
||||
/** Build a list of N consecutive ISO dates ending today (local). */
|
||||
export function buildPastWindowDates(today: Date, pastDays: number): string[] {
|
||||
/** The N consecutive ISO dates of a window ending at `today` ("YYYY-MM-DD"). */
|
||||
export function buildPastWindowDates(today: string, pastDays: number): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = pastDays - 1; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
out.push(isoDateLocal(d));
|
||||
}
|
||||
for (let i = pastDays - 1; i >= 0; i--) out.push(addDaysIso(today, -i));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -364,10 +344,12 @@ export function buildPastWindowDates(today: Date, pastDays: number): string[] {
|
||||
* Each history entry whose timestamp falls inside the window becomes one
|
||||
* CalendarEvent with `history_timestamp` set — the calendar card uses that
|
||||
* timestamp as the stable key when dispatching an edit-history click event.
|
||||
* An entry lands on its HA calendar day (helpers/ha-time stampDate): the
|
||||
* first ten characters were the wrong day for an imported "+00:00" stamp.
|
||||
*/
|
||||
export function buildPastBuckets(
|
||||
objects: MaintenanceObjectResponse[],
|
||||
today: Date,
|
||||
today: string,
|
||||
pastDays: number,
|
||||
userFilter: string | null = null,
|
||||
/** Full histories fetched for the tasks `pastHistoryGaps` named, keyed by
|
||||
@@ -390,8 +372,8 @@ export function buildPastBuckets(
|
||||
const history = fullHistory[pastHistoryKey(entryId, task.id)] ?? ((task.history || []) as HistoryEntryShape[]);
|
||||
for (const h of history) {
|
||||
if (typeof h?.timestamp !== "string") continue;
|
||||
const dateKey = h.timestamp.slice(0, 10); // YYYY-MM-DD
|
||||
if (dateKey < windowStart || dateKey > windowEnd) continue;
|
||||
const dateKey = stampDate(h.timestamp); // YYYY-MM-DD, HA's day
|
||||
if (!dateKey || dateKey < windowStart || dateKey > windowEnd) continue;
|
||||
const bucket = byDate.get(dateKey);
|
||||
if (!bucket) continue;
|
||||
const evType = h.type ?? "completed";
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* deterministically without faking the system clock.
|
||||
*/
|
||||
|
||||
import { daysBetweenIso, haToday } from "./ha-time";
|
||||
|
||||
export type WarrantyStatusKind = "valid" | "expiring" | "expired" | "none";
|
||||
|
||||
export interface WarrantyStatus {
|
||||
@@ -21,20 +23,18 @@ export const WARRANTY_WARN_DAYS = 60;
|
||||
* Classify an object's warranty expiry date relative to `today`.
|
||||
*
|
||||
* @param iso ISO `YYYY-MM-DD` warranty expiry date (null/undefined/""/invalid → "none").
|
||||
* @param today Reference date; defaults to now. Injectable for deterministic tests.
|
||||
* @param today Reference day "YYYY-MM-DD"; defaults to HA's today (the
|
||||
* browser's date was a day off around midnight on a device in
|
||||
* another zone). Injectable for deterministic tests.
|
||||
*/
|
||||
export function warrantyStatus(
|
||||
iso: string | null | undefined,
|
||||
today: Date = new Date(),
|
||||
today: string = haToday(),
|
||||
): WarrantyStatus {
|
||||
if (!iso) return { kind: "none", days: null, date: null };
|
||||
const exp = new Date(`${iso}T00:00:00`);
|
||||
if (isNaN(exp.getTime())) return { kind: "none", days: null, date: null };
|
||||
// Normalize both ends to local midnight so the difference is whole days
|
||||
// regardless of the time-of-day component of `today`.
|
||||
const t0 = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const t1 = Date.UTC(exp.getFullYear(), exp.getMonth(), exp.getDate());
|
||||
const days = Math.round((t1 - t0) / 86400000);
|
||||
// Whole calendar days between two date-only strings — no time of day, no zone.
|
||||
const days = daysBetweenIso(today, iso);
|
||||
if (!Number.isFinite(days)) return { kind: "none", days: null, date: null };
|
||||
if (days < 0) return { kind: "expired", days, date: iso };
|
||||
if (days <= WARRANTY_WARN_DAYS) return { kind: "expiring", days, date: iso };
|
||||
return { kind: "valid", days, date: iso };
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import type { MaintenanceTask } from "../types";
|
||||
import { isoDateLocal } from "./calendar-bucket";
|
||||
import { displayTimeZone, ymdIn } from "./ha-time";
|
||||
|
||||
export interface WorksheetLabels {
|
||||
title: string; // "Work sheet"
|
||||
@@ -47,14 +47,14 @@ export interface WorksheetExcerpt {
|
||||
vendorBase?: string;
|
||||
}
|
||||
|
||||
/** The printing day in the USER's calendar and date format. The raw
|
||||
* `nowIso.slice(0, 10)` was the UTC day (`toISOString()`), so a sheet
|
||||
* printed after midnight east of UTC — or in the evening west of it —
|
||||
* carried the wrong date, and in ISO order regardless of the profile's
|
||||
* date format (bug audit 2026-09-26). */
|
||||
/** The printing day in the USER's calendar — the profile's time zone — and
|
||||
* date format. The raw `nowIso.slice(0, 10)` was the UTC day
|
||||
* (`toISOString()`), so a sheet printed after midnight east of UTC — or in
|
||||
* the evening west of it — carried the wrong date, and in ISO order
|
||||
* regardless of the profile's date format (bug audit 2026-09-26). */
|
||||
function printedOn(nowIso: string, formatDate: (iso: string) => string): string {
|
||||
const d = new Date(nowIso);
|
||||
return Number.isNaN(d.getTime()) ? nowIso.slice(0, 10) : formatDate(isoDateLocal(d));
|
||||
const ms = Date.parse(nowIso);
|
||||
return Number.isNaN(ms) ? nowIso.slice(0, 10) : formatDate(ymdIn(ms, displayTimeZone()));
|
||||
}
|
||||
|
||||
const esc = (v: unknown): string =>
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
|
||||
"completed_at_pick": "Nastavit datum a čas",
|
||||
"completed_at_future_error": "Datum dokončení nesmí být v budoucnosti.",
|
||||
"ha_time_zone_hint": "Čas v časovém pásmu Home Assistantu ({zone})",
|
||||
"days": "dní",
|
||||
"day": "den",
|
||||
"today": "Dnes",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Budoucí okna",
|
||||
"history_edit_title": "Upravit záznam historie",
|
||||
"history_delete_entry": "Smazat záznam",
|
||||
"history_delete_confirm": "Smazat tento záznam historie? Datum posledního provedení se vrátí k předchozímu dokončení; fotografie zůstanou u objektu a spotřebované díly se nevrátí na sklad.",
|
||||
"history_delete_confirm_entry": "Smazat tento záznam historie? Datum posledního provedení se vrátí na předchozí; fotky zůstanou u objektu.",
|
||||
"history_delete_returns_parts": "Použité díly se vrátí do zásoby: {parts}.",
|
||||
"history_edit_timestamp": "Časové razítko",
|
||||
"manufacturer": "Výrobce",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Načítání…",
|
||||
"archive": "Archivovat",
|
||||
"undo": "Zpět",
|
||||
"task_completed_named": "Dokončeno: {task}",
|
||||
"undo_done": "Dokončení vráceno zpět",
|
||||
"undo_done_action_ran": "Dokončení vráceno zpět. Akce při dokončení už proběhla a nelze ji vrátit.",
|
||||
"undo_nothing": "Není co vrátit. Vrátit jde jen vaše poslední dokončení, do 10 minut.",
|
||||
"undo_changed": "Nelze vrátit. Úkol se mezitím změnil.",
|
||||
"task_archived": "Úkol archivován",
|
||||
"object_archived": "Objekt archivován",
|
||||
"unarchive": "Obnovit z archivu",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
|
||||
"completed_at_pick": "Vælg dato og tidspunkt",
|
||||
"completed_at_future_error": "Udførelsesdatoen må ikke ligge i fremtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidszone ({zone})",
|
||||
"days": "dage",
|
||||
"day": "dag",
|
||||
"today": "I dag",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Kommende vinduer",
|
||||
"history_edit_title": "Rediger historikpost",
|
||||
"history_delete_entry": "Slet post",
|
||||
"history_delete_confirm": "Slet denne historikpost? Datoen for seneste udførelse falder tilbage til den forrige; fotos bliver hos objektet, og forbrugte dele lægges ikke tilbage på lager.",
|
||||
"history_delete_confirm_entry": "Slet denne historikpost? Datoen for sidste udførelse falder tilbage til den forrige; billeder bliver ved objektet.",
|
||||
"history_delete_returns_parts": "De brugte dele går tilbage på lager: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstempel",
|
||||
"manufacturer": "Producent",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Indlæser…",
|
||||
"archive": "Arkivér",
|
||||
"undo": "Fortryd",
|
||||
"task_completed_named": "Fuldført: {task}",
|
||||
"undo_done": "Udførelsen fortrudt",
|
||||
"undo_done_action_ran": "Udførelsen fortrudt. Handlingen ved udførelse var allerede kørt og kan ikke fortrydes.",
|
||||
"undo_nothing": "Intet at fortryde. Kun din egen seneste udførelse kan fortrydes, inden for 10 minutter.",
|
||||
"undo_changed": "Kan ikke fortrydes. Opgaven er ændret siden.",
|
||||
"task_archived": "Opgave arkiveret",
|
||||
"object_archived": "Objekt arkiveret",
|
||||
"unarchive": "Gendan",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
|
||||
"completed_at_pick": "Datum & Uhrzeit wählen",
|
||||
"completed_at_future_error": "Das Erledigungsdatum darf nicht in der Zukunft liegen.",
|
||||
"ha_time_zone_hint": "Uhrzeit in der Zeitzone von Home Assistant ({zone})",
|
||||
"days": "Tage",
|
||||
"day": "Tag",
|
||||
"today": "Heute",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Zukünftige Fenster",
|
||||
"history_edit_title": "Verlaufseintrag bearbeiten",
|
||||
"history_delete_entry": "Eintrag löschen",
|
||||
"history_delete_confirm": "Diesen Verlaufseintrag löschen? Das Datum der letzten Erledigung fällt auf die vorherige zurück; Fotos bleiben beim Objekt, verbrauchte Teile werden nicht zurückgebucht.",
|
||||
"history_delete_confirm_entry": "Diesen Verlaufseintrag löschen? Das Datum der letzten Erledigung fällt auf die vorherige zurück; Fotos bleiben beim Objekt.",
|
||||
"history_delete_returns_parts": "Die dabei verbrauchten Teile gehen zurück in den Bestand: {parts}.",
|
||||
"history_edit_timestamp": "Zeitstempel",
|
||||
"manufacturer": "Hersteller",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laden…",
|
||||
"archive": "Archivieren",
|
||||
"undo": "Rückgängig",
|
||||
"task_completed_named": "Erledigt: {task}",
|
||||
"undo_done": "Erledigung zurückgenommen",
|
||||
"undo_done_action_ran": "Erledigung zurückgenommen. Die Aktion beim Erledigen war schon gelaufen und lässt sich nicht zurücknehmen.",
|
||||
"undo_nothing": "Nichts zurückzunehmen. Nur deine eigene letzte Erledigung lässt sich zurücknehmen, innerhalb von 10 Minuten.",
|
||||
"undo_changed": "Zurücknehmen nicht möglich. Die Aufgabe wurde inzwischen geändert.",
|
||||
"task_archived": "Aufgabe archiviert",
|
||||
"object_archived": "Objekt archiviert",
|
||||
"unarchive": "Wiederherstellen",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Completed at (optional, empty = now)",
|
||||
"completed_at_pick": "Set date & time",
|
||||
"completed_at_future_error": "The completion date cannot be in the future.",
|
||||
"ha_time_zone_hint": "Time in Home Assistant's time zone ({zone})",
|
||||
"days": "days",
|
||||
"day": "day",
|
||||
"today": "Today",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Forward windows",
|
||||
"history_edit_title": "Edit history entry",
|
||||
"history_delete_entry": "Delete entry",
|
||||
"history_delete_confirm": "Delete this history entry? The task's last-performed date falls back to the previous completion; photos stay with the object and consumed parts are not restocked.",
|
||||
"history_delete_confirm_entry": "Delete this history entry? The task's last-performed date falls back to the previous completion; photos stay with the object.",
|
||||
"history_delete_returns_parts": "The parts it used go back to stock: {parts}.",
|
||||
"history_edit_timestamp": "Timestamp",
|
||||
"manufacturer": "Manufacturer",
|
||||
"model": "Model",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Loading…",
|
||||
"archive": "Archive",
|
||||
"undo": "Undo",
|
||||
"task_completed_named": "Completed: {task}",
|
||||
"undo_done": "Completion undone",
|
||||
"undo_done_action_ran": "Completion undone. Its completion action had already run and cannot be taken back.",
|
||||
"undo_nothing": "Nothing to undo. Only your own last completion can be taken back, within 10 minutes.",
|
||||
"undo_changed": "Can't undo. The task has changed since.",
|
||||
"task_archived": "Task archived",
|
||||
"object_archived": "Object archived",
|
||||
"unarchive": "Unarchive",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
|
||||
"completed_at_pick": "Elegir fecha y hora",
|
||||
"completed_at_future_error": "La fecha de finalización no puede estar en el futuro.",
|
||||
"ha_time_zone_hint": "Hora en la zona horaria de Home Assistant ({zone})",
|
||||
"days": "días",
|
||||
"day": "día",
|
||||
"today": "Hoy",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Ventanas futuras",
|
||||
"history_edit_title": "Editar entrada del historial",
|
||||
"history_delete_entry": "Eliminar entrada",
|
||||
"history_delete_confirm": "¿Eliminar esta entrada del historial? La fecha de última realización vuelve a la anterior; las fotos permanecen con el objeto y las piezas consumidas no se reponen.",
|
||||
"history_delete_confirm_entry": "¿Eliminar esta entrada del historial? La fecha de la última realización vuelve a la anterior; las fotos se quedan con el objeto.",
|
||||
"history_delete_returns_parts": "Las piezas usadas vuelven al stock: {parts}.",
|
||||
"history_edit_timestamp": "Marca de tiempo",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Cargando…",
|
||||
"archive": "Archivar",
|
||||
"undo": "Deshacer",
|
||||
"task_completed_named": "Completada: {task}",
|
||||
"undo_done": "Realización deshecha",
|
||||
"undo_done_action_ran": "Realización deshecha. La acción al completar ya se había ejecutado y no se puede deshacer.",
|
||||
"undo_nothing": "No hay nada que deshacer. Solo puedes deshacer tu última realización, en un plazo de 10 minutos.",
|
||||
"undo_changed": "No se puede deshacer. La tarea ha cambiado desde entonces.",
|
||||
"task_archived": "Tarea archivada",
|
||||
"object_archived": "Objeto archivado",
|
||||
"unarchive": "Desarchivar",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
|
||||
"completed_at_pick": "Aseta päivä ja aika",
|
||||
"completed_at_future_error": "Suorituspäivä ei voi olla tulevaisuudessa.",
|
||||
"ha_time_zone_hint": "Aika Home Assistantin aikavyöhykkeellä ({zone})",
|
||||
"days": "päivää",
|
||||
"day": "päivä",
|
||||
"today": "Tänään",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Tulevat jaksot",
|
||||
"history_edit_title": "Muokkaa historiamerkintää",
|
||||
"history_delete_entry": "Poista merkintä",
|
||||
"history_delete_confirm": "Poistetaanko tämä historiamerkintä? Viimeisimmän suorituksen päivä palautuu edelliseen; kuvat jäävät kohteelle eikä kulutettuja osia palauteta varastoon.",
|
||||
"history_delete_confirm_entry": "Poistetaanko tämä historiamerkintä? Viimeisin suorituspäivä palaa edelliseen; kuvat jäävät kohteelle.",
|
||||
"history_delete_returns_parts": "Käytetyt osat palautuvat varastoon: {parts}.",
|
||||
"history_edit_timestamp": "Aikaleima",
|
||||
"manufacturer": "Valmistaja",
|
||||
"model": "Malli",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Ladataan…",
|
||||
"archive": "Arkistoi",
|
||||
"undo": "Kumoa",
|
||||
"task_completed_named": "Suoritettu: {task}",
|
||||
"undo_done": "Suoritus peruttu",
|
||||
"undo_done_action_ran": "Suoritus peruttu. Suorituksen toiminto oli jo ajettu, eikä sitä voi perua.",
|
||||
"undo_nothing": "Ei mitään peruttavaa. Vain oman viimeisimmän suorituksen voi perua, 10 minuutin sisällä.",
|
||||
"undo_changed": "Perua ei voi. Tehtävä on muuttunut sen jälkeen.",
|
||||
"task_archived": "Tehtävä arkistoitu",
|
||||
"object_archived": "Kohde arkistoitu",
|
||||
"unarchive": "Palauta arkistosta",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
|
||||
"completed_at_pick": "Choisir la date et l'heure",
|
||||
"completed_at_future_error": "La date d'achèvement ne peut pas être dans le futur.",
|
||||
"ha_time_zone_hint": "Heure dans le fuseau horaire de Home Assistant ({zone})",
|
||||
"days": "jours",
|
||||
"day": "jour",
|
||||
"today": "Aujourd'hui",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Fenêtres à venir",
|
||||
"history_edit_title": "Modifier l'entrée d'historique",
|
||||
"history_delete_entry": "Supprimer l'entrée",
|
||||
"history_delete_confirm": "Supprimer cette entrée d'historique ? La date de dernière réalisation revient à la précédente ; les photos restent avec l'objet et les pièces consommées ne sont pas remises en stock.",
|
||||
"history_delete_confirm_entry": "Supprimer cette entrée d'historique ? La date de dernière réalisation revient à la précédente ; les photos restent avec l'objet.",
|
||||
"history_delete_returns_parts": "Les pièces utilisées retournent en stock : {parts}.",
|
||||
"history_edit_timestamp": "Horodatage",
|
||||
"manufacturer": "Fabricant",
|
||||
"model": "Modèle",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Chargement…",
|
||||
"archive": "Archiver",
|
||||
"undo": "Annuler",
|
||||
"task_completed_named": "Terminé : {task}",
|
||||
"undo_done": "Réalisation annulée",
|
||||
"undo_done_action_ran": "Réalisation annulée. L'action de fin s'était déjà exécutée et ne peut pas être annulée.",
|
||||
"undo_nothing": "Rien à annuler. Seule votre dernière réalisation peut être annulée, dans les 10 minutes.",
|
||||
"undo_changed": "Annulation impossible. La tâche a changé depuis.",
|
||||
"task_archived": "Tâche archivée",
|
||||
"object_archived": "Objet archivé",
|
||||
"unarchive": "Désarchiver",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
|
||||
"completed_at_pick": "तारीख और समय चुनें",
|
||||
"completed_at_future_error": "पूर्णता की तारीख भविष्य में नहीं हो सकती।",
|
||||
"ha_time_zone_hint": "Home Assistant के समय क्षेत्र का समय ({zone})",
|
||||
"days": "दिन",
|
||||
"day": "दिन",
|
||||
"today": "आज",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "आगामी अवधियाँ",
|
||||
"history_edit_title": "इतिहास प्रविष्टि संपादित करें",
|
||||
"history_delete_entry": "प्रविष्टि हटाएँ",
|
||||
"history_delete_confirm": "क्या यह इतिहास प्रविष्टि हटाएँ? अंतिम निष्पादन की तिथि पिछली पूर्णता पर लौट जाएगी; फ़ोटो ऑब्जेक्ट के पास रहेंगे और उपयोग किए गए पुर्जे स्टॉक में वापस नहीं जोड़े जाएँगे।",
|
||||
"history_delete_confirm_entry": "यह इतिहास प्रविष्टि हटाएँ? अंतिम बार करने की तिथि पिछली पूर्णता पर लौट आएगी; फ़ोटो ऑब्जेक्ट के साथ रहेंगी।",
|
||||
"history_delete_returns_parts": "इस्तेमाल किए गए पुर्ज़े स्टॉक में वापस जाएँगे: {parts}।",
|
||||
"history_edit_timestamp": "टाइमस्टैम्प",
|
||||
"manufacturer": "निर्माता",
|
||||
"model": "मॉडल",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "लोड हो रहा है…",
|
||||
"archive": "संग्रह करें",
|
||||
"undo": "पूर्ववत करें",
|
||||
"task_completed_named": "पूर्ण हुआ: {task}",
|
||||
"undo_done": "पूर्णता वापस ली गई",
|
||||
"undo_done_action_ran": "पूर्णता वापस ली गई। पूरा करने पर चलने वाली कार्रवाई पहले ही चल चुकी थी, उसे वापस नहीं लिया जा सकता।",
|
||||
"undo_nothing": "वापस लेने को कुछ नहीं है। केवल आपकी अपनी पिछली पूर्णता 10 मिनट के भीतर वापस ली जा सकती है।",
|
||||
"undo_changed": "वापस नहीं लिया जा सकता। तब से कार्य बदल चुका है।",
|
||||
"task_archived": "कार्य संग्रहित",
|
||||
"object_archived": "ऑब्जेक्ट संग्रहित",
|
||||
"unarchive": "संग्रह से हटाएँ",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
|
||||
"completed_at_pick": "Dátum és idő megadása",
|
||||
"completed_at_future_error": "Az elvégzés dátuma nem lehet a jövőben.",
|
||||
"ha_time_zone_hint": "Idő a Home Assistant időzónájában ({zone})",
|
||||
"days": "nap",
|
||||
"day": "nap",
|
||||
"today": "Ma",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Jövőbeli időszakok",
|
||||
"history_edit_title": "Előzménybejegyzés szerkesztése",
|
||||
"history_delete_entry": "Bejegyzés törlése",
|
||||
"history_delete_confirm": "Törli ezt az előzménybejegyzést? Az utolsó elvégzés dátuma az előzőre áll vissza; a fotók az objektumnál maradnak, a felhasznált alkatrészek nem kerülnek vissza a készletbe.",
|
||||
"history_delete_confirm_entry": "Törlöd ezt az előzménybejegyzést? Az utolsó elvégzés dátuma visszaáll az előzőre; a fotók az objektumnál maradnak.",
|
||||
"history_delete_returns_parts": "A felhasznált alkatrészek visszakerülnek a készletbe: {parts}.",
|
||||
"history_edit_timestamp": "Időbélyeg",
|
||||
"manufacturer": "Gyártó",
|
||||
"model": "Modell",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Betöltés…",
|
||||
"archive": "Archiválás",
|
||||
"undo": "Visszavonás",
|
||||
"task_completed_named": "Elvégezve: {task}",
|
||||
"undo_done": "Elvégzés visszavonva",
|
||||
"undo_done_action_ran": "Elvégzés visszavonva. Az elvégzéskor futó művelet már lefutott, azt nem lehet visszavonni.",
|
||||
"undo_nothing": "Nincs mit visszavonni. Csak a saját legutóbbi elvégzésed vonható vissza, 10 percen belül.",
|
||||
"undo_changed": "Nem vonható vissza. A feladat azóta megváltozott.",
|
||||
"task_archived": "Feladat archiválva",
|
||||
"object_archived": "Objektum archiválva",
|
||||
"unarchive": "Archiválás visszavonása",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
|
||||
"completed_at_pick": "Imposta data e ora",
|
||||
"completed_at_future_error": "La data di completamento non può essere nel futuro.",
|
||||
"ha_time_zone_hint": "Ora nel fuso orario di Home Assistant ({zone})",
|
||||
"days": "giorni",
|
||||
"day": "giorno",
|
||||
"today": "Oggi",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Finestre future",
|
||||
"history_edit_title": "Modifica voce dello storico",
|
||||
"history_delete_entry": "Elimina voce",
|
||||
"history_delete_confirm": "Eliminare questa voce della cronologia? La data dell'ultima esecuzione torna al completamento precedente; le foto restano con l'oggetto e le parti consumate non vengono rimesse a magazzino.",
|
||||
"history_delete_confirm_entry": "Eliminare questa voce della cronologia? La data dell'ultima esecuzione torna a quella precedente; le foto restano con l'oggetto.",
|
||||
"history_delete_returns_parts": "I ricambi usati tornano in magazzino: {parts}.",
|
||||
"history_edit_timestamp": "Data e ora",
|
||||
"manufacturer": "Produttore",
|
||||
"model": "Modello",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Caricamento…",
|
||||
"archive": "Archivia",
|
||||
"undo": "Annulla",
|
||||
"task_completed_named": "Completato: {task}",
|
||||
"undo_done": "Completamento annullato",
|
||||
"undo_done_action_ran": "Completamento annullato. L'azione al completamento era già stata eseguita e non si può annullare.",
|
||||
"undo_nothing": "Niente da annullare. Si può annullare solo il tuo ultimo completamento, entro 10 minuti.",
|
||||
"undo_changed": "Impossibile annullare. L'attività è cambiata nel frattempo.",
|
||||
"task_archived": "Attività archiviata",
|
||||
"object_archived": "Oggetto archiviato",
|
||||
"unarchive": "Annulla archiviazione",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
|
||||
"completed_at_pick": "日時を設定",
|
||||
"completed_at_future_error": "完了日時に未来は指定できません。",
|
||||
"ha_time_zone_hint": "Home Assistant のタイムゾーンの時刻({zone})",
|
||||
"days": "日",
|
||||
"day": "日",
|
||||
"today": "今日",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "今後の期間",
|
||||
"history_edit_title": "履歴項目を編集",
|
||||
"history_delete_entry": "記録を削除",
|
||||
"history_delete_confirm": "この履歴記録を削除しますか?最終実施日は前回の完了に戻ります。写真はオブジェクトに残り、消費した部品は在庫に戻りません。",
|
||||
"history_delete_confirm_entry": "この履歴記録を削除しますか?最終実施日は前回の完了に戻ります。写真はオブジェクトに残ります。",
|
||||
"history_delete_returns_parts": "使用した部品は在庫に戻ります:{parts}。",
|
||||
"history_edit_timestamp": "日時",
|
||||
"manufacturer": "メーカー",
|
||||
"model": "型番",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "読み込み中…",
|
||||
"archive": "アーカイブ",
|
||||
"undo": "元に戻す",
|
||||
"task_completed_named": "完了しました:{task}",
|
||||
"undo_done": "完了を取り消しました",
|
||||
"undo_done_action_ran": "完了を取り消しました。完了時のアクションは既に実行されており、取り消せません。",
|
||||
"undo_nothing": "取り消せるものはありません。取り消せるのは自分の直近の完了だけで、10分以内です。",
|
||||
"undo_changed": "取り消せません。その後タスクが変更されています。",
|
||||
"task_archived": "タスクをアーカイブしました",
|
||||
"object_archived": "オブジェクトをアーカイブしました",
|
||||
"unarchive": "アーカイブ解除",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
|
||||
"completed_at_pick": "날짜 및 시간 설정",
|
||||
"completed_at_future_error": "완료 날짜는 미래일 수 없습니다.",
|
||||
"ha_time_zone_hint": "Home Assistant 시간대 기준 시간 ({zone})",
|
||||
"days": "일",
|
||||
"day": "일",
|
||||
"today": "오늘",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "이후 기간",
|
||||
"history_edit_title": "기록 항목 편집",
|
||||
"history_delete_entry": "기록 삭제",
|
||||
"history_delete_confirm": "이 기록을 삭제할까요? 마지막 수행일은 이전 완료로 되돌아가며, 사진은 개체에 남고 사용한 부품은 재고로 복구되지 않습니다.",
|
||||
"history_delete_confirm_entry": "이 기록 항목을 삭제할까요? 마지막 수행 날짜가 이전 완료로 돌아갑니다. 사진은 객체에 남습니다.",
|
||||
"history_delete_returns_parts": "사용한 부품은 재고로 돌아갑니다: {parts}.",
|
||||
"history_edit_timestamp": "타임스탬프",
|
||||
"manufacturer": "제조사",
|
||||
"model": "모델",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "불러오는 중…",
|
||||
"archive": "보관",
|
||||
"undo": "실행 취소",
|
||||
"task_completed_named": "완료됨: {task}",
|
||||
"undo_done": "완료를 취소했습니다",
|
||||
"undo_done_action_ran": "완료를 취소했습니다. 완료 시 동작은 이미 실행되어 되돌릴 수 없습니다.",
|
||||
"undo_nothing": "취소할 항목이 없습니다. 자신의 마지막 완료만 10분 안에 취소할 수 있습니다.",
|
||||
"undo_changed": "취소할 수 없습니다. 그 사이 작업이 변경되었습니다.",
|
||||
"task_archived": "작업이 보관되었습니다",
|
||||
"object_archived": "객체가 보관되었습니다",
|
||||
"unarchive": "보관 해제",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
|
||||
"completed_at_pick": "Velg dato og klokkeslett",
|
||||
"completed_at_future_error": "Fullføringsdatoen kan ikke ligge i fremtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidssone ({zone})",
|
||||
"days": "dager",
|
||||
"day": "dag",
|
||||
"today": "I dag",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Fremtidige vinduer",
|
||||
"history_edit_title": "Rediger historikkoppføring",
|
||||
"history_delete_entry": "Slett oppføring",
|
||||
"history_delete_confirm": "Slette denne historikkoppføringen? Datoen for siste utførelse faller tilbake til den forrige; bilder blir hos objektet, og forbrukte deler legges ikke tilbake på lager.",
|
||||
"history_delete_confirm_entry": "Slette denne historikkoppføringen? Datoen for siste utførelse faller tilbake til den forrige; bilder blir værende på objektet.",
|
||||
"history_delete_returns_parts": "De brukte delene går tilbake på lager: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstempel",
|
||||
"manufacturer": "Produsent",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laster…",
|
||||
"archive": "Arkivér",
|
||||
"undo": "Angre",
|
||||
"task_completed_named": "Fullført: {task}",
|
||||
"undo_done": "Utførelsen angret",
|
||||
"undo_done_action_ran": "Utførelsen angret. Handlingen ved utførelse hadde allerede kjørt og kan ikke angres.",
|
||||
"undo_nothing": "Ingenting å angre. Bare din egen siste utførelse kan angres, innen 10 minutter.",
|
||||
"undo_changed": "Kan ikke angres. Oppgaven er endret siden.",
|
||||
"task_archived": "Oppgave arkivert",
|
||||
"object_archived": "Objekt arkivert",
|
||||
"unarchive": "Gjenopprett",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
|
||||
"completed_at_pick": "Datum & tijd kiezen",
|
||||
"completed_at_future_error": "De voltooiingsdatum mag niet in de toekomst liggen.",
|
||||
"ha_time_zone_hint": "Tijd in de tijdzone van Home Assistant ({zone})",
|
||||
"days": "dagen",
|
||||
"day": "dag",
|
||||
"today": "Vandaag",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Toekomstige vensters",
|
||||
"history_edit_title": "Geschiedenisitem bewerken",
|
||||
"history_delete_entry": "Vermelding verwijderen",
|
||||
"history_delete_confirm": "Deze geschiedenisvermelding verwijderen? De datum van laatste uitvoering valt terug op de vorige voltooiing; foto's blijven bij het object en verbruikte onderdelen worden niet teruggeboekt.",
|
||||
"history_delete_confirm_entry": "Deze geschiedenisregel verwijderen? De datum van de laatste uitvoering valt terug op de vorige; foto's blijven bij het object.",
|
||||
"history_delete_returns_parts": "De gebruikte onderdelen gaan terug naar de voorraad: {parts}.",
|
||||
"history_edit_timestamp": "Tijdstempel",
|
||||
"manufacturer": "Fabrikant",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laden…",
|
||||
"archive": "Archiveren",
|
||||
"undo": "Ongedaan maken",
|
||||
"task_completed_named": "Voltooid: {task}",
|
||||
"undo_done": "Afronding ongedaan gemaakt",
|
||||
"undo_done_action_ran": "Afronding ongedaan gemaakt. De actie bij afronden was al uitgevoerd en kan niet worden teruggedraaid.",
|
||||
"undo_nothing": "Niets om ongedaan te maken. Alleen je eigen laatste afronding kan worden teruggedraaid, binnen 10 minuten.",
|
||||
"undo_changed": "Ongedaan maken kan niet. De taak is sindsdien gewijzigd.",
|
||||
"task_archived": "Taak gearchiveerd",
|
||||
"object_archived": "Object gearchiveerd",
|
||||
"unarchive": "Dearchiveren",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
|
||||
"completed_at_pick": "Ustaw datę i godzinę",
|
||||
"completed_at_future_error": "Data wykonania nie może być w przyszłości.",
|
||||
"ha_time_zone_hint": "Czas w strefie czasowej Home Assistant ({zone})",
|
||||
"days": "dni",
|
||||
"day": "dzień",
|
||||
"today": "Dzisiaj",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Przyszłe okna",
|
||||
"history_edit_title": "Edytuj wpis historii",
|
||||
"history_delete_entry": "Usuń wpis",
|
||||
"history_delete_confirm": "Usunąć ten wpis historii? Data ostatniego wykonania cofnie się do poprzedniego; zdjęcia pozostaną przy obiekcie, a zużyte części nie wrócą na stan.",
|
||||
"history_delete_confirm_entry": "Usunąć ten wpis historii? Data ostatniego wykonania wróci do poprzedniej; zdjęcia zostaną przy obiekcie.",
|
||||
"history_delete_returns_parts": "Zużyte części wrócą do magazynu: {parts}.",
|
||||
"history_edit_timestamp": "Znacznik czasu",
|
||||
"manufacturer": "Producent",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Ładowanie…",
|
||||
"archive": "Archiwizuj",
|
||||
"undo": "Cofnij",
|
||||
"task_completed_named": "Wykonano: {task}",
|
||||
"undo_done": "Wykonanie cofnięte",
|
||||
"undo_done_action_ran": "Wykonanie cofnięte. Akcja przy wykonaniu już się odbyła i nie da się jej cofnąć.",
|
||||
"undo_nothing": "Nie ma czego cofnąć. Cofnąć można tylko swoje ostatnie wykonanie, w ciągu 10 minut.",
|
||||
"undo_changed": "Nie można cofnąć. Zadanie zmieniło się w międzyczasie.",
|
||||
"task_archived": "Zadanie zarchiwizowane",
|
||||
"object_archived": "Obiekt zarchiwizowany",
|
||||
"unarchive": "Przywróć z archiwum",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
"completed_at_pick": "Definir data e hora",
|
||||
"completed_at_future_error": "A data de conclusão não pode estar no futuro.",
|
||||
"ha_time_zone_hint": "Horário no fuso horário do Home Assistant ({zone})",
|
||||
"days": "dias",
|
||||
"day": "dia",
|
||||
"today": "Hoje",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Janelas futuras",
|
||||
"history_edit_title": "Editar entrada do histórico",
|
||||
"history_delete_entry": "Excluir entrada",
|
||||
"history_delete_confirm": "Excluir esta entrada do histórico? A data da última execução volta para a conclusão anterior; as fotos permanecem com o objeto e as peças consumidas não são repostas.",
|
||||
"history_delete_confirm_entry": "Excluir esta entrada do histórico? A data da última realização volta para a anterior; as fotos continuam com o objeto.",
|
||||
"history_delete_returns_parts": "As peças usadas voltam ao estoque: {parts}.",
|
||||
"history_edit_timestamp": "Data e hora",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Carregando…",
|
||||
"archive": "Arquivar",
|
||||
"undo": "Desfazer",
|
||||
"task_completed_named": "Concluída: {task}",
|
||||
"undo_done": "Conclusão desfeita",
|
||||
"undo_done_action_ran": "Conclusão desfeita. A ação de conclusão já tinha sido executada e não pode ser revertida.",
|
||||
"undo_nothing": "Nada para desfazer. Só a sua última conclusão pode ser desfeita, em até 10 minutos.",
|
||||
"undo_changed": "Não é possível desfazer. A tarefa mudou desde então.",
|
||||
"task_archived": "Tarefa arquivada",
|
||||
"object_archived": "Objeto arquivado",
|
||||
"unarchive": "Desarquivar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
"completed_at_pick": "Definir data e hora",
|
||||
"completed_at_future_error": "A data de conclusão não pode estar no futuro.",
|
||||
"ha_time_zone_hint": "Hora no fuso horário do Home Assistant ({zone})",
|
||||
"days": "dias",
|
||||
"day": "dia",
|
||||
"today": "Hoje",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Janelas futuras",
|
||||
"history_edit_title": "Editar entrada do histórico",
|
||||
"history_delete_entry": "Eliminar entrada",
|
||||
"history_delete_confirm": "Eliminar esta entrada do histórico? A data da última execução recua para a conclusão anterior; as fotos ficam com o objeto e as peças consumidas não são repostas.",
|
||||
"history_delete_confirm_entry": "Eliminar esta entrada do histórico? A data da última realização volta à anterior; as fotos ficam com o objeto.",
|
||||
"history_delete_returns_parts": "As peças usadas voltam ao stock: {parts}.",
|
||||
"history_edit_timestamp": "Carimbo de data/hora",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "A carregar…",
|
||||
"archive": "Arquivar",
|
||||
"undo": "Desfazer",
|
||||
"task_completed_named": "Concluída: {task}",
|
||||
"undo_done": "Conclusão anulada",
|
||||
"undo_done_action_ran": "Conclusão anulada. A ação de conclusão já tinha sido executada e não pode ser revertida.",
|
||||
"undo_nothing": "Nada para anular. Só a sua última conclusão pode ser anulada, dentro de 10 minutos.",
|
||||
"undo_changed": "Não é possível anular. A tarefa mudou entretanto.",
|
||||
"task_archived": "Tarefa arquivada",
|
||||
"object_archived": "Objeto arquivado",
|
||||
"unarchive": "Desarquivar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
|
||||
"completed_at_pick": "Указать дату и время",
|
||||
"completed_at_future_error": "Дата выполнения не может быть в будущем.",
|
||||
"ha_time_zone_hint": "Время в часовом поясе Home Assistant ({zone})",
|
||||
"days": "дней",
|
||||
"day": "день",
|
||||
"today": "Сегодня",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Будущие окна",
|
||||
"history_edit_title": "Редактировать запись истории",
|
||||
"history_delete_entry": "Удалить запись",
|
||||
"history_delete_confirm": "Удалить эту запись истории? Дата последнего выполнения вернётся к предыдущему; фотографии останутся у объекта, а израсходованные детали не будут возвращены на склад.",
|
||||
"history_delete_confirm_entry": "Удалить эту запись истории? Дата последнего выполнения вернётся к предыдущей; фото останутся у объекта.",
|
||||
"history_delete_returns_parts": "Использованные запчасти вернутся на склад: {parts}.",
|
||||
"history_edit_timestamp": "Метка времени",
|
||||
"manufacturer": "Производитель",
|
||||
"model": "Модель",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Загрузка…",
|
||||
"archive": "Архивировать",
|
||||
"undo": "Отменить",
|
||||
"task_completed_named": "Выполнено: {task}",
|
||||
"undo_done": "Выполнение отменено",
|
||||
"undo_done_action_ran": "Выполнение отменено. Действие при выполнении уже сработало, его нельзя отменить.",
|
||||
"undo_nothing": "Нечего отменять. Можно отменить только своё последнее выполнение, в течение 10 минут.",
|
||||
"undo_changed": "Отменить нельзя. Задача с тех пор изменилась.",
|
||||
"task_archived": "Задача архивирована",
|
||||
"object_archived": "Объект архивирован",
|
||||
"unarchive": "Разархивировать",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
|
||||
"completed_at_pick": "Ange datum och tid",
|
||||
"completed_at_future_error": "Slutförandedatumet får inte ligga i framtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidszon ({zone})",
|
||||
"days": "dagar",
|
||||
"day": "dag",
|
||||
"today": "Idag",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Kommande fönster",
|
||||
"history_edit_title": "Redigera historikpost",
|
||||
"history_delete_entry": "Ta bort post",
|
||||
"history_delete_confirm": "Ta bort den här historikposten? Datumet för senaste utförande faller tillbaka till det föregående; foton stannar hos objektet och förbrukade delar läggs inte tillbaka i lager.",
|
||||
"history_delete_confirm_entry": "Ta bort den här historikposten? Datumet för senaste utförandet faller tillbaka på det föregående; foton finns kvar på objektet.",
|
||||
"history_delete_returns_parts": "De använda delarna går tillbaka till lagret: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstämpel",
|
||||
"manufacturer": "Tillverkare",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laddar…",
|
||||
"archive": "Arkivera",
|
||||
"undo": "Ångra",
|
||||
"task_completed_named": "Slutförd: {task}",
|
||||
"undo_done": "Utförandet ångrat",
|
||||
"undo_done_action_ran": "Utförandet ångrat. Åtgärden vid utförande hade redan körts och kan inte ångras.",
|
||||
"undo_nothing": "Inget att ångra. Bara ditt eget senaste utförande kan ångras, inom 10 minuter.",
|
||||
"undo_changed": "Kan inte ångras. Uppgiften har ändrats sedan dess.",
|
||||
"task_archived": "Uppgift arkiverad",
|
||||
"object_archived": "Objekt arkiverat",
|
||||
"unarchive": "Återställ",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
|
||||
"completed_at_pick": "Tarih ve saat seç",
|
||||
"completed_at_future_error": "Tamamlanma tarihi gelecekte olamaz.",
|
||||
"ha_time_zone_hint": "Home Assistant saat dilimindeki saat ({zone})",
|
||||
"days": "gün",
|
||||
"day": "gün",
|
||||
"today": "Bugün",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Gelecek dönemler",
|
||||
"history_edit_title": "Geçmiş kaydını düzenle",
|
||||
"history_delete_entry": "Kaydı sil",
|
||||
"history_delete_confirm": "Bu geçmiş kaydı silinsin mi? Son yapılma tarihi bir önceki tamamlamaya döner; fotoğraflar nesnede kalır, kullanılan parçalar stoğa geri eklenmez.",
|
||||
"history_delete_confirm_entry": "Bu geçmiş kaydı silinsin mi? Son yapılma tarihi bir önceki tamamlamaya geri döner; fotoğraflar nesnede kalır.",
|
||||
"history_delete_returns_parts": "Kullanılan parçalar stoğa geri döner: {parts}.",
|
||||
"history_edit_timestamp": "Zaman damgası",
|
||||
"manufacturer": "Üretici",
|
||||
"model": "Model",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Yükleniyor…",
|
||||
"archive": "Arşivle",
|
||||
"undo": "Geri al",
|
||||
"task_completed_named": "Tamamlandı: {task}",
|
||||
"undo_done": "Tamamlama geri alındı",
|
||||
"undo_done_action_ran": "Tamamlama geri alındı. Tamamlama eylemi zaten çalışmıştı ve geri alınamaz.",
|
||||
"undo_nothing": "Geri alınacak bir şey yok. Yalnızca kendi son tamamlamanızı 10 dakika içinde geri alabilirsiniz.",
|
||||
"undo_changed": "Geri alınamıyor. Görev o zamandan beri değişti.",
|
||||
"task_archived": "Görev arşivlendi",
|
||||
"object_archived": "Nesne arşivlendi",
|
||||
"unarchive": "Arşivden çıkar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
|
||||
"completed_at_pick": "Вказати дату й час",
|
||||
"completed_at_future_error": "Дата виконання не може бути в майбутньому.",
|
||||
"ha_time_zone_hint": "Час у часовому поясі Home Assistant ({zone})",
|
||||
"days": "днів",
|
||||
"day": "день",
|
||||
"today": "Сьогодні",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Майбутні вікна",
|
||||
"history_edit_title": "Редагувати запис історії",
|
||||
"history_delete_entry": "Видалити запис",
|
||||
"history_delete_confirm": "Видалити цей запис історії? Дата останнього виконання повернеться до попереднього; фотографії залишаться в об'єкта, а витрачені деталі не повернуться на склад.",
|
||||
"history_delete_confirm_entry": "Видалити цей запис історії? Дата останнього виконання повернеться до попередньої; фото залишаться в об'єкта.",
|
||||
"history_delete_returns_parts": "Використані запчастини повернуться на склад: {parts}.",
|
||||
"history_edit_timestamp": "Позначка часу",
|
||||
"manufacturer": "Виробник",
|
||||
"model": "Модель",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Завантаження…",
|
||||
"archive": "Архівувати",
|
||||
"undo": "Скасувати",
|
||||
"task_completed_named": "Виконано: {task}",
|
||||
"undo_done": "Виконання скасовано",
|
||||
"undo_done_action_ran": "Виконання скасовано. Дія під час виконання вже спрацювала, її не можна скасувати.",
|
||||
"undo_nothing": "Нічого скасовувати. Можна скасувати лише своє останнє виконання, протягом 10 хвилин.",
|
||||
"undo_changed": "Скасувати не можна. Завдання відтоді змінилося.",
|
||||
"task_archived": "Завдання архівовано",
|
||||
"object_archived": "Об'єкт архівовано",
|
||||
"unarchive": "Розархівувати",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "完成时间(可选,留空 = 现在)",
|
||||
"completed_at_pick": "设置日期和时间",
|
||||
"completed_at_future_error": "完成日期不能是未来时间。",
|
||||
"ha_time_zone_hint": "Home Assistant 时区的时间({zone})",
|
||||
"days": "天",
|
||||
"day": "天",
|
||||
"today": "今天",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "未来的窗口",
|
||||
"history_edit_title": "编辑历史记录",
|
||||
"history_delete_entry": "删除记录",
|
||||
"history_delete_confirm": "删除此历史记录?上次完成日期将回退到前一次完成;照片仍保留在对象中,已消耗的配件不会退回库存。",
|
||||
"history_delete_confirm_entry": "删除此历史记录?上次执行日期将退回到上一次完成;照片仍保留在对象中。",
|
||||
"history_delete_returns_parts": "所用的配件将退回库存:{parts}。",
|
||||
"history_edit_timestamp": "时间戳",
|
||||
"manufacturer": "制造商",
|
||||
"model": "型号",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "加载中…",
|
||||
"archive": "归档",
|
||||
"undo": "撤销",
|
||||
"task_completed_named": "已完成:{task}",
|
||||
"undo_done": "已撤销完成",
|
||||
"undo_done_action_ran": "已撤销完成。完成时的操作已经执行,无法撤销。",
|
||||
"undo_nothing": "没有可撤销的内容。只能在 10 分钟内撤销你自己最近一次完成。",
|
||||
"undo_changed": "无法撤销。此后任务已发生变化。",
|
||||
"task_archived": "任务已归档",
|
||||
"object_archived": "对象已归档",
|
||||
"unarchive": "取消归档",
|
||||
|
||||
@@ -31,12 +31,12 @@ import { property, state } from "lit/decorators.js";
|
||||
import {
|
||||
buildCalendarBuckets,
|
||||
buildPastBuckets,
|
||||
isoDateLocal,
|
||||
pastHistoryGaps,
|
||||
pastHistoryKey,
|
||||
type CalendarEvent,
|
||||
type HistoryEntryShape,
|
||||
} from "./helpers/calendar-bucket";
|
||||
import { haToday } from "./helpers/ha-time";
|
||||
import { calendarStyles } from "./calendar-styles";
|
||||
import { syncLocaleFromHass, sharedStyles, currencySymbolOf, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, formatInterval, formatWeekday, formatMonth, langOf, formatCost, syncCurrencyDecimals} from "./styles";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
@@ -218,9 +218,7 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
* only when that set or its entry counts change); only the newest round
|
||||
* may land. */
|
||||
private async _loadPastHistories(): Promise<void> {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const gaps = pastHistoryGaps(this._objects, today, this._pastDays || 30);
|
||||
const gaps = pastHistoryGaps(this._objects, haToday(), this._pastDays || 30);
|
||||
const sig = gaps.map((g) => g.sig).join("|");
|
||||
if (sig === this._pastHistorySig) return;
|
||||
this._pastHistorySig = sig;
|
||||
@@ -370,14 +368,15 @@ export class MaintenanceCalendarCard extends LitElement {
|
||||
? base.filter((o) => o.entry_id === filterEntryId)
|
||||
: base;
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
// HA's today: next_due and the overdue status count from it, so the
|
||||
// window and the Today marker must too — the browser's date is a day
|
||||
// off around midnight on a device in another zone.
|
||||
const todayIso = haToday();
|
||||
const isPast = this._pastDays > 0;
|
||||
const buckets = isPast
|
||||
? buildPastBuckets(objects, today, this._pastDays, userFilter, this._pastHistory)
|
||||
: buildCalendarBuckets(objects, today, this._windowDays, userFilter);
|
||||
? buildPastBuckets(objects, todayIso, this._pastDays, userFilter, this._pastHistory)
|
||||
: buildCalendarBuckets(objects, todayIso, this._windowDays, userFilter);
|
||||
|
||||
const todayIso = isoDateLocal(today);
|
||||
// Year view AND past views collapse empty days — past windows can be
|
||||
// sparse (most days have no maintenance recorded), so listing "no events"
|
||||
// for 30 rows is noise.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { hydrateObjects } from "./helpers/hydrate-objects";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, langOf, currencySymbolOf, syncCurrencyDecimals} from "./styles";
|
||||
import { openSignedDocument } from "./helpers/document-url";
|
||||
import { isPhotoDocument } from "./helpers/document-categories";
|
||||
import { isSafeHttpUrl } from "./helpers/url";
|
||||
import { registerCustomCard } from "./helpers/register-card";
|
||||
import { healCardRegistry } from "./helpers/registry-heal";
|
||||
@@ -29,7 +30,8 @@ import { canWrite } from "./helpers/permissions";
|
||||
import { ACTIONABLE_STATUSES, isActionableStatus, statusRank } from "./status-constants";
|
||||
import "./maintenance-card-editor";
|
||||
import "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog, TaskCompletedDetail } from "./components/complete-dialog";
|
||||
import { offerUndoToast } from "./helpers/undo-completion";
|
||||
// The Battery Fleet card ships in this globally-loaded bundle so it is
|
||||
// available on every dashboard without another extra_module_url entry.
|
||||
import "./components/battery-fleet-card";
|
||||
@@ -46,6 +48,9 @@ interface CardDoc {
|
||||
url?: string | null;
|
||||
}
|
||||
|
||||
/** Document chips on one task row before the rest is counted ("+N"). */
|
||||
const MAX_ROW_CHIPS = 2;
|
||||
|
||||
interface FlatTask {
|
||||
entry_id: string;
|
||||
object_name: string;
|
||||
@@ -243,9 +248,12 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
const res = (await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/documents/list",
|
||||
entry_id: entryId,
|
||||
})) as { documents: Array<CardDoc & { task_ids?: string[] }> };
|
||||
})) as { documents: Array<CardDoc & { task_ids?: string[]; tags?: string[] }> };
|
||||
const byTask: Record<string, CardDoc[]> = {};
|
||||
for (const doc of res.documents || []) {
|
||||
// Completion photos are linked to their task too; a row of
|
||||
// "photo-2026…" chips is no way to the manual.
|
||||
if (isPhotoDocument(doc)) continue;
|
||||
for (const taskId of doc.task_ids || []) {
|
||||
(byTask[taskId] ||= []).push({ id: doc.id, title: doc.title, kind: doc.kind, url: doc.url });
|
||||
}
|
||||
@@ -268,6 +276,31 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A row's document chips: at most two, the rest as "+N" — not a button:
|
||||
* a tap on it opens the task like the row does, where every document is
|
||||
* listed (its tooltip names them). */
|
||||
private _renderDocChips(entryId: string, task: MaintenanceTask) {
|
||||
const docs = this._docsFor(entryId, task);
|
||||
if (!docs.length) return nothing;
|
||||
const rest = docs.slice(MAX_ROW_CHIPS);
|
||||
return html`<div class="doc-chips">
|
||||
${docs.slice(0, MAX_ROW_CHIPS).map((doc) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="doc-chip"
|
||||
title="${doc.title}"
|
||||
@click=${(e: Event) => { e.stopPropagation(); void this._openDoc(doc); }}
|
||||
>
|
||||
<ha-icon icon=${doc.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline"}></ha-icon>
|
||||
<span>${doc.title}</span>
|
||||
</button>
|
||||
`)}
|
||||
${rest.length
|
||||
? html`<span class="doc-chip doc-more" title="${rest.map((d) => d.title).join("\n")}">+${rest.length}</span>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Open a chip: a web link directly, a stored file through a signed path
|
||||
* (the same route the panel uses, so it works in the Companion app). */
|
||||
private async _openDoc(doc: CardDoc): Promise<void> {
|
||||
@@ -412,8 +445,13 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private _onCompleted = async (): Promise<void> => {
|
||||
/** After a completion: reload, and — when the server remembered it for
|
||||
* this person — Home Assistant's own toast offers Undo (the card has no
|
||||
* toast of its own). */
|
||||
private _onCompleted = async (e: CustomEvent<TaskCompletedDetail>): Promise<void> => {
|
||||
await this._loadData();
|
||||
const d = e.detail;
|
||||
if (d?.undo) offerUndoToast(this, this.hass, d, this._lang, () => void this._loadData());
|
||||
};
|
||||
|
||||
/** The row's Complete action — the SAME derivation the panel uses (phase
|
||||
@@ -557,21 +595,7 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._docsFor(entry_id, task).length
|
||||
? html`<div class="doc-chips">
|
||||
${this._docsFor(entry_id, task).map((doc) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="doc-chip"
|
||||
title="${doc.title}"
|
||||
@click=${(e: Event) => { e.stopPropagation(); void this._openDoc(doc); }}
|
||||
>
|
||||
<ha-icon icon=${doc.kind === "weblink" ? "mdi:link-variant" : "mdi:file-document-outline"}></ha-icon>
|
||||
<span>${doc.title}</span>
|
||||
</button>
|
||||
`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._renderDocChips(entry_id, task)}
|
||||
<div class="task-due">
|
||||
${task.days_until_due !== null && task.days_until_due !== undefined
|
||||
? task.days_until_due < 0
|
||||
@@ -686,7 +710,9 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
}
|
||||
.empty-link:hover { text-decoration: underline; }
|
||||
.all-caught-up { color: var(--success-color, #4caf50); font-weight: 500; }
|
||||
.task-list { padding: 0 16px 16px; }
|
||||
/* The list measures itself: a dashboard column on a wide screen needs
|
||||
the narrow row layout as much as a phone does. */
|
||||
.task-list { padding: 0 16px 16px; container: ms-tasks / inline-size; }
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
@@ -730,18 +756,24 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* One line of chips that shrinks before the name does; a chip's label
|
||||
ends in an ellipsis (on the span — a flex button clips its text
|
||||
without one). */
|
||||
.doc-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 40%;
|
||||
gap: 4px;
|
||||
margin-right: 6px;
|
||||
max-width: 45%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.doc-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
max-width: 14ch;
|
||||
max-width: 22ch;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--divider-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
@@ -750,12 +782,25 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.doc-chip span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.doc-chip:hover { color: var(--primary-color); border-color: var(--primary-color); }
|
||||
.doc-chip ha-icon { --mdc-icon-size: 12px; width: 12px; height: 12px; }
|
||||
.doc-chip ha-icon { --mdc-icon-size: 12px; width: 12px; height: 12px; flex: none; }
|
||||
.doc-more { flex: none; }
|
||||
/* Narrow (a phone, a dashboard column): the chips get a line of their
|
||||
own under the task. Beside it, three of them left the task's name
|
||||
one letter wide and broke its object and type over three lines. */
|
||||
@container ms-tasks (max-width: 600px) {
|
||||
.task-item { flex-wrap: wrap; row-gap: 2px; }
|
||||
.doc-chips {
|
||||
order: 1;
|
||||
flex: 1 0 100%;
|
||||
max-width: none;
|
||||
box-sizing: border-box;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
}
|
||||
/* nowrap: the due label is localized via formatDueDays ("5 d overdue",
|
||||
"5 T überfällig") — without it a narrow phone card wraps that onto a
|
||||
second line and the row grows taller. The name column ellipsizes
|
||||
|
||||
@@ -88,7 +88,8 @@ import "./components/parts-section";
|
||||
import "./components/object-history-section";
|
||||
import "./components/task-documents";
|
||||
import type { MaintenanceTaskDialog } from "./components/task-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
import type { MaintenanceCompleteDialog, TaskCompletedDetail } from "./components/complete-dialog";
|
||||
import { undoCompletion } from "./helpers/undo-completion";
|
||||
import type { MaintenanceQrDialog } from "./components/qr-dialog";
|
||||
import type { MaintenanceAdoptProblemSensorsDialog } from "./components/adopt-problem-sensors-dialog";
|
||||
import "./components/battery-fleet-section";
|
||||
@@ -113,7 +114,8 @@ import type { MaintenanceGroupDialog } from "./components/group-dialog";
|
||||
import "./components/saved-views-dialog";
|
||||
import type { MaintenanceSavedViewsDialog } from "./components/saved-views-dialog";
|
||||
import { type SparklineContext } from "./renderers/sparkline";
|
||||
import { buildCalendarBuckets, isoDateLocal, type CalendarEvent } from "./helpers/calendar-bucket";
|
||||
import { buildCalendarBuckets, type CalendarEvent } from "./helpers/calendar-bucket";
|
||||
import { haToday } from "./helpers/ha-time";
|
||||
import { renderTriggerProgress, renderMiniSparkline, computeTrend } from "./renderers/progress";
|
||||
import { type HistoryContext } from "./renderers/history";
|
||||
import { renderUserBadge, type TaskDetailContext } from "./renderers/task-detail";
|
||||
@@ -2909,7 +2911,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._showToast(t("quick_complete_success", this._lang), "info");
|
||||
if ((res as { undo?: boolean } | null)?.undo) this._offerUndo(entryId, taskId, t("quick_complete_success", this._lang));
|
||||
else this._showToast(t("quick_complete_success", this._lang), "info");
|
||||
// Silent success: the list must reflect the completion right away — the
|
||||
// subscription delta may lag or be coalesced.
|
||||
try { await this._loadData(); } catch { /* subscription will sync */ }
|
||||
@@ -3125,6 +3128,24 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
try { await this._loadData(); } catch { /* subscription will sync */ }
|
||||
};
|
||||
|
||||
/** A completion from the complete dialog: refresh, and offer Undo when the
|
||||
* server remembered it for this person (helpers/undo-completion). */
|
||||
private _onTaskCompleted = async (e: CustomEvent<TaskCompletedDetail>): Promise<void> => {
|
||||
await this._onDialogEvent();
|
||||
const d = e.detail;
|
||||
if (d?.undo) this._offerUndo(d.entryId, d.taskId, t("task_completed_named", this._lang).replace("{task}", d.taskName));
|
||||
};
|
||||
|
||||
private _offerUndo(entryId: string, taskId: string, message: string): void {
|
||||
this._showUndoToast(message, () => void this._undoCompletion(entryId, taskId));
|
||||
}
|
||||
|
||||
private async _undoCompletion(entryId: string, taskId: string): Promise<void> {
|
||||
const res = await undoCompletion(this.hass, entryId, taskId, this._lang);
|
||||
this._showToast(res.message, res.ok ? "info" : "error");
|
||||
if (res.ok) await this._onDialogEvent();
|
||||
}
|
||||
|
||||
// --- Render ---
|
||||
|
||||
render() {
|
||||
@@ -3191,7 +3212,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
></maintenance-task-dialog>
|
||||
<maintenance-complete-dialog
|
||||
.hass=${this.hass}
|
||||
@task-completed=${this._onDialogEvent}
|
||||
@task-completed=${this._onTaskCompleted}
|
||||
></maintenance-complete-dialog>
|
||||
<maintenance-history-edit-dialog
|
||||
.hass=${this.hass}
|
||||
@@ -4271,7 +4292,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
esc(row.consumers.map((c) => `${c.object_name ?? ""}/${c.task_name ?? c.task_id}×${c.quantity}`).join(" | ")),
|
||||
].join(","));
|
||||
}
|
||||
const ts = isoDateLocal(new Date());
|
||||
const ts = haToday();
|
||||
downloadTextFile(lines.join("\n"), `maintenance_parts_${ts}.csv`, "text/csv;charset=utf-8");
|
||||
}
|
||||
|
||||
@@ -4279,7 +4300,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
private async _exportObjectsCsv(): Promise<void> {
|
||||
const result = await runWs<{ csv: string }>(this, { type: "maintenance_supporter/objects/csv" }, { onError: (m) => this._showToast(m) });
|
||||
if (!result) return;
|
||||
const ts = isoDateLocal(new Date());
|
||||
const ts = haToday();
|
||||
downloadTextFile(result.csv, `maintenance_objects_${ts}.csv`, "text/csv;charset=utf-8");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Shared chart helpers: nice axis ticks and consistent number/date formats. */
|
||||
|
||||
import { formatDateShort, formatTimeOfDay, formatNumber } from "../styles";
|
||||
import { displayTimeZone, ymdIn } from "../helpers/ha-time";
|
||||
|
||||
/** SVG / CSS coordinate: one decimal, ALWAYS a dot — never a user-facing
|
||||
* number. The only place outside styles.ts allowed to call toFixed (tripwire
|
||||
@@ -75,9 +76,12 @@ export function fmtDateTime(ts: number, lang: string): string {
|
||||
}
|
||||
|
||||
/** Whether date ticks need the year: the range crosses a calendar-year
|
||||
* boundary (a "Jun … Feb" sequence would otherwise read backwards). */
|
||||
* boundary (a "Jun … Feb" sequence would otherwise read backwards) — in
|
||||
* the zone the ticks are shown in. */
|
||||
export function needsYear(tsMin: number, tsMax: number): boolean {
|
||||
return new Date(tsMin).getFullYear() !== new Date(tsMax).getFullYear();
|
||||
if (!Number.isFinite(tsMin) || !Number.isFinite(tsMax)) return false;
|
||||
const zone = displayTimeZone();
|
||||
return ymdIn(tsMin, zone).slice(0, 4) !== ymdIn(tsMax, zone).slice(0, 4);
|
||||
}
|
||||
|
||||
/** Evenly-spaced x-axis tick timestamps (first/last inclusive). */
|
||||
|
||||
@@ -11,6 +11,7 @@ import { t, formatCost } from "../styles";
|
||||
import { niceTicks, fmtNum, fmtDateTick, timeTicks, needsYear, px } from "./chart-utils";
|
||||
import type { MaintenanceTask } from "../types";
|
||||
import { entrySpend } from "../helpers/parts-cost";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
|
||||
const COST_CHART_H = 200;
|
||||
const PAD_T = 10;
|
||||
@@ -63,7 +64,7 @@ export function renderCostDurationCard(
|
||||
function renderHistoryChart(task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both", currencySymbol: string) {
|
||||
const entries = task.history
|
||||
.filter((h) => h.type === "completed" && (entrySpend(h) != null || h.duration != null))
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), cost: entrySpend(h) ?? 0, duration: h.duration ?? 0 }))
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), cost: entrySpend(h) ?? 0, duration: h.duration ?? 0 }))
|
||||
.sort((a, b) => a.ts - b.ts);
|
||||
|
||||
if (entries.length < 2) return nothing;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { renderRefChip } from "../helpers/reference";
|
||||
import { entryReadingValues } from "../helpers/reading-slots";
|
||||
import { matchesQuery } from "../helpers/search-match";
|
||||
import { historyNoteText } from "../helpers/history-note";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
|
||||
/** v2.2.0: only "lifecycle + cost-bearing" entries are user-editable.
|
||||
* Triggers are auto-generated by sensors and shouldn't be retroactively
|
||||
@@ -34,9 +35,9 @@ export const EDITABLE_HISTORY_TYPES: readonly HistoryEntryType[] = ["completed",
|
||||
* an unparsable) timestamp keep "later appended = newer". */
|
||||
export function newestFirst<T extends { timestamp?: string | null }>(history: readonly T[] | null | undefined): T[] {
|
||||
const ms = (e: T): number => {
|
||||
const raw = e.timestamp ?? "";
|
||||
// Date-only strings are local days (like formatDate) — not UTC midnight.
|
||||
const v = Date.parse(raw.length === 10 ? `${raw}T00:00:00` : raw);
|
||||
// A zone-less stamp (the history editor writes one) is HA local time,
|
||||
// a date-only one HA's midnight — never UTC midnight.
|
||||
const v = stampMs(e.timestamp);
|
||||
return Number.isNaN(v) ? -Infinity : v;
|
||||
};
|
||||
return (history ?? [])
|
||||
|
||||
@@ -15,6 +15,7 @@ import { html, nothing } from "lit";
|
||||
import { t, formatDate, formatDueDays, formatNumber } from "../styles";
|
||||
import { px } from "./chart-utils";
|
||||
import { daysProgress } from "../helpers/interval";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
import type { MaintenanceTask, TaskRow, StatisticsPoint } from "../types";
|
||||
|
||||
const MINI_SPARKLINE_W = 60;
|
||||
@@ -61,7 +62,7 @@ export function computeTrend(
|
||||
? []
|
||||
: (row.history || [])
|
||||
.filter((h) => h.trigger_value != null)
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value as number }));
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), val: h.trigger_value as number }));
|
||||
if (row.trigger_current_value != null) points = [...points, { ts: Date.now(), val: row.trigger_current_value }];
|
||||
if (points.length < 2) return null;
|
||||
points.sort((a, b) => a.ts - b.ts);
|
||||
@@ -249,7 +250,7 @@ export function renderMiniSparkline(
|
||||
if (!row.history) return nothing;
|
||||
for (const h of row.history) {
|
||||
if (h.trigger_value != null) {
|
||||
points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value });
|
||||
points.push({ ts: stampMs(h.timestamp), val: h.trigger_value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { html, svg, nothing } from "lit";
|
||||
import { t, formatNumber } from "../styles";
|
||||
import { px } from "./chart-utils";
|
||||
import { haToday } from "../helpers/ha-time";
|
||||
import type { MaintenanceTask, AdvancedFeatures } from "../types";
|
||||
|
||||
const MONTH_KEYS = [
|
||||
@@ -11,13 +12,18 @@ const MONTH_KEYS = [
|
||||
"month_sep", "month_oct", "month_nov", "month_dec",
|
||||
];
|
||||
|
||||
/** HA's current month (0 = Jan) — the one the backend applies the factor of. */
|
||||
function haMonth(): number {
|
||||
return Number(haToday().slice(5, 7)) - 1;
|
||||
}
|
||||
|
||||
export function renderSeasonalCardCompact(task: MaintenanceTask, lang: string, features: AdvancedFeatures) {
|
||||
if (!features.seasonal || !task.seasonal_factor || task.seasonal_factor === 1.0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const months = MONTH_KEYS.map(k => t(k, lang));
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentMonth = haMonth();
|
||||
|
||||
const realFactors = task.seasonal_factors || task.interval_analysis?.seasonal_factors || null;
|
||||
const seasonalData = realFactors && realFactors.length === 12
|
||||
@@ -66,7 +72,7 @@ function renderSeasonalChart(task: MaintenanceTask, lang: string) {
|
||||
if (!factors || factors.length !== 12) return nothing;
|
||||
|
||||
const reason = task.interval_analysis?.seasonal_reason;
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentMonth = haMonth();
|
||||
const W = 300, H = 100;
|
||||
const PAD_T = 8, PAD_B = 4;
|
||||
const chartH = H - PAD_T - PAD_B;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { html, nothing } from "lit";
|
||||
import { t, fireMoreInfo, formatDueDays } from "../styles";
|
||||
import { fmtNum, fmtVal } from "./chart-utils";
|
||||
import { dueDaysOf } from "./progress";
|
||||
import { stampMs } from "../helpers/ha-time";
|
||||
import "../components/trigger-chart";
|
||||
import type { ChartPoint, ChartEvent } from "../components/trigger-chart";
|
||||
import type { MaintenanceTask, TriggerConfig, StatisticsPoint } from "../types";
|
||||
@@ -299,8 +300,8 @@ function counterBaseline(task: MaintenanceTask, rawPoints: ChartPoint[]): { valu
|
||||
function lastServiceTs(task: MaintenanceTask): number | null {
|
||||
const e = [...task.history]
|
||||
.filter((h) => h.type === "completed" || h.type === "reset")
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0];
|
||||
return e ? new Date(e.timestamp).getTime() : null;
|
||||
.sort((a, b) => stampMs(b.timestamp) - stampMs(a.timestamp))[0];
|
||||
return e ? stampMs(e.timestamp) : null;
|
||||
}
|
||||
|
||||
/** "8,507 / 15,000 km · 57 %" header + progress bar (counter / state_change / runtime). */
|
||||
@@ -351,7 +352,7 @@ function rawStatsPoints(task: MaintenanceTask, ctx: SparklineContext): ChartPoin
|
||||
} else {
|
||||
for (const h of task.history) {
|
||||
if (h.trigger_value != null) {
|
||||
points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value });
|
||||
points.push({ ts: stampMs(h.timestamp), val: h.trigger_value });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,7 +473,7 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
|
||||
const events: ChartEvent[] = task.history
|
||||
.filter((h) => ["completed", "skipped", "reset"].includes(h.type))
|
||||
.map((h) => ({ ts: new Date(h.timestamp).getTime(), type: h.type }));
|
||||
.map((h) => ({ ts: stampMs(h.timestamp), type: h.type }));
|
||||
|
||||
return html`
|
||||
<maintenance-trigger-chart
|
||||
|
||||
@@ -33,23 +33,29 @@ export { STATUS_COLORS, STATUS_ICONS } from "./status-constants";
|
||||
// `from "./styles"` import keeps working.
|
||||
import { ensureLocale, isLocaleLoaded, normLang, seedEnglish, t, type Translations } from "./helpers/locale-core";
|
||||
import { primeBackendErrors } from "./helpers/backend-errors";
|
||||
import { displayTimeZone, haTimeZone, minuteIn, stampMs, ymdIn } from "./helpers/ha-time";
|
||||
export { ensureLocale, isLocaleLoaded, seedEnglish, setLocale, t } from "./helpers/locale-core";
|
||||
seedEnglish(EN as Translations);
|
||||
|
||||
/** The per-`updated()` locale boot every top-level surface needs: date/time
|
||||
* prefs follow the HA profile (#97), and the user's UI language is lazily
|
||||
* fetched with a re-render once it arrives (EN is bundled, so strings read
|
||||
* in English until then rather than as raw keys). Was copy-pasted across the
|
||||
* panel and both cards (drift audit 2026-08). */
|
||||
* prefs follow the HA profile (#97) — its time zone included — while
|
||||
* schedule dates follow HA's own zone (helpers/ha-time), and the user's UI
|
||||
* language is lazily fetched with a re-render once it arrives (EN is
|
||||
* bundled, so strings read in English until then rather than as raw keys).
|
||||
* Was copy-pasted across the panel and both cards (drift audit 2026-08). */
|
||||
export function syncLocaleFromHass(
|
||||
host: {
|
||||
hass?: { language?: string; locale?: unknown; config?: { country?: string | null } };
|
||||
hass?: { language?: string; locale?: unknown; config?: { country?: string | null; time_zone?: string | null } };
|
||||
requestUpdate: () => void;
|
||||
},
|
||||
changedProps: Map<string, unknown>,
|
||||
): void {
|
||||
if (changedProps.has("hass")) {
|
||||
setProfilePrefs(host.hass?.locale as Parameters<typeof setProfilePrefs>[0], host.hass?.config?.country);
|
||||
setProfilePrefs(
|
||||
host.hass?.locale as Parameters<typeof setProfilePrefs>[0],
|
||||
host.hass?.config?.country,
|
||||
host.hass?.config?.time_zone,
|
||||
);
|
||||
}
|
||||
const lang = host.hass?.language;
|
||||
if (lang && !isLocaleLoaded(lang)) {
|
||||
@@ -101,6 +107,11 @@ interface ProfilePrefs {
|
||||
time?: string;
|
||||
number?: string;
|
||||
country?: string;
|
||||
/** The profile "Time zone" option ("local" | "server") and
|
||||
* hass.config.time_zone — read by helpers/ha-time (HA's clock for the
|
||||
* schedule, the display zone for timestamps). */
|
||||
timeZone?: string;
|
||||
serverTimeZone?: string;
|
||||
}
|
||||
const _w = window as unknown as { __msDateTimePrefs?: ProfilePrefs };
|
||||
const DT_PREFS: ProfilePrefs = (_w.__msDateTimePrefs ??= {});
|
||||
@@ -124,18 +135,24 @@ export function syncCurrencyDecimals(src: { currency_decimals?: number | null }
|
||||
if (src && src.currency_decimals !== undefined && src.currency_decimals !== null) setCurrencyDecimals(Number(src.currency_decimals));
|
||||
}
|
||||
|
||||
/** Feed HA's per-user date/time/number formats into the formatters below.
|
||||
* `country` is the SERVER's configured country (#140): with the profile
|
||||
* date format left on "language" it regionalizes the language default —
|
||||
* "en" + AU renders DD/MM/YYYY instead of the hard en-US mapping. */
|
||||
/** Feed HA's per-user date/time/number formats and time zone option into
|
||||
* the formatters below. `country` is the SERVER's configured country
|
||||
* (#140): with the profile date format left on "language" it regionalizes
|
||||
* the language default — "en" + AU renders DD/MM/YYYY instead of the hard
|
||||
* en-US mapping. `serverTimeZone` is hass.config.time_zone, the clock every
|
||||
* schedule date runs on (helpers/ha-time); it is kept even without a locale
|
||||
* (a test mock, an old core). For both, undefined keeps the stored value. */
|
||||
export function setProfilePrefs(
|
||||
locale?: { date_format?: string; time_format?: string; number_format?: string },
|
||||
locale?: { date_format?: string; time_format?: string; number_format?: string; time_zone?: string },
|
||||
country?: string | null,
|
||||
serverTimeZone?: string | null,
|
||||
): void {
|
||||
if (serverTimeZone !== undefined) DT_PREFS.serverTimeZone = serverTimeZone || undefined;
|
||||
if (!locale) return;
|
||||
DT_PREFS.date = locale.date_format;
|
||||
DT_PREFS.time = locale.time_format;
|
||||
DT_PREFS.number = locale.number_format;
|
||||
DT_PREFS.timeZone = locale.time_zone;
|
||||
if (country !== undefined) DT_PREFS.country = country || undefined;
|
||||
}
|
||||
|
||||
@@ -217,56 +234,79 @@ function resolveLocale(lang?: string): string {
|
||||
return base;
|
||||
}
|
||||
|
||||
function _formatDateObj(d: Date, lang?: string): string {
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = String(d.getFullYear());
|
||||
switch (DT_PREFS.date) {
|
||||
case "DMY": return `${dd}/${mm}/${yyyy}`;
|
||||
case "MDY": return `${mm}/${dd}/${yyyy}`;
|
||||
case "YMD": return `${yyyy}-${mm}-${dd}`;
|
||||
case "system":
|
||||
return d.toLocaleDateString(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
default: // "language" or unset — UI language, regionalized by the server country (#140)
|
||||
return d.toLocaleDateString(resolveLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
/** The profile's numeric date of an instant, read in `timeZone` — the
|
||||
* display zone for a timestamp, UTC for a date-only value pinned to UTC
|
||||
* midnight. The explicit orders are built from formatToParts (ha-time
|
||||
* ymdIn): getDate()/getMonth() read the BROWSER's zone whatever the
|
||||
* profile says. */
|
||||
function _formatDateObj(d: Date, lang: string | undefined, timeZone: string): string {
|
||||
const order = DT_PREFS.date;
|
||||
if (order === "DMY" || order === "MDY" || order === "YMD") {
|
||||
const [yyyy, mm, dd] = ymdIn(d, timeZone).split("-");
|
||||
return order === "DMY" ? `${dd}/${mm}/${yyyy}` : order === "MDY" ? `${mm}/${dd}/${yyyy}` : `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
// "system" = the browser's locale; "language" or unset = the UI language,
|
||||
// regionalized by the server country (#140).
|
||||
const locale = order === "system" ? undefined : resolveLocale(lang);
|
||||
return d.toLocaleDateString(locale, { day: "2-digit", month: "2-digit", year: "numeric", timeZone });
|
||||
}
|
||||
|
||||
/** Time of day honouring the HA profile time format (12/24h). Every
|
||||
* clock-time string in the UI goes through here — chart crosshairs included
|
||||
* (#163: a bare toLocaleDateString({hour, minute}) ignored the profile). */
|
||||
/** Time of day of an instant honouring the HA profile time format (12/24h)
|
||||
* and its time zone. Every clock-time string in the UI goes through here —
|
||||
* chart crosshairs included (#163: a bare toLocaleDateString({hour,
|
||||
* minute}) ignored the profile). */
|
||||
export function formatTimeOfDay(d: Date, lang?: string): string {
|
||||
const timeZone = displayTimeZone();
|
||||
switch (DT_PREFS.time) {
|
||||
case "12": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true });
|
||||
case "24": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false });
|
||||
case "system": return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
||||
default: return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit" });
|
||||
case "12": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: true, timeZone });
|
||||
case "24": return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", hour12: false, timeZone });
|
||||
case "system": return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", timeZone });
|
||||
default: return d.toLocaleTimeString(resolveLocale(lang), { hour: "2-digit", minute: "2-digit", timeZone });
|
||||
}
|
||||
}
|
||||
|
||||
/** Format a date string (ISO) honouring the HA profile date format.
|
||||
* Appends T00:00:00 to date-only strings so JS parses them as local time, not UTC. */
|
||||
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
|
||||
/** Format a date string (ISO) honouring the HA profile date format. A
|
||||
* date-only value ("YYYY-MM-DD": next_due, warranty, …) has no zone and
|
||||
* keeps its day whatever the zones say; a timestamp shows its day in the
|
||||
* profile's time zone (helpers/ha-time — a zone-less one is HA local time). */
|
||||
export function formatDate(iso: string | null | undefined, lang?: string): string {
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
const local = iso.includes("T") ? iso : iso + "T00:00:00";
|
||||
return _formatDateObj(new Date(local), lang);
|
||||
const day = DATE_ONLY.exec(iso);
|
||||
if (day) return _formatDateObj(new Date(Date.UTC(+day[1], +day[2] - 1, +day[3])), lang, "UTC");
|
||||
return _formatDateObj(new Date(stampMs(iso)), lang, displayTimeZone());
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
/** Format a datetime string (ISO) honouring the HA profile date/time formats. */
|
||||
/** Format a datetime string (ISO) honouring the HA profile date/time formats
|
||||
* and time zone (a zone-less value is HA local time; a date-only one has no
|
||||
* time of day to show). */
|
||||
export function formatDateTime(iso: string | null | undefined, lang?: string): string {
|
||||
if (!iso) return "—";
|
||||
if (DATE_ONLY.test(iso)) return formatDate(iso, lang);
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return _formatDateObj(d, lang) + " " + formatTimeOfDay(d, lang);
|
||||
const d = new Date(stampMs(iso));
|
||||
return _formatDateObj(d, lang, displayTimeZone()) + " " + formatTimeOfDay(d, lang);
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
/** The hint under a field whose value is HA local time (a back-dated
|
||||
* completion, a history entry's timestamp): it names HA's zone when the
|
||||
* times shown everywhere else run on another clock — a device elsewhere
|
||||
* with the profile's "local" zone — and is "" otherwise. Compared by the
|
||||
* clock, not by name: "Europe/Berlin" and "Europe/Zurich" agree. */
|
||||
export function haTimeZoneHint(lang?: string, at: number = Date.now()): string {
|
||||
const zone = haTimeZone();
|
||||
if (minuteIn(at, zone) === minuteIn(at, displayTimeZone())) return "";
|
||||
return t("ha_time_zone_hint", lang).replace("{zone}", zone);
|
||||
}
|
||||
|
||||
/** Format "days until due" in localized manner. */
|
||||
export function formatDueDays(days: number | null | undefined, lang?: string): string {
|
||||
if (days === null || days === undefined) return "—";
|
||||
@@ -320,13 +360,12 @@ export function formatMonth(d: Date, lang?: string, style: "long" | "short" = "l
|
||||
return monthName(d.getMonth(), lang, style);
|
||||
}
|
||||
|
||||
/** Short day+month ("Jul 3" / "3. Juli"), with a 2-digit year when the
|
||||
* caller's range needs disambiguating — chart axis ticks and tooltips. */
|
||||
/** Short day+month of an instant ("Jul 3" / "3. Juli") in the profile's
|
||||
* time zone, with a 2-digit year when the caller's range needs
|
||||
* disambiguating — chart axis ticks and tooltips. */
|
||||
export function formatDateShort(d: Date, lang?: string, withYear = false): string {
|
||||
return d.toLocaleDateString(
|
||||
resolveLocale(lang),
|
||||
withYear ? { month: "short", day: "numeric", year: "2-digit" } : { month: "short", day: "numeric" },
|
||||
);
|
||||
const shape: Intl.DateTimeFormatOptions = withYear ? { month: "short", day: "numeric", year: "2-digit" } : { month: "short", day: "numeric" };
|
||||
return d.toLocaleDateString(resolveLocale(lang), { ...shape, timeZone: displayTimeZone() });
|
||||
}
|
||||
|
||||
/** Recurrence shape carried on the WS payload (see types.TaskSchedule). */
|
||||
|
||||
@@ -673,10 +673,13 @@ export interface HomeAssistant {
|
||||
// date_format/time_format are HA's per-user PROFILE settings ("language" |
|
||||
// "system" | "DMY" | "MDY" | "YMD" resp. "language" | "system" | "12" | "24")
|
||||
// — issue #97: dates must follow them, not just the UI language.
|
||||
locale?: { language: string; number_format?: string; date_format?: string; time_format?: string };
|
||||
// time_zone is the profile's "Time zone": "local" (the browser's, HA's
|
||||
// default) or "server" — the zone timestamps are SHOWN in.
|
||||
locale?: { language: string; number_format?: string; date_format?: string; time_format?: string; time_zone?: string };
|
||||
/** Server config — country (#140) feeds the "language"-default date
|
||||
* format ("en" + AU → en-AU → DD/MM/YYYY). */
|
||||
config?: { country?: string | null };
|
||||
* format ("en" + AU → en-AU → DD/MM/YYYY); time_zone is HA's own zone,
|
||||
* the one next_due / days_until_due count in (helpers/ha-time). */
|
||||
config?: { country?: string | null; time_zone?: string | null };
|
||||
localize(key: string, ...args: unknown[]): string;
|
||||
user?: { id: string; name: string; is_admin: boolean; is_owner: boolean };
|
||||
/** HA's registered panels; a custom panel carries its module URL under
|
||||
|
||||
@@ -25,6 +25,9 @@ export const WS_ERROR_CODE_KEYS: Record<string, string> = {
|
||||
already_archived: "ws_err_already_archived",
|
||||
already_paused: "ws_err_already_paused",
|
||||
archived: "ws_err_archived",
|
||||
// task/undo (2026-10): the person's last completion is another task / gone,
|
||||
// or the task changed since.
|
||||
changed_since: "undo_changed",
|
||||
create_failed: "ws_err_create_failed",
|
||||
duplicate_failed: "ws_err_duplicate_failed",
|
||||
empty: "ws_err_empty",
|
||||
@@ -55,6 +58,7 @@ export const WS_ERROR_CODE_KEYS: Record<string, string> = {
|
||||
not_found: "ws_err_not_found",
|
||||
not_loaded: "ws_err_not_loaded",
|
||||
not_paused: "ws_err_not_paused",
|
||||
nothing_to_undo: "undo_nothing",
|
||||
replace_failed: "ws_err_replace_failed",
|
||||
self_link_device: "ws_err_self_link_device",
|
||||
storage_unavailable: "ws_err_storage_unavailable",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Dokončeno dne (volitelné, prázdné = nyní)",
|
||||
"completed_at_pick": "Nastavit datum a čas",
|
||||
"completed_at_future_error": "Datum dokončení nesmí být v budoucnosti.",
|
||||
"ha_time_zone_hint": "Čas v časovém pásmu Home Assistantu ({zone})",
|
||||
"days": "dní",
|
||||
"day": "den",
|
||||
"today": "Dnes",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Budoucí okna",
|
||||
"history_edit_title": "Upravit záznam historie",
|
||||
"history_delete_entry": "Smazat záznam",
|
||||
"history_delete_confirm": "Smazat tento záznam historie? Datum posledního provedení se vrátí k předchozímu dokončení; fotografie zůstanou u objektu a spotřebované díly se nevrátí na sklad.",
|
||||
"history_delete_confirm_entry": "Smazat tento záznam historie? Datum posledního provedení se vrátí na předchozí; fotky zůstanou u objektu.",
|
||||
"history_delete_returns_parts": "Použité díly se vrátí do zásoby: {parts}.",
|
||||
"history_edit_timestamp": "Časové razítko",
|
||||
"manufacturer": "Výrobce",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Načítání…",
|
||||
"archive": "Archivovat",
|
||||
"undo": "Zpět",
|
||||
"task_completed_named": "Dokončeno: {task}",
|
||||
"undo_done": "Dokončení vráceno zpět",
|
||||
"undo_done_action_ran": "Dokončení vráceno zpět. Akce při dokončení už proběhla a nelze ji vrátit.",
|
||||
"undo_nothing": "Není co vrátit. Vrátit jde jen vaše poslední dokončení, do 10 minut.",
|
||||
"undo_changed": "Nelze vrátit. Úkol se mezitím změnil.",
|
||||
"task_archived": "Úkol archivován",
|
||||
"object_archived": "Objekt archivován",
|
||||
"unarchive": "Obnovit z archivu",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Udført den (valgfrit, tomt = nu)",
|
||||
"completed_at_pick": "Vælg dato og tidspunkt",
|
||||
"completed_at_future_error": "Udførelsesdatoen må ikke ligge i fremtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidszone ({zone})",
|
||||
"days": "dage",
|
||||
"day": "dag",
|
||||
"today": "I dag",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Kommende vinduer",
|
||||
"history_edit_title": "Rediger historikpost",
|
||||
"history_delete_entry": "Slet post",
|
||||
"history_delete_confirm": "Slet denne historikpost? Datoen for seneste udførelse falder tilbage til den forrige; fotos bliver hos objektet, og forbrugte dele lægges ikke tilbage på lager.",
|
||||
"history_delete_confirm_entry": "Slet denne historikpost? Datoen for sidste udførelse falder tilbage til den forrige; billeder bliver ved objektet.",
|
||||
"history_delete_returns_parts": "De brugte dele går tilbage på lager: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstempel",
|
||||
"manufacturer": "Producent",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Indlæser…",
|
||||
"archive": "Arkivér",
|
||||
"undo": "Fortryd",
|
||||
"task_completed_named": "Fuldført: {task}",
|
||||
"undo_done": "Udførelsen fortrudt",
|
||||
"undo_done_action_ran": "Udførelsen fortrudt. Handlingen ved udførelse var allerede kørt og kan ikke fortrydes.",
|
||||
"undo_nothing": "Intet at fortryde. Kun din egen seneste udførelse kan fortrydes, inden for 10 minutter.",
|
||||
"undo_changed": "Kan ikke fortrydes. Opgaven er ændret siden.",
|
||||
"task_archived": "Opgave arkiveret",
|
||||
"object_archived": "Objekt arkiveret",
|
||||
"unarchive": "Gendan",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Erledigt am (optional, leer = jetzt)",
|
||||
"completed_at_pick": "Datum & Uhrzeit wählen",
|
||||
"completed_at_future_error": "Das Erledigungsdatum darf nicht in der Zukunft liegen.",
|
||||
"ha_time_zone_hint": "Uhrzeit in der Zeitzone von Home Assistant ({zone})",
|
||||
"days": "Tage",
|
||||
"day": "Tag",
|
||||
"today": "Heute",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Zukünftige Fenster",
|
||||
"history_edit_title": "Verlaufseintrag bearbeiten",
|
||||
"history_delete_entry": "Eintrag löschen",
|
||||
"history_delete_confirm": "Diesen Verlaufseintrag löschen? Das Datum der letzten Erledigung fällt auf die vorherige zurück; Fotos bleiben beim Objekt, verbrauchte Teile werden nicht zurückgebucht.",
|
||||
"history_delete_confirm_entry": "Diesen Verlaufseintrag löschen? Das Datum der letzten Erledigung fällt auf die vorherige zurück; Fotos bleiben beim Objekt.",
|
||||
"history_delete_returns_parts": "Die dabei verbrauchten Teile gehen zurück in den Bestand: {parts}.",
|
||||
"history_edit_timestamp": "Zeitstempel",
|
||||
"manufacturer": "Hersteller",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laden…",
|
||||
"archive": "Archivieren",
|
||||
"undo": "Rückgängig",
|
||||
"task_completed_named": "Erledigt: {task}",
|
||||
"undo_done": "Erledigung zurückgenommen",
|
||||
"undo_done_action_ran": "Erledigung zurückgenommen. Die Aktion beim Erledigen war schon gelaufen und lässt sich nicht zurücknehmen.",
|
||||
"undo_nothing": "Nichts zurückzunehmen. Nur deine eigene letzte Erledigung lässt sich zurücknehmen, innerhalb von 10 Minuten.",
|
||||
"undo_changed": "Zurücknehmen nicht möglich. Die Aufgabe wurde inzwischen geändert.",
|
||||
"task_archived": "Aufgabe archiviert",
|
||||
"object_archived": "Objekt archiviert",
|
||||
"unarchive": "Wiederherstellen",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Completed at (optional, empty = now)",
|
||||
"completed_at_pick": "Set date & time",
|
||||
"completed_at_future_error": "The completion date cannot be in the future.",
|
||||
"ha_time_zone_hint": "Time in Home Assistant's time zone ({zone})",
|
||||
"days": "days",
|
||||
"day": "day",
|
||||
"today": "Today",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Forward windows",
|
||||
"history_edit_title": "Edit history entry",
|
||||
"history_delete_entry": "Delete entry",
|
||||
"history_delete_confirm": "Delete this history entry? The task's last-performed date falls back to the previous completion; photos stay with the object and consumed parts are not restocked.",
|
||||
"history_delete_confirm_entry": "Delete this history entry? The task's last-performed date falls back to the previous completion; photos stay with the object.",
|
||||
"history_delete_returns_parts": "The parts it used go back to stock: {parts}.",
|
||||
"history_edit_timestamp": "Timestamp",
|
||||
"manufacturer": "Manufacturer",
|
||||
"model": "Model",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Loading…",
|
||||
"archive": "Archive",
|
||||
"undo": "Undo",
|
||||
"task_completed_named": "Completed: {task}",
|
||||
"undo_done": "Completion undone",
|
||||
"undo_done_action_ran": "Completion undone. Its completion action had already run and cannot be taken back.",
|
||||
"undo_nothing": "Nothing to undo. Only your own last completion can be taken back, within 10 minutes.",
|
||||
"undo_changed": "Can't undo. The task has changed since.",
|
||||
"task_archived": "Task archived",
|
||||
"object_archived": "Object archived",
|
||||
"unarchive": "Unarchive",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Completado el (opcional, vacío = ahora)",
|
||||
"completed_at_pick": "Elegir fecha y hora",
|
||||
"completed_at_future_error": "La fecha de finalización no puede estar en el futuro.",
|
||||
"ha_time_zone_hint": "Hora en la zona horaria de Home Assistant ({zone})",
|
||||
"days": "días",
|
||||
"day": "día",
|
||||
"today": "Hoy",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Ventanas futuras",
|
||||
"history_edit_title": "Editar entrada del historial",
|
||||
"history_delete_entry": "Eliminar entrada",
|
||||
"history_delete_confirm": "¿Eliminar esta entrada del historial? La fecha de última realización vuelve a la anterior; las fotos permanecen con el objeto y las piezas consumidas no se reponen.",
|
||||
"history_delete_confirm_entry": "¿Eliminar esta entrada del historial? La fecha de la última realización vuelve a la anterior; las fotos se quedan con el objeto.",
|
||||
"history_delete_returns_parts": "Las piezas usadas vuelven al stock: {parts}.",
|
||||
"history_edit_timestamp": "Marca de tiempo",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Cargando…",
|
||||
"archive": "Archivar",
|
||||
"undo": "Deshacer",
|
||||
"task_completed_named": "Completada: {task}",
|
||||
"undo_done": "Realización deshecha",
|
||||
"undo_done_action_ran": "Realización deshecha. La acción al completar ya se había ejecutado y no se puede deshacer.",
|
||||
"undo_nothing": "No hay nada que deshacer. Solo puedes deshacer tu última realización, en un plazo de 10 minutos.",
|
||||
"undo_changed": "No se puede deshacer. La tarea ha cambiado desde entonces.",
|
||||
"task_archived": "Tarea archivada",
|
||||
"object_archived": "Objeto archivado",
|
||||
"unarchive": "Desarchivar",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Suoritettu (valinnainen, tyhjä = nyt)",
|
||||
"completed_at_pick": "Aseta päivä ja aika",
|
||||
"completed_at_future_error": "Suorituspäivä ei voi olla tulevaisuudessa.",
|
||||
"ha_time_zone_hint": "Aika Home Assistantin aikavyöhykkeellä ({zone})",
|
||||
"days": "päivää",
|
||||
"day": "päivä",
|
||||
"today": "Tänään",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Tulevat jaksot",
|
||||
"history_edit_title": "Muokkaa historiamerkintää",
|
||||
"history_delete_entry": "Poista merkintä",
|
||||
"history_delete_confirm": "Poistetaanko tämä historiamerkintä? Viimeisimmän suorituksen päivä palautuu edelliseen; kuvat jäävät kohteelle eikä kulutettuja osia palauteta varastoon.",
|
||||
"history_delete_confirm_entry": "Poistetaanko tämä historiamerkintä? Viimeisin suorituspäivä palaa edelliseen; kuvat jäävät kohteelle.",
|
||||
"history_delete_returns_parts": "Käytetyt osat palautuvat varastoon: {parts}.",
|
||||
"history_edit_timestamp": "Aikaleima",
|
||||
"manufacturer": "Valmistaja",
|
||||
"model": "Malli",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Ladataan…",
|
||||
"archive": "Arkistoi",
|
||||
"undo": "Kumoa",
|
||||
"task_completed_named": "Suoritettu: {task}",
|
||||
"undo_done": "Suoritus peruttu",
|
||||
"undo_done_action_ran": "Suoritus peruttu. Suorituksen toiminto oli jo ajettu, eikä sitä voi perua.",
|
||||
"undo_nothing": "Ei mitään peruttavaa. Vain oman viimeisimmän suorituksen voi perua, 10 minuutin sisällä.",
|
||||
"undo_changed": "Perua ei voi. Tehtävä on muuttunut sen jälkeen.",
|
||||
"task_archived": "Tehtävä arkistoitu",
|
||||
"object_archived": "Kohde arkistoitu",
|
||||
"unarchive": "Palauta arkistosta",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Effectué le (optionnel, vide = maintenant)",
|
||||
"completed_at_pick": "Choisir la date et l'heure",
|
||||
"completed_at_future_error": "La date d'achèvement ne peut pas être dans le futur.",
|
||||
"ha_time_zone_hint": "Heure dans le fuseau horaire de Home Assistant ({zone})",
|
||||
"days": "jours",
|
||||
"day": "jour",
|
||||
"today": "Aujourd'hui",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Fenêtres à venir",
|
||||
"history_edit_title": "Modifier l'entrée d'historique",
|
||||
"history_delete_entry": "Supprimer l'entrée",
|
||||
"history_delete_confirm": "Supprimer cette entrée d'historique ? La date de dernière réalisation revient à la précédente ; les photos restent avec l'objet et les pièces consommées ne sont pas remises en stock.",
|
||||
"history_delete_confirm_entry": "Supprimer cette entrée d'historique ? La date de dernière réalisation revient à la précédente ; les photos restent avec l'objet.",
|
||||
"history_delete_returns_parts": "Les pièces utilisées retournent en stock : {parts}.",
|
||||
"history_edit_timestamp": "Horodatage",
|
||||
"manufacturer": "Fabricant",
|
||||
"model": "Modèle",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Chargement…",
|
||||
"archive": "Archiver",
|
||||
"undo": "Annuler",
|
||||
"task_completed_named": "Terminé : {task}",
|
||||
"undo_done": "Réalisation annulée",
|
||||
"undo_done_action_ran": "Réalisation annulée. L'action de fin s'était déjà exécutée et ne peut pas être annulée.",
|
||||
"undo_nothing": "Rien à annuler. Seule votre dernière réalisation peut être annulée, dans les 10 minutes.",
|
||||
"undo_changed": "Annulation impossible. La tâche a changé depuis.",
|
||||
"task_archived": "Tâche archivée",
|
||||
"object_archived": "Objet archivé",
|
||||
"unarchive": "Désarchiver",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "पूर्ण होने का समय (वैकल्पिक, खाली = अभी)",
|
||||
"completed_at_pick": "तारीख और समय चुनें",
|
||||
"completed_at_future_error": "पूर्णता की तारीख भविष्य में नहीं हो सकती।",
|
||||
"ha_time_zone_hint": "Home Assistant के समय क्षेत्र का समय ({zone})",
|
||||
"days": "दिन",
|
||||
"day": "दिन",
|
||||
"today": "आज",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "आगामी अवधियाँ",
|
||||
"history_edit_title": "इतिहास प्रविष्टि संपादित करें",
|
||||
"history_delete_entry": "प्रविष्टि हटाएँ",
|
||||
"history_delete_confirm": "क्या यह इतिहास प्रविष्टि हटाएँ? अंतिम निष्पादन की तिथि पिछली पूर्णता पर लौट जाएगी; फ़ोटो ऑब्जेक्ट के पास रहेंगे और उपयोग किए गए पुर्जे स्टॉक में वापस नहीं जोड़े जाएँगे।",
|
||||
"history_delete_confirm_entry": "यह इतिहास प्रविष्टि हटाएँ? अंतिम बार करने की तिथि पिछली पूर्णता पर लौट आएगी; फ़ोटो ऑब्जेक्ट के साथ रहेंगी।",
|
||||
"history_delete_returns_parts": "इस्तेमाल किए गए पुर्ज़े स्टॉक में वापस जाएँगे: {parts}।",
|
||||
"history_edit_timestamp": "टाइमस्टैम्प",
|
||||
"manufacturer": "निर्माता",
|
||||
"model": "मॉडल",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "लोड हो रहा है…",
|
||||
"archive": "संग्रह करें",
|
||||
"undo": "पूर्ववत करें",
|
||||
"task_completed_named": "पूर्ण हुआ: {task}",
|
||||
"undo_done": "पूर्णता वापस ली गई",
|
||||
"undo_done_action_ran": "पूर्णता वापस ली गई। पूरा करने पर चलने वाली कार्रवाई पहले ही चल चुकी थी, उसे वापस नहीं लिया जा सकता।",
|
||||
"undo_nothing": "वापस लेने को कुछ नहीं है। केवल आपकी अपनी पिछली पूर्णता 10 मिनट के भीतर वापस ली जा सकती है।",
|
||||
"undo_changed": "वापस नहीं लिया जा सकता। तब से कार्य बदल चुका है।",
|
||||
"task_archived": "कार्य संग्रहित",
|
||||
"object_archived": "ऑब्जेक्ट संग्रहित",
|
||||
"unarchive": "संग्रह से हटाएँ",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Elvégezve ekkor (opcionális, üres = most)",
|
||||
"completed_at_pick": "Dátum és idő megadása",
|
||||
"completed_at_future_error": "Az elvégzés dátuma nem lehet a jövőben.",
|
||||
"ha_time_zone_hint": "Idő a Home Assistant időzónájában ({zone})",
|
||||
"days": "nap",
|
||||
"day": "nap",
|
||||
"today": "Ma",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Jövőbeli időszakok",
|
||||
"history_edit_title": "Előzménybejegyzés szerkesztése",
|
||||
"history_delete_entry": "Bejegyzés törlése",
|
||||
"history_delete_confirm": "Törli ezt az előzménybejegyzést? Az utolsó elvégzés dátuma az előzőre áll vissza; a fotók az objektumnál maradnak, a felhasznált alkatrészek nem kerülnek vissza a készletbe.",
|
||||
"history_delete_confirm_entry": "Törlöd ezt az előzménybejegyzést? Az utolsó elvégzés dátuma visszaáll az előzőre; a fotók az objektumnál maradnak.",
|
||||
"history_delete_returns_parts": "A felhasznált alkatrészek visszakerülnek a készletbe: {parts}.",
|
||||
"history_edit_timestamp": "Időbélyeg",
|
||||
"manufacturer": "Gyártó",
|
||||
"model": "Modell",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Betöltés…",
|
||||
"archive": "Archiválás",
|
||||
"undo": "Visszavonás",
|
||||
"task_completed_named": "Elvégezve: {task}",
|
||||
"undo_done": "Elvégzés visszavonva",
|
||||
"undo_done_action_ran": "Elvégzés visszavonva. Az elvégzéskor futó művelet már lefutott, azt nem lehet visszavonni.",
|
||||
"undo_nothing": "Nincs mit visszavonni. Csak a saját legutóbbi elvégzésed vonható vissza, 10 percen belül.",
|
||||
"undo_changed": "Nem vonható vissza. A feladat azóta megváltozott.",
|
||||
"task_archived": "Feladat archiválva",
|
||||
"object_archived": "Objektum archiválva",
|
||||
"unarchive": "Archiválás visszavonása",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Completato il (opzionale, vuoto = adesso)",
|
||||
"completed_at_pick": "Imposta data e ora",
|
||||
"completed_at_future_error": "La data di completamento non può essere nel futuro.",
|
||||
"ha_time_zone_hint": "Ora nel fuso orario di Home Assistant ({zone})",
|
||||
"days": "giorni",
|
||||
"day": "giorno",
|
||||
"today": "Oggi",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Finestre future",
|
||||
"history_edit_title": "Modifica voce dello storico",
|
||||
"history_delete_entry": "Elimina voce",
|
||||
"history_delete_confirm": "Eliminare questa voce della cronologia? La data dell'ultima esecuzione torna al completamento precedente; le foto restano con l'oggetto e le parti consumate non vengono rimesse a magazzino.",
|
||||
"history_delete_confirm_entry": "Eliminare questa voce della cronologia? La data dell'ultima esecuzione torna a quella precedente; le foto restano con l'oggetto.",
|
||||
"history_delete_returns_parts": "I ricambi usati tornano in magazzino: {parts}.",
|
||||
"history_edit_timestamp": "Data e ora",
|
||||
"manufacturer": "Produttore",
|
||||
"model": "Modello",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Caricamento…",
|
||||
"archive": "Archivia",
|
||||
"undo": "Annulla",
|
||||
"task_completed_named": "Completato: {task}",
|
||||
"undo_done": "Completamento annullato",
|
||||
"undo_done_action_ran": "Completamento annullato. L'azione al completamento era già stata eseguita e non si può annullare.",
|
||||
"undo_nothing": "Niente da annullare. Si può annullare solo il tuo ultimo completamento, entro 10 minuti.",
|
||||
"undo_changed": "Impossibile annullare. L'attività è cambiata nel frattempo.",
|
||||
"task_archived": "Attività archiviata",
|
||||
"object_archived": "Oggetto archiviato",
|
||||
"unarchive": "Annulla archiviazione",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "完了日時(任意・空欄 = 現在)",
|
||||
"completed_at_pick": "日時を設定",
|
||||
"completed_at_future_error": "完了日時に未来は指定できません。",
|
||||
"ha_time_zone_hint": "Home Assistant のタイムゾーンの時刻({zone})",
|
||||
"days": "日",
|
||||
"day": "日",
|
||||
"today": "今日",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "今後の期間",
|
||||
"history_edit_title": "履歴項目を編集",
|
||||
"history_delete_entry": "記録を削除",
|
||||
"history_delete_confirm": "この履歴記録を削除しますか?最終実施日は前回の完了に戻ります。写真はオブジェクトに残り、消費した部品は在庫に戻りません。",
|
||||
"history_delete_confirm_entry": "この履歴記録を削除しますか?最終実施日は前回の完了に戻ります。写真はオブジェクトに残ります。",
|
||||
"history_delete_returns_parts": "使用した部品は在庫に戻ります:{parts}。",
|
||||
"history_edit_timestamp": "日時",
|
||||
"manufacturer": "メーカー",
|
||||
"model": "型番",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "読み込み中…",
|
||||
"archive": "アーカイブ",
|
||||
"undo": "元に戻す",
|
||||
"task_completed_named": "完了しました:{task}",
|
||||
"undo_done": "完了を取り消しました",
|
||||
"undo_done_action_ran": "完了を取り消しました。完了時のアクションは既に実行されており、取り消せません。",
|
||||
"undo_nothing": "取り消せるものはありません。取り消せるのは自分の直近の完了だけで、10分以内です。",
|
||||
"undo_changed": "取り消せません。その後タスクが変更されています。",
|
||||
"task_archived": "タスクをアーカイブしました",
|
||||
"object_archived": "オブジェクトをアーカイブしました",
|
||||
"unarchive": "アーカイブ解除",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "완료 시각 (선택, 비우면 지금)",
|
||||
"completed_at_pick": "날짜 및 시간 설정",
|
||||
"completed_at_future_error": "완료 날짜는 미래일 수 없습니다.",
|
||||
"ha_time_zone_hint": "Home Assistant 시간대 기준 시간 ({zone})",
|
||||
"days": "일",
|
||||
"day": "일",
|
||||
"today": "오늘",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "이후 기간",
|
||||
"history_edit_title": "기록 항목 편집",
|
||||
"history_delete_entry": "기록 삭제",
|
||||
"history_delete_confirm": "이 기록을 삭제할까요? 마지막 수행일은 이전 완료로 되돌아가며, 사진은 개체에 남고 사용한 부품은 재고로 복구되지 않습니다.",
|
||||
"history_delete_confirm_entry": "이 기록 항목을 삭제할까요? 마지막 수행 날짜가 이전 완료로 돌아갑니다. 사진은 객체에 남습니다.",
|
||||
"history_delete_returns_parts": "사용한 부품은 재고로 돌아갑니다: {parts}.",
|
||||
"history_edit_timestamp": "타임스탬프",
|
||||
"manufacturer": "제조사",
|
||||
"model": "모델",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "불러오는 중…",
|
||||
"archive": "보관",
|
||||
"undo": "실행 취소",
|
||||
"task_completed_named": "완료됨: {task}",
|
||||
"undo_done": "완료를 취소했습니다",
|
||||
"undo_done_action_ran": "완료를 취소했습니다. 완료 시 동작은 이미 실행되어 되돌릴 수 없습니다.",
|
||||
"undo_nothing": "취소할 항목이 없습니다. 자신의 마지막 완료만 10분 안에 취소할 수 있습니다.",
|
||||
"undo_changed": "취소할 수 없습니다. 그 사이 작업이 변경되었습니다.",
|
||||
"task_archived": "작업이 보관되었습니다",
|
||||
"object_archived": "객체가 보관되었습니다",
|
||||
"unarchive": "보관 해제",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Utført den (valgfritt, tomt = nå)",
|
||||
"completed_at_pick": "Velg dato og klokkeslett",
|
||||
"completed_at_future_error": "Fullføringsdatoen kan ikke ligge i fremtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidssone ({zone})",
|
||||
"days": "dager",
|
||||
"day": "dag",
|
||||
"today": "I dag",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Fremtidige vinduer",
|
||||
"history_edit_title": "Rediger historikkoppføring",
|
||||
"history_delete_entry": "Slett oppføring",
|
||||
"history_delete_confirm": "Slette denne historikkoppføringen? Datoen for siste utførelse faller tilbake til den forrige; bilder blir hos objektet, og forbrukte deler legges ikke tilbake på lager.",
|
||||
"history_delete_confirm_entry": "Slette denne historikkoppføringen? Datoen for siste utførelse faller tilbake til den forrige; bilder blir værende på objektet.",
|
||||
"history_delete_returns_parts": "De brukte delene går tilbake på lager: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstempel",
|
||||
"manufacturer": "Produsent",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laster…",
|
||||
"archive": "Arkivér",
|
||||
"undo": "Angre",
|
||||
"task_completed_named": "Fullført: {task}",
|
||||
"undo_done": "Utførelsen angret",
|
||||
"undo_done_action_ran": "Utførelsen angret. Handlingen ved utførelse hadde allerede kjørt og kan ikke angres.",
|
||||
"undo_nothing": "Ingenting å angre. Bare din egen siste utførelse kan angres, innen 10 minutter.",
|
||||
"undo_changed": "Kan ikke angres. Oppgaven er endret siden.",
|
||||
"task_archived": "Oppgave arkivert",
|
||||
"object_archived": "Objekt arkivert",
|
||||
"unarchive": "Gjenopprett",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Voltooid op (optioneel, leeg = nu)",
|
||||
"completed_at_pick": "Datum & tijd kiezen",
|
||||
"completed_at_future_error": "De voltooiingsdatum mag niet in de toekomst liggen.",
|
||||
"ha_time_zone_hint": "Tijd in de tijdzone van Home Assistant ({zone})",
|
||||
"days": "dagen",
|
||||
"day": "dag",
|
||||
"today": "Vandaag",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Toekomstige vensters",
|
||||
"history_edit_title": "Geschiedenisitem bewerken",
|
||||
"history_delete_entry": "Vermelding verwijderen",
|
||||
"history_delete_confirm": "Deze geschiedenisvermelding verwijderen? De datum van laatste uitvoering valt terug op de vorige voltooiing; foto's blijven bij het object en verbruikte onderdelen worden niet teruggeboekt.",
|
||||
"history_delete_confirm_entry": "Deze geschiedenisregel verwijderen? De datum van de laatste uitvoering valt terug op de vorige; foto's blijven bij het object.",
|
||||
"history_delete_returns_parts": "De gebruikte onderdelen gaan terug naar de voorraad: {parts}.",
|
||||
"history_edit_timestamp": "Tijdstempel",
|
||||
"manufacturer": "Fabrikant",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laden…",
|
||||
"archive": "Archiveren",
|
||||
"undo": "Ongedaan maken",
|
||||
"task_completed_named": "Voltooid: {task}",
|
||||
"undo_done": "Afronding ongedaan gemaakt",
|
||||
"undo_done_action_ran": "Afronding ongedaan gemaakt. De actie bij afronden was al uitgevoerd en kan niet worden teruggedraaid.",
|
||||
"undo_nothing": "Niets om ongedaan te maken. Alleen je eigen laatste afronding kan worden teruggedraaid, binnen 10 minuten.",
|
||||
"undo_changed": "Ongedaan maken kan niet. De taak is sindsdien gewijzigd.",
|
||||
"task_archived": "Taak gearchiveerd",
|
||||
"object_archived": "Object gearchiveerd",
|
||||
"unarchive": "Dearchiveren",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Wykonano dnia (opcjonalne, puste = teraz)",
|
||||
"completed_at_pick": "Ustaw datę i godzinę",
|
||||
"completed_at_future_error": "Data wykonania nie może być w przyszłości.",
|
||||
"ha_time_zone_hint": "Czas w strefie czasowej Home Assistant ({zone})",
|
||||
"days": "dni",
|
||||
"day": "dzień",
|
||||
"today": "Dzisiaj",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Przyszłe okna",
|
||||
"history_edit_title": "Edytuj wpis historii",
|
||||
"history_delete_entry": "Usuń wpis",
|
||||
"history_delete_confirm": "Usunąć ten wpis historii? Data ostatniego wykonania cofnie się do poprzedniego; zdjęcia pozostaną przy obiekcie, a zużyte części nie wrócą na stan.",
|
||||
"history_delete_confirm_entry": "Usunąć ten wpis historii? Data ostatniego wykonania wróci do poprzedniej; zdjęcia zostaną przy obiekcie.",
|
||||
"history_delete_returns_parts": "Zużyte części wrócą do magazynu: {parts}.",
|
||||
"history_edit_timestamp": "Znacznik czasu",
|
||||
"manufacturer": "Producent",
|
||||
"model": "Model",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Ładowanie…",
|
||||
"archive": "Archiwizuj",
|
||||
"undo": "Cofnij",
|
||||
"task_completed_named": "Wykonano: {task}",
|
||||
"undo_done": "Wykonanie cofnięte",
|
||||
"undo_done_action_ran": "Wykonanie cofnięte. Akcja przy wykonaniu już się odbyła i nie da się jej cofnąć.",
|
||||
"undo_nothing": "Nie ma czego cofnąć. Cofnąć można tylko swoje ostatnie wykonanie, w ciągu 10 minut.",
|
||||
"undo_changed": "Nie można cofnąć. Zadanie zmieniło się w międzyczasie.",
|
||||
"task_archived": "Zadanie zarchiwizowane",
|
||||
"object_archived": "Obiekt zarchiwizowany",
|
||||
"unarchive": "Przywróć z archiwum",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
"completed_at_pick": "Definir data e hora",
|
||||
"completed_at_future_error": "A data de conclusão não pode estar no futuro.",
|
||||
"ha_time_zone_hint": "Horário no fuso horário do Home Assistant ({zone})",
|
||||
"days": "dias",
|
||||
"day": "dia",
|
||||
"today": "Hoje",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Janelas futuras",
|
||||
"history_edit_title": "Editar entrada do histórico",
|
||||
"history_delete_entry": "Excluir entrada",
|
||||
"history_delete_confirm": "Excluir esta entrada do histórico? A data da última execução volta para a conclusão anterior; as fotos permanecem com o objeto e as peças consumidas não são repostas.",
|
||||
"history_delete_confirm_entry": "Excluir esta entrada do histórico? A data da última realização volta para a anterior; as fotos continuam com o objeto.",
|
||||
"history_delete_returns_parts": "As peças usadas voltam ao estoque: {parts}.",
|
||||
"history_edit_timestamp": "Data e hora",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Carregando…",
|
||||
"archive": "Arquivar",
|
||||
"undo": "Desfazer",
|
||||
"task_completed_named": "Concluída: {task}",
|
||||
"undo_done": "Conclusão desfeita",
|
||||
"undo_done_action_ran": "Conclusão desfeita. A ação de conclusão já tinha sido executada e não pode ser revertida.",
|
||||
"undo_nothing": "Nada para desfazer. Só a sua última conclusão pode ser desfeita, em até 10 minutos.",
|
||||
"undo_changed": "Não é possível desfazer. A tarefa mudou desde então.",
|
||||
"task_archived": "Tarefa arquivada",
|
||||
"object_archived": "Objeto arquivado",
|
||||
"unarchive": "Desarquivar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Concluído em (opcional, vazio = agora)",
|
||||
"completed_at_pick": "Definir data e hora",
|
||||
"completed_at_future_error": "A data de conclusão não pode estar no futuro.",
|
||||
"ha_time_zone_hint": "Hora no fuso horário do Home Assistant ({zone})",
|
||||
"days": "dias",
|
||||
"day": "dia",
|
||||
"today": "Hoje",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Janelas futuras",
|
||||
"history_edit_title": "Editar entrada do histórico",
|
||||
"history_delete_entry": "Eliminar entrada",
|
||||
"history_delete_confirm": "Eliminar esta entrada do histórico? A data da última execução recua para a conclusão anterior; as fotos ficam com o objeto e as peças consumidas não são repostas.",
|
||||
"history_delete_confirm_entry": "Eliminar esta entrada do histórico? A data da última realização volta à anterior; as fotos ficam com o objeto.",
|
||||
"history_delete_returns_parts": "As peças usadas voltam ao stock: {parts}.",
|
||||
"history_edit_timestamp": "Carimbo de data/hora",
|
||||
"manufacturer": "Fabricante",
|
||||
"model": "Modelo",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "A carregar…",
|
||||
"archive": "Arquivar",
|
||||
"undo": "Desfazer",
|
||||
"task_completed_named": "Concluída: {task}",
|
||||
"undo_done": "Conclusão anulada",
|
||||
"undo_done_action_ran": "Conclusão anulada. A ação de conclusão já tinha sido executada e não pode ser revertida.",
|
||||
"undo_nothing": "Nada para anular. Só a sua última conclusão pode ser anulada, dentro de 10 minutos.",
|
||||
"undo_changed": "Não é possível anular. A tarefa mudou entretanto.",
|
||||
"task_archived": "Tarefa arquivada",
|
||||
"object_archived": "Objeto arquivado",
|
||||
"unarchive": "Desarquivar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Выполнено (необязательно, пусто = сейчас)",
|
||||
"completed_at_pick": "Указать дату и время",
|
||||
"completed_at_future_error": "Дата выполнения не может быть в будущем.",
|
||||
"ha_time_zone_hint": "Время в часовом поясе Home Assistant ({zone})",
|
||||
"days": "дней",
|
||||
"day": "день",
|
||||
"today": "Сегодня",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Будущие окна",
|
||||
"history_edit_title": "Редактировать запись истории",
|
||||
"history_delete_entry": "Удалить запись",
|
||||
"history_delete_confirm": "Удалить эту запись истории? Дата последнего выполнения вернётся к предыдущему; фотографии останутся у объекта, а израсходованные детали не будут возвращены на склад.",
|
||||
"history_delete_confirm_entry": "Удалить эту запись истории? Дата последнего выполнения вернётся к предыдущей; фото останутся у объекта.",
|
||||
"history_delete_returns_parts": "Использованные запчасти вернутся на склад: {parts}.",
|
||||
"history_edit_timestamp": "Метка времени",
|
||||
"manufacturer": "Производитель",
|
||||
"model": "Модель",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Загрузка…",
|
||||
"archive": "Архивировать",
|
||||
"undo": "Отменить",
|
||||
"task_completed_named": "Выполнено: {task}",
|
||||
"undo_done": "Выполнение отменено",
|
||||
"undo_done_action_ran": "Выполнение отменено. Действие при выполнении уже сработало, его нельзя отменить.",
|
||||
"undo_nothing": "Нечего отменять. Можно отменить только своё последнее выполнение, в течение 10 минут.",
|
||||
"undo_changed": "Отменить нельзя. Задача с тех пор изменилась.",
|
||||
"task_archived": "Задача архивирована",
|
||||
"object_archived": "Объект архивирован",
|
||||
"unarchive": "Разархивировать",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Utförd den (valfritt, tomt = nu)",
|
||||
"completed_at_pick": "Ange datum och tid",
|
||||
"completed_at_future_error": "Slutförandedatumet får inte ligga i framtiden.",
|
||||
"ha_time_zone_hint": "Tid i Home Assistants tidszon ({zone})",
|
||||
"days": "dagar",
|
||||
"day": "dag",
|
||||
"today": "Idag",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Kommande fönster",
|
||||
"history_edit_title": "Redigera historikpost",
|
||||
"history_delete_entry": "Ta bort post",
|
||||
"history_delete_confirm": "Ta bort den här historikposten? Datumet för senaste utförande faller tillbaka till det föregående; foton stannar hos objektet och förbrukade delar läggs inte tillbaka i lager.",
|
||||
"history_delete_confirm_entry": "Ta bort den här historikposten? Datumet för senaste utförandet faller tillbaka på det föregående; foton finns kvar på objektet.",
|
||||
"history_delete_returns_parts": "De använda delarna går tillbaka till lagret: {parts}.",
|
||||
"history_edit_timestamp": "Tidsstämpel",
|
||||
"manufacturer": "Tillverkare",
|
||||
"model": "Modell",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Laddar…",
|
||||
"archive": "Arkivera",
|
||||
"undo": "Ångra",
|
||||
"task_completed_named": "Slutförd: {task}",
|
||||
"undo_done": "Utförandet ångrat",
|
||||
"undo_done_action_ran": "Utförandet ångrat. Åtgärden vid utförande hade redan körts och kan inte ångras.",
|
||||
"undo_nothing": "Inget att ångra. Bara ditt eget senaste utförande kan ångras, inom 10 minuter.",
|
||||
"undo_changed": "Kan inte ångras. Uppgiften har ändrats sedan dess.",
|
||||
"task_archived": "Uppgift arkiverad",
|
||||
"object_archived": "Objekt arkiverat",
|
||||
"unarchive": "Återställ",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "Tamamlanma zamanı (isteğe bağlı, boş = şimdi)",
|
||||
"completed_at_pick": "Tarih ve saat seç",
|
||||
"completed_at_future_error": "Tamamlanma tarihi gelecekte olamaz.",
|
||||
"ha_time_zone_hint": "Home Assistant saat dilimindeki saat ({zone})",
|
||||
"days": "gün",
|
||||
"day": "gün",
|
||||
"today": "Bugün",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "Gelecek dönemler",
|
||||
"history_edit_title": "Geçmiş kaydını düzenle",
|
||||
"history_delete_entry": "Kaydı sil",
|
||||
"history_delete_confirm": "Bu geçmiş kaydı silinsin mi? Son yapılma tarihi bir önceki tamamlamaya döner; fotoğraflar nesnede kalır, kullanılan parçalar stoğa geri eklenmez.",
|
||||
"history_delete_confirm_entry": "Bu geçmiş kaydı silinsin mi? Son yapılma tarihi bir önceki tamamlamaya geri döner; fotoğraflar nesnede kalır.",
|
||||
"history_delete_returns_parts": "Kullanılan parçalar stoğa geri döner: {parts}.",
|
||||
"history_edit_timestamp": "Zaman damgası",
|
||||
"manufacturer": "Üretici",
|
||||
"model": "Model",
|
||||
@@ -855,6 +857,11 @@
|
||||
"loading": "Yükleniyor…",
|
||||
"archive": "Arşivle",
|
||||
"undo": "Geri al",
|
||||
"task_completed_named": "Tamamlandı: {task}",
|
||||
"undo_done": "Tamamlama geri alındı",
|
||||
"undo_done_action_ran": "Tamamlama geri alındı. Tamamlama eylemi zaten çalışmıştı ve geri alınamaz.",
|
||||
"undo_nothing": "Geri alınacak bir şey yok. Yalnızca kendi son tamamlamanızı 10 dakika içinde geri alabilirsiniz.",
|
||||
"undo_changed": "Geri alınamıyor. Görev o zamandan beri değişti.",
|
||||
"task_archived": "Görev arşivlendi",
|
||||
"object_archived": "Nesne arşivlendi",
|
||||
"unarchive": "Arşivden çıkar",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"completed_at_optional": "Виконано (необов'язково, порожньо = зараз)",
|
||||
"completed_at_pick": "Вказати дату й час",
|
||||
"completed_at_future_error": "Дата виконання не може бути в майбутньому.",
|
||||
"ha_time_zone_hint": "Час у часовому поясі Home Assistant ({zone})",
|
||||
"days": "днів",
|
||||
"day": "день",
|
||||
"today": "Сьогодні",
|
||||
@@ -554,7 +555,8 @@
|
||||
"cal_forward_windows": "Майбутні вікна",
|
||||
"history_edit_title": "Редагувати запис історії",
|
||||
"history_delete_entry": "Видалити запис",
|
||||
"history_delete_confirm": "Видалити цей запис історії? Дата останнього виконання повернеться до попереднього; фотографії залишаться в об'єкта, а витрачені деталі не повернуться на склад.",
|
||||
"history_delete_confirm_entry": "Видалити цей запис історії? Дата останнього виконання повернеться до попередньої; фото залишаться в об'єкта.",
|
||||
"history_delete_returns_parts": "Використані запчастини повернуться на склад: {parts}.",
|
||||
"history_edit_timestamp": "Позначка часу",
|
||||
"manufacturer": "Виробник",
|
||||
"model": "Модель",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "Завантаження…",
|
||||
"archive": "Архівувати",
|
||||
"undo": "Скасувати",
|
||||
"task_completed_named": "Виконано: {task}",
|
||||
"undo_done": "Виконання скасовано",
|
||||
"undo_done_action_ran": "Виконання скасовано. Дія під час виконання вже спрацювала, її не можна скасувати.",
|
||||
"undo_nothing": "Нічого скасовувати. Можна скасувати лише своє останнє виконання, протягом 10 хвилин.",
|
||||
"undo_changed": "Скасувати не можна. Завдання відтоді змінилося.",
|
||||
"task_archived": "Завдання архівовано",
|
||||
"object_archived": "Об'єкт архівовано",
|
||||
"unarchive": "Розархівувати",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"completed_at_optional": "完成时间(可选,留空 = 现在)",
|
||||
"completed_at_pick": "设置日期和时间",
|
||||
"completed_at_future_error": "完成日期不能是未来时间。",
|
||||
"ha_time_zone_hint": "Home Assistant 时区的时间({zone})",
|
||||
"days": "天",
|
||||
"day": "天",
|
||||
"today": "今天",
|
||||
@@ -556,7 +557,8 @@
|
||||
"cal_forward_windows": "未来的窗口",
|
||||
"history_edit_title": "编辑历史记录",
|
||||
"history_delete_entry": "删除记录",
|
||||
"history_delete_confirm": "删除此历史记录?上次完成日期将回退到前一次完成;照片仍保留在对象中,已消耗的配件不会退回库存。",
|
||||
"history_delete_confirm_entry": "删除此历史记录?上次执行日期将退回到上一次完成;照片仍保留在对象中。",
|
||||
"history_delete_returns_parts": "所用的配件将退回库存:{parts}。",
|
||||
"history_edit_timestamp": "时间戳",
|
||||
"manufacturer": "制造商",
|
||||
"model": "型号",
|
||||
@@ -848,6 +850,11 @@
|
||||
"loading": "加载中…",
|
||||
"archive": "归档",
|
||||
"undo": "撤销",
|
||||
"task_completed_named": "已完成:{task}",
|
||||
"undo_done": "已撤销完成",
|
||||
"undo_done_action_ran": "已撤销完成。完成时的操作已经执行,无法撤销。",
|
||||
"undo_nothing": "没有可撤销的内容。只能在 10 分钟内撤销你自己最近一次完成。",
|
||||
"undo_changed": "无法撤销。此后任务已发生变化。",
|
||||
"task_archived": "任务已归档",
|
||||
"object_archived": "对象已归档",
|
||||
"unarchive": "取消归档",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,2 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.97.1 */
|
||||
var v="2.97.1";var f="maintenance-supporter",L=`ll-strategy-dashboard-${f}`,C="hui-maintenance-supporter-strategy-editor",M=["maintenance-supporter-section","maintenance-supporter-vacation","maintenance-supporter-budget","maintenance-supporter-groups"],D=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${v}`,w=null;function h(){return w||(w=import(D)),w}async function I(){let e=await h();if(!e.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return e.MaintenanceDashboardStrategy}async function P(e){let i=(await h()).SECTION_STRATEGIES?.[e];if(!i)throw new Error(`[maintenance-supporter] strategy bundle loaded but has no section strategy "${e}"`);return i}function B(e){return class extends HTMLElement{static async generate(i,l){return(await P(e)).generate(i,l)}}}var y=class extends HTMLElement{static getCreateSuggestions(s){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await h(),document.createElement(C)}static async generate(s,i){return(await I()).generate(s,i)}};function O(){try{customElements.define(L,y)}catch{}for(let e of M)try{customElements.define(`ll-strategy-section-${e}`,B(e))}catch{}}O();var m=window;m.customStrategies=m.customStrategies||[];var U=[{type:f,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/blob/master/docs/EXAMPLES.md#dashboard-strategy"},{type:"maintenance-supporter-section",strategyType:"section",name:"Maintenance Supporter \u2014 Section",description:"Embed maintenance tasks (filterable by area, status, due date) as a section in any dashboard view.",documentationURL:"https://github.com/iluebbe/maintenance_supporter/blob/master/docs/EXAMPLES.md#section-strategy"},{type:"maintenance-supporter-vacation",strategyType:"section",name:"Maintenance Supporter \u2014 Vacation Status",description:"Compact vacation-mode status banner. Tap to open settings in the panel."},{type:"maintenance-supporter-budget",strategyType:"section",name:"Maintenance Supporter \u2014 Budget Status",description:"Monthly + yearly maintenance budget overview. Tap for details."},{type:"maintenance-supporter-groups",strategyType:"section",name:"Maintenance Supporter \u2014 Groups",description:"List of configured task groups with member counts."}];for(let e of U)m.customStrategies.some(s=>s.type===e.type&&s.strategyType===e.strategyType)||m.customStrategies.push(e);(()=>{let e=window;if(e.__msStrategyHealActive)return;e.__msStrategyHealActive=!0;let s=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,i=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,l=`custom:${f}`;function S(r){let t=[document.documentElement],a=0;for(;t.length&&a<9e3;){let o=t.pop();if(a++,!o)continue;let n=o;if(n.nodeType===1&&n.tagName&&n.tagName.toLowerCase()===r)return n;n.shadowRoot&&t.push(n.shadowRoot);let u=o.children;if(u)for(let p of Array.from(u))t.push(p)}return null}function R(r){let t=r?.views;if(!Array.isArray(t)||!t.length)return null;let a=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(u=>u?.path===a);if(o)return o;let n=Number(a);return Number.isInteger(n)&&t[n]?t[n]:t[0]}function k(){try{let t=S("ha-panel-lovelace")?.lovelace;if(!t)return!1;let a=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(a(o.strategy)===l)return!0;let n=R(o);if(n&&a(n.strategy)===l)return!0}return!1}catch{return!1}}function T(){let r=!1,t=0,a=!1,o=!1,n=[document.documentElement],u=0;for(;n.length&&u<9e3;){let p=n.pop();if(u++,!p)continue;let d=p;if(d.nodeType===1&&d.tagName){let c=d.tagName.toLowerCase();(c==="hui-view"||c==="hui-sections-view")&&(r=!0),(c==="ha-card"||c==="hui-card")&&t++,c==="hui-empty-state-card"&&(o=!0),c==="hui-error-card"&&i.test(d.textContent||"")&&(a=!0)}d.shadowRoot&&n.push(d.shadowRoot);let _=p.children;if(_)for(let c of Array.from(_))n.push(c)}return a?!0:o?!1:r&&t<3&&k()}let N="/maintenance_supporter_strategy_shim.js",E=0,b=0;function A(){let r=Date.now();r-b<5e3||E>=3||(b=r,E+=1,import(`${N}?heal=${r}`).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 g(){if(s.test(window.location.pathname))return;let r=0,t=Date.now(),a=window.setInterval(()=>{r++;try{if(Date.now()-t<6e3)return;if(s.test(window.location.pathname)){window.clearInterval(a);return}T()?A():window.clearInterval(a),r>=30&&window.clearInterval(a)}catch{window.clearInterval(a)}},500)}try{document.readyState==="loLine truncated
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
var v="2.98.0";var f="maintenance-supporter",L=`ll-strategy-dashboard-${f}`,C="hui-maintenance-supporter-strategy-editor",M=["maintenance-supporter-section","maintenance-supporter-vacation","maintenance-supporter-budget","maintenance-supporter-groups"],D=`/maintenance_supporter_strategy/maintenance-dashboard-strategy.js?v=${v}`,w=null;function h(){return w||(w=import(D)),w}async function I(){let e=await h();if(!e.MaintenanceDashboardStrategy)throw new Error("[maintenance-supporter] strategy bundle loaded but did not export MaintenanceDashboardStrategy");return e.MaintenanceDashboardStrategy}async function P(e){let i=(await h()).SECTION_STRATEGIES?.[e];if(!i)throw new Error(`[maintenance-supporter] strategy bundle loaded but has no section strategy "${e}"`);return i}function B(e){return class extends HTMLElement{static async generate(i,l){return(await P(e)).generate(i,l)}}}var y=class extends HTMLElement{static getCreateSuggestions(s){return{title:"Maintenance Supporter",icon:"mdi:wrench-clock"}}static async getConfigElement(){return await h(),document.createElement(C)}static async generate(s,i){return(await I()).generate(s,i)}};function O(){try{customElements.define(L,y)}catch{}for(let e of M)try{customElements.define(`ll-strategy-section-${e}`,B(e))}catch{}}O();var m=window;m.customStrategies=m.customStrategies||[];var U=[{type:f,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/blob/master/docs/EXAMPLES.md#dashboard-strategy"},{type:"maintenance-supporter-section",strategyType:"section",name:"Maintenance Supporter \u2014 Section",description:"Embed maintenance tasks (filterable by area, status, due date) as a section in any dashboard view.",documentationURL:"https://github.com/iluebbe/maintenance_supporter/blob/master/docs/EXAMPLES.md#section-strategy"},{type:"maintenance-supporter-vacation",strategyType:"section",name:"Maintenance Supporter \u2014 Vacation Status",description:"Compact vacation-mode status banner. Tap to open settings in the panel."},{type:"maintenance-supporter-budget",strategyType:"section",name:"Maintenance Supporter \u2014 Budget Status",description:"Monthly + yearly maintenance budget overview. Tap for details."},{type:"maintenance-supporter-groups",strategyType:"section",name:"Maintenance Supporter \u2014 Groups",description:"List of configured task groups with member counts."}];for(let e of U)m.customStrategies.some(s=>s.type===e.type&&s.strategyType===e.strategyType)||m.customStrategies.push(e);(()=>{let e=window;if(e.__msStrategyHealActive)return;e.__msStrategyHealActive=!0;let s=/^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/,i=/Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i,l=`custom:${f}`;function S(r){let t=[document.documentElement],a=0;for(;t.length&&a<9e3;){let o=t.pop();if(a++,!o)continue;let n=o;if(n.nodeType===1&&n.tagName&&n.tagName.toLowerCase()===r)return n;n.shadowRoot&&t.push(n.shadowRoot);let u=o.children;if(u)for(let p of Array.from(u))t.push(p)}return null}function R(r){let t=r?.views;if(!Array.isArray(t)||!t.length)return null;let a=window.location.pathname.split("/").filter(Boolean).pop()||"",o=t.find(u=>u?.path===a);if(o)return o;let n=Number(a);return Number.isInteger(n)&&t[n]?t[n]:t[0]}function k(){try{let t=S("ha-panel-lovelace")?.lovelace;if(!t)return!1;let a=o=>o?.type;for(let o of[t.config,t.rawConfig]){if(!o)continue;if(a(o.strategy)===l)return!0;let n=R(o);if(n&&a(n.strategy)===l)return!0}return!1}catch{return!1}}function T(){let r=!1,t=0,a=!1,o=!1,n=[document.documentElement],u=0;for(;n.length&&u<9e3;){let p=n.pop();if(u++,!p)continue;let d=p;if(d.nodeType===1&&d.tagName){let c=d.tagName.toLowerCase();(c==="hui-view"||c==="hui-sections-view")&&(r=!0),(c==="ha-card"||c==="hui-card")&&t++,c==="hui-empty-state-card"&&(o=!0),c==="hui-error-card"&&i.test(d.textContent||"")&&(a=!0)}d.shadowRoot&&n.push(d.shadowRoot);let _=p.children;if(_)for(let c of Array.from(_))n.push(c)}return a?!0:o?!1:r&&t<3&&k()}let N="/maintenance_supporter_strategy_shim.js",E=0,b=0;function A(){let r=Date.now();r-b<5e3||E>=3||(b=r,E+=1,import(`${N}?heal=${r}`).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 g(){if(s.test(window.location.pathname))return;let r=0,t=Date.now(),a=window.setInterval(()=>{r++;try{if(Date.now()-t<6e3)return;if(s.test(window.location.pathname)){window.clearInterval(a);return}T()?A():window.clearInterval(a),r>=30&&window.clearInterval(a)}catch{window.clearInterval(a)}},500)}try{document.readyState==="loLine truncated
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{e as y}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-FFPNBBZ6.js";import{d as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-QFIANZWP.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MN2RZ73S.js";import{H as b,a as o,b as u,c as n,f as l,h as v,l as g,m as d,s as r,u as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._objectNames={};this._objects=[];this._covered={};this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=t=>{let i=new Set(this._selected);i.has(t)?i.delete(t):i.add(t),this._selected=i};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(t=>t.entity_id))};this._adopt=async()=>{if(this._selected.size===0||this._adopting)return;this._error="";let t=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)??void 0,object_name:this._effectiveName(e),device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),i=await h(this,{type:"maintenance_supporter/problem_sensors/adopt",selections:t},{busy:e=>{this._adopting=e},onError:e=>{this._error=e}});i!==void 0&&(this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:i})),this._open=!1)}}get _lang(){return b(this.hass)}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._objectNames={},this._responsible="",this._forMinutes="0",this._userService?this._userService.updateHass(this.hass):this._userService=new y(this.hass);let[t,i,e]=await Promise.all([h(this,{type:"maintenance_supporter/problem_sensors/discover"},{onError:s=>{this._error=s}}),this._userService.getUsers().catch(()=>[]),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}).catch(()=>({objects:[]}))]);this._loading=!1,t!==void 0&&(this._objects=(e?.objects||[]).filter(s=>!s.object.archived_at).map(s=>({entry_id:s.entry_id,name:s.object.name,tasks:(s.tasks||[]).length})),this._sensors=t?.sensors||[],this._covered=Object.fromEntries(this._sensors.map(s=>[s.entity_id,s.covered_by??null])),this._selected=new Set(this._sensors.filter(s=>!s.covered_by).map(s=>s.entity_id)),this._users=i)}_defaultName(t){return t.suggested_entry_id?t.suggested_object_name:t.candidate?.name??t.suggested_object_name}_effectiveName(t){return(this._objectNames[t.entity_id]??"").trim()||this._defaultName(t)}_existingEntryFor(t){let i=this._effectiveName(t),e=t.suggested_entry_id||t.target_entry_id||null;if(i===this._defaultName(t)&&e)return e;let s=this._objects.find(c=>c.name.trim().toLowerCase()===i.toLowerCase());return s?s.entry_id:null}_schedulePreview(){clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._preview()},350)}async _preview(){let t=this._sensors.map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)})),i=await h(this,{type:"maintenance_supporter/problem_sensors/preview",selections:t},{onError:()=>{}});i?.covered&&(this._covered={...this._covered,...i.covered})}_summary(t){let i=new Map;for(let e of this._sensors){if(!this._selected.has(e.entity_id))continue;let s=this._existingEntryFor(e),c=this._effectiveName(e),_=s??`new:${c.toLowerCase()}`,p=i.get(_)??{name:c,entry:s,adding:0};p.adding+=1,i.set(_,p)}return i.size===0?l:n`
|
||||
<div class="summary">
|
||||
<div class="summary-title">${r("adopt_summary_title",t)}</div>
|
||||
${[...i.values()].map(e=>{if(!e.entry)return n`<div class="summary-line new">
|
||||
${r("setups_new_object_count",t).replace("{name}",e.name).replace("{count}",String(e.adding))}
|
||||
</div>`;let s=this._objects.find(c=>c.entry_id===e.entry)?.tasks??0;return n`<div class="summary-line">
|
||||
${r("adopt_summary_existing",t).replace("{name}",e.name).replace("{before}",String(s)).replace("{after}",String(s+e.adding))}
|
||||
</div>`})}
|
||||
</div>
|
||||
`}_close(){clearTimeout(this._previewTimer),this._open=!1}_targetLabel(t,i){if(t.candidate&&this._existingEntryFor(t)===t.candidate.entry_id){let e=t.candidate.reasons.map(s=>r(`setups_reason_${s}`,i)).join(", ");return r("setups_target_match",i).replace("{name}",t.candidate.name).replace("{reasons}",e)}return this._effectiveName(t)}render(){if(!this._open)return n``;let t=this._lang,i=this._sensors.length>0&&this._selected.size===this._sensors.length;return n`
|
||||
<div class="overlay" @click=${this._close}>
|
||||
<div class="card" @click=${e=>e.stopPropagation()}>
|
||||
<div class="title">${r("adopt_problem_title",t)}</div>
|
||||
<div class="hint">${r("adopt_problem_hint",t)}</div>
|
||||
${this._error?n`<div class="error">${this._error}</div>`:l}
|
||||
|
||||
${this._loading?n`<div class="loading">…</div>`:this._sensors.length===0?n`<div class="empty">${r("adopt_problem_none",t)}</div>`:n`
|
||||
<label class="select-all">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${i}
|
||||
@change=${this._toggleAll}
|
||||
/>
|
||||
<span>${r("selected",t)}: ${this._selected.size} / ${this._sensors.length}</span>
|
||||
</label>
|
||||
<div class="row-sub adopt-object-hint">${r("adopt_object_hint",t)}</div>
|
||||
<datalist id="adopt-object-names">
|
||||
${this._objects.map(e=>n`<option value=${e.name}></option>`)}
|
||||
</datalist>
|
||||
<div class="list">
|
||||
${this._sensors.map(e=>{let s=this._selected.has(e.entity_id),c=e.state==="on",_=[e.device_name,e.area_name].filter(Boolean).join(" \xB7 ");return n`
|
||||
<label class="row">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${s}
|
||||
@change=${()=>this._toggle(e.entity_id)}
|
||||
/>
|
||||
<div class="row-main">
|
||||
<div class="row-top">
|
||||
<span class="row-name">${e.name}</span>
|
||||
<span class="chip ${c?"chip-active":"chip-ok"}">
|
||||
${c?r("adopt_problem_active",t):r("adopt_problem_ok",t)}
|
||||
</span>
|
||||
</div>
|
||||
${_?n`<div class="row-sub">${_}</div>`:l}
|
||||
${s?n`<div class="row-object" @click=${p=>p.stopPropagation()}>
|
||||
<input
|
||||
class="adopt-object"
|
||||
list="adopt-object-names"
|
||||
aria-label=${r("object",t)}
|
||||
placeholder=${this._defaultName(e)}
|
||||
.value=${this._objectNames[e.entity_id]??this._defaultName(e)}
|
||||
@input=${p=>{this._objectNames={...this._objectNames,[e.entity_id]:p.target.value},this._schedulePreview()}}
|
||||
/>
|
||||
</div>`:l}
|
||||
<div class="row-target">
|
||||
→ ${this._targetLabel(e,t)}${this._existingEntryFor(e)?l:n` <span class="new-tag">${r("adopt_problem_new_object",t)}</span>`}
|
||||
</div>
|
||||
${this._covered[e.entity_id]?n`<div class="maybe">
|
||||
${r("setups_maybe_covered",t).replace("{name}",this._covered[e.entity_id].name)}
|
||||
</div>`:l}
|
||||
${e.suggested_part_name?n`<div class="row-part">
|
||||
<ha-icon icon="mdi:package-variant-closed"></ha-icon>
|
||||
${r("adopt_problem_part",t).replace("{name}",e.suggested_part_name)}
|
||||
</div>`:l}
|
||||
</div>
|
||||
</label>
|
||||
`})}
|
||||
</div>
|
||||
`}
|
||||
|
||||
${!this._loading&&this._sensors.length>0?this._summary(t):l}
|
||||
|
||||
${!this._loading&&this._sensors.length>0?n`
|
||||
<label class="responsible">
|
||||
<span>${r("for_at_least_minutes",t)}</span>
|
||||
<input
|
||||
class="for-input"
|
||||
type="number"
|
||||
min=${m[0]}
|
||||
max=${m[1]}
|
||||
.value=${this._forMinutes}
|
||||
@input=${e=>this._forMinutes=e.target.value}
|
||||
/>
|
||||
</label>
|
||||
<div class="for-hint">${r("adopt_for_minutes_hint",t)}</div>
|
||||
`:l}
|
||||
|
||||
${!this._loading&&this._sensors.length>0&&this._users.length>0?n`
|
||||
<label class="responsible">
|
||||
<span>${r("adopt_problem_responsible",t)}</span>
|
||||
<select
|
||||
.value=${this._responsible}
|
||||
@change=${e=>{this._responsible=e.target.value}}
|
||||
>
|
||||
<option value="" ?selected=${!this._responsible}>${r("no_user_assigned",t)}</option>
|
||||
${this._users.map(e=>n`<option value=${e.id} ?selected=${e.id===this._responsible}>${e.name}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
`:l}
|
||||
|
||||
<div class="actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${r("cancel",t)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._adopt}
|
||||
.disabled=${this._selected.size===0||this._adopting}
|
||||
>
|
||||
${r("adopt_problem_adopt",t)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}};a.styles=u`
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 560px;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.hint {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
.loading,
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.select-all input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row input {
|
||||
margin-top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.row-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
.row-sub {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-object { margin-top: 6px; }
|
||||
.row-object input {
|
||||
width: 100%; box-sizing: border-box; padding: 6px 8px; font: inherit;
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
background: var(--card-background-color); color: var(--primary-text-color);
|
||||
}
|
||||
.adopt-object-hint { margin: 0 0 8px; }
|
||||
.row-target {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
.row-part {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.row-part ha-icon {
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
.new-tag {
|
||||
font-style: italic;
|
||||
}
|
||||
.maybe {
|
||||
font-size: 11px;
|
||||
color: var(--warning-color, #ff9800);
|
||||
}
|
||||
.summary {
|
||||
font-size: 12px;
|
||||
border-top: 1px dashed var(--divider-color);
|
||||
padding-top: 6px;
|
||||
}
|
||||
.summary-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.summary-line {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.chip {
|
||||
font-size: 11px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip-active {
|
||||
background: var(--error-color, #f44336);
|
||||
color: #fff;
|
||||
}
|
||||
.chip-ok {
|
||||
background: var(--divider-color);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.responsible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.for-input {
|
||||
width: 70px;
|
||||
background: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.for-hint {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
margin: -4px 0 2px;
|
||||
}
|
||||
.responsible select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
`,o([g({attribute:!1})],a.prototype,"hass",2),o([d()],a.prototype,"_open",2),o([d()],a.prototype,"_loading",2),o([d()],a.prototype,"_adopting",2),o([d()],a.prototype,"_error",2),o([d()],a.prototype,"_sensors",2),o([d()],a.prototype,"_selected",2),o([d()],a.prototype,"_objectNames",2),o([d()],a.prototype,"_objects",2),o([d()],a.prototype,"_covered",2),o([d()],a.prototype,"_users",2),o([d()],a.prototype,"_responsible",2),o([d()],a.prototype,"_forMinutes",2);customElements.get("maintenance-adopt-problem-sensors-dialog")||customElements.define("maintenance-adopt-problem-sensors-dialog",a);export{a as MaintenanceAdoptProblemSensorsDialog};
|
||||
@@ -0,0 +1,279 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{a as X}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WH65ZHYZ.js";import{a as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-DY5FF6TM.js";import{f as E,g as M,h as z,i as Q,j as U,k as Y,l as q,m as G,n as R,o as A,p as W}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-S7XZBRRU.js";import{a as B}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GGO5IKKD.js";import{e as P,f as K}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-DLWOWMV7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4PKMHUVO.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-NEWWPCTD.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-QKEXGIRG.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-OUHDI2FL.js";import{B as S,H as F,K as g,L as v,O as N,P as I,T as w,V as T,a as $,c as p,f as y,h as H,l as k,m as j,n as C,q as D,s as a,u as L,y as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";function J(b,i,t,s){let l=b.entries.filter(e=>e.type==="completed").sort((e,n)=>e.ts-n.ts),d=`${b.from?t.date(b.from):"\u2026"} \u2013 ${b.to?t.date(b.to):"\u2026"}`,r=b.totals,h=[[i.completions,t.number(r.completions)],[i.totalCost,t.cost(r.totalCost)],[i.avgCost,r.avgCost!=null?t.cost(r.avgCost):i.none],[i.totalTime,r.totalDuration?t.duration(r.totalDuration):i.none]].map(([e,n])=>`<div class="kpi"><div class="k">${o(e)}</div><div class="v">${o(n)}</div></div>`).join(""),u=b.byObject.map(e=>`<tr>
|
||||
<td>${o(e.objectName)}</td>
|
||||
<td class="num">${o(t.number(e.completions))}</td>
|
||||
<td class="num">${o(t.cost(e.cost))}</td>
|
||||
<td class="num">${o(t.share(e.share))}</td>
|
||||
</tr>`).join(""),f=b.buckets.buckets.reduce((e,n)=>Math.max(e,n.cost),0),c=b.buckets.buckets.map(e=>{let n=f>0?Math.round(e.cost/f*100):0;return`<tr>
|
||||
<td class="nowrap">${o(t.bucket(e))}</td>
|
||||
<td class="num">${o(t.number(e.completions))}</td>
|
||||
<td class="bar-cell">${n>0?`<span class="bar" style="width:${n}%"></span>`:""}</td>
|
||||
<td class="num">${o(t.cost(e.cost))}</td>
|
||||
</tr>`}).join(""),m=l.map(e=>{let n=[e.notes,e.completedBy?`${i.completedBy}: ${e.completedBy}`:null].filter(Boolean).join(" \xB7 "),x=e.phaseName?`${e.taskName} \xB7 ${e.phaseName}`:e.taskName;return`<tr>
|
||||
<td class="nowrap">${o(t.date(e.timestamp))}</td>
|
||||
<td>${o(e.objectName)}</td>
|
||||
<td>${o(x)}</td>
|
||||
<td class="num">${e.cost!=null?o(t.cost(e.cost)):o(i.none)}</td>
|
||||
<td class="num">${e.duration!=null?o(t.duration(e.duration)):o(i.none)}</td>
|
||||
<td class="notes">${o(n)||o(i.none)}</td>
|
||||
</tr>`}).join(`
|
||||
`);return`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${o(i.title)} \u2014 ${o(b.areaName)}</title>
|
||||
<style>
|
||||
/* Printable sheet: it opens as a blob in whatever viewer the OS supplies
|
||||
(Companion = WebView, dark phones paint a dark default canvas), so the
|
||||
document states its own light scheme and paints its background. */
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font: 13px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #fff; margin: 32px; }
|
||||
h1 { font-size: 22px; margin: 0 0 2px; }
|
||||
h2 { font-size: 15px; margin: 24px 0 8px; }
|
||||
.sub { color: #666; margin: 0 0 16px; }
|
||||
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 0 0 8px; }
|
||||
.kpi { border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; }
|
||||
.kpi .k { color: #777; font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.kpi .v { font-size: 16px; font-weight: 600; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: #666; border-bottom: 2px solid #ccc; padding: 6px 8px; }
|
||||
td { border-bottom: 1px solid #e5e5e5; padding: 6px 8px; vertical-align: top; }
|
||||
td.num, th.num { text-align: right; white-space: nowrap; }
|
||||
td.nowrap { white-space: nowrap; }
|
||||
td.notes { color: #444; white-space: pre-wrap; }
|
||||
td.bar-cell { width: 40%; vertical-align: middle; }
|
||||
.bar { display: block; height: 10px; background: #9aa7b8; border-radius: 0 3px 3px 0;
|
||||
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
tfoot td { border-bottom: none; border-top: 2px solid #ccc; font-weight: 600; }
|
||||
tr { break-inside: avoid; }
|
||||
.cap-note { margin-top: 14px; color: #888; font-size: 11px; }
|
||||
@media print { body { margin: 12mm; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${o(i.title)} \u2014 ${o(b.areaName)}</h1>
|
||||
<p class="sub">${o(i.period)}: ${o(d)} \xB7 ${o(i.generated)} ${o(t.date(s))}</p>
|
||||
<div class="kpis">${h}</div>
|
||||
|
||||
<h2>${o(i.costPerObject)}</h2>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>${o(i.colObject)}</th><th class="num">${o(i.completions)}</th>
|
||||
<th class="num">${o(i.colCost)}</th><th class="num">${o(i.share)}</th>
|
||||
</tr></thead>
|
||||
<tbody>${u||`<tr><td colspan="4">${o(i.none)}</td></tr>`}</tbody>
|
||||
<tfoot><tr>
|
||||
<td>${o(i.totalCost)}</td><td class="num">${o(t.number(r.completions))}</td>
|
||||
<td class="num">${o(t.cost(r.totalCost))}</td><td></td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
|
||||
<h2>${o(i.costPerBucket)}</h2>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>${o(i.period)}</th><th class="num">${o(i.completions)}</th>
|
||||
<th></th><th class="num">${o(i.colCost)}</th>
|
||||
</tr></thead>
|
||||
<tbody>${c||`<tr><td colspan="4">${o(i.none)}</td></tr>`}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>${o(i.historyHeading)} (${o(t.number(l.length))})</h2>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>${o(i.colDate)}</th><th>${o(i.colObject)}</th><th>${o(i.colTask)}</th>
|
||||
<th class="num">${o(i.colCost)}</th><th class="num">${o(i.colDuration)}</th><th>${o(i.colNotes)}</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${m||`<tr><td colspan="6">${o(i.none)}</td></tr>`}
|
||||
</tbody>
|
||||
<tfoot><tr>
|
||||
<td colspan="3">${o(i.totalCost)}</td>
|
||||
<td class="num">${o(t.cost(r.totalCost))}</td>
|
||||
<td class="num">${r.totalDuration?o(t.duration(r.totalDuration)):""}</td><td></td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
${b.capped?`<p class="cap-note">${o(i.capNote)}</p>`:""}
|
||||
</body>
|
||||
</html>`}var Z=25,V=5,tt={completed:C.ok,missed:C.overdue,reset:C.paused,skipped:"var(--secondary-text-color)"},_=class extends H{constructor(){super(...arguments);this.areaKey="";this.objects=[];this.showArchived=!1;this.currencySymbol="";this.userName=()=>null;this._from="";this._to="";this._entryFilter="";this._taskQuery="";this._expanded=!1;this._printing=!1;this._histories=new X(this);this._localeReady=!1;this._filtersFor=null;this._memo=null}createRenderRoot(){return this}get _lang(){return F(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("areaKey")&&this._filtersFor!==this.areaKey){this._filtersFor=this.areaKey;let s=R();this._from=s.from,this._to=s.to,this._entryFilter="",this._taskQuery="",this._expanded=!1}if(t.has("objects")||t.has("areaKey")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let s=this._areaObjects().flatMap(l=>l.tasks.map(d=>({entryId:l.entry_id,task:d})));this._histories.sync(this.hass,s)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,L(this._lang).then(()=>this.requestUpdate()))}_areaObjects(){return z(this.objects,this.areaKey,this.showArchived).sort((t,s)=>t.object.name.localeCompare(s.object.name))}_entries(t){let s=[this.objects,this.areaKey,this.showArchived,this._histories.version];if(this._memo&&this._memo.deps.every((d,r)=>d===s[r]))return this._memo.entries;let l=Q(t,(d,r)=>this._histories.historyOf(d,r));return this._memo={deps:s,entries:l},l}_figures(){let t=this._areaObjects(),s=this._entries(t),l={from:this._from||null,to:this._to||null},d=U(s,{...l,entryId:this._entryFilter||null,taskQuery:this._taskQuery}),r=this._entryFilter?t.filter(h=>h.entry_id===this._entryFilter):t;return{objects:t,entries:s,filtered:d,totals:Y(d),buckets:G(d,l),byObject:q(d,r)}}_setRange(t){this._from=t.from,this._to=t.to,this._expanded=!1}_openObject(t){this.dispatchEvent(new CustomEvent("open-object",{detail:{entryId:t},bubbles:!0,composed:!0}))}_openTask(t,s){this.dispatchEvent(new CustomEvent("open-task",{detail:{entryId:t,taskId:s},bubbles:!0,composed:!0}))}_bucketLabel(t,s){return t.month==null?String(t.year):`${T(t.month,this._lang,s)} ${t.year}`}_countText(t,s,l){let d=this._lang;return l===1?a(s,d):a(t,d).replace("{n}",g(l,d))}async _print(){if(this._printing)return;let t=K();this._printing=!0;try{await this._histories.settled()}finally{this._printing=!1}let s=this._lang,l=this.currencySymbol,{filtered:d,totals:r,buckets:h,byObject:u}=this._figures(),f={title:a("area_report_title",s),generated:a("report_generated",s),period:a("area_report_period",s),completions:a("area_kpi_completions",s),totalCost:a("total_cost",s),avgCost:a("avg_cost",s),totalTime:a("area_kpi_total_time",s),costPerObject:a("area_cost_per_object",s),costPerBucket:h.unit==="month"?a("area_cost_per_month",s):a("area_cost_per_year",s),share:a("area_cost_share",s),historyHeading:a("area_history_section",s),colDate:a("date",s),colObject:a("object",s),colTask:a("task_name",s),colCost:a("cost",s),colDuration:a("duration",s),colNotes:a("notes_label",s),completedBy:a("completed_by",s),capNote:a("object_history_cap_note",s),none:"\u2014"},c=d.filter(n=>n.type==="completed"),m=d.map(n=>({...n,completedBy:n.completedBy?this.userName(n.completedBy):null,notes:n.notes?B(n.notes,s):null})),e=J({areaName:E(this.areaKey,this.hass?.areas,a("no_area",s)),from:this._from||S(c[c.length-1]?.timestamp)||null,to:this._to||O(),totals:r,byObject:u,buckets:h,entries:m,capped:this._histories.capped},f,{date:n=>N(n,s),cost:n=>v(n,l,s),duration:n=>w(n,s),bucket:n=>this._bucketLabel(n,"long"),share:n=>`${g(n*100,s,0)} %`,number:n=>g(n,s)},new Date().toISOString());P(e,t)}_renderRangeChips(t){let s=this._lang,l=O(),d=R(l),r=u=>this._from===u.from&&this._to===u.to,h=W(t,Number(l.slice(0,4))).slice(0,V);return p`
|
||||
<div class="filter-chips area-range-chips">
|
||||
<button class="filter-chip ${r(d)?"active":""}" @click=${()=>this._setRange(R())}>
|
||||
${a("area_range_12m",s)}
|
||||
</button>
|
||||
${h.map(u=>p`
|
||||
<button class="filter-chip ${r(A(u))?"active":""}" @click=${()=>this._setRange(A(u))}>${u}</button>
|
||||
`)}
|
||||
<button class="filter-chip ${!this._from&&!this._to?"active":""}" @click=${()=>this._setRange({from:"",to:""})}>
|
||||
${a("area_range_all",s)}
|
||||
</button>
|
||||
</div>
|
||||
`}_renderChart(t){let s=this._lang,l=this.currencySymbol,d=t.reduce((c,m)=>Math.max(c,m.cost),0),r=t.reduce((c,m)=>Math.min(c,m.cost),0);if(d<=0&&r>=0)return p`<p class="area-empty">${a("area_no_costs",s)}</p>`;let h=c=>Math.round(c/(d-r)*1e3)/10,u=Math.max(1,Math.ceil(t.length/12)),f=t.map((c,m)=>({b:c,i:m})).filter(({i:c})=>c%u===0);return p`
|
||||
<div class="area-chart">
|
||||
<div class="area-chart-max">${v(d,l,s)}</div>
|
||||
<div class="area-chart-bars">
|
||||
${r<0?p`<div class="area-chart-zero" style="bottom: ${h(-r)}%"></div>`:y}
|
||||
${t.map(c=>{let m=`${this._bucketLabel(c,"long")}: ${v(c.cost,l,s)} \xB7 ${a("area_kpi_completions",s)}: ${g(c.completions,s)}`,e=c.cost<0?h(c.cost-r):h(-r);return p`<div class="area-bar" title=${m} aria-label=${m}>
|
||||
${c.cost!==0?p`<div class="area-bar-fill ${c.cost<0?"credit":""}" style="height: ${h(Math.abs(c.cost))}%${e?`; bottom: ${e}%`:""}"></div>`:y}
|
||||
</div>`})}
|
||||
</div>
|
||||
${r<0?p`<div class="area-chart-min">${v(r,l,s)}</div>`:y}
|
||||
<div class="area-chart-axis ${u>1?"grouped":""}">
|
||||
${f.map(({b:c,i:m})=>p`<span class="area-bar-label" style="flex-grow: ${Math.min(u,t.length-m)}">${c.month==null?String(c.year):p`${T(c.month,s,"short")}${c.month===0||m===0?p`<br />${c.year}`:y}`}</span>`)}
|
||||
</div>
|
||||
</div>
|
||||
`}render(){if(!this.hass||!this.areaKey)return y;let t=this._lang,s=this.currencySymbol,{objects:l,entries:d,filtered:r,totals:h,buckets:u,byObject:f}=this._figures(),c=l.reduce((e,n)=>e+n.tasks.filter(x=>this.showArchived||!x.archived).length,0),m=this._expanded?r:r.slice(0,Z);return p`
|
||||
<div class="detail-section area-view">
|
||||
<div class="detail-header">
|
||||
<h2 class="area-title">
|
||||
<ha-icon .icon=${M(this.areaKey,this.hass.areas)}></ha-icon>
|
||||
${E(this.areaKey,this.hass.areas,a("no_area",t))}
|
||||
</h2>
|
||||
<div class="action-buttons">
|
||||
<ha-button appearance="plain" class="area-print" .disabled=${this._printing} @click=${()=>this._print()}>
|
||||
<ha-icon icon="mdi:printer-outline"></ha-icon>
|
||||
${this._printing?a("loading",t):a("area_report_print",t)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="meta">
|
||||
${this._countText("area_object_count","area_object_count_one",l.length)} ·
|
||||
${this._countText("templates_task_count","templates_task_count_one",c)}
|
||||
${this._histories.loading?p` · <span class="area-loading">${a("loading",t)}</span>`:y}
|
||||
</p>
|
||||
|
||||
${this._renderRangeChips(d)}
|
||||
<div class="filter-bar area-filters">
|
||||
<ms-date-field
|
||||
kind="date"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${t}
|
||||
.label=${a("date_from",t)}
|
||||
.value=${this._from}
|
||||
@value-changed=${e=>{this._from=e.detail.value}}
|
||||
></ms-date-field>
|
||||
<ms-date-field
|
||||
kind="date"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${t}
|
||||
.label=${a("date_to",t)}
|
||||
.value=${this._to}
|
||||
@value-changed=${e=>{this._to=e.detail.value}}
|
||||
></ms-date-field>
|
||||
<label class="filter-field">
|
||||
<span class="filter-label">${a("object",t)}</span>
|
||||
<select class="area-object-filter" .value=${this._entryFilter}
|
||||
@change=${e=>{this._entryFilter=e.target.value}}>
|
||||
<option value="" ?selected=${!this._entryFilter}>${a("all_objects",t)}</option>
|
||||
${l.map(e=>p`<option value=${e.entry_id} ?selected=${e.entry_id===this._entryFilter}>${e.object.name}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
<input
|
||||
type="search"
|
||||
class="search-input area-task-filter"
|
||||
aria-label=${a("area_task_filter",t)}
|
||||
placeholder=${a("area_task_filter",t)}
|
||||
.value=${this._taskQuery}
|
||||
@input=${e=>{this._taskQuery=e.target.value}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="kpi-bar area-kpis">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">${a("area_kpi_completions",t)}</div>
|
||||
<div class="kpi-value-large area-kpi-completions">${g(h.completions,t)}</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">${a("total_cost",t)}</div>
|
||||
<div class="kpi-value-large area-kpi-cost">${v(h.totalCost,s,t)}</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">${a("avg_cost",t)}</div>
|
||||
<div class="kpi-value-large">${h.avgCost!=null?v(h.avgCost,s,t):"\u2014"}</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">${a("area_kpi_total_time",t)}</div>
|
||||
<div class="kpi-value-large">${h.totalDuration?w(h.totalDuration,t):"\u2014"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>${u.unit==="month"?a("area_cost_per_month",t):a("area_cost_per_year",t)}</h3>
|
||||
${this._renderChart(u.buckets)}
|
||||
|
||||
<h3>${a("area_cost_per_object",t)}</h3>
|
||||
<div class="objects-table-wrap">
|
||||
<table class="objects-table area-object-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${a("object",t)}</th>
|
||||
<th class="num">${a("area_kpi_completions",t)}</th>
|
||||
<th class="num">${a("cost",t)}</th>
|
||||
<th class="num">${a("area_cost_share",t)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${f.map(e=>p`
|
||||
<tr class="objects-table-row" @click=${()=>this._openObject(e.entryId)}>
|
||||
<td><span class="objects-table-name">${e.objectName}</span></td>
|
||||
<td class="num">${g(e.completions,t)}</td>
|
||||
<td class="num">${v(e.cost,s,t)}</td>
|
||||
<td class="num">
|
||||
<span class="area-share"><span class="area-share-fill" style="width: ${Math.round(e.share*100)}%"></span></span>
|
||||
${g(e.share*100,t,0)} %
|
||||
</td>
|
||||
</tr>
|
||||
`)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
${a("area_history_section",t)}
|
||||
<span class="area-count">${g(r.length,t)}</span>
|
||||
</h3>
|
||||
${r.length===0?p`<p class="area-empty">${a("object_history_empty",t)}</p>`:p`
|
||||
<div class="objects-table-wrap">
|
||||
<table class="objects-table area-history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${a("date",t)}</th>
|
||||
<th></th>
|
||||
<th>${a("object",t)}</th>
|
||||
<th>${a("task_name",t)}</th>
|
||||
<th class="num">${a("cost",t)}</th>
|
||||
<th class="num">${a("duration",t)}</th>
|
||||
<th>${a("notes_label",t)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${m.map(e=>{let n=e.notes?B(e.notes,t):"";return p`
|
||||
<tr class="objects-table-row area-history-row" @click=${()=>this._openTask(e.entryId,e.taskId)}>
|
||||
<td title=${I(e.timestamp,t)}>${N(e.timestamp,t)}</td>
|
||||
<td>
|
||||
<span class="area-entry-type" style="color: ${tt[e.type]??"inherit"}">
|
||||
<ha-icon .icon=${D[e.type]||"mdi:circle"}></ha-icon>${a(e.type,t)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="area-object-link" @click=${x=>{x.stopPropagation(),this._openObject(e.entryId)}}>
|
||||
${e.objectName}
|
||||
</button>
|
||||
</td>
|
||||
<td><button class="area-task-link">${e.taskName}${e.phaseName?` \xB7 ${e.phaseName}`:""}</button></td>
|
||||
<td class="num">${e.cost!=null?v(e.cost,s,t):"\u2014"}</td>
|
||||
<td class="num">${e.duration!=null?w(e.duration,t):"\u2014"}</td>
|
||||
<td class="oc-notes" title=${n}>${n||"\u2014"}</td>
|
||||
</tr>
|
||||
`})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${r.length>m.length?p`<ha-button appearance="plain" class="area-more" @click=${()=>{this._expanded=!0}}>
|
||||
${a("show_all",t)} (${g(r.length,t)})
|
||||
</ha-button>`:y}
|
||||
`}
|
||||
${this._histories.capped?p`<p class="area-cap-note">${a("object_history_cap_note",t)}</p>`:y}
|
||||
</div>
|
||||
`}};$([k({attribute:!1})],_.prototype,"hass",2),$([k()],_.prototype,"areaKey",2),$([k({attribute:!1})],_.prototype,"objects",2),$([k({type:Boolean})],_.prototype,"showArchived",2),$([k()],_.prototype,"currencySymbol",2),$([k({attribute:!1})],_.prototype,"userName",2),$([j()],_.prototype,"_from",2),$([j()],_.prototype,"_to",2),$([j()],_.prototype,"_entryFilter",2),$([j()],_.prototype,"_taskQuery",2),$([j()],_.prototype,"_expanded",2),$([j()],_.prototype,"_printing",2);customElements.get("maintenance-area-view")||customElements.define("maintenance-area-view",_);export{_ as MaintenanceAreaView};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{a as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-WH65ZHYZ.js";import{q as j,r as E,s as L,t as R,u as C}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-S7XZBRRU.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-4PKMHUVO.js";import{a as _,b as S,c as A}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKVMZFOV.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-NEWWPCTD.js";import{H as g,K as k,L as v,O as w,a as l,c as a,f as m,h as y,l as p,m as b,s as r,u as $,y as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var q=[{key:"name",label:"area",num:!1},{key:"objects",label:"objects",num:!0},{key:"tasks",label:"tasks",num:!0},{key:"overdue",label:"overdue",num:!0},{key:"due_soon",label:"due_soon",num:!0},{key:"cost_year",label:"area_cost_year",num:!0},{key:"cost_total",label:"total_cost",num:!0},{key:"last",label:"last_performed",num:!1}],i=class extends y{constructor(){super(...arguments);this.objects=[];this.showArchived=!1;this.currencySymbol="";this._query="";this._sort=E(S(_.areaSort));this._histories=new O(this);this._localeReady=!1;this._memo=null}createRenderRoot(){return this}get _lang(){return g(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("objects")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let o=this.objects.filter(s=>this.showArchived||!s.object.archived).flatMap(s=>s.tasks.map(n=>({entryId:s.entry_id,task:n})));this._histories.sync(this.hass,o)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,$(this._lang).then(()=>this.requestUpdate()))}_rows(t){let o=this._lang,s=[this.objects,this.showArchived,this._histories.version,this.hass?.areas,o,t];if(this._memo&&this._memo.deps.every((c,h)=>c===s[h]))return this._memo.rows;let n=j(this.objects,{showArchived:this.showArchived,areas:this.hass?.areas,noAreaLabel:r("no_area",o),year:t,historyOf:(c,h)=>this._histories.historyOf(c,h)});return this._memo={deps:s,rows:n},n}_setSort(t){this._sort=L(this._sort,t),A(_.areaSort,`${this._sort.key}:${this._sort.dir}`)}_open(t){this.dispatchEvent(new CustomEvent("open-area",{detail:{areaKey:t},bubbles:!0,composed:!0}))}_toggleArchived(){this.dispatchEvent(new CustomEvent("archived-toggle",{bubbles:!0,composed:!0}))}_headerLabel(t,o,s){return t==="area_cost_year"?r("area_cost_year",s).replace("{year}",String(o)):r(t,s)}render(){if(!this.hass)return m;let t=this._lang,o=Number(f().slice(0,4)),s=this.currencySymbol,n=this._rows(o),c=R(C(n,this._query),this._sort),h=this.objects.filter(e=>e.object.archived).length,d=e=>k(e,t);return a`
|
||||
<div class="filter-bar area-filter-bar">
|
||||
<input
|
||||
type="search"
|
||||
class="search-input"
|
||||
aria-label=${r("areas_filter",t)}
|
||||
placeholder=${r("areas_filter",t)}
|
||||
.value=${this._query}
|
||||
@input=${e=>{this._query=e.target.value}}
|
||||
/>
|
||||
${this._histories.loading?a`<span class="area-loading">${r("loading",t)}</span>`:m}
|
||||
${h>0?a`<ha-button class="archived-toggle ${this.showArchived?"active":""}" @click=${()=>this._toggleArchived()}>
|
||||
<ha-icon icon="mdi:archive-outline"></ha-icon>
|
||||
${this.showArchived?r("hide_archived",t):`${r("show_archived",t)} (${h})`}
|
||||
</ha-button>`:m}
|
||||
</div>
|
||||
${n.length===0?a`<div class="empty-state">${r("no_objects",t)}</div>`:c.length===0?a`<div class="empty-state">${r("areas_filter_none",t)}</div>`:a`
|
||||
<div class="objects-table-wrap">
|
||||
<table class="objects-table areas-table">
|
||||
<thead>
|
||||
<tr>
|
||||
${q.map(e=>{let u=this._sort.key===e.key;return a`<th class=${e.num?"num":""}
|
||||
aria-sort=${u?this._sort.dir==="asc"?"ascending":"descending":"none"}>
|
||||
<button class="area-sort ${u?"active":""}" title=${r("sort_label",t)} @click=${()=>this._setSort(e.key)}>
|
||||
${this._headerLabel(e.label,o,t)}
|
||||
${u?a`<ha-icon icon=${this._sort.dir==="asc"?"mdi:arrow-up":"mdi:arrow-down"}></ha-icon>`:m}
|
||||
</button>
|
||||
</th>`})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${c.map(e=>a`
|
||||
<tr class="objects-table-row" tabindex="0"
|
||||
@click=${()=>this._open(e.key)}
|
||||
@keydown=${u=>{u.key==="Enter"&&this._open(e.key)}}>
|
||||
<td>
|
||||
<ha-icon class="area-row-icon" .icon=${e.icon}></ha-icon>
|
||||
<span class="objects-table-name">${e.name}</span>
|
||||
</td>
|
||||
<td class="num">${d(e.objects)}</td>
|
||||
<td class="num">${d(e.tasks)}</td>
|
||||
<td class="num">${e.overdue?a`<span class="area-count overdue">${d(e.overdue)}</span>`:d(0)}</td>
|
||||
<td class="num">${e.dueSoon?a`<span class="area-count due-soon">${d(e.dueSoon)}</span>`:d(0)}</td>
|
||||
<td class="num">${v(e.costYear,s,t)}</td>
|
||||
<td class="num">${v(e.costTotal,s,t)}</td>
|
||||
<td>${e.lastCompletion?w(e.lastCompletion,t):"\u2014"}</td>
|
||||
</tr>
|
||||
`)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`}
|
||||
`}};l([p({attribute:!1})],i.prototype,"hass",2),l([p({attribute:!1})],i.prototype,"objects",2),l([p({type:Boolean})],i.prototype,"showArchived",2),l([p()],i.prototype,"currencySymbol",2),l([b()],i.prototype,"_query",2),l([b()],i.prototype,"_sort",2);customElements.get("maintenance-areas-view")||customElements.define("maintenance-areas-view",i);export{i as MaintenanceAreasView};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ISGNEPYP.js";import{H as u,a as n,b as _,c as o,f as p,h as c,l as g,m as a,s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var d=["round_robin","least_completed","random"],v=["low","normal","high"],m=365,i=class extends c{constructor(){super(...arguments);this._open=!1;this._mode="assign";this._count=0;this._users=[];this._present=[];this._assignKind="one";this._person="";this._pool=new Set;this._rotation=d[0];this._addText="";this._remove=new Set;this._setWarning=!1;this._warning=7;this._setPriority=!1;this._priority="normal";this._setMute=!1;this._mute=!1;this._resolve=null}get _lang(){return u(this.hass)}open(t,e,r){return this._resolve?.(null),this._mode=t,this._count=e.length,this._users=[...r].sort((l,h)=>l.name.localeCompare(h.name)),this._present=[...new Set(e.flatMap(l=>l.labels||[]))].sort((l,h)=>l.localeCompare(h)),this._assignKind="one",this._person="",this._pool=new Set,this._rotation=d[0],this._addText="",this._remove=new Set,this._setWarning=!1,this._warning=7,this._setPriority=!1,this._priority="normal",this._setMute=!1,this._mute=!1,this._open=!0,new Promise(l=>{this._resolve=l})}_finish(t){this._open=!1;let e=this._resolve;this._resolve=null,e?.(t)}changes(){if(this._mode==="assign"){if(this._assignKind==="one")return{responsible_user_id:this._person||null,assignee_pool:null,rotation_strategy:null};let e=this._users.map(r=>r.id).filter(r=>this._pool.has(r));return e.length<2?null:{assignee_pool:e,rotation_strategy:this._rotation,responsible_user_id:null}}if(this._mode==="labels"){let e=[...new Set(this._addText.split(",").map(l=>l.trim()).filter(Boolean))],r=[...this._remove];return e.length||r.length?{labels_add:e,labels_remove:r}:null}let t={};return this._setWarning&&(t.warning_days=this._warning),this._setPriority&&(t.priority=this._priority),this._setMute&&(t.notify_enabled=!this._mute),Object.keys(t).length?t:null}_toggle(t,e){let r=new Set(t);return r.has(e)?r.delete(e):r.add(e),r}_renderAssign(t){return o`
|
||||
<div class="hint">${s("bulk_assign_hint",t)}</div>
|
||||
<label class="choice">
|
||||
<input type="radio" name="kind" .checked=${this._assignKind==="one"} @change=${()=>this._assignKind="one"} />
|
||||
${s("bulk_assign_one",t)}
|
||||
</label>
|
||||
${this._assignKind==="one"?o`<select class="person" .value=${this._person} @change=${e=>this._person=e.target.value}>
|
||||
<option value="" ?selected=${!this._person}>${s("unassigned",t)}</option>
|
||||
${this._users.map(e=>o`<option value=${e.id} ?selected=${this._person===e.id}>${e.name}</option>`)}
|
||||
</select>`:p}
|
||||
<label class="choice">
|
||||
<input type="radio" name="kind" .checked=${this._assignKind==="rotate"} @change=${()=>this._assignKind="rotate"} />
|
||||
${s("bulk_assign_rotate",t)}
|
||||
</label>
|
||||
${this._assignKind==="rotate"?o`<div class="pool">
|
||||
${this._users.map(e=>o`<label class="row">
|
||||
<input type="checkbox" .checked=${this._pool.has(e.id)} @change=${()=>this._pool=this._toggle(this._pool,e.id)} />
|
||||
${e.name}
|
||||
</label>`)}
|
||||
</div>
|
||||
<label class="field">
|
||||
<span>${s("rotation_strategy",t)}</span>
|
||||
<select class="rotation" .value=${this._rotation} @change=${e=>this._rotation=e.target.value}>
|
||||
${d.map(e=>o`<option value=${e} ?selected=${this._rotation===e}>${s(`rotation_${e}`,t)}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
${this._pool.size<2?o`<div class="hint">${s("bulk_rotation_min_two",t)}</div>`:p}`:p}
|
||||
`}_renderLabels(t){return o`
|
||||
<ms-textfield
|
||||
class="labels-add"
|
||||
label=${s("bulk_labels_add",t)}
|
||||
.helper=${s("labels_help",t)}
|
||||
.value=${this._addText}
|
||||
@input=${e=>this._addText=e.target.value}
|
||||
></ms-textfield>
|
||||
${this._present.length?o`<div class="section-title">${s("bulk_labels_remove",t)}</div>
|
||||
<div class="pool">
|
||||
${this._present.map(e=>o`<label class="row">
|
||||
<input type="checkbox" .checked=${this._remove.has(e)} @change=${()=>this._remove=this._toggle(this._remove,e)} />
|
||||
${e}
|
||||
</label>`)}
|
||||
</div>`:p}
|
||||
`}_renderEdit(t){return o`
|
||||
<div class="hint">${s("bulk_edit_hint",t)}</div>
|
||||
<div class="row edit-row">
|
||||
<input type="checkbox" class="set-warning" .checked=${this._setWarning} @change=${e=>this._setWarning=e.target.checked} />
|
||||
<span class="grow">${s("warning_days",t)}</span>
|
||||
<input type="number" class="warning" min="0" max=${m} .value=${String(this._warning)} ?disabled=${!this._setWarning}
|
||||
@change=${e=>{let r=parseInt(e.target.value,10);this._warning=Number.isInteger(r)?Math.min(Math.max(r,0),m):this._warning}} />
|
||||
</div>
|
||||
<div class="row edit-row">
|
||||
<input type="checkbox" class="set-priority" .checked=${this._setPriority} @change=${e=>this._setPriority=e.target.checked} />
|
||||
<span class="grow">${s("priority",t)}</span>
|
||||
<select class="priority" .value=${this._priority} ?disabled=${!this._setPriority} @change=${e=>this._priority=e.target.value}>
|
||||
${v.map(e=>o`<option value=${e} ?selected=${this._priority===e}>${s(`priority_${e}`,t)}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="row edit-row">
|
||||
<input type="checkbox" class="set-mute" .checked=${this._setMute} @change=${e=>this._setMute=e.target.checked} />
|
||||
<span class="grow">${s("bulk_no_notifications",t)}</span>
|
||||
<input type="checkbox" class="mute" .checked=${this._mute} ?disabled=${!this._setMute} @change=${e=>this._mute=e.target.checked} />
|
||||
</div>
|
||||
`}render(){if(!this._open)return o``;let t=this._lang,e=this._mode==="assign"?s("bulk_assign",t):this._mode==="labels"?s("labels",t):s("edit",t),r=this.changes()!==null;return o`
|
||||
<ha-dialog open @closed=${()=>this._finish(null)}>
|
||||
<div class="dialog-title">${e}</div>
|
||||
<div class="content">
|
||||
<div class="count">${s("bulk_n_selected",t).replace("{n}",String(this._count))}</div>
|
||||
${this._mode==="assign"?this._renderAssign(t):this._mode==="labels"?this._renderLabels(t):this._renderEdit(t)}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${()=>this._finish(null)}>${s("cancel",t)}</ha-button>
|
||||
<ha-button class="apply" .disabled=${!r} @click=${()=>this._finish(this.changes())}>${s("save",t)}</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};i.styles=_`
|
||||
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: min(360px, calc(100vw - 24px));
|
||||
max-width: 520px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.content { min-width: 0; max-width: none; max-height: none; }
|
||||
}
|
||||
.count, .hint { color: var(--secondary-text-color); font-size: 13px; }
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.choice, .row { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }
|
||||
.pool { display: flex; flex-direction: column; gap: 4px; padding-left: 26px; }
|
||||
.field { display: flex; align-items: center; gap: 8px; justify-content: space-between; font-size: 14px; }
|
||||
.grow { flex: 1; min-width: 0; }
|
||||
select, input[type="number"] {
|
||||
font: inherit;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
max-width: 100%;
|
||||
}
|
||||
input[type="number"] { width: 72px; }
|
||||
select.person { margin-left: 26px; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 16px; }
|
||||
`,n([g({attribute:!1})],i.prototype,"hass",2),n([a()],i.prototype,"_open",2),n([a()],i.prototype,"_mode",2),n([a()],i.prototype,"_count",2),n([a()],i.prototype,"_users",2),n([a()],i.prototype,"_present",2),n([a()],i.prototype,"_assignKind",2),n([a()],i.prototype,"_person",2),n([a()],i.prototype,"_pool",2),n([a()],i.prototype,"_rotation",2),n([a()],i.prototype,"_addText",2),n([a()],i.prototype,"_remove",2),n([a()],i.prototype,"_setWarning",2),n([a()],i.prototype,"_warning",2),n([a()],i.prototype,"_setPriority",2),n([a()],i.prototype,"_priority",2),n([a()],i.prototype,"_setMute",2),n([a()],i.prototype,"_mute",2);customElements.get("maintenance-bulk-edit-dialog")||customElements.define("maintenance-bulk-edit-dialog",i);export{i as MaintenanceBulkEditDialog};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
var E={\u00DF:"s","\u1E9E":"s",\u00E6:"a",\u0153:"o",\u00F8:"o",\u0111:"d",\u00F0:"d",\u00FE:"t",\u0142:"l",\u0131:"i"},d=10,R=7,g=4,h=2,S=5,x=3,_=/[a-z0-9]+/g;function u(r){let e="";for(let t of r.toLowerCase()){if(t.charCodeAt(0)<128&&t.length===1){e+=t;continue}let n=E[t];if(n){e+=n;continue}let o=t.normalize("NFKD")[0]||t;e+=o.charCodeAt(0)<128?o:t}return e}function m(r){return u(r).match(_)||[]}function C(r){return u(r).replace(/[^a-z0-9]+/g,"")}function b(r){return r.replace(/ue/g,"u").replace(/oe/g,"o").replace(/ae/g,"a").replace(/ss/g,"s")}function F(r){let e=[];for(let t of m(r)){let n=[t],o=b(t);o&&o!==t&&n.push(o),e.push(n)}return e}function p(r,e){if(r===e)return!0;let t=r.length,n=e.length;if(Math.abs(t-n)>1)return!1;if(t===n){let l=[];for(let i=0;i<t;i++)r[i]!==e[i]&&l.push(i);if(l.length===1)return!0;if(l.length===2&&l[1]===l[0]+1){let i=l[0];return r[i]===e[i+1]&&r[i+1]===e[i]}return!1}let[o,s]=t>n?[r,e]:[e,r],c=0,f=0,a=!1;for(;c<o.length&&f<s.length;)if(o[c]===s[f])c++,f++;else{if(a)return!1;a=!0,c++}return!0}function N(r,e){let t=0;for(let n of r)if(n){if(e===n)return d;if(e.startsWith(n)){t=Math.max(t,R);continue}if(n.length>=x&&e.includes(n)){t=Math.max(t,g);continue}n.length>=S&&t<h&&(p(n,e)||e.length>n.length&&p(n,e.slice(0,n.length)))&&(t=h)}return t}function I(r,e){let t=0;for(let n of m(e))if(t=Math.max(t,N(r,n)),t===d)return t;if(t<g){let n=C(e);for(let o of r)if(o.length>=x&&n.includes(o))return g}return t}function O(r,e){if(!r)return!1;let t=F(e);if(!t.length){let n=u(e.trim());return n.length>0&&u(r).includes(n)}return M(t,[{text:r,weight:1}])>0}function M(r,e){if(!r.length)return 0;let t=e.filter(o=>o.text),n=0;for(let o of r){let s=0;for(let c of t){let f=I(o,c.text)*c.weight;f>s&&(s=f)}if(s===0)return 0;n+=s}return n}export{F as a,O as b,M as c};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{$ as w,A as k,K as R,M as x,a as m,b as h,c as g,f as v,h as b,l as y,m as _,s as l}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";function E(i){return`${i.entry_id??""}\0${i.part_id}`}function S(i,t,e,o){let n=!!i.entry_id&&i.entry_id!==t,r=n?i.entry_id:t,s=e.find(p=>p.entry_id===r),a=(s?.parts||[]).find(p=>p.id===i.part_id)||null,c=n&&s?.object?.name||"",u=a?.name||l("shared_part_unknown",o);return{part:a,foreign:n,ownerName:c,label:c?`${u} (${c})`:u}}function N(i,t,e,o){let{part:n,label:r}=S(i,t,e,o),s=n&&n.stock!==null&&n.stock!==void 0?` (${x(n.stock,n.unit,o)})`:"",a=n?.storage_location?` \u2014 ${n.storage_location}`:"";return`${R(i.quantity,o)}\xD7 ${r}${s}${a}`}function V(i,t,e,o){let r=(e.find(a=>a.entry_id===t)?.parts||[]).map(a=>({...a})),s=new Set(r.map(a=>E({part_id:a.id})));for(let a of i?.consumes_parts||[]){if(!a.entry_id||a.entry_id===t)continue;let c=E(a);if(s.has(c))continue;s.add(c);let{part:u,ownerName:p}=S(a,t,e,o);r.push({id:a.part_id,name:u?.name||l("shared_part_unknown",o),unit:u?.unit,stock:u?.stock??null,storage_location:u?.storage_location,entry_id:a.entry_id,owner_name:p})}return r}var L=20;function T(){let i=new Uint8Array(4);return(globalThis.crypto??{getRandomValues:t=>t.map(()=>Math.floor(Math.random()*256))}).getRandomValues(i),Array.from(i,t=>t.toString(16).padStart(2,"0")).join("")}function f(i){let t=i?.reading_values;if(!Array.isArray(t))return[];let e=[],o=new Set;for(let n of t){if(!n||typeof n!="object")continue;let r=n;typeof r.id!="string"||!r.id||o.has(r.id)||typeof r.value!="number"||!Number.isFinite(r.value)||(o.add(r.id),e.push({id:r.id,name:typeof r.name=="string"?r.name:r.id,unit:typeof r.unit=="string"?r.unit:null,value:r.value}))}return e}function P(i){return i.filter(t=>t.type==="completed"&&(t.reading_value!=null||f(t).length>0)).sort((t,e)=>t.timestamp.localeCompare(e.timestamp))}function M(i){return P(i??[]).map(t=>({timestamp:t.timestamp,values:f(t)})).filter(t=>t.values.length>0)}function A(i,t,e){let o;for(let n of i){if(e!==void 0){let s=k(n.timestamp);if(!isNaN(s)&&s>=e)break}let r=n.values.find(s=>s.id===t);r&&(o=r)}return o}function C(i,t,e){let o=f(t).find(r=>r.id===e);if(!o)return null;let n=null;for(let r of P(i)){if(r.timestamp>=t.timestamp)break;let s=f(r).find(a=>a.id===e);s&&(n=s.value)}return n==null?null:o.value-n}function D(i){let t=new Set,e=new Set;for(let o of i){let n=(o.name||"").trim().toLowerCase();n&&(t.has(n)?e.add(o.id):t.add(n))}return e}function F(i){let t=[],e=new Set,o=new Set;for(let n of i){let r=(n.name||"").trim();if(!(!r||e.has(n.id)||o.has(r.toLowerCase()))&&(e.add(n.id),o.add(r.toLowerCase()),t.push({id:n.id,name:r,unit:(n.unit||"").trim()||null}),t.length>=L))break}return t}function j(i){let t=(i??"").trim();if(t==="")return null;let e=Number(t.replace(",","."));return!Number.isFinite(e)||e<0?null:Math.round(e)}var B=["notes","cost","duration","photo","user"],I={notes:"notes_label",cost:"cost",duration:"duration",photo:"photo_label",user:"user_label"};var d=class extends b{constructor(){super(...arguments);this.value="";this.lang="en";this._credit=!1;this._amount=""}willUpdate(e){if(!e.has("value"))return;let o=(this.value??"").trim();if(o===this._signed())return;let n=parseFloat(o);if(o===""||Number.isNaN(n)){this._amount="",this._credit=!1;return}this._credit=n<0,this._amount=String(Math.abs(n))}_signed(){let e=this._amount.trim();return e===""?"":this._credit?`-${e}`:e}_emit(){this.value=this._signed(),this.dispatchEvent(new CustomEvent("value-changed",{detail:{value:this.value},bubbles:!0,composed:!0}))}_setCredit(e){this._credit!==e&&(this._credit=e,this._emit())}_onInput(e){let o=e.target.value;o.startsWith("-")&&(this._credit=!0),this._amount=o.replace(/^-+/,""),this._emit()}render(){let e=this.lang;return g`
|
||||
<div class="wrap">
|
||||
<div class="sign" role="group">
|
||||
<button type="button" class="kind cost ${this._credit?"":"on"}" aria-pressed=${String(!this._credit)}
|
||||
@click=${()=>this._setCredit(!1)}>${l("cost",e)}</button>
|
||||
<button type="button" class="kind credit ${this._credit?"on":""}" aria-pressed=${String(this._credit)}
|
||||
@click=${()=>this._setCredit(!0)}>${l("cost_kind_credit",e)}</button>
|
||||
</div>
|
||||
<input type="number" step="0.01" min="0" inputmode="decimal" class="field-input amount"
|
||||
.value=${this._amount} @input=${this._onInput} />
|
||||
</div>
|
||||
${this._credit?g`<div class="hint">${l("cost_credit_hint",e)}</div>`:v}
|
||||
`}};d.styles=[w,h`
|
||||
:host { display: block; }
|
||||
.wrap { display: flex; gap: 8px; align-items: stretch; }
|
||||
.sign {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.kind {
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.kind.on { background: var(--primary-color); color: var(--text-primary-color, #fff); }
|
||||
.kind.credit.on { background: var(--success-color, #43a047); }
|
||||
.amount { flex: 1; min-width: 0; }
|
||||
.hint { margin-top: 4px; font-size: 12px; color: var(--secondary-text-color); }
|
||||
`],m([y()],d.prototype,"value",2),m([y()],d.prototype,"lang",2),m([_()],d.prototype,"_credit",2),m([_()],d.prototype,"_amount",2);customElements.get("ms-cost-input")||customElements.define("ms-cost-input",d);export{E as a,N as b,V as c,L as d,T as e,f,M as g,A as h,C as i,D as j,F as k,j as l,B as m,I as n};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{s as a}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var _=[{key:"name",labelKey:"name",required:!0},{key:"ref_no",labelKey:"ref_number"},{key:"manufacturer",labelKey:"manufacturer"},{key:"model",labelKey:"model"},{key:"serial_number",labelKey:"serial_number_label"},{key:"installation_date",labelKey:"installed"},{key:"warranty_expiry",labelKey:"warranty"},{key:"area_id",labelKey:"area"},{key:"documentation_url",labelKey:"documentation_url_label"},{key:"notes",labelKey:"object_notes_label"},{key:"task_count",labelKey:"tasks"},{key:"actions",labelKey:"actions"}],u=_.map(e=>e.key),i=["name","manufacturer","model","serial_number","installation_date","warranty_expiry","area_id","task_count","actions"];function g(e){if(!Array.isArray(e))return[...i];let n=new Set,r=[];for(let t of e)typeof t=="string"&&u.includes(t)&&!n.has(t)&&(n.add(t),r.push(t));return r.length?(r.includes("name")||r.unshift("name"),r):[...i]}function f(e,n){return a(`home_dwelling_${e==="house"||e==="apartment"?e:"unknown"}`,n)}function m(e,n){if(!e)return"";try{return new Intl.DisplayNames([n],{type:"region"}).of(e)||e}catch{return e}}var l={floors:"home_reason_floors",basement_floor:"home_reason_basement",area_basement:"home_reason_basement",area_garden:"home_reason_garden",area_shed:"home_reason_shed",area_driveway:"home_reason_driveway",area_garage:"home_reason_garage",entity_garage:"home_reason_garage",area_attic:"home_reason_attic",entity_lawn_mower:"home_reason_lawn_mower",entity_gate:"home_reason_gate",area_balcony:"home_reason_balcony",small_home:"home_reason_small_home"},c={ups:"home_reason_ups"};function d(e,n){let r=[];for(let t of e){let o=l[t];if(!o)continue;let s=a(o,n);r.includes(s)||r.push(s)}return r.join(", ")}function b(e,n,r){if(e==="starter")return a("home_reason_starter",n);if(e==="country")return a("home_reason_country",n).replace("{country}",m(r,n));if(e.startsWith("feature_")){let t=e.slice(8),o=l[`area_${t}`]??c[t];return a("home_reason_feature",n).replace("{feature}",o?a(o,n):e)}return a(`home_trait_${e}`,n)}export{_ as a,i as b,g as c,f as d,m as e,d as f,b as g};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,488 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{d as M,e as R}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-W3CDWY4U.js";import{a as L,b as z,c as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KKVMZFOV.js";import{c as U}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NEWWPCTD.js";import{a as I,h as H,l as V,n as B}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-5SEU5WUY.js";import{j as Q,k as w}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-QFIANZWP.js";import{a as j}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MN2RZ73S.js";import{$ as C,A,K as E,L as q,M as F,Q as N,a,b as x,c as l,f,h as k,l as h,m as _,s as o,z as P}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var T=class{constructor(p,e){this.host=p;this.opts=e;this.photos=[];this.uploadedIds=[];this.uploading=!1;this.errorKey="";this._session=0;this._uploadEntry=new Map;this.max=e.max??10,p.addController(this)}hostConnected(){}get ids(){return this.photos.map(p=>p.id)}get remaining(){return Math.max(this.max-this.photos.length,0)}get full(){return this.remaining===0}errorText(p){return this.errorKey?o(this.errorKey,p).replace("{max}",String(this.max)):""}clearError(){this.errorKey=""}reset(p=[]){this._endSession(),this._revokeAll(),this.photos=p.map(e=>({id:e,preview:""})),this.uploadedIds=[],this._uploadEntry.clear(),this.errorKey="",this.host.requestUpdate()}async addFiles(p){if(p.length===0)return;let e=this._session,i=this.opts.hass(),t=this.opts.entryId(),r=p.slice(0,this.remaining);this.uploading=!0,this.errorKey="",this.host.requestUpdate();try{for(let n of r){let s=await M(i,t,n);if(e!==this._session){R(i,t,[s]);return}this.uploadedIds=[...this.uploadedIds,s],this._uploadEntry.set(s,t),this.photos=[...this.photos,{id:s,preview:URL.createObjectURL(n)}],this.host.requestUpdate()}p.length>r.length&&(this.errorKey="photos_limit")}catch(n){if(e!==this._session)return;this.errorKey=n instanceof Error&&n.message==="doc_too_large"?"doc_too_large":"doc_upload_failed"}finally{e===this._session&&(this.uploading=!1,this.host.requestUpdate())}}remove(p){let e=this.photos.find(i=>i.id===p);e?.preview&&URL.revokeObjectURL(e.preview),this.photos=this.photos.filter(i=>i.id!==p),this.uploadedIds.includes(p)&&(this.uploadedIds=this.uploadedIds.filter(i=>i!==p),this._discard([p])),this.host.requestUpdate()}discardOrphans(){if(this._endSession(),this.uploadedIds.length===0)return;let p=this.uploadedIds;this.uploadedIds=[],this._discard(p)}markAttached(){this._endSession(),this.uploadedIds=[],this._uploadEntry.clear()}_endSession(){this._session++,this.uploading=!1}_discard(p){let e=new Map;for(let t of p){let r=this._uploadEntry.get(t)??this.opts.entryId();this._uploadEntry.delete(t),e.set(r,[...e.get(r)??[],t])}let i=this.opts.hass();for(let[t,r]of e)R(i,t,r)}_revokeAll(){for(let p of this.photos)p.preview&&URL.revokeObjectURL(p.preview)}};function S(){if(window.externalApp!==void 0)return!0;let p=typeof navigator<"u"&&navigator.userAgent||"";return/Home Assistant\//.test(p)&&/Android/.test(p)}function W(){if(!S())return!1;let g=typeof navigator<"u"?navigator.mediaDevices:void 0;return!!g&&typeof g.getUserMedia=="function"}var te=1920,ie=.88,se={retryMs:500,windowMs:5e3},re=new Set(["NotReadableError","AbortError","TrackStartError"]),K=80,J=/back|rear|environment|rück|hinten/i,G=/front|user|selfie|vorne/i;function Y(g){let p=/camera2?\s*(\d+)/i.exec(g);return p?Number(p[1]):Number.MAX_SAFE_INTEGER}function D(g,p){if(p){let i=g.find(t=>t.label===p);if(i)return i}return g.filter(i=>i.label&&J.test(i.label)).sort((i,t)=>Y(i.label)-Y(t.label))[0]??null}var X=g=>g?.name||"Error",ae=g=>g?.message||"",b=class extends k{constructor(){super(...arguments);this.lang="en";this._open=!1;this._busy=!1;this._switching=!1;this._devices=[];this._deviceIndex=-1;this._switchFailed=!1;this._copied=!1;this.timing={...se};this._facing="environment";this._stream=null;this._opening=!1;this._gen=0;this._refused=new Set;this._log=[];this._t0=0;this._lastError=""}async open(){if(this._open||this._opening)return;this._opening=!0;let e=++this._gen;try{await this._acquireAndShow(e)}finally{this._opening=!1}}_live(e){return e===this._gen&&this.isConnected}async _acquireAndShow(e){let i=typeof navigator<"u"?navigator.mediaDevices:void 0;if(!i||typeof i.getUserMedia!="function"){this._unavailable("no_media_devices");return}this._log=[],this._t0=Date.now(),this._lastError="",this._refused.clear(),this._switchFailed=!1,this._copied=!1,this._note(navigator.userAgent);let t=z(L.cameraLabel),r=await this._enumerate(i);if(!this._live(e))return;let n=D(r,t),s="refused";if(n&&(s=await this._openPatiently(i,e,this._byDevice(n))),s==="cancelled")return;if(s!=="ok"){if(s=await this._openPatiently(i,e,{video:{facingMode:{ideal:"environment"}},what:"facing environment"}),s==="cancelled")return;if(s!=="ok"){this._unavailable(this._lastError||"camera_unavailable");return}if(r=await this._enumerate(i),!this._live(e)){this._stopStream();return}}if(this._devLine truncated
|
||||
`),i=!1;try{navigator.clipboard?.writeText&&(await navigator.clipboard.writeText(e),i=!0)}catch{}if(!i){let t=this.shadowRoot?.querySelector(".details textarea");if(t){t.focus(),t.select();try{i=document.execCommand("copy")}catch{i=!1}}}this._copied=i}async _shoot(){let e=this._video;if(!(!e||this._busy||this._switching)){this._busy=!0;try{let i=e.videoWidth||640,t=e.videoHeight||480,r=Math.min(1,te/Math.max(i,t)),n=document.createElement("canvas");n.width=Math.round(i*r),n.height=Math.round(t*r);let s=n.getContext("2d");if(!s)throw new Error("no_canvas");s.drawImage(e,0,0,n.width,n.height);let d=await new Promise(v=>n.toBlob(v,"image/jpeg",ie));if(!d)throw new Error("no_blob");let m=new Date().toISOString().replace(/[-:]/g,"").slice(0,15),u=new File([d],`photo-${m}.jpg`,{type:"image/jpeg"});this.close(),this.dispatchEvent(new CustomEvent("photo-captured",{detail:{file:u},bubbles:!0,composed:!0}))}catch(i){this._unavailable(i instanceof Error?i.message:String(i))}finally{this._busy=!1}}}render(){if(!this._open)return f;let e=this.lang,i=this._busy||this._switching;return l`
|
||||
<div class="overlay" role="dialog" aria-modal="true" aria-label=${o("doc_camera",e)}>
|
||||
<video autoplay playsinline muted></video>
|
||||
${this._switching?l`<div class="switching" role="status">${o("camera_switching",e)}</div>`:f}
|
||||
${this._switchFailed?l`<div class="switch-note" role="status">
|
||||
<div>${o("camera_not_answered",e)}</div>
|
||||
<details class="details">
|
||||
<summary>${o("camera_diagnostics",e)}</summary>
|
||||
<textarea readonly rows="6" .value=${this._log.join(`
|
||||
`)}></textarea>
|
||||
<button type="button" class="copy" @click=${this._copyLog}>${o(this._copied?"camera_copied":"camera_copy",e)}</button>
|
||||
</details>
|
||||
</div>`:f}
|
||||
<div class="bar">
|
||||
<button type="button" class="cancel" @click=${this.close}>${o("cancel",e)}</button>
|
||||
${this._devices.length>1?l`<button type="button" class="switch" ?disabled=${i} title=${o("camera_switch_lens",e)} aria-label=${o("camera_switch_lens",e)} @click=${this._switchCamera}>
|
||||
<ha-icon icon="mdi:camera-flip-outline"></ha-icon>
|
||||
<span class="switch-pos">${this._deviceIndex>=0?`${this._deviceIndex+1}/${this._devices.length}`:`?/${this._devices.length}`}</span>
|
||||
</button>`:f}
|
||||
<button type="button" class="shoot" ?disabled=${i} @click=${this._shoot}>
|
||||
<ha-icon icon="mdi:camera"></ha-icon><span>${o("camera_capture_shoot",e)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`}};b.styles=x`
|
||||
:host { display: contents; }
|
||||
.overlay {
|
||||
position: fixed; inset: 0; z-index: 10000;
|
||||
background: #000; color: #fff;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
video { flex: 1; min-height: 0; width: 100%; object-fit: contain; background: #000; }
|
||||
.bar {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 16px;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
button {
|
||||
font: inherit; border-radius: 24px; padding: 10px 18px; cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.cancel { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.6); }
|
||||
.switch { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.6); padding: 10px 12px; }
|
||||
.switch[disabled] { opacity: 0.5; cursor: default; }
|
||||
.switch ha-icon { --mdc-icon-size: 22px; }
|
||||
.switch-pos { font-size: 12px; opacity: 0.85; }
|
||||
.switching {
|
||||
position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%);
|
||||
padding: 8px 16px; border-radius: 16px; background: rgba(0, 0, 0, 0.6); font-size: 14px;
|
||||
}
|
||||
.switch-note {
|
||||
position: absolute; left: 12px; right: 12px; bottom: 84px; max-height: 60%; overflow: auto;
|
||||
text-align: center; color: #fff; font-size: 13px; text-shadow: 0 1px 2px #000;
|
||||
}
|
||||
.details { margin-top: 6px; text-align: start; text-shadow: none; }
|
||||
.details summary { cursor: pointer; text-align: center; opacity: 0.85; }
|
||||
.details textarea {
|
||||
display: block; box-sizing: border-box; width: 100%; margin: 6px 0;
|
||||
font: 11px/1.35 monospace; color: #fff; background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 6px; padding: 6px; resize: vertical;
|
||||
}
|
||||
.copy { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.6); padding: 6px 14px; font-size: 13px; }
|
||||
.shoot { background: var(--primary-color, #03a9f4); color: #fff; border: none; font-weight: 600; }
|
||||
.shoot[disabled] { opacity: 0.6; cursor: default; }
|
||||
.shoot ha-icon { --mdc-icon-size: 22px; }
|
||||
`,a([h({type:String})],b.prototype,"lang",2),a([_()],b.prototype,"_open",2),a([_()],b.prototype,"_busy",2),a([_()],b.prototype,"_switching",2),a([_()],b.prototype,"_devices",2),a([_()],b.prototype,"_deviceIndex",2),a([_()],b.prototype,"_switchFailed",2),a([_()],b.prototype,"_copied",2);customElements.get("ms-camera-capture")||customElements.define("ms-camera-capture",b);var Z=x`
|
||||
ms-photo-picker { display: contents; }
|
||||
.photo-pickers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.photo-pick {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
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:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
||||
.photo-pick.disabled { opacity: 0.6; cursor: default; }
|
||||
.photo-pick input[type="file"] { display: none; }
|
||||
.photo-android-hint {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: 4px;
|
||||
}
|
||||
`,y=class extends k{constructor(){super(...arguments);this.lang="en";this.disabled=!1;this.busy=!1;this.accept="image/*";this.showCamera=!0;this.showGallery=!0;this.compact=!1;this.remaining=1/0;this._singlePick=S();this._inAppCamera=W()}createRenderRoot(){return this}get _locked(){return this.disabled||this.busy}_emit(e){e.length!==0&&this.dispatchEvent(new CustomEvent("files-picked",{detail:{files:e},bubbles:!0,composed:!0}))}_onInput(e){let i=e.target,t=Array.from(i.files??[]);i.value="",this._emit(t)}_onCameraClick(e){!this._inAppCamera||this._locked||(e.preventDefault(),this.querySelector("ms-camera-capture")?.open())}_onCameraUnavailable(){this._inAppCamera=!1,this.querySelector(".photo-pick-camera input")?.click()}_onKeydown(e){e.key!=="Enter"&&e.key!==" "||(e.preventDefault(),!this._locked&&e.currentTarget.click())}render(){if(this.remaining<=0||!this.showCamera&&!this.showGallery)return f;let e=this.lang,i=this._locked?"disabled":"",t=this.busy?o("uploading",e):o("doc_camera",e),r=o(this._singlePick?"choose_photo":"choose_photos",e);return l`
|
||||
<div class="photo-pickers">
|
||||
${this.showCamera?l`<label class="photo-pick photo-pick-camera ${i}" role="button" tabindex="0"
|
||||
aria-label=${t} title=${this.compact?t:f}
|
||||
@click=${this._onCameraClick} @keydown=${this._onKeydown}>
|
||||
<ha-icon icon="mdi:camera"></ha-icon>
|
||||
${this.compact?f:l`<span>${t}</span>`}
|
||||
<input type="file" accept=${this.accept} capture="environment"
|
||||
?disabled=${this._locked} @change=${this._onInput} />
|
||||
</label>`:f}
|
||||
${this.showGallery?l`<label class="photo-pick photo-pick-gallery ${i}" role="button" tabindex="0"
|
||||
aria-label=${r} title=${this.compact?r:f}
|
||||
@keydown=${this._onKeydown}>
|
||||
<ha-icon icon="mdi:image-multiple"></ha-icon>
|
||||
${this.compact?f:l`<span>${r}</span>`}
|
||||
<input type="file" accept=${this.accept} ?multiple=${!this._singlePick}
|
||||
?disabled=${this._locked} @change=${this._onInput} />
|
||||
</label>`:f}
|
||||
</div>
|
||||
${this.showGallery&&this._singlePick?l`<div class="photo-android-hint">${o("photos_android_hint",e)}</div>`:f}
|
||||
${this.showCamera&&this._inAppCamera?l`<ms-camera-capture .lang=${e}
|
||||
@photo-captured=${n=>this._emit([n.detail.file])}
|
||||
@capture-unavailable=${this._onCameraUnavailable}></ms-camera-capture>`:f}
|
||||
`}};a([h({type:String})],y.prototype,"lang",2),a([h({type:Boolean})],y.prototype,"disabled",2),a([h({type:Boolean})],y.prototype,"busy",2),a([h({type:String})],y.prototype,"accept",2),a([h({type:Boolean})],y.prototype,"showCamera",2),a([h({type:Boolean})],y.prototype,"showGallery",2),a([h({type:Boolean})],y.prototype,"compact",2),a([h({type:Number})],y.prototype,"remaining",2),a([_()],y.prototype,"_inAppCamera",2);customElements.get("ms-photo-picker")||customElements.define("ms-photo-picker",y);var $=Q,c=class extends k{constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this.readings=[];this.readingHistory=[];this.restockDefault=null;this.restockUnitCost=null;this.restockPackage="";this.currencySymbol="";this.partsCostMode="purchase";this.parts=[];this.consumesParts=[];this.consumesInfo=[];this.requiredFields=[];this.phaseLabel="";this.requireTagScan=!1;this.viaTagScan=!1;this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photos=new T(this,{entryId:()=>this.entryId,hass:()=>this.hass});this._readingValue="";this._readingValues={};this._restockQty="";this._completedAt="";this._usedParts={};this._usedQtyText={};this.checklistPrefill={}}open(e={}){this._open||(this._open=!0,this.viaTagScan=!!e.viaTagScan,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState=Object.fromEntries(this.checklist.map((i,t)=>[String(t),!!this.checklistPrefill[i]]).filter(([,i])=>i)),this._feedback="needed",this._photos.reset(),this._readingValue="",this._readingValues={},this._restockQty=this.restockDefault!==null?String(this.restockDefault):"",this._completedAt="",this._usedParts=Object.fromEntries(this.consumesParts.map(i=>[I(i),{...i}])),this._usedQtyText={})}_rangeError(e,i){let t=this.lang;return o("settings_value_out_of_range",t).replace("{min}",E(e,t,{maximumFractionDigits:2})).replace("{max}",E(i,t,{maximumFractionDigits:2}))}_toggleCheck(e){let i=String(e);this._checklistState={...this._checklistState,[i]:!this._checklistState[i]}}_setFeedback(e){this._feedback=e}async _complete(){if(this._loading||this._photos.uploading)return;this._error="",this._photos.clearError();let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId},i=this._notes.trim();if(i&&(e.notes=i),this._cost){let s=parseFloat(this._cost);isNaN(s)||(e.cost=s)}let t=V(this._duration);if(t!==null&&(e.duration=t),this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photos.photos.length>0&&(e.photo_doc_ids=this._photos.ids),this.viaTagScan&&(e.via_tag_scan=!0),this._completedAt){if(A(this._completedAt)>Date.now()){this._error=o("completed_at_future_error",this.lang);return}e.completed_at=this._completedAt.length===16?`${this._completedAt}:00`:this._completedAt}if(this.readings.length>0){let s={};for(let d of this.readings){let m=(this._readingValues[d.id]??"").trim();if(m==="")continue;let u=parseFloat(m.replace(",","."));isNaN(u)||(s[d.id]=u)}Object.keys(s).length>0&&(e.reading_values=s)}else if(this._readingValue!==""){let s=parseFloat(this._readingValue);isNaN(s)||(e.reading_value=s)}if(this.restockDefault!==null&&this._restockQty.trim()!==""){let s=parseFloat(this._restockQty.replace(",","."));if(!Number.isFinite(s)||s<w[0]||s>w[1]){this._error=this._rangeError(...w);return}e.restock_quantity=s}if(this.parts.length>0){let s=[];for(let[d,m]of Object.entries(this._usedParts)){let u=this._usedQtyText[d],v=u===void 0?m.quantity:parseFloat(u.replace(",","."));if(!Number.isFinite(v)||v<$[0]||v>$[1]){this._error=this._rangeError(...$);return}s.push({...m,quantity:v})}e.used_parts=s.map(d=>d.entry_id?{part_id:d.part_id,quantity:d.quantity,entry_id:d.entry_id}:{part_id:d.part_id,quantity:d.quantity})}let r=await j(this,e,{busy:s=>{this._loading=s},lang:this.lang,fallbackKey:"save_error",onError:s=>{this._error=s}});if(r===void 0)return;this._photos.markAttached(),this._open=!1;let n={entryId:this.entryId,taskId:this.taskId,taskName:this.taskName,undo:r?.undo===!0};this.dispatchEvent(new CustomEvent("task-completed",{detail:n}))}_renderReadingField(e,i){let t=this._completedAt?A(this._completedAt):NaN,r=H(this.readingHistory,e.id,isNaN(t)?void 0:t),n=e.unit||this.readingUnit,s=(this._readingValues[e.id]??"").trim(),d=s===""?NaN:parseFloat(s.replace(",",".")),m=r!==void 0&&!isNaN(d)&&d<r.value,u=r!==void 0?E(r.value,i,{maximumFractionDigits:3}):"";return l`
|
||||
<label class="field reading-field">
|
||||
<span class="field-label">${e.name}${n?` (${n})`:""}</span>
|
||||
<input type="text" inputmode="decimal" class="field-input"
|
||||
placeholder=${r!==void 0?o("reading_last",i).replace("{value}",u):""}
|
||||
.value=${this._readingValues[e.id]??""}
|
||||
@input=${v=>{this._readingValues={...this._readingValues,[e.id]:v.target.value}}} />
|
||||
${m?l`<span class="reading-warn">${o("reading_below_last",i).replace("{value}",u)}</span>`:f}
|
||||
</label>`}get _missingRequired(){let e={notes:this._notes.trim()!=="",cost:this._cost.trim()!=="",duration:this._duration.trim()!=="",photo:this._photos.photos.length>0,user:!!this.hass?.user};return this.requiredFields.filter(i=>!e[i])}_demands(e){return this.requiredFields.includes(e)}_req(e){return this._demands(e)?l`<span class="req-mark" aria-hidden="true">*</span>`:f}_purchaseSuggestion(){if(this.restockDefault===null)return null;let e=parseFloat(this._restockQty);return this.restockUnitCost==null||!Number.isFinite(e)||e<=0?null:Math.round(this.restockUnitCost*e*100)/100}_partsValue(){if(this.restockDefault!==null||!this.parts.length)return null;let e=0,i=!1;for(let t of Object.values(this._usedParts)){let r=this.parts.find(s=>I({part_id:s.id,entry_id:s.entry_id})===I(t)),n=U(r);n!==null&&(e+=n*(t.quantity||1),i=!0)}return i?Math.round(e*100)/100:null}_renderCostSuggestion(e){let i=this._purchaseSuggestion();if(this.restockDefault!==null){let s=this.partsCostMode==="use"?l`<div class="cost-note">${o("cost_purchase_use_hint",e)}</div>`:f;if(this._cost.trim()!==""||i==null||i<=0)return s;let d=q(i,this.currencySymbol,e);return l`<button
|
||||
type="button"
|
||||
class="cost-suggestion"
|
||||
@click=${()=>this._cost=String(Math.round(i*100)/100)}
|
||||
>${o("cost_from_parts",e).replace("{amount}",d)}</button>${s}`}let t=this._partsValue();if(t==null||t<=0)return f;let r=q(t,this.currencySymbol,e),n=this.partsCostMode==="use"?"cost_parts_booked":"cost_parts_info";return l`<div class="cost-note">${o(n,e).replace("{amount}",r)}</div>`}_close(){this._open=!1,this._photos.discardOrphans()}_pickCompletedAt(){this._completedAt=`${P()}:00`}render(){if(!this._open)return l``;let e=this.lang||this.hass?.language||"en",i=this._error||this._photos.errorText(e);return l`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${o("complete_title",e)}${this.taskName}</div>
|
||||
${this.phaseLabel?l`<div class="phase-line">${o("phase_current",e)}: ${this.phaseLabel}</div>`:f}
|
||||
${this.requireTagScan&&!this.viaTagScan?l`<div class="scan-required-note">${o("require_tag_scan_hint",e)}</div>`:f}
|
||||
<div class="content">
|
||||
${i?l`<div class="error">${i}</div>`:f}
|
||||
${this.checklist.length>0?l`
|
||||
<div class="checklist-section">
|
||||
<label class="checklist-label">${o("checklist",e)}</label>
|
||||
${this.checklist.map((t,r)=>l`
|
||||
<label class="checklist-item" @click=${()=>this._toggleCheck(r)}>
|
||||
<input type="checkbox" .checked=${!!this._checklistState[String(r)]} />
|
||||
<span>${t}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
`:f}
|
||||
${this.readings.length>0?l`<div class="readings-block">
|
||||
<span class="field-label">${o("readings_section",e)}</span>
|
||||
${this.readings.map(t=>this._renderReadingField(t,e))}
|
||||
</div>`:this.taskType==="reading"?l`
|
||||
<label class="field">
|
||||
<span class="field-label">${o("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>`:f}
|
||||
${this.parts.length?l`<div class="used-parts">
|
||||
<span class="field-label">${o("complete_parts_used",e)}</span>
|
||||
${this.parts.map(t=>{let r=I({part_id:t.id,entry_id:t.entry_id}),n=this._usedParts[r],s=n!==void 0,d=t.entry_id?{part_id:t.id,quantity:1,entry_id:t.entry_id}:{part_id:t.id,quantity:1};return l`<div class="used-part-row">
|
||||
<label class="used-part-check">
|
||||
<input type="checkbox" .checked=${s}
|
||||
@change=${m=>{let u={...this._usedParts};if(m.target.checked)u[r]=u[r]||d;else{delete u[r];let v={...this._usedQtyText};delete v[r],this._usedQtyText=v}this._usedParts=u}} />
|
||||
<span
|
||||
>${t.name}${t.owner_name?l`<span class="used-part-owner"> (${t.owner_name})</span>`:f}${t.stock!==null&&t.stock!==void 0?` (${F(t.stock,t.unit,e)})`:""}</span
|
||||
>
|
||||
</label>
|
||||
${s?l`<input class="used-part-qty" type="number" min=${$[0]} max=${$[1]} step="0.01"
|
||||
.value=${this._usedQtyText[r]??String(n.quantity)}
|
||||
@input=${m=>{let u=m.target.value;this._usedQtyText={...this._usedQtyText,[r]:u};let v=parseFloat(u.replace(",","."));Number.isFinite(v)&&v>=$[0]&&(this._usedParts={...this._usedParts,[r]:{...d,quantity:v}})}} />`:f}
|
||||
</div>`})}
|
||||
</div>`:this.consumesInfo.length?l`<div class="consumes-hint">
|
||||
${this.consumesInfo.map(t=>l`<div>${t}</div>`)}
|
||||
</div>`:f}
|
||||
${this.restockDefault!==null?l`
|
||||
<label class="field">
|
||||
<span class="field-label">${o("restock_quantity_label",e)}${this.restockPackage?` (\xD7 ${this.restockPackage})`:""}</span>
|
||||
<input type="number" step="0.01" min=${w[0]} max=${w[1]} class="field-input"
|
||||
.value=${this._restockQty}
|
||||
@input=${t=>this._restockQty=t.target.value} />
|
||||
</label>`:f}
|
||||
<!-- 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">${this._demands("notes")?o("notes_label",e):o("notes_optional",e)}${this._req("notes")}</span>
|
||||
<!-- Several lines (#202): what was done, one line each. The
|
||||
history and the printouts keep the line breaks; they show
|
||||
notes as plain text, so no Markdown hint here. -->
|
||||
<textarea class="field-input notes-input" rows="3"
|
||||
.value=${this._notes}
|
||||
@input=${t=>this._notes=t.target.value}></textarea>
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">${this._demands("cost")?o("cost",e):o("cost_optional",e)}${this._req("cost")}</span>
|
||||
<ms-cost-input .lang=${e} .value=${this._cost}
|
||||
@value-changed=${t=>this._cost=t.detail.value}></ms-cost-input>
|
||||
${this._renderCostSuggestion(e)}
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">${this._demands("duration")?o("quick_complete_defaults_duration",e):o("duration_minutes",e)}${this._req("duration")}</span>
|
||||
<input type="number" step="1" min="0" inputmode="numeric" class="field-input"
|
||||
.value=${this._duration}
|
||||
@input=${t=>this._duration=t.target.value} />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">${o("completed_at_optional",e)}</span>
|
||||
${this._completedAt?l`<ms-date-field
|
||||
kind="datetime"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${e}
|
||||
.value=${this._completedAt}
|
||||
.helper=${N(e)}
|
||||
@value-changed=${t=>this._completedAt=t.detail.value}
|
||||
></ms-date-field>`:l`<button type="button" class="backdate-pick" @click=${this._pickCompletedAt}>
|
||||
<ha-icon icon="mdi:calendar-clock"></ha-icon>${o("completed_at_pick",e)}
|
||||
</button>`}
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">${this._demands("photo")?o("completion_photos",e):o("completion_photos_optional",e)}${this._req("photo")}</span>
|
||||
${this._photos.photos.length>0?l`<div class="photo-strip">
|
||||
${this._photos.photos.map(t=>l`
|
||||
<div class="photo-preview">
|
||||
<img src=${t.preview} alt="" />
|
||||
<button type="button" class="photo-remove" @click=${()=>this._photos.remove(t.id)}
|
||||
title="${o("remove",e)}">✕</button>
|
||||
</div>`)}
|
||||
</div>`:f}
|
||||
${this._photos.full?l`<div class="photo-limit">${o("photos_limit",e).replace("{max}",String(this._photos.max))}</div>`:l`<ms-photo-picker .lang=${e} .busy=${this._photos.uploading} .remaining=${this._photos.remaining}
|
||||
@files-picked=${t=>this._photos.addFiles(t.detail.files)}
|
||||
></ms-photo-picker>`}
|
||||
</div>
|
||||
${this.adaptiveEnabled?l`
|
||||
<div class="feedback-section">
|
||||
<label class="feedback-label">${o("was_maintenance_needed",e)}</label>
|
||||
<div class="feedback-buttons">
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="needed"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("needed")}
|
||||
>${o("feedback_needed",e)}</button>
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="not_needed"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("not_needed")}
|
||||
>${o("feedback_not_needed",e)}</button>
|
||||
<button
|
||||
class="feedback-btn ${this._feedback==="not_sure"?"selected":""}"
|
||||
@click=${()=>this._setFeedback("not_sure")}
|
||||
>${o("feedback_not_sure",e)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`:f}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${o("cancel",e)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._complete}
|
||||
.disabled=${this._loading||this._photos.uploading||this._missingRequired.length>0}
|
||||
title=${this._photos.uploading?o("uploading",e):this._missingRequired.length?this._missingRequired.map(t=>o("err_required",e).replace("{field}",o(B[t]??t,e))).join(" \xB7 "):""}
|
||||
>
|
||||
${this._loading?o("completing",e):o("complete",e)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};c.styles=[C,Z,x`
|
||||
.req-mark {
|
||||
color: var(--error-color, #f44336);
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.notes-input { resize: vertical; line-height: 1.4; }
|
||||
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
|
||||
.cost-note {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.scan-required-note {
|
||||
margin: -4px 0 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 152, 0, 0.12);
|
||||
color: var(--primary-text-color);
|
||||
font-size: 13px;
|
||||
}
|
||||
.phase-line {
|
||||
margin-top: -8px;
|
||||
padding-bottom: 12px;
|
||||
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 */
|
||||
/* #161 phase 2: the per-slot reading fields */
|
||||
.readings-block { display: flex; flex-direction: column; gap: 8px; }
|
||||
.readings-block > .field-label { margin-bottom: -4px; }
|
||||
.reading-warn {
|
||||
font-size: 12px;
|
||||
color: var(--warning-color, #ff9800);
|
||||
}
|
||||
/* .photo-pick / .photo-pickers / .photo-android-hint come from photoPickerStyles */
|
||||
/* #163: the backdate moment starts EMPTY (= now); the button seeds the
|
||||
HA date+time picker with the current minute instead of the picker's
|
||||
own 00:00 default, so a backdated completion never lands at midnight
|
||||
by accident. */
|
||||
.backdate-pick {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px dashed var(--divider-color);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
width: fit-content;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.backdate-pick:hover { border-color: var(--primary-color); }
|
||||
/* #161: several photos per completion — tiles wrap into a strip,
|
||||
the two pickers (camera / gallery) sit underneath while there is
|
||||
room left under the cap. */
|
||||
.photo-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 8px 0; /* room for the remove badges above the tiles */
|
||||
}
|
||||
.photo-limit {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.photo-preview {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
/* Uniform tiles: a tiny or portrait shot must not collapse the strip. */
|
||||
.photo-preview img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
}
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
`],a([h({attribute:!1})],c.prototype,"hass",2),a([h()],c.prototype,"entryId",2),a([h()],c.prototype,"taskId",2),a([h()],c.prototype,"taskName",2),a([h()],c.prototype,"lang",2),a([h({type:Array})],c.prototype,"checklist",2),a([h({type:Boolean})],c.prototype,"adaptiveEnabled",2),a([h()],c.prototype,"taskType",2),a([h()],c.prototype,"readingUnit",2),a([h({attribute:!1})],c.prototype,"readings",2),a([h({attribute:!1})],c.prototype,"readingHistory",2),a([h({attribute:!1})],c.prototype,"restockDefault",2),a([h({attribute:!1})],c.prototype,"restockUnitCost",2),a([h({attribute:!1})],c.prototype,"restockPackage",2),a([h()],c.prototype,"currencySymbol",2),a([h({attribute:!1})],c.prototype,"partsCostMode",2),a([h({attribute:!1})],c.prototype,"parts",2),a([h({attribute:!1})],c.prototype,"consumesParts",2),a([h({type:Array})],c.prototype,"consumesInfo",2),a([h({type:Array})],c.prototype,"requiredFields",2),a([h()],c.prototype,"phaseLabel",2),a([h({type:Boolean})],c.prototype,"requireTagScan",2),a([h({type:Boolean})],c.prototype,"viaTagScan",2),a([_()],c.prototype,"_open",2),a([_()],c.prototype,"_notes",2),a([_()],c.prototype,"_cost",2),a([_()],c.prototype,"_duration",2),a([_()],c.prototype,"_loading",2),a([_()],c.prototype,"_error",2),a([_()],c.prototype,"_checklistState",2),a([_()],c.prototype,"_feedback",2),a([_()],c.prototype,"_readingValue",2),a([_()],c.prototype,"_readingValues",2),a([_()],c.prototype,"_restockQty",2),a([_()],c.prototype,"_completedAt",2),a([_()],c.prototype,"_usedParts",2),a([_()],c.prototype,"_usedQtyText",2),a([h({attribute:!1})],c.prototype,"checklistPrefill",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",c);export{S as a,Z as b,T as c,c as d};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{b as s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-OUHDI2FL.js";async function a(o,n,t=300){return(await o.connection.sendMessagePromise({type:"auth/sign_path",path:n,expires:t})).path}async function r(o,n,t=300){return a(o,`/api/maintenance_supporter/document/${n}`,t)}async function p(o,n,t=""){let e=window.open("about:blank","_blank");try{let i=await r(o,n);e&&(e.location.href=new URL(i+t,window.location.origin).href)}catch(i){throw e&&e.close(),i}}async function l(o,n,t){s(await r(o,n,30),t)}function m(o,n=null){let t=URL.createObjectURL(new Blob([o],{type:"text/html"}));n&&!n.closed?n.location.href=t:window.open(t,"_blank"),setTimeout(()=>URL.revokeObjectURL(t),6e4)}function u(){return window.open("about:blank","_blank")}export{a,r as b,p as c,l as d,m as e,u as f};
|
||||
@@ -0,0 +1,234 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{a as q}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-OUHDI2FL.js";import{H as k,a,b as f,c as n,f as u,h as x,l as h,m as c,s as e}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";function d(r){return r.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function _(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":d(r)}function y(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var l=class extends x{constructor(){super(...arguments);this.lang="";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._quickResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}get _dialogLang(){return this.lang||k(this.hass)}openForObject(t,i){this._entryId=t,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}openForTask(t,i,s,o){this._entryId=t,this._taskId=i,this._objectName=s,this._taskName=o,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}async _generate(){let t=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let s=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&(s.push(this.hass.connection.sendMessagePromise({...i,action:"complete"})),await this._hasQuickCompleteDefaults()&&s.push(this.hass.connection.sendMessagePromise({...i,action:"quick_complete"})));let o=await Promise.all(s);if(t!==this._generateSeq)return;this._viewResult=o[0],o.length>1&&(this._completeResult=o[1]),o.length>2&&(this._quickResult=o[2])}catch(i){if(t!==this._generateSeq)return;let s=i?.code,o=i?.message;this._error=s==="no_url"||typeof o=="string"&&o.includes("No Home Assistant URL")?e("qr_error_no_url",this._dialogLang):e("qr_error",this._dialogLang)}finally{t===this._generateSeq&&(this._loading=!1)}}async _hasQuickCompleteDefaults(){try{let i=((await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId})).tasks||[]).find(s=>s.id===this._taskId);return!!i?.quick_complete_defaults&&Object.keys(i.quick_complete_defaults).length>0}catch{return!1}}_setUrlMode(t){this._urlMode!==t&&(this._urlMode=t,this._generate())}_print(){if(!this._viewResult)return;let t=this._viewResult,i=t.label.task_name?`${t.label.object_name} \u2014 ${t.label.task_name}`:t.label.object_name,s=[t.label.manufacturer,t.label.model].filter(Boolean).join(" "),o=window.open("","_blank","width=600,height=500");if(!o)return;let p=this._dialogLang,m=d(i),g=d(s),v=!!this._completeResult,b=!!this._quickResult,w=d(e("qr_action_view",p)),$=d(e("qr_action_complete",p)),R=d(e("qr_action_quick_complete",p));o.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${m}</title>
|
||||
<style>
|
||||
/* Printable sheet \u2014 must not inherit the phone's dark theme. The QR images
|
||||
carry their own white quiet zone and stay scannable either way, but the
|
||||
labels below are explicit dark greys and would vanish on a WebView's dark
|
||||
canvas. Same reasoning as helpers/report.ts. */
|
||||
:root{color-scheme:light}
|
||||
body{font-family:sans-serif;text-align:center;padding:20px;background:#fff;color:#1a1a1a}
|
||||
h2{margin:0 0 4px}
|
||||
.sub{color:#666;font-size:14px;margin-bottom:16px}
|
||||
.qr-row{display:flex;justify-content:center;gap:24px;margin:12px 0}
|
||||
.qr-col{display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||
.qr-col img{width:${b?"170px":v?"200px":"280px"}}
|
||||
.qr-label{font-size:13px;font-weight:500;color:#333}
|
||||
.url{font-size:10px;color:#999;word-break:break-all;margin-top:8px;max-width:480px}
|
||||
</style></head><body>
|
||||
<h2>${m}</h2>
|
||||
${g?`<div class="sub">${g}</div>`:""}
|
||||
<div class="qr-row">
|
||||
<div class="qr-col">
|
||||
<img src="${_(this._viewResult.svg_data_uri)}" alt="QR Info" />
|
||||
<div class="qr-label">${w}</div>
|
||||
</div>
|
||||
${v?`<div class="qr-col">
|
||||
<img src="${_(this._completeResult.svg_data_uri)}" alt="QR Complete" />
|
||||
<div class="qr-label">${$}</div>
|
||||
</div>`:""}
|
||||
${b?`<div class="qr-col">
|
||||
<img src="${_(this._quickResult.svg_data_uri)}" alt="QR Quick-complete" />
|
||||
<div class="qr-label">${R}</div>
|
||||
</div>`:""}
|
||||
</div>
|
||||
<div class="url">${d(this._viewResult.url)}</div>
|
||||
<script>setTimeout(()=>window.print(),300)<\/script>
|
||||
</body></html>`),o.document.close()}_downloadSvg(t,i){let s=decodeURIComponent(t.svg_data_uri.replace("data:image/svg+xml,","")),o=this._taskName?`${this._objectName}-${this._taskName}`:this._objectName;q(s,`qr-${y(o)}-${i}.svg`,"image/svg+xml")}_close(){this._open=!1,this._viewResult=null,this._completeResult=null,this._quickResult=null,this._error="",this._loading=!1}render(){if(!this._open)return n``;let t=this._dialogLang,i=this._taskName?`${e("qr_code",t)}: ${this._objectName} \u2014 ${this._taskName}`:`${e("qr_code",t)}: ${this._objectName}`,s=!!this._viewResult;return n`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${i}</div>
|
||||
<div class="content">
|
||||
${this._loading?n`<div class="loading">${e("qr_generating",t)}</div>`:this._error?n`<div class="error">${this._error}</div>`:s?n`
|
||||
<div class="qr-pair">
|
||||
<div class="qr-item">
|
||||
<img
|
||||
class="qr-image ${this._completeResult?"small":""}"
|
||||
src="${this._viewResult.svg_data_uri}"
|
||||
alt="QR Info"
|
||||
/>
|
||||
<div class="qr-item-label">${e("qr_action_view",t)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${()=>this._downloadSvg(this._viewResult,"info")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${e("qr_download",t)}
|
||||
</button>
|
||||
</div>
|
||||
${this._completeResult?n`
|
||||
<div class="qr-item">
|
||||
<img
|
||||
class="qr-image small"
|
||||
src="${this._completeResult.svg_data_uri}"
|
||||
alt="QR Complete"
|
||||
/>
|
||||
<div class="qr-item-label">${e("qr_action_complete",t)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${()=>this._downloadSvg(this._completeResult,"complete")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${e("qr_download",t)}
|
||||
</button>
|
||||
</div>
|
||||
`:u}
|
||||
${this._quickResult?n`
|
||||
<div class="qr-item quick">
|
||||
<img
|
||||
class="qr-image small"
|
||||
src="${this._quickResult.svg_data_uri}"
|
||||
alt="QR Quick-complete"
|
||||
/>
|
||||
<div class="qr-item-label">${e("qr_action_quick_complete",t)}</div>
|
||||
<button class="dl-btn"
|
||||
@click=${()=>this._downloadSvg(this._quickResult,"quick-complete")}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>
|
||||
${e("qr_download",t)}
|
||||
</button>
|
||||
</div>
|
||||
`:u}
|
||||
</div>
|
||||
<div class="url-display">${this._viewResult.url}</div>
|
||||
`:u}
|
||||
<div class="action-row">
|
||||
<label>${e("qr_url_mode",t)}</label>
|
||||
<div class="action-toggle">
|
||||
<button class="toggle-btn ${this._urlMode==="companion"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("companion")}>${e("qr_mode_companion",t)}</button>
|
||||
<button class="toggle-btn ${this._urlMode==="local"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("local")}>${e("qr_mode_local",t)}</button>
|
||||
<button class="toggle-btn ${this._urlMode==="server"?"active":""}"
|
||||
@click=${()=>this._setUrlMode("server")}>${e("qr_mode_server",t)}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${e("cancel",t)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._print}
|
||||
.disabled=${!s}
|
||||
>
|
||||
${e("qr_print",t)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};l.styles=f`
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.qr-pair {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* three codes (quick-complete, #192) wrap on a phone */
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.qr-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.qr-image {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.qr-image.small {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
.qr-item-label {
|
||||
max-width: 180px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
.dl-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color, #e0e0e0);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 6px 14px;
|
||||
border-radius: 18px;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
.dl-btn:hover {
|
||||
background: var(--secondary-background-color, #f5f5f5);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.dl-btn ha-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.url-display {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
.loading {
|
||||
padding: 40px 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.error {
|
||||
padding: 20px 0;
|
||||
color: var(--error-color, #f44336);
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
.action-row label {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.action-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--primary-text-color);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.toggle-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-primary-color, #fff);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
`,a([h({attribute:!1})],l.prototype,"hass",2),a([h()],l.prototype,"lang",2),a([c()],l.prototype,"_open",2),a([c()],l.prototype,"_loading",2),a([c()],l.prototype,"_error",2),a([c()],l.prototype,"_viewResult",2),a([c()],l.prototype,"_completeResult",2),a([c()],l.prototype,"_quickResult",2),a([c()],l.prototype,"_urlMode",2);customElements.get("maintenance-qr-dialog")||customElements.define("maintenance-qr-dialog",l);export{l as a};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
function r(t){return String(t??"").replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}export{r as a};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{c as r,f as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var a=["#c62828","#ad1457","#6a1b9a","#4527a0","#283593","#1565c0","#00838f","#2e7d32","#558b2f","#ef6c00","#6d4c41","#546e7a"];function u(e){let s=(e||"").split(/\s+/).filter(Boolean);return s.length===0?"?":s.length===1?s[0][0].toUpperCase():(s[0][0]+s[s.length-1][0]).toUpperCase()}function c(e){let s=0;for(let n of e)s=s*31+n.charCodeAt(0)>>>0;return a[s%a.length]}function o(e){return e?{id:e.id,name:e.name,initials:e.initials||u(e.name),color:e.color||c(e.id)}:null}function h(e){return e?r`<span class="person-avatar" style="--person-color: ${e.color}" title=${e.name}>${e.initials}</span>`:i}function m(e,s=""){return e?r`<span class="person-chip ${s}" title=${e.name}>${h(e)}<span class="person-name">${e.name}</span></span>`:i}var l=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let n=Date.now();if(!s&&this.usersCache&&n-this.cacheTimestamp<this.CACHE_TTL_MS)return this.usersCache;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/users/list"});return this.usersCache=t.users,this.cacheTimestamp=n,this.usersCache}catch(t){return console.error("Failed to fetch users:",t),this.usersCache||[]}}async assignUser(s,n,t){await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/assign_user",entry_id:s,task_id:n,user_id:t})}async getTasksByUser(s){return(await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tasks/by_user",user_id:s})).tasks}getUserName(s){return!s||!this.usersCache?null:this.usersCache.find(t=>t.id===s)?.name||null}getPerson(s){return o(this.getUser(s))}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(n=>n.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}};export{a,o as b,h as c,m as d,l as e};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{O as n,s as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var l={"Completed from dashboard button":"hist_note_button","Completed from the To-do list":"hist_note_todo","Completed by voice":"hist_note_voice","Completed from the notification":"hist_note_notification","Completed via NFC tag":"hist_note_nfc","Completed from the shopping list":"hist_note_shopping_list","Completed from a mirrored to-do list":"hist_note_todo_mirror","Skipped from notification":"hist_note_skipped_notification","Skipped from dashboard button":"hist_note_skipped_button","Sensor trigger activated":"hist_note_sensor_triggered","Initial value set during task creation":"hist_note_initial"},m=[[/^Auto-completed: sensor recovered \((.+)\)$/,"hist_note_auto_recovered",t=>({value:t[1]})],[/^Reset to (\d{4}-\d{2}-\d{2})$/,"hist_note_reset",(t,e)=>({date:n(t[1],e)})],[/^Trigger entity replaced: (.+) → (.+)$/,"hist_note_trigger_replaced",t=>({old:t[1],new:t[2]})],[/^Sensor trigger removed \(entity was: (.+)\)\. Schedule converted to (\w+)\.$/,"hist_note_trigger_removed",(t,e)=>({entity:t[1],schedule:o(t[2],e)})],[/^Entity (.+) removed from compound trigger; (\d+) conditions remain\.$/,"hist_note_compound_removed",t=>({entity:t[1],count:t[2]})]];function h(t,e){if(!t)return"";let i=l[t.trim()];if(i)return o(i,e);for(let[s,d,_]of m){let r=t.trim().match(s);if(r){let c=_(r,e);return o(d,e).replace(/\{(\w+)\}/g,(a,g)=>c[g]??a)}}return t}export{h as a};
|
||||
@@ -0,0 +1,79 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{a as r,b as l,c as i,f as o,h as n,l as t}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var e=class extends n{constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1;this.multiline=!1;this.rows=3}_onInput(s){let a=s.target.value;this.value=a,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:a}}))}render(){return i`
|
||||
<label class="field">
|
||||
${this.label?i`<span class="label">${this.label}${this.required?i`<span class="req">*</span>`:o}</span>`:o}
|
||||
${this.multiline?i`
|
||||
<textarea
|
||||
.value=${this.value??""}
|
||||
rows=${this.rows}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
></textarea>`:i`
|
||||
<input
|
||||
.value=${this.value??""}
|
||||
.type=${this.type}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
step=${this.step??o}
|
||||
min=${this.min??o}
|
||||
max=${this.max??o}
|
||||
pattern=${this.pattern??o}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onInput}
|
||||
/>`}
|
||||
${this.helper?i`<span class="helper">${this.helper}</span>`:o}
|
||||
</label>
|
||||
`}};e.styles=l`
|
||||
:host { display: block; }
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color, #888);
|
||||
font-weight: 500;
|
||||
}
|
||||
.req { color: var(--error-color, #f44336); margin-left: 2px; }
|
||||
input {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
input:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
textarea {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
}
|
||||
textarea:focus { border-color: var(--primary-color); }
|
||||
textarea:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
`,r([t()],e.prototype,"label",2),r([t()],e.prototype,"value",2),r([t()],e.prototype,"placeholder",2),r([t()],e.prototype,"type",2),r([t({type:Boolean})],e.prototype,"required",2),r([t({type:Boolean})],e.prototype,"disabled",2),r([t()],e.prototype,"step",2),r([t()],e.prototype,"min",2),r([t()],e.prototype,"max",2),r([t()],e.prototype,"pattern",2),r([t()],e.prototype,"helper",2),r([t({type:Boolean})],e.prototype,"multiline",2),r([t({type:Number})],e.prototype,"rows",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",e);
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
var r={objectSections:"msp-object-sections",printOptions:"msp-print-options",batteryRosterOpen:"msp-bf-roster-open",docSort:"msp-doc-sort",cameraLabel:"msp-camera-label",overviewTab:"msp-overview-tab",collapsedSections:"msp-collapsed-sections",chartRange:"msp-chart-range",chartHideOutliers:"msp-chart-hide-outliers",taskSort:"maintenance_supporter_sort",objectSort:"maintenance_supporter_object_sort",groupBy:"maintenance_supporter_groupby",objectView:"maintenance_supporter_object_view",objectsCache:"msp-objects-cache",gettingStartedDismissed:"msp-gs-dismissed",batteryRosterSort:"ms_bf_roster_sort",areaSort:"msp-area-sort"};function s(t){try{return localStorage.getItem(t)}catch{return null}}function o(t,e){try{localStorage.setItem(t,e)}catch{}}export{r as a,s as b,o as c};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NEWWPCTD.js";import{a as l}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-QFIANZWP.js";var a={operatorWriteEnabled:!1,operatorIds:[]};function m(e,t=a){return e?e.is_admin?!0:t.operatorWriteEnabled&&t.operatorIds.includes(e.id):!1}var _=["buttons_compact","buttons","icons"];function p(e){let[t,n]=l.default_warning_days,s=e?.general?.default_warning_days,r=e?.general?.row_action_style;return{features:{...o.features,...e?.features??{}},defaultWarningDays:typeof s=="number"&&Number.isFinite(s)&&s>=t&&s<=n?s:o.defaultWarningDays,rowActionStyle:_.includes(r??"")?r:o.rowActionStyle,access:{operatorWriteEnabled:e?.operator_write_enabled===!0,operatorIds:Array.isArray(e?.admin_panel_user_ids)?e.admin_panel_user_ids.filter(d=>typeof d=="string"):[]},budget:e?.budget??null,rowActionNoticePending:e?.general?.row_action_notice_pending===!0,refsInLists:e?.general?.ref_numbers_in_lists===!0,objectsTableColumns:Array.isArray(e?.objects_table_columns)?e.objects_table_columns:void 0,partsCostMode:i(e?.budget?.parts_cost_mode)}}var o={features:{adaptive:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1},defaultWarningDays:7,rowActionStyle:"buttons_compact",access:a,budget:null,rowActionNoticePending:!1,refsInLists:!1,objectsTableColumns:void 0,partsCostMode:"purchase"};function c(){let e=window;return e.__msSettingsCache??={promise:null}}function S(e){let t=c();if(t.promise)return t.promise;let n=e.connection.sendMessagePromise({type:"maintenance_supporter/settings"}).then(s=>p(s)).catch(()=>(t.promise===n&&(t.promise=null),o));return t.promise=n,n}function y(){c().promise=null}var u=class{constructor(){this._handle=null}schedule(t,n=4e3){this.clear(),this._handle=setTimeout(()=>{this._handle=null,t()},n)}clear(){this._handle!==null&&(clearTimeout(this._handle),this._handle=null)}get pending(){return this._handle!==null}};export{a,m as b,_ as c,p as d,S as e,y as f,u as g};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{H as p,r as g,s as o,v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var w={already_archived:"ws_err_already_archived",already_paused:"ws_err_already_paused",archived:"ws_err_archived",changed_since:"undo_changed",create_failed:"ws_err_create_failed",duplicate_failed:"ws_err_duplicate_failed",empty:"ws_err_empty",empty_csv:"ws_err_empty_csv",invalid_cursor:"ws_err_invalid_cursor",invalid_date:"ws_err_invalid_date",invalid_device:"ws_err_invalid_device",invalid_entity_slug:"ws_err_invalid_entity_slug",invalid_format:"ws_err_invalid_format",invalid_input:"ws_err_invalid_input",invalid_mirror_todo:"ws_err_invalid_mirror_todo",invalid_parent:"ws_err_invalid_parent",invalid_icon:"ws_err_invalid_icon",invalid_range:"ws_err_invalid_range",invalid_search_template:"ws_err_invalid_search_template",invalid_target:"ws_err_invalid_target",invalid_trigger_config:"ws_err_invalid_trigger_config",invalid_url:"ws_err_invalid_url",invalid_user:"ws_err_invalid_user",invalid_view:"ws_err_invalid_view",limit_reached:"ws_err_limit_reached",no_defaults:"ws_err_no_defaults",no_phases:"ws_err_no_phases",no_url:"ws_err_no_url",not_archived:"ws_err_not_archived",not_available:"ws_err_not_available",not_configured:"ws_err_not_configured",not_found:"ws_err_not_found",not_loaded:"ws_err_not_loaded",not_paused:"ws_err_not_paused",nothing_to_undo:"undo_nothing",replace_failed:"ws_err_replace_failed",self_link_device:"ws_err_self_link_device",storage_unavailable:"ws_err_storage_unavailable",too_early:"ws_err_too_early",too_large:"ws_err_too_large",too_many:"ws_err_too_many",too_many_views:"ws_err_too_many_views",unauthorized:"ws_err_unauthorized",unavailable:"ws_err_unavailable"},h={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",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 y(n){let t=n.replace(/_ids?$/,"").split("_").filter(Boolean);if(t.length===0)return n;let e=t.join(" ");return e.charAt(0).toUpperCase()+e.slice(1)}function b(n,t){let e=h[n];if(e){let r=o(e,t);if(r&&r!==e)return r}return y(n)}function k(n){let t=[...n.matchAll(/\['([^']+)'\]/g)].map(i=>i[1]),e=t.length?t[t.length-1]:void 0,r;return(r=n.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:r[1]}:(r=n.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:r[1]}:(r=n.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:r[1]}:(r=n.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:r[1]}:/required key not provided/.test(n)?{field:e,rule:"required"}:(r=n.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:r[1]}:/value must be one of/.test(n)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(n)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}var f=n=>n.toLowerCase().split(/[^a-z0-9]+/).filter(t=>t.length>1);function E(n,t){let e=new Set(f(t));return f(n).every(r=>e.has(r))}function c(n,t,e){if(e=e??o("action_error",t),typeof n=="string")return n;if(typeof n!="object"||n===null)return e;let r=n,i=r.message||r.error?.message||"",_=r.code||r.error?.code||"",l=_?w[_]:void 0,d=v(r.translation_domain??r.error?.translation_domain,r.translation_key??r.error?.translation_key,r.translation_placeholders??r.error?.translation_placeholders);if(d)return d;if(!i&&!l)return e;let s=k(i),m=s.field?b(s.field,t):"",a=u=>o(u,t).replace("{field}",m).replace("{n}",s.param??"");switch(s.rule){case"too_long":return a("err_too_long");case"too_short":return a("err_too_short");case"value_too_high":return a("err_value_too_high");case"value_too_low":return a("err_value_too_low");case"required":return a("err_required");case"wrong_type":return a("err_wrong_type").replace("{type}",s.param??"");case"invalid_choice":return a("err_invalid_choice");case"invalid_value":return a("err_invalid_value");default:if(l){let u=o(l,t);return g(t)==="en"&&i&&!E(i,_)?`${u} (${i})`:u}return i||e}}async function W(n,t,e={}){e.busy?.(!0);try{let r=typeof t=="function"?await t():await n.hass.coLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
function c(n){return n==="use"?"use":"purchase"}var s=n=>typeof n=="number"&&Number.isFinite(n)?n:null;function a(n){let t=s(n?.cost);if(t===null)return null;let o=s(n?.package_size);return o&&o>0?t/o:t}function p(n,t){let o=s(t)&&t>0?t:s(n.restock_quantity)||1,e=s(n.package_size);return Math.round(o*(e&&e>0?e:1)*100)/100}function l(n){let t=s(n.cost);if(n.cost_basis!=="use")return t;let o=s(n.parts_cost),e=n.purchase?null:t;return e===null&&o===null?null:(e??0)+(o??0)}function _(n){if(!n)return[];let t=[],o=n.photo_doc_id;typeof o=="string"&&t.push(o);let e=n.photo_doc_ids;Array.isArray(e)&&t.push(...e);let r=[];for(let u of t){if(typeof u!="string")continue;let i=u.trim();if(!(!i||r.includes(i))&&(r.push(i),r.length>=10))break}return r}export{_ as a,c as b,a as c,p as d,l as e};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.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};
|
||||
@@ -0,0 +1,217 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{a as _}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-MN2RZ73S.js";import{H as v,a as s,b as p,c as l,f as o,h,l as c,m as n,s as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var r=class extends h{constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null;this._replacing=!1;this._oldDeviceId="";this._replaceDevice="keep"}get _lang(){return v(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._replacing=!1,this._error="",this._open=!0}openReplace(e,a){this._entryId=e,this._replacing=!0,this._name=a.name||"",this._oldDeviceId=a.ha_device_id||"",this._haDeviceId="",this._replaceDevice="keep",this._error="",this._open=!0}openEdit(e,a){this._entryId=e,this._name=a.name||"",this._manufacturer=a.manufacturer||"",this._model=a.model||"",this._serialNumber=a.serial_number||"",this._areaId=a.area_id||"",this._installationDate=a.installation_date||"",this._warrantyExpiry=a.warranty_expiry||"",this._documentationUrl=a.documentation_url||"",this._notes=a.notes||"",this._haDeviceId=a.ha_device_id||"",this._parentEntryId=a.parent_entry_id||"",this._replacing=!1,this._error="",this._open=!0}async _replace(){if(this._loading||!this._entryId)return;let e={type:"maintenance_supporter/object/replace",entry_id:this._entryId,name:this._name.trim()||null};this._oldDeviceId?(this._replaceDevice==="other"&&(e.ha_device_id=this._haDeviceId),this._replaceDevice==="none"&&(e.ha_device_id=null)):this._haDeviceId&&(e.ha_device_id=this._haDeviceId);let a=await _(this,e,{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-replaced",{detail:{entry_id:a?.entry_id,device_swap:a?.device_swap}})))}_deviceName(e){let t=this.hass?.devices?.[e];return t?.name_by_user||t?.name||e}_deviceSelector(e){return l`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${{device:this._haDeviceId||void 0}}
|
||||
.schema=${[{name:"device",selector:{device:{}}}]}
|
||||
.computeLabel=${()=>e}
|
||||
@value-changed=${a=>this._haDeviceId=a.detail.value?.device||""}
|
||||
></ha-form>`}_renderReplace(){let e=this._lang,a=(d,u)=>l`<label class="radio-row">
|
||||
<input
|
||||
type="radio"
|
||||
name="replace-device"
|
||||
.checked=${this._replaceDevice===d}
|
||||
@change=${()=>{this._replaceDevice=d}}
|
||||
/>
|
||||
<span>${u}</span>
|
||||
</label>`,t=this._replaceDevice==="other"&&!this._haDeviceId;return l`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${i("replace_object",e)}</div>
|
||||
<div class="content">
|
||||
${this._error?l`<div class="error">${this._error}</div>`:o}
|
||||
<div class="hint">${i("replace_object_prompt",e)}</div>
|
||||
<ms-textfield
|
||||
label="${i("replace_name_label",e)}"
|
||||
.value=${this._name}
|
||||
@input=${d=>this._name=d.target.value}
|
||||
></ms-textfield>
|
||||
${this._oldDeviceId?l`<div class="radio-group" role="radiogroup" aria-label=${i("replace_device_heading",e)}>
|
||||
<div class="textarea-label">${i("replace_device_heading",e)}</div>
|
||||
${a("keep",i("replace_device_keep",e).replace("{device}",this._deviceName(this._oldDeviceId)))}
|
||||
${this._replaceDevice==="keep"?l`<div class="hint">${i("replace_device_keep_hint",e)}</div>`:o}
|
||||
${a("other",i("replace_device_other",e))}
|
||||
${this._replaceDevice==="other"?l`${this._deviceSelector(i("replace_device_pick",e))}
|
||||
<div class="hint">${i("replace_device_other_hint",e)}</div>`:o}
|
||||
${a("none",i("replace_device_none",e))}
|
||||
</div>`:this._deviceSelector(i("link_device_optional",e))}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${i("cancel",e)}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._replace} .disabled=${this._loading||t}>
|
||||
${this._loading?i("saving",e):i("replace_object",e)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}async _save(){if(this._loading||!this._name.trim())return;this._error="";let e={name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null},a=await _(this,this._entryId?{type:"maintenance_supporter/object/update",entry_id:this._entryId,...e}:{type:"maintenance_supporter/object/create",...e},{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-saved",{detail:{device_swap:a?.device_swap}})))}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return l``;if(this._replacing)return this._renderReplace();let e=this._lang,a=this._entryId?i("edit_object",e):i("new_object",e);return l`
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${a}</div>
|
||||
<div class="content">
|
||||
${this._error?l`<div class="error">${this._error}</div>`:o}
|
||||
<ms-textfield
|
||||
label="${i("name",e)}"
|
||||
required
|
||||
.value=${this._name}
|
||||
@input=${t=>this._name=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("manufacturer_optional",e)}"
|
||||
.value=${this._manufacturer}
|
||||
@input=${t=>this._manufacturer=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("model_optional",e)}"
|
||||
.value=${this._model}
|
||||
@input=${t=>this._model=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("serial_number_optional",e)}"
|
||||
.value=${this._serialNumber}
|
||||
@input=${t=>this._serialNumber=t.target.value}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${i("documentation_url_optional",e)}"
|
||||
type="url"
|
||||
.value=${this._documentationUrl}
|
||||
@input=${t=>this._documentationUrl=t.target.value}
|
||||
></ms-textfield>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
label="${i("area_id_optional",e)}"
|
||||
.value=${this._areaId}
|
||||
@value-changed=${t=>this._areaId=t.detail.value||""}
|
||||
></ha-area-picker>
|
||||
<ms-date-field
|
||||
kind="date"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${e}
|
||||
label="${i("installation_date_optional",e)}"
|
||||
.value=${this._installationDate}
|
||||
@value-changed=${t=>this._installationDate=t.detail.value}
|
||||
></ms-date-field>
|
||||
<ms-date-field
|
||||
kind="date"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${e}
|
||||
label="${i("warranty_expiry_optional",e)}"
|
||||
.value=${this._warrantyExpiry}
|
||||
@value-changed=${t=>this._warrantyExpiry=t.detail.value}
|
||||
></ms-date-field>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${{device:this._haDeviceId||void 0}}
|
||||
.schema=${[{name:"device",selector:{device:{}}}]}
|
||||
.computeLabel=${()=>i("link_device_optional",e)}
|
||||
@value-changed=${t=>this._haDeviceId=t.detail.value?.device||""}
|
||||
></ha-form>
|
||||
${this._parentChoices().length?l`<label class="textarea-field">
|
||||
<span class="textarea-label">${i("parent_object_optional",e)}</span>
|
||||
<select
|
||||
class="parent-select"
|
||||
.value=${this._parentEntryId}
|
||||
@change=${t=>this._parentEntryId=t.target.value}
|
||||
>
|
||||
<option value="" ?selected=${!this._parentEntryId}>
|
||||
${i("parent_none",e)}
|
||||
</option>
|
||||
${this._parentChoices().map(t=>l`<option
|
||||
value=${t.entry_id}
|
||||
?selected=${this._parentEntryId===t.entry_id}
|
||||
>${t.object.name}</option>`)}
|
||||
</select>
|
||||
</label>`:o}
|
||||
<label class="textarea-field">
|
||||
<span class="textarea-label">${i("object_notes_optional",e)}</span>
|
||||
<textarea
|
||||
rows="3"
|
||||
.value=${this._notes}
|
||||
@input=${t=>this._notes=t.target.value}
|
||||
></textarea>
|
||||
<span class="md-hint">${i("notes_markdown_hint",e)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<ha-button appearance="plain" @click=${this._close}>
|
||||
${i("cancel",this._lang)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${this._loading||!this._name.trim()}
|
||||
>
|
||||
${this._loading?i("saving",this._lang):i("save",this._lang)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`}};r.styles=p`
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
ms-textfield {
|
||||
display: block;
|
||||
}
|
||||
.textarea-field {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.textarea-label {
|
||||
font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500;
|
||||
}
|
||||
.textarea-field textarea {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
.textarea-field textarea:focus {
|
||||
outline: none; border-color: var(--primary-color);
|
||||
}
|
||||
.md-hint {
|
||||
font-size: 11px; color: var(--secondary-text-color); font-style: italic;
|
||||
}
|
||||
.parent-select {
|
||||
padding: 8px 10px; font-size: 14px; font-family: inherit;
|
||||
background: var(--secondary-background-color, rgba(0,0,0,0.06));
|
||||
color: var(--primary-text-color);
|
||||
border: 1px solid var(--divider-color); border-radius: 6px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color, #f44336);
|
||||
font-size: 13px;
|
||||
}
|
||||
.hint {
|
||||
font-size: 13px;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.radio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio-row input {
|
||||
margin: 0;
|
||||
}
|
||||
`,s([c({attribute:!1})],r.prototype,"hass",2),s([c({attribute:!1})],r.prototype,"objects",2),s([n()],r.prototype,"_open",2),s([n()],r.prototype,"_loading",2),s([n()],r.prototype,"_error",2),s([n()],r.prototype,"_name",2),s([n()],r.prototype,"_manufacturer",2),s([n()],r.prototype,"_model",2),s([n()],r.prototype,"_serialNumber",2),s([n()],r.prototype,"_areaId",2),s([n()],r.prototype,"_installationDate",2),s([n()],r.prototype,"_warrantyExpiry",2),s([n()],r.prototype,"_documentationUrl",2),s([n()],r.prototype,"_notes",2),s([n()],r.prototype,"_haDeviceId",2),s([n()],r.prototype,"_parentEntryId",2),s([n()],r.prototype,"_entryId",2),s([n()],r.prototype,"_replacing",2),s([n()],r.prototype,"_oldDeviceId",2),s([n()],r.prototype,"_replaceDevice",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",r);export{r as a};
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
var n={default_warning_days:[0,365],default_consumable_threshold:[1,90],battery_low_percent:[1,90],battery_recovered_percent:[20,100],archive_oneoff_days:[0,3650],delete_archived_oneoff_days:[0,3650],notify_due_soon_interval_hours:[0,720],notify_overdue_interval_hours:[0,720],notify_triggered_interval_hours:[0,720],max_notifications_per_day:[0,1e3],notification_bundle_threshold:[2,20],snooze_duration_hours:[1,168],warranty_reminder_days:[1,365],budget_alert_threshold:[10,100],currency_decimals:[0,3]};function o(e){let r=n[e];if(!r)throw new Error(`not an int-ranged setting: ${e}`);return r}var t=[0,14],_=[0,1440],u=[1,1e4],a=[0,365],d=15,s=[.1,.9],b=[0,365],A=[.01,1e4],E=[.01,1e5],i=[.1,5];export{n as a,o as b,t as c,_ as d,u as e,a as f,d as g,s as h,b as i,A as j,E as k,i as l};
|
||||
@@ -0,0 +1,88 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{O as m,a,b as y,c as s,f as l,h as g,l as o,m as f,s as d}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";function $(n){let t=m("2001-02-03",n).match(/\d+/g)||[],e=v=>t.findIndex(_=>Number(_)===v),h=e(2001),c=e(2),u=e(3);return h===0?"YMD":c>=0&&u>=0&&c<u?"MDY":"DMY"}function p(n,i,t){if(n<1e3||n>9999||i<1||i>12||t<1||t>31)return null;let e=new Date(n,i-1,t);return e.getFullYear()!==n||e.getMonth()!==i-1||e.getDate()!==t?null:`${n}-${String(i).padStart(2,"0")}-${String(t).padStart(2,"0")}`}function b(n,i){let t=n.trim(),e;if(e=t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/))return p(+e[1],+e[2],+e[3]);if(e=t.match(/^(\d{4})$/))return p(+e[1],1,1);if(e=t.match(/^(\d{1,2})[./](\d{4})$/))return p(+e[2],+e[1],1);if(e=t.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/))return p(+e[3],+e[2],+e[1]);if(e=t.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/)){let h=+e[1],c=+e[2],u=+e[3];return $(i)==="MDY"?p(u,h,c):p(u,c,h)}return null}function x(n,i){if(i)switch(n){case"date":return i.split("T")[0];case"time":return i.length===5?`${i}:00`:i;case"datetime":{let[t,e="00:00:00"]=i.split("T");return`${t} ${e.length===5?`${e}:00`:e}`}}}function k(n,i){if(typeof i!="string"||!i)return"";switch(n){case"date":return i.slice(0,10);case"time":return i.slice(0,5);case"datetime":{let[t,e="00:00:00"]=i.split(" ");return`${t}T${e.length===5?`${e}:00`:e}`}}}var r=class extends g{constructor(){super(...arguments);this.kind="date";this.label="";this.value="";this.clearable=!1;this.disabled=!1;this.required=!1;this.lang="en";this._typing=!1;this._typed="";this._typedInvalid=!1}_startTyping(){this._typed=this.value,this._typedInvalid=!1,this._typing=!0,this.updateComplete.then(()=>this.shadowRoot?.querySelector("input.typed")?.focus())}_commitTyped(){if(!this._typed.trim()){this._typing=!1;return}let t=b(this._typed,this.lang);if(!t){this._typedInvalid=!0;return}this._typing=!1,this._typedInvalid=!1,this._emit(t)}_onTypedKey(t){t.key==="Enter"?(t.preventDefault(),this._commitTyped()):t.key==="Escape"&&(t.preventDefault(),this._typing=!1,this._typedInvalid=!1)}_selector(){switch(this.kind){case"date":return{date:{}};case"time":return{time:{no_second:!0}};case"datetime":return{datetime:{}}}}_onSelectorChange(t){t.stopPropagation(),this._emit(k(this.kind,t.detail?.value))}_clear(){this._emit("")}_emit(t){t!==this.value&&(this.value=t,this.dispatchEvent(new CustomEvent("value-changed",{bubbles:!0,composed:!0,detail:{value:t}})))}render(){return s`
|
||||
<div class="field">
|
||||
${this.label?s`<span class="label">${this.label}${this.required?s`<span class="req">*</span>`:l}</span>`:l}
|
||||
<div class="row">
|
||||
${this._typing?s`<input class="typed" type="text" inputmode="numeric" autocomplete="off"
|
||||
.value=${this._typed}
|
||||
placeholder=${m("1978-03-15",this.lang)}
|
||||
aria-invalid=${this._typedInvalid?"true":"false"}
|
||||
@input=${t=>{this._typed=t.target.value,this._typedInvalid=!1}}
|
||||
@keydown=${this._onTypedKey}
|
||||
@blur=${this._commitTyped} />`:s`<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._selector()}
|
||||
.value=${x(this.kind,this.value)}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._onSelectorChange}
|
||||
></ha-selector>`}
|
||||
${this.kind==="date"&&!this.disabled&&!this._typing?s`<button type="button" class="type-toggle" title=${d("date_type_toggle",this.lang)} aria-label=${d("date_type_toggle",this.lang)} @click=${this._startTyping}>
|
||||
<ha-icon icon="mdi:keyboard-outline"></ha-icon>
|
||||
</button>`:l}
|
||||
${this.clearable&&this.value&&!this.disabled&&!this._typing?s`<button type="button" class="clear" title=${d("clear",this.lang)} aria-label=${d("clear",this.lang)} @click=${this._clear}>
|
||||
<ha-icon icon="mdi:close"></ha-icon>
|
||||
</button>`:l}
|
||||
</div>
|
||||
${this._typedInvalid?s`<span class="helper invalid">${d("date_type_invalid",this.lang)}</span>`:this.helper?s`<span class="helper">${this.helper}</span>`:l}
|
||||
</div>
|
||||
`}};r.styles=y`
|
||||
:host { display: block; }
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color, #888);
|
||||
font-weight: 500;
|
||||
}
|
||||
.req { color: var(--error-color, #f44336); margin-left: 2px; }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
ha-selector {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.clear, .type-toggle {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.clear:hover, .type-toggle:hover { background: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.08); }
|
||||
.clear:focus-visible, .type-toggle:focus-visible { outline: 2px solid var(--primary-color); }
|
||||
.helper {
|
||||
font-size: 11px;
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
.helper.invalid { color: var(--error-color, #f44336); font-style: normal; }
|
||||
input.typed {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--divider-color, #ccc);
|
||||
border-radius: 4px;
|
||||
background: var(--card-background-color, #fff);
|
||||
color: var(--primary-text-color);
|
||||
font: inherit;
|
||||
}
|
||||
input.typed[aria-invalid="true"] { border-color: var(--error-color, #f44336); }
|
||||
input.typed:focus { outline: 2px solid var(--primary-color); outline-offset: -1px; }
|
||||
`,a([o({attribute:!1})],r.prototype,"hass",2),a([o()],r.prototype,"kind",2),a([o()],r.prototype,"label",2),a([o()],r.prototype,"value",2),a([o()],r.prototype,"helper",2),a([o({type:Boolean})],r.prototype,"clearable",2),a([o({type:Boolean})],r.prototype,"disabled",2),a([o({type:Boolean})],r.prototype,"required",2),a([o()],r.prototype,"lang",2),a([f()],r.prototype,"_typing",2),a([f()],r.prototype,"_typed",2),a([f()],r.prototype,"_typedInvalid",2);customElements.get("ms-date-field")||customElements.define("ms-date-field",r);
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
import{b as x}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4PKMHUVO.js";import{a as H,e as S}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-NEWWPCTD.js";import{A as k,B as A,y as v}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-27MZ7YLE.js";var V=500,B=new Set(["completed","skipped","reset","missed"]);function M(r,e){let o=r.reading_values||[];return o.length?o.filter(n=>typeof n.value=="number").map(n=>({id:n.id,name:n.name||"",value:n.value,unit:n.unit||""})):typeof r.reading_value=="number"?[{id:"",name:"",value:r.reading_value,unit:e||""}]:[]}function O(r){let e=[];for(let o of r){let n=[...o.history??[]].map(t=>({h:t,ts:k(t.timestamp)})).filter(t=>Number.isFinite(t.ts)).sort((t,a)=>t.ts-a.ts),s=new Map;for(let{h:t,ts:a}of n){let y=M(t,o.reading_unit).map(u=>{let f=s.get(u.id);return s.set(u.id,u.value),{name:u.name,value:u.value,unit:u.unit,delta:f==null?null:u.value-f}});if(!B.has(t.type))continue;let c=t.checklist_state&&typeof t.checklist_state=="object"?Object.values(t.checklist_state):null;e.push({ts:a,timestamp:t.timestamp,taskId:o.id,taskName:o.name,type:t.type,cost:S(t),partsCost:typeof t.parts_cost=="number"?t.parts_cost:null,partsCounted:t.cost_basis==="use",purchaseCost:t.cost_basis==="use"&&t.purchase&&typeof t.cost=="number"?t.cost:null,duration:typeof t.duration=="number"?t.duration:null,notes:t.notes??null,completedBy:t.completed_by??null,phaseName:t.phase_id&&o.phases?.[t.phase_id]?.name||null,refNo:typeof t.ref_no=="number"?t.ref_no:null,taskRefNo:typeof o.ref_no=="number"?o.ref_no:null,readings:y,parts:(t.used_parts||[]).filter(u=>u&&(u.name||u.part_id)).map(u=>({name:u.name||u.part_id,quantity:typeof u.quantity=="number"?u.quantity:1})),photoIds:H(t),checklist:c&&c.length?{done:c.filter(Boolean).length,total:c.length}:null})}}return e.sort((o,n)=>n.ts-o.ts||o.taskName.localeCompare(n.taskName)),e}function R(r,e){return r.filter(o=>{if(e.taskId&&o.taskId!==e.taskId)return!1;if(!e.from&&!e.to)return!0;let n=A(o.timestamp);return!(!n||e.from&&n<e.from||e.to&&n>e.to)})}function E(r){let e=0,o=0;for(let n of r)n.type==="completed"&&(e++,n.cost!=null&&(o+=n.cost));return{completed:e,totalCost:o}}var d="__none__",L=36;function T(r){return r.area_id||d}function K(r,e,o){return r===d?o:e?.[r]?.name||r}function Y(r,e){return r===d?"mdi:map-marker-off-outline":e?.[r]?.icon||"mdi:map-marker-outline"}function J(r,e,o){return r.filter(n=>(o||!n.object.archived)&&T(n.object)===e)}function D(r,e){let o=[];for(let n of r){let s=O(n.tasks.map(t=>({id:t.id,name:t.name,history:e(n.entry_id,t),phases:t.phases,ref_no:t.ref_no,reading_unit:t.reading_unit})));for(let t of s)o.push({...t,entryId:n.entry_id,objectName:n.object.name})}return o.sort((n,s)=>s.ts-n.ts||n.objectName.localeCompare(s.objectName)||n.taskName.localeCompare(s.taskName)),o}function W(r,e){let o=(e.taskQuery??"").trim();return R(r,{from:e.from,to:e.to}).filter(n=>(!e.entryId||n.entryId===e.entryId)&&(!o||x(n.taskName,o)))}function Z(r){let{completed:e,totalCost:o}=E(r),n=0,s=0,t=0;for(let a of r)a.type==="completed"&&(a.duration!=null&&(n+=a.duration),!((a.cost??0)<0)&&(s++,t+=a.cost??0));return{completions:e,totalCost:o,avgCost:s?t/s:null,totalDuration:n}}function tt(r,e){let o=new Map;for(let t of e)o.set(t.entry_id,{entryId:t.entry_id,objectName:t.object.name,completions:0,cost:0,share:0});for(let t of r){if(t.type!=="completed")continue;let a=o.get(t.entryId);a&&(a.completions++,t.cost!=null&&(a.cost+=t.cost))}let n=[...o.values()].reduce((t,a)=>t+Math.max(a.cost,0),0),s=[...o.values()].map(t=>({...t,share:n>0?Math.max(t.cost,0)/n:0}));return s.sort((t,a)=>a.cost-t.cost||a.completions-t.completions||t.objectName.localeCompare(a.objectName)),s}function C(r){let[e,o]=r.split("-").map(Number);return[e,o-1]}function I(r,e){return`${r}-${String(e+1).padStart(2,"0")}`}function et(r,e,o=v()){let n=r.filter(i=>i.type==="completed"),s=n.map(i=>A(i.timestamp)),t=s.filter(i=>i!==null),a=t.reduce((i,m)=>m<i?m:i,o),y=t.reduce((i,m)=>m>i?m:i,o),[c,u]=C(e.from||a),[f,h]=C(e.to||y),l=f*12+h-(c*12+u)+1;if(l<=0)return{unit:"month",buckets:[]};let g=l>L?"year":"month",_=[],j=new Map;if(g==="month")for(let i=0;i<l;i++){let m=c+Math.floor((u+i)/12),b=(u+i)%12,p={key:I(m,b),year:m,month:b,cost:0,completions:0};_.push(p),j.set(p.key,p)}else for(let i=c;i<=f;i++){let m={key:String(i),year:i,month:null,cost:0,completions:0};_.push(m),j.set(m.key,m)}return n.forEach((i,m)=>{let b=s[m];if(!b)return;let p=j.get(g==="month"?b.slice(0,7):b.slice(0,4));p&&(p.completions++,i.cost!=null&&(p.cost+=i.cost))}),{unit:g,buckets:_}}function rt(r=v()){let[e,o]=C(r),n=e*12+o-11;return{from:`${I(Math.floor(n/12),n%12)}-01`,to:r}}function nt(r){return{from:`${r}-01-01`,to:`${r}-12-31`}}function w(r){let e=A(r.timestamp);return e?Number(e.slice(0,4)):null}function ot(r,e){let o=new Set([e]);for(let n of r){let s=n.type==="completed"?w(n):null;s!==null&&o.add(s)}return[...o].sort((n,s)=>s-n)}function st(r,e){let o=new Map;for(let s of r){if(!Line truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! maintenance_supporter frontend 2.98.0 */
|
||||
async function i(o,n,e){let t=o.auth;if(t?.expired&&t.refreshAccessToken)try{await t.refreshAccessToken()}catch{}return fetch(n,{...e,headers:{...e.headers??{},Authorization:`Bearer ${t?.data?.access_token??""}`}})}var u=500*1024*1024;async function d(o,n,e,t="doc_too_large",s=[]){let r=await i(o,n,{method:"POST",body:e});if(r.status===413)throw new Error(t);if(!r.ok){let a=await r.json().catch(()=>null);throw typeof a?.code=="string"&&s.includes(a.code)?new Error(a.code):new Error("doc_upload_failed")}return await r.json()}async function c(o,n,e,t){let s=new FormData;s.append("entry_id",n);for(let a of t)s.append("tags",a);s.append("file",e,e.name);let r=await d(o,"/api/maintenance_supporter/document/upload",s);if(!r.id)throw new Error("doc_upload_failed");return{id:r.id,deduped:!!r.deduped,duplicate_in_object:r.duplicate_in_object??null}}async function l(o,n,e){return(await c(o,n,e,["photo"])).id}async function m(o,n,e){await Promise.all(e.map(t=>o.connection.sendMessagePromise({type:"maintenance_supporter/documents/discard_upload",entry_id:n,doc_id:t}).catch(()=>{})))}export{u as a,d as b,c,l as d,m as e};
|
||||
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user