189 files
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, t, weekdayName, monthName, langOf } from "../styles";
|
||||
import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, ReadingSlot, TaskPartLink, TriggerConfig, HAUser } from "../types";
|
||||
import { formatDate, formatNumber, t, weekdayName, monthName, langOf } from "../styles";
|
||||
import { UserService } from "../user-service";
|
||||
import { partLinkKey } from "../helpers/shared-parts";
|
||||
import { MAX_READING_SLOTS, cleanReadingSlots, duplicateReadingSlotIds, newReadingSlotId } from "../helpers/reading-slots";
|
||||
import {
|
||||
ENVIRONMENTAL_PICKER_DEVICE_CLASSES,
|
||||
ENVIRONMENTAL_PICKER_DOMAINS,
|
||||
@@ -21,6 +22,9 @@ const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibra
|
||||
const PRIORITY_KEYS = ["low", "normal", "high"];
|
||||
const SCHEDULE_TYPE_KEYS = ["time_based", "weekdays", "nth_weekday", "day_of_month", "sensor_based", "one_time", "manual"];
|
||||
const CALENDAR_KINDS = ["weekdays", "nth_weekday", "day_of_month"];
|
||||
// #168: kinds that produce a due DATE and therefore take a time of day
|
||||
// (mirrors SCHEDULE_TIME_KINDS in config_flow_helpers.py).
|
||||
const SCHEDULE_TIME_KINDS = ["time_based", "one_time", ...CALENDAR_KINDS];
|
||||
const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"];
|
||||
// The type selector additionally offers "compound" (a group of conditions
|
||||
// joined by AND/OR); its per-condition sub-type is limited to the flat kinds.
|
||||
@@ -249,6 +253,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@state() private _allowSkip = true;
|
||||
// v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...)
|
||||
@state() private _readingUnit = "";
|
||||
// #161 phase 2: named reading slots — several values per completion.
|
||||
@state() private _readings: ReadingSlot[] = [];
|
||||
/** The picked links, keyed by `partLinkKey` — the (entry_id, part_id) pair,
|
||||
* since the same part id can exist on two objects (battery fleet). */
|
||||
@state() private _consumesParts: Record<string, TaskPartLink> = {};
|
||||
@@ -285,6 +291,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
|
||||
// Schedule time (HH:MM, advanced feature)
|
||||
@state() private _scheduleTime = "";
|
||||
// The picker only shows once this is ticked (#168 follow-up) — most tasks
|
||||
// are due "on the day", so the form stays lean. Unticking clears the time.
|
||||
@state() private _scheduleTimeOn = false;
|
||||
|
||||
// v1.3.0: on_complete_action (gated by completionActionsEnabled)
|
||||
// v1.3.1: _actionData is the parsed object (was: _actionDataJson string)
|
||||
@@ -404,6 +413,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requireTagScan = !!task.require_tag_scan;
|
||||
this._allowSkip = task.allow_skip !== false;
|
||||
this._readingUnit = task.reading_unit || "";
|
||||
this._readings = (task.readings || []).map((s) => ({ ...s }));
|
||||
// Whole link, entry_id included — hydrating only part_id would turn every
|
||||
// shared-pool link into an own-part link on the next save (#111).
|
||||
this._consumesParts = Object.fromEntries(
|
||||
@@ -443,6 +453,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = [...(task.phase_sequence || [])];
|
||||
this._requiredCompletion = [...(task.required_completion_fields || [])];
|
||||
this._scheduleTime = task.schedule_time || "";
|
||||
this._scheduleTimeOn = !!task.schedule_time;
|
||||
|
||||
// v1.3.0: hydrate on_complete_action + quick_complete_defaults
|
||||
const oca = task.on_complete_action;
|
||||
@@ -557,6 +568,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requireTagScan = false;
|
||||
this._allowSkip = true;
|
||||
this._readingUnit = "";
|
||||
this._readings = [];
|
||||
this._consumesParts = {};
|
||||
this._responsibleUserId = null;
|
||||
this._assigneePool = [];
|
||||
@@ -566,6 +578,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._phaseSeq = [];
|
||||
this._requiredCompletion = [];
|
||||
this._scheduleTime = "";
|
||||
this._scheduleTimeOn = false;
|
||||
this._environmentalEntity = "";
|
||||
this._environmentalAttribute = "";
|
||||
this._environmentalInitial = "";
|
||||
@@ -1052,6 +1065,47 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
this._requiredCompletion = [...next];
|
||||
}
|
||||
|
||||
// ── Reading slots (#161 phase 2) ─────────────────────────────────────
|
||||
|
||||
private _patchReading(id: string, patch: Partial<ReadingSlot>): void {
|
||||
this._readings = this._readings.map((s) => (s.id === id ? { ...s, ...patch } : s));
|
||||
}
|
||||
|
||||
private _renderReadingsEditor(L: string) {
|
||||
const dupes = duplicateReadingSlotIds(this._readings);
|
||||
return html`
|
||||
<div class="readings-editor">
|
||||
<div class="field-label">${t("readings_section", L)}</div>
|
||||
<div class="field-help">${t("readings_hint", L)}</div>
|
||||
${this._readings.map((s) => html`
|
||||
${dupes.has(s.id) ? html`<div class="field-help reading-dup">${t("reading_duplicate_name", L)}</div>` : nothing}
|
||||
<div class="reading-row">
|
||||
<ms-textfield
|
||||
class="reading-name"
|
||||
label="${t("reading_name_label", L)}"
|
||||
.value=${s.name}
|
||||
@input=${(e: Event) => this._patchReading(s.id, { name: (e.target as HTMLInputElement).value })}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
class="reading-unit"
|
||||
label="${t("reading_unit_short", L)}"
|
||||
.value=${s.unit || ""}
|
||||
@input=${(e: Event) => this._patchReading(s.id, { unit: (e.target as HTMLInputElement).value })}
|
||||
></ms-textfield>
|
||||
<mwc-icon-button class="phase-remove reading-remove" @click=${() => (this._readings = this._readings.filter((x) => x.id !== s.id))}>
|
||||
<ha-icon icon="mdi:delete-outline"></ha-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
`)}
|
||||
${this._readings.length < MAX_READING_SLOTS ? html`
|
||||
<ha-button appearance="plain" class="reading-add"
|
||||
@click=${() => (this._readings = [...this._readings, { id: newReadingSlotId(), name: "", unit: this._readings.length ? this._readings[this._readings.length - 1].unit : this._readingUnit }])}>
|
||||
<ha-icon icon="mdi:plus"></ha-icon> ${t("reading_add", L)}
|
||||
</ha-button>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Task phases (#139) ───────────────────────────────────────────────
|
||||
|
||||
private _phaseSlug(name: string): string {
|
||||
@@ -1265,6 +1319,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.require_tag_scan = this._requireTagScan;
|
||||
data.allow_skip = this._allowSkip;
|
||||
data.reading_unit = this._readingUnit.trim() || null;
|
||||
// #161 phase 2: always sent — [] clears the slots (single-value task).
|
||||
// Sent as hydrated even when the editor is hidden (type != reading), so
|
||||
// a no-op edit never wipes stored slots (the #42/#50 wipe class).
|
||||
data.readings = cleanReadingSlots(this._readings);
|
||||
// Task phases (#139): always sent — null clears a removed cycle.
|
||||
{
|
||||
const defs: Record<string, unknown> = {};
|
||||
@@ -1388,9 +1446,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
data.trigger_config = null;
|
||||
}
|
||||
|
||||
// Schedule time only sent when feature is enabled — empty string clears.
|
||||
if (this.scheduleTimeEnabled && this._scheduleType === "time_based") {
|
||||
const t = this._scheduleTime.trim();
|
||||
// Schedule time only sent when feature is enabled — null clears.
|
||||
// Every date-driven kind takes one (#168), not only the interval; an
|
||||
// unticked checkbox (or a ticked one with an empty picker) clears it.
|
||||
if (this.scheduleTimeEnabled && SCHEDULE_TIME_KINDS.includes(this._scheduleType)) {
|
||||
const t = this._scheduleTimeOn ? this._scheduleTime.trim() : "";
|
||||
data.schedule_time = /^([01]\d|2[0-3]):[0-5]\d$/.test(t) ? t : null;
|
||||
}
|
||||
|
||||
@@ -2103,7 +2163,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
private _renderSchedulePreview() {
|
||||
if (this._schedulePreview.length === 0) return nothing;
|
||||
const L = this._lang;
|
||||
const time = this.scheduleTimeEnabled && this._scheduleTime ? ` ${this._scheduleTime}` : "";
|
||||
const time = this.scheduleTimeEnabled && this._scheduleTimeOn && this._scheduleTime ? ` ${this._scheduleTime}` : "";
|
||||
const chips = this._schedulePreview
|
||||
.map((iso, i) => {
|
||||
const js = new Date(`${iso}T12:00:00`).getDay(); // 0=Sun
|
||||
@@ -2331,7 +2391,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
: st.state;
|
||||
const num = typeof raw === "number" ? raw : parseFloat(String(raw));
|
||||
const hasNum = raw !== "unknown" && raw !== "unavailable" && raw != null && !isNaN(num);
|
||||
const fmt = (v: number) => (Number.isInteger(v) ? String(v) : String(Math.round(v * 10) / 10));
|
||||
// Profile number format like every other figure (#163) — the hint used
|
||||
// to print "1.2" next to a "1,2 bar" entity card (bug review 2026-09-04).
|
||||
const fmt = (v: number) => formatNumber(v, L, { maximumFractionDigits: 1 });
|
||||
|
||||
const parts: string[] = [];
|
||||
if (this._triggerType === "threshold") {
|
||||
@@ -2487,6 +2549,9 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => (this._triggerTargetChanges = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("target_changes_help", L)}</div>
|
||||
${(this._triggerTargetChanges || "1") === "1" && (this._triggerFromState || this._triggerToState)
|
||||
? html`<div class="field-help">${t("state_latch_help", L)}</div>`
|
||||
: nothing}
|
||||
<ms-textfield
|
||||
label="${t("for_at_least_minutes", L)}"
|
||||
type="number"
|
||||
@@ -2579,6 +2644,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
@input=${(e: Event) => (this._readingUnit = (e.target as HTMLInputElement).value)}
|
||||
></ms-textfield>
|
||||
<div class="field-help">${t("reading_unit_help", L)}</div>
|
||||
${this._renderReadingsEditor(L)}
|
||||
`
|
||||
: nothing}
|
||||
${this._partsLoadFailed
|
||||
@@ -2659,18 +2725,6 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
<option value="planned" ?selected=${this._intervalAnchor === "planned"}>${t("anchor_planned", L)}</option>
|
||||
</select>
|
||||
</div>
|
||||
${this.scheduleTimeEnabled ? html`
|
||||
<ms-date-field
|
||||
kind="time"
|
||||
clearable
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
label="${t("schedule_time_optional", L)}"
|
||||
.value=${this._scheduleTime}
|
||||
helper="${t("schedule_time_help", L)}"
|
||||
@value-changed=${(e: CustomEvent) => (this._scheduleTime = e.detail.value as string)}
|
||||
></ms-date-field>
|
||||
` : nothing}
|
||||
`
|
||||
: nothing}
|
||||
${this._renderCalendarFields()}
|
||||
@@ -2686,6 +2740,23 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
></ms-date-field>
|
||||
`
|
||||
: nothing}
|
||||
${this.scheduleTimeEnabled && SCHEDULE_TIME_KINDS.includes(this._scheduleType) ? html`
|
||||
<label class="checkbox-row schedule-time-toggle">
|
||||
<input type="checkbox" .checked=${this._scheduleTimeOn}
|
||||
@change=${(e: Event) => (this._scheduleTimeOn = (e.target as HTMLInputElement).checked)} />
|
||||
<span>${t("schedule_time_toggle", L)}</span>
|
||||
</label>
|
||||
${this._scheduleTimeOn ? html`
|
||||
<ms-date-field
|
||||
kind="time"
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.value=${this._scheduleTime}
|
||||
helper="${t("schedule_time_help", L)}"
|
||||
@value-changed=${(e: CustomEvent) => (this._scheduleTime = e.detail.value as string)}
|
||||
></ms-date-field>
|
||||
` : nothing}
|
||||
` : nothing}
|
||||
${this._renderRecurrenceExtras()}
|
||||
${this._renderSchedulePreview()}
|
||||
<ms-textfield
|
||||
@@ -3067,6 +3138,17 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
--mdc-icon-button-size: 36px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
/* #161 phase 2: reading slots — name wide, unit narrow, trash. */
|
||||
.readings-editor { margin-top: 14px; }
|
||||
.reading-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.reading-row .reading-name { flex: 2; min-width: 0; }
|
||||
.reading-row .reading-unit { flex: 1; min-width: 0; max-width: 140px; }
|
||||
.reading-dup { color: var(--warning-color, #ff9800); margin-top: 6px; }
|
||||
.phase-seq-label {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -3214,6 +3296,10 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.schedule-time-toggle {
|
||||
display: flex;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.pool-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user