124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
/** 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`
|
|
<div class="history-filters-new">
|
|
<div class="filter-chips">
|
|
${_FILTER_TYPES.map((type) => {
|
|
const count = task.history.filter((h) => h.type === type).length;
|
|
if (count === 0) return nothing;
|
|
return html`
|
|
<span class="filter-chip ${ctx.filter === type ? "active" : ""}"
|
|
@click=${() => ctx.setFilter(ctx.filter === type ? null : type)}>
|
|
${t(type, L)} (${count})
|
|
</span>
|
|
`;
|
|
})}
|
|
${ctx.filter ? html`<span class="filter-chip clear" @click=${() => ctx.setFilter(null)}>${t("show_all", L)}</span>` : nothing}
|
|
</div>
|
|
<div class="filter-controls">
|
|
<input type="text" class="search-input" placeholder="${t("search_notes", L)}..." .value=${ctx.search} @input=${(e: Event) => ctx.setSearch((e.target as HTMLInputElement).value)} />
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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`<p class="empty">${t("no_history", L)}</p>`;
|
|
}
|
|
|
|
return html`
|
|
<div class="history-timeline">
|
|
${[...filtered].reverse().map((entry: HistoryEntry) => renderHistoryEntry(entry, ctx))}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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`
|
|
<div class="history-entry">
|
|
<div class="history-icon ${entry.type}">
|
|
<ha-icon .icon=${STATUS_ICONS[entry.type] || "mdi:circle"}></ha-icon>
|
|
</div>
|
|
<div class="history-content">
|
|
<div class="history-row">
|
|
<strong>${t(entry.type, L)}</strong>
|
|
${entry.auto ? html`<span class="history-auto-badge">${t("history_auto", L)}</span>` : nothing}
|
|
${editable
|
|
? html`<button class="history-edit-btn"
|
|
title=${t("history_edit_button", L) || "Edit entry"}
|
|
@click=${() => ctx.openEdit(entry)}>
|
|
<ha-icon icon="mdi:pencil"></ha-icon>
|
|
</button>`
|
|
: nothing}
|
|
</div>
|
|
<div class="history-date">${formatDateTime(entry.timestamp, L)}</div>
|
|
${entry.notes ? html`<div>${entry.notes}</div>` : nothing}
|
|
${entry.photo_doc_id
|
|
? html`<maintenance-history-photo .hass=${ctx.hass} .docId=${entry.photo_doc_id}></maintenance-history-photo>`
|
|
: nothing}
|
|
<div class="history-details">
|
|
${entry.cost != null ? html`<span>${t("cost", L)}: ${entry.cost.toFixed(2)} ${ctx.currencySymbol}</span>` : nothing}
|
|
${entry.duration != null ? html`<span>${t("duration", L)}: ${entry.duration} min</span>` : nothing}
|
|
${entry.trigger_value != null ? html`<span>${t("trigger_val", L)}: ${entry.trigger_value}</span>` : nothing}
|
|
${entry.reading_value != null
|
|
? html`<span>${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))})`;
|
|
})()}</span>`
|
|
: nothing}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|