/** v2.4.0 — Interactive Groups Section Card. * * Replaces the read-only markdown groups card. Lets the admin add / rename / * delete groups inline. Task assignment lives in the panel (it needs the * full task picker), so each group has a "Manage tasks" link that deep-links * there. */ 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 GroupEntry { name?: string; description?: string; task_refs?: { entry_id: string; task_id: string }[]; } interface GroupsResp { groups?: Record; } interface CardConfig { type: string; title?: string; } export class MaintenanceGroupsSectionCard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config: CardConfig = { type: "" }; @state() private _groups: Record = {}; @state() private _loaded = false; @state() private _busy = false; @state() private _error = ""; @state() private _newName = ""; @state() private _editingId: string | null = null; @state() private _editingName = ""; private _hasInitiallyLoaded = 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._hasInitiallyLoaded) { this._hasInitiallyLoaded = 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/groups", }); this._groups = r.groups || {}; this._loaded = true; } catch (e) { this._error = describeWsError(e, this._lang); } } private async _addGroup(): Promise { if (!this._isAdmin) return; const name = this._newName.trim(); if (!name) return; this._busy = true; this._error = ""; try { await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/group/create", name, }); this._newName = ""; await this._load(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private _startEdit(id: string): void { this._editingId = id; this._editingName = this._groups[id]?.name || ""; } private async _saveEdit(): Promise { if (!this._isAdmin || !this._editingId) return; const name = this._editingName.trim(); if (!name) return; this._busy = true; this._error = ""; try { await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/group/update", group_id: this._editingId, name, }); this._editingId = null; this._editingName = ""; await this._load(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private async _deleteGroup(id: string, name: string): Promise { if (!this._isAdmin) return; const confirmText = (t("group_delete_confirm", this._lang) || "Delete group \"{name}\"?").replace("{name}", name); if (!window.confirm(confirmText)) return; this._busy = true; try { await this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/group/delete", group_id: id, }); await this._load(); } catch (e) { this._error = describeWsError(e, this._lang); } finally { this._busy = false; } } private _onDeepLink(): void { const path = "/maintenance-supporter?ms_action=open_groups"; history.pushState(null, "", path); window.dispatchEvent(new CustomEvent("location-changed")); } private _onKeyDown(e: KeyboardEvent, action: () => void): void { if (e.key === "Enter") { e.preventDefault(); action(); } else if (e.key === "Escape") { e.preventDefault(); this._editingId = null; this._editingName = ""; } } render() { const L = this._lang; if (!this._loaded) { return html`
${t("loading", L) || "Loading…"}
`; } const ids = Object.keys(this._groups); return html`
🏷️ ${this._config.title || (t("groups", L) || "Groups")} ${ids.length}
${this._error ? html`
${this._error}
` : nothing} ${ids.length === 0 ? html`
${t("groups_empty", L) || "No groups yet."}
` : html`
${ids.map((id) => { const g = this._groups[id]; const taskCount = g.task_refs?.length ?? 0; const isEditing = this._editingId === id; return html`
${isEditing ? html` { this._editingName = (e.target as HTMLInputElement).value; }} @keydown=${(e: KeyboardEvent) => this._onKeyDown(e, this._saveEdit.bind(this))} /> ` : html` ${g.name || "Unnamed"} ${taskCount} ${this._isAdmin ? html` ` : nothing} `}
`; })}
`} ${this._isAdmin ? html`
{ this._newName = (e.target as HTMLInputElement).value; }} @keydown=${(e: KeyboardEvent) => this._onKeyDown(e, this._addGroup.bind(this))} />
` : html` `}
`; } static styles = [sectionCardSharedStyles, css` .count { font-size: 12px; color: var(--secondary-text-color); background: var(--secondary-background-color); padding: 2px 8px; border-radius: 999px; } .empty { padding: 16px; text-align: center; color: var(--secondary-text-color); font-style: italic; } .group-list { display: flex; flex-direction: column; gap: 4px; } .group-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; background: var(--secondary-background-color, rgba(255,255,255,0.03)); } .group-name { flex: 1; font-size: 14px; } .task-count { font-size: 11px; color: var(--secondary-text-color); background: var(--card-background-color, rgba(0,0,0,0.2)); padding: 1px 8px; border-radius: 999px; font-weight: 500; } .edit-input { flex: 1; padding: 4px 8px; font-size: 14px; background: var(--card-background-color, #1c1c1c); color: var(--primary-text-color); border: 1px solid var(--primary-color); border-radius: 4px; font-family: inherit; } .icon-btn { background: transparent; border: none; cursor: pointer; color: var(--secondary-text-color); padding: 4px; border-radius: 4px; } .icon-btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); color: var(--primary-text-color); } .icon-btn.danger:hover { color: var(--error-color); } .icon-btn ha-icon { --mdc-icon-size: 18px; } .add-row { display: flex; gap: 6px; padding-top: 8px; border-top: 1px solid var(--divider-color); } .add-row input { flex: 1; 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; } /* Card-specific overrides on the shared .btn */ .btn.small { padding: 4px 8px; font-size: 12px; } .btn ha-icon { --mdc-icon-size: 16px; } `]; } if (!customElements.get("maintenance-groups-section-card")) { customElements.define( "maintenance-groups-section-card", MaintenanceGroupsSectionCard, ); } (window as { customCards?: unknown[] }).customCards = (window as { customCards?: unknown[] }).customCards || []; ((window as { customCards?: unknown[] }).customCards!).push({ type: "maintenance-groups-section-card", name: "Maintenance Supporter — Groups", description: "Inline group CRUD", preview: false, });