/** Task history sub-view: filter chips + search + the timeline of entries. * * Extracted from the panel (renderers/ pattern). State the panel owns — * the active filter, the search text, the currency symbol — plus the * callbacks that mutate it or open the edit dialog are passed in via * HistoryContext, keeping these functions pure of component internals. */ import { html, nothing } from "lit"; import { t, formatDateTime, STATUS_ICONS } from "../styles"; import type { MaintenanceTask, HistoryEntry, HomeAssistant } from "../types"; import "../components/history-photo"; export interface HistoryContext { lang: string; /** Home Assistant object — used to sign completion-photo URLs. */ hass: HomeAssistant; /** Active type filter, or null for "all". */ filter: string | null; /** Free-text search over entry notes. */ search: string; /** Currency symbol for cost display (defaults to €). */ currencySymbol: string; setFilter: (filter: string | null) => void; setSearch: (search: string) => void; openEdit: (entry: HistoryEntry) => void; /** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */ readingUnit?: string | null; readingDelta?: (entry: HistoryEntry) => number | null; } const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered"] as const; export function renderHistoryFilters(task: MaintenanceTask, ctx: HistoryContext) { const L = ctx.lang; return html`
${_FILTER_TYPES.map((type) => { const count = task.history.filter((h) => h.type === type).length; if (count === 0) return nothing; return html` ctx.setFilter(ctx.filter === type ? null : type)}> ${t(type, L)} (${count}) `; })} ${ctx.filter ? html` ctx.setFilter(null)}>${t("show_all", L)}` : nothing}
ctx.setSearch((e.target as HTMLInputElement).value)} />
`; } export function renderHistoryList(task: MaintenanceTask, ctx: HistoryContext) { const L = ctx.lang; let filtered = ctx.filter ? task.history.filter((h) => h.type === ctx.filter) : task.history; // Apply search filter if (ctx.search) { const search = ctx.search.toLowerCase(); filtered = filtered.filter((h) => h.notes?.toLowerCase().includes(search)); } if (filtered.length === 0) { return html`

${t("no_history", L)}

`; } return html`
${[...filtered].reverse().map((entry: HistoryEntry) => renderHistoryEntry(entry, ctx))}
`; } export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) { const L = ctx.lang; // v2.2.0: only "lifecycle + cost-bearing" entries are user-editable. // Triggers are auto-generated by sensors and shouldn't be retroactively // rewritten. Allow edits for completed / reset / skipped entries. const editable = ["completed", "reset", "skipped"].includes(entry.type); return html`
${t(entry.type, L)} ${entry.auto ? html`${t("history_auto", L)}` : nothing} ${editable ? html`` : nothing}
${formatDateTime(entry.timestamp, L)}
${entry.notes ? html`
${entry.notes}
` : nothing} ${entry.photo_doc_id ? html`` : nothing}
${entry.cost != null ? html`${t("cost", L)}: ${entry.cost.toFixed(2)} ${ctx.currencySymbol}` : nothing} ${entry.duration != null ? html`${t("duration", L)}: ${entry.duration} min` : nothing} ${entry.trigger_value != null ? html`${t("trigger_val", L)}: ${entry.trigger_value}` : nothing} ${entry.reading_value != null ? html`${t("reading_label", L)}: ${entry.reading_value}${ctx.readingUnit ? ` ${ctx.readingUnit}` : ""}${(() => { const d = ctx.readingDelta?.(entry); return d == null ? "" : ` (${d >= 0 ? "+" : ""}${Number(d.toFixed(3))})`; })()}` : nothing}
`; }