/** Dialog for completing a maintenance task with optional notes, cost, duration. */ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import type { HomeAssistant } from "../types"; import { t, nativeFieldStyles } from "../styles"; import { describeWsError } from "../ws-errors"; export class MaintenanceCompleteDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public entryId = ""; @property() public taskId = ""; @property() public taskName = ""; @property() public lang = "en"; @property({ type: Array }) public checklist: string[] = []; @property({ type: Boolean }) public adaptiveEnabled = false; // v2.20 (#83): task type + unit drive the reading-value field below. @property() public taskType = ""; @property() public readingUnit = ""; /** Buy task (part_ref): default restock quantity — shows an editable qty field. */ @property({ attribute: false }) public restockDefault: number | null = null; /** "Consumes: 1× HEPA-Filter (Shelf B)" hint lines for consuming tasks. */ @property({ type: Array }) public consumesInfo: string[] = []; @state() private _open = false; @state() private _notes = ""; @state() private _cost = ""; @state() private _duration = ""; @state() private _loading = false; @state() private _error = ""; @state() private _checklistState: Record = {}; @state() private _feedback: string = "needed"; @state() private _photoDocId = ""; @state() private _photoPreview = ""; @state() private _photoUploading = false; @state() private _readingValue = ""; @state() private _restockQty = ""; public open(): void { if (this._open) return; this._open = true; this._notes = ""; this._cost = ""; this._duration = ""; this._error = ""; this._checklistState = {}; this._feedback = "needed"; this._photoDocId = ""; this._photoPreview = ""; this._photoUploading = false; this._readingValue = ""; this._restockQty = this.restockDefault !== null ? String(this.restockDefault) : ""; } private _toggleCheck(idx: number): void { const key = String(idx); this._checklistState = { ...this._checklistState, [key]: !this._checklistState[key], }; } private _setFeedback(value: string): void { this._feedback = value; } private async _onPhotoInput(e: Event): Promise { const input = e.target as HTMLInputElement; const file = input.files?.[0]; input.value = ""; // allow re-picking the same file if (!file) return; this._photoUploading = true; this._error = ""; try { const form = new FormData(); form.append("entry_id", this.entryId); form.append("tags", "photo"); form.append("file", file, file.name); const resp = await fetch("/api/maintenance_supporter/document/upload", { method: "POST", headers: { Authorization: `Bearer ${this.hass.auth?.data?.access_token ?? ""}` }, body: form, }); if (!resp.ok) { this._error = resp.status === 413 ? t("doc_too_large", this.lang) : t("doc_upload_failed", this.lang); return; } const doc = (await resp.json()) as { id?: string }; if (doc.id) { this._photoDocId = doc.id; this._photoPreview = URL.createObjectURL(file); } } catch { this._error = t("doc_upload_failed", this.lang); } finally { this._photoUploading = false; } } private _removePhoto(): void { if (this._photoPreview) URL.revokeObjectURL(this._photoPreview); this._photoDocId = ""; this._photoPreview = ""; } private async _complete(): Promise { this._loading = true; this._error = ""; try { const data: Record = { type: "maintenance_supporter/task/complete", entry_id: this.entryId, task_id: this.taskId, }; if (this._notes) data.notes = this._notes; if (this._cost) { const cost = parseFloat(this._cost); if (!isNaN(cost) && cost >= 0) data.cost = cost; } if (this._duration) { const dur = parseInt(this._duration, 10); if (!isNaN(dur) && dur >= 0) data.duration = dur; } if (this.checklist.length > 0) { data.checklist_state = this._checklistState; } if (this.adaptiveEnabled) { data.feedback = this._feedback; } if (this._photoDocId) { data.photo_doc_id = this._photoDocId; } if (this._readingValue !== "") { const rv = parseFloat(this._readingValue); if (!isNaN(rv)) data.reading_value = rv; } if (this.restockDefault !== null && this._restockQty !== "") { const rq = parseInt(this._restockQty, 10); if (!isNaN(rq) && rq >= 1) data.restock_quantity = rq; } await this.hass.connection.sendMessagePromise(data); this._open = false; this.dispatchEvent(new CustomEvent("task-completed")); } catch (e) { this._error = describeWsError(e, this.lang, t("save_error", this.lang)); } finally { this._loading = false; } } private _close(): void { this._open = false; } render() { if (!this._open) return html``; const L = this.lang || this.hass?.language || "en"; return html`
${t("complete_title", L)}${this.taskName}
${this._error ? html`
${this._error}
` : nothing} ${this.checklist.length > 0 ? html`
${this.checklist.map((item, idx) => html` `)}
` : nothing} ${this.taskType === "reading" ? html` ` : nothing} ${this.consumesInfo.length ? html`
${this.consumesInfo.map((line) => html`
${line}
`)}
` : nothing} ${this.restockDefault !== null ? html` ` : nothing}
${t("completion_photo_optional", L)} ${this._photoPreview ? html`
` : html` `}
${this.adaptiveEnabled ? html` ` : nothing}
${t("cancel", L)} ${this._loading ? t("completing", L) : t("complete", L)}
`; } static styles = [nativeFieldStyles, css` .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; } .consumes-hint { font-size: 13px; color: var(--secondary-text-color); border-left: 3px solid var(--primary-color); padding: 4px 8px; margin: 4px 0 8px; } .error { color: var(--error-color, #f44336); font-size: 13px; } /* .field/.field-label/.field-input come from nativeFieldStyles */ .photo-pick { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px dashed var(--divider-color); border-radius: 8px; cursor: pointer; font-size: 13px; color: var(--secondary-text-color); width: fit-content; } .photo-pick:hover { border-color: var(--primary-color); } .photo-pick input[type="file"] { display: none; } .photo-preview { position: relative; width: fit-content; } .photo-preview img { max-width: 160px; max-height: 160px; border-radius: 8px; display: block; } .photo-remove { position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; border-radius: 50%; border: none; background: var(--error-color, #db4437); color: #fff; cursor: pointer; font-size: 12px; line-height: 1; } .checklist-section { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--divider-color); margin-bottom: 4px; } .checklist-label { font-weight: 500; font-size: 13px; color: var(--secondary-text-color); } .checklist-item { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px 0; font-size: 14px; } .checklist-item input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; } .feedback-section { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; border-top: 1px solid var(--divider-color); } .feedback-label { font-weight: 500; font-size: 13px; color: var(--secondary-text-color); } .feedback-buttons { display: flex; gap: 8px; } .feedback-btn { flex: 1; padding: 8px 12px; border: 1px solid var(--divider-color); border-radius: 8px; background: var(--card-background-color, #fff); color: var(--primary-text-color); font-size: 13px; cursor: pointer; text-align: center; transition: all 0.2s; } .feedback-btn:hover { background: var(--secondary-background-color, #f5f5f5); } .feedback-btn.selected { background: var(--primary-color); color: var(--text-primary-color, #fff); border-color: var(--primary-color); } `]; } // Safe registration — avoids duplicate define when both panel and card load if (!customElements.get("maintenance-complete-dialog")) { customElements.define("maintenance-complete-dialog", MaintenanceCompleteDialog); }