/** v2.4.0 — Interactive Vacation Section Card. * * Replaces the read-only markdown card from Phase 5. Lets the user toggle * vacation mode + edit start/end/buffer inline, without leaving Lovelace. * * Used as a section-strategy card (HA 2026.5+ sections) AND as a stand-alone * card the user can drop onto any dashboard. * * Permissions: only admins see edit affordances; non-admins get a read-only * view with a deep-link to the panel (vacation config is admin-only at the * WS layer too). */ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { t, ensureLocale } from "../styles"; import { describeWsError } from "../ws-errors"; import { sectionCardSharedStyles } from "./section-card-shared-styles"; import type { HomeAssistant } from "../types"; interface VacationState { enabled?: boolean; is_active?: boolean; start?: string | null; end?: string | null; buffer_days?: number; window_end?: string | null; exempt_task_ids?: string[]; } interface CardConfig { type: string; title?: string; } export class MaintenanceVacationSectionCard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config: CardConfig = { type: "" }; @state() private _state: VacationState | null = null; @state() private _busy = false; @state() private _error = ""; @state() private _localStart = ""; @state() private _localEnd = ""; @state() private _localBuffer = 7; @state() private _dirty = false; private _loaded = false; setConfig(config: CardConfig): void { this._config = config; } getCardSize(): number { return 2; } private get _lang(): string { return this.hass?.language || "en"; } private get _isAdmin(): boolean { return (this.hass?.user?.is_admin ?? true) as boolean; } updated(changedProps: Map): void { super.updated(changedProps); if (changedProps.has("hass") && this.hass && !this._loaded) { this._loaded = true; void this._load(); void ensureLocale(this._lang).then(() => this.requestUpdate()); } } private async _load(): Promise { try { const r = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/vacation/state", }); this._state = r; this._localStart = r.start || ""; this._localEnd = r.end || ""; this._localBuffer = r.buffer_days ?? 7; this._dirty = false; } catch (e) { this._error = describeWsError(e, this._lang); } } private async _toggleEnabled(on: boolean): Promise { this._busy = true; this._error = ""; try { const r = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/vacation/update", enabled: on, }); this._state = r; } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private async _save(): Promise { if (!this._isAdmin) return; this._busy = true; this._error = ""; try { const r = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/vacation/update", start: this._localStart || null, end: this._localEnd || null, buffer_days: this._localBuffer, }); this._state = r; this._dirty = false; } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private async _endNow(): Promise { if (!this._isAdmin) return; if (!window.confirm(t("vacation_end_now_confirm", this._lang) || "End vacation immediately?")) return; this._busy = true; try { const r = await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/vacation/end_now", }); this._state = r; this._localStart = r.start || ""; this._localEnd = r.end || ""; } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private _onDeepLink(): void { const path = "/maintenance-supporter?ms_action=open_vacation"; history.pushState(null, "", path); window.dispatchEvent(new CustomEvent("location-changed")); } render() { const L = this._lang; const s = this._state; if (!s) { return html`
${t("loading", L) || "Loading…"}
`; } const active = s.is_active === true; const enabled = s.enabled === true; const exemptCount = s.exempt_task_ids?.length ?? 0; const statusLabel = active ? (t("vacation_status_active", L) || "Active now") : enabled ? (t("vacation_status_scheduled", L) || "Scheduled") : (t("vacation_status_inactive", L) || "Inactive"); const statusClass = active ? "active" : enabled ? "scheduled" : "inactive"; return html`
🏖️ ${this._config.title || (t("vacation_mode", L) || "Vacation mode")}
${statusLabel}
${this._error ? html`
${this._error}
` : nothing} ${this._isAdmin ? html`
this._toggleEnabled((e.target as HTMLInputElement).checked)} >
{ this._localStart = (e.target as HTMLInputElement).value; this._dirty = true; }} />
{ this._localEnd = (e.target as HTMLInputElement).value; this._dirty = true; }} />
{ this._localBuffer = parseInt( (e.target as HTMLInputElement).value, 10) || 0; this._dirty = true; }} />
${active ? html`` : nothing} ${exemptCount > 0 ? html`` : html``}
` : html`
${enabled && s.start && s.end ? html`
${s.start} → ${s.end}
` : nothing}
`}
`; } static styles = [sectionCardSharedStyles, css` .status-pill { font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.5px; } .status-pill.active { background: rgba(76, 175, 80, 0.15); color: #4caf50; } .status-pill.scheduled { background: rgba(255, 152, 0, 0.15); color: #ff9800; } .status-pill.inactive { background: rgba(158, 158, 158, 0.15); color: var(--secondary-text-color); } .row.toggle-row { display: flex; align-items: center; justify-content: space-between; } .row.toggle-row label { font-size: 14px; color: var(--primary-text-color); } .dates-row { display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px; } .date-field.buffer label { white-space: nowrap; } .date-field { display: flex; flex-direction: column; gap: 4px; } .date-field label { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.3px; } .date-field input { padding: 6px 8px; font-size: 13px; background: var(--secondary-background-color, #2c2c2c); color: var(--primary-text-color); border: 1px solid var(--divider-color); border-radius: 6px; font-family: inherit; } .date-field input:disabled { opacity: 0.5; cursor: not-allowed; } .actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .readonly { display: flex; flex-direction: column; gap: 8px; } `]; } if (!customElements.get("maintenance-vacation-section-card")) { customElements.define( "maintenance-vacation-section-card", MaintenanceVacationSectionCard, ); } (window as { customCards?: unknown[] }).customCards = (window as { customCards?: unknown[] }).customCards || []; ((window as { customCards?: unknown[] }).customCards!).push({ type: "maintenance-vacation-section-card", name: "Maintenance Supporter — Vacation", description: "Inline vacation mode toggle + dates", preview: false, });