/** Dialog for editing an existing history entry (timestamp / notes / cost / * duration / completed_by). Backed by maintenance_supporter/task/history/update. * * Opened from: * - Task detail page → history tab → Edit button per entry * - Calendar past-window event click (via ll-custom dispatch from * maintenance-supporter-calendar-card) */ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { t } from "../styles"; import type { HomeAssistant } from "../types"; import { describeWsError } from "../ws-errors"; export interface HistoryEntryDraft { entry_id: string; task_id: string; original_timestamp: string; // identifies the entry on save type: string; // for display only — read-only timestamp: string; notes: string | null; cost: number | null; duration: number | null; completed_by: string | null; } export class MaintenanceHistoryEditDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _open = false; @state() private _saving = false; @state() private _error = ""; @state() private _draft: HistoryEntryDraft | null = null; // Original snapshot so we can detect "no change" and skip the WS call private _originalSnapshot: HistoryEntryDraft | null = null; private get _lang(): string { return this.hass?.language || "en"; } /** Open the dialog with the given history-entry data. The caller must * pass `original_timestamp` (the entry's current timestamp before edit) * so the backend can find the entry. */ public openEdit(draft: HistoryEntryDraft): void { this._draft = { ...draft }; this._originalSnapshot = { ...draft }; this._error = ""; this._open = true; } public close(): void { this._open = false; this._error = ""; this._draft = null; this._originalSnapshot = null; } private _set( key: K, value: HistoryEntryDraft[K], ): void { if (!this._draft) return; this._draft = { ...this._draft, [key]: value }; } private async _save(): Promise { if (!this._draft || !this._originalSnapshot) return; this._saving = true; this._error = ""; try { const patch: Record = { type: "maintenance_supporter/task/history/update", entry_id: this._draft.entry_id, task_id: this._draft.task_id, original_timestamp: this._originalSnapshot.original_timestamp, }; // Only send fields that actually changed — keeps the patch minimal // and the WS schema happy (it treats missing as "no change"). if (this._draft.timestamp !== this._originalSnapshot.timestamp) { patch.timestamp = this._draft.timestamp; } if (this._draft.notes !== this._originalSnapshot.notes) { patch.notes = this._draft.notes; } if (this._draft.cost !== this._originalSnapshot.cost) { patch.cost = this._draft.cost; } if (this._draft.duration !== this._originalSnapshot.duration) { patch.duration = this._draft.duration; } if (this._draft.completed_by !== this._originalSnapshot.completed_by) { patch.completed_by = this._draft.completed_by; } // Nothing changed → close without WS call const changedKeys = Object.keys(patch).filter( (k) => !["type", "entry_id", "task_id", "original_timestamp"].includes(k), ); if (changedKeys.length === 0) { this.close(); return; } await this.hass.connection.sendMessagePromise(patch); // Notify upstream so they can refresh this.dispatchEvent( new CustomEvent("history-entry-saved", { detail: { entry_id: this._draft.entry_id, task_id: this._draft.task_id, new_timestamp: this._draft.timestamp, }, bubbles: true, composed: true, }), ); this.close(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._saving = false; } } render() { if (!this._open || !this._draft) return nothing; const L = this._lang; const d = this._draft; return html`
`; } static styles = css` :host { display: contents; } .backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100; } .dialog { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 95vw; max-width: 480px; background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); color: var(--primary-text-color); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); padding: 20px; display: flex; flex-direction: column; gap: 12px; z-index: 101; max-height: 90vh; overflow: auto; } h2 { margin: 0; font-size: 18px; } .entry-type { display: flex; align-items: center; gap: 6px; color: var(--secondary-text-color); font-size: 13px; } label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; } label span { color: var(--secondary-text-color); } .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } input, textarea { padding: 8px; font-size: 14px; background: var(--secondary-background-color, #2c2c2c); color: var(--primary-text-color); border: 1px solid var(--divider-color, #444); border-radius: 6px; width: 100%; box-sizing: border-box; font-family: inherit; } .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; } button { padding: 8px 16px; font-size: 14px; border-radius: 6px; cursor: pointer; border: none; font-weight: 500; } button.cancel { background: transparent; color: var(--primary-text-color); border: 1px solid var(--divider-color); } button.save { background: var(--primary-color); color: var(--text-primary-color, white); } button[disabled] { opacity: 0.5; cursor: wait; } .error { color: var(--error-color, #d32f2f); font-size: 13px; padding: 8px; background: rgba(211,47,47,0.1); border-radius: 6px; } `; } if (!customElements.get("maintenance-history-edit-dialog")) { customElements.define( "maintenance-history-edit-dialog", MaintenanceHistoryEditDialog, ); }