257 files
This commit is contained in:
+103
-3
@@ -6,7 +6,8 @@
|
||||
*
|
||||
* • Quick info — name + status + next due + last performed + interval
|
||||
* • Primary actions — Complete (existing dialog), Skip (inline), Reset (inline)
|
||||
* • Secondary (admin) — Edit settings (existing dialog), QR Code, Delete
|
||||
* • Secondary — QR Code, Postpone (inline), Snooze for everyone; Edit
|
||||
* settings (existing dialog), Archive, Delete for writers
|
||||
* • Footer — "Open in Maintenance Panel" deep-link for History/Statistics
|
||||
*
|
||||
* Mounted via dialog-mount.openTaskQuickActions(entryId, taskId).
|
||||
@@ -16,16 +17,19 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, formatDate, formatInterval, formatRecurrence, formatCost, formatDuration, currencySymbolOf, langOf, syncCurrencyDecimals} from "../styles";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { snoozedMessage, type SnoozeResult } from "../helpers/snooze";
|
||||
import { focusModalShell, modalShellStyles, renderModalShell } from "../helpers/modal-shell";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { renderNotesMarkdown } from "../helpers/notes-markdown";
|
||||
import { buildHistoryEntryDraft } from "../helpers/history-draft";
|
||||
import { readingSlotDelta } from "../helpers/reading-slots";
|
||||
import { taskRef } from "../helpers/reference";
|
||||
import { newestFirst, renderHistoryEntry } from "../renderers/history";
|
||||
import { renderStatusBadge, statusColor } from "../renderers/status";
|
||||
import { fetchSettingsOnce } from "../helpers/settings-cache";
|
||||
import type { PartsCostMode } from "../helpers/parts-cost";
|
||||
import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions";
|
||||
import { confirmAction } from "../helpers/confirm";
|
||||
import { ToastTimer } from "../helpers/toast";
|
||||
@@ -65,12 +69,14 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
@state() private _error = "";
|
||||
@state() private _showSkip = false;
|
||||
@state() private _showReset = false;
|
||||
@state() private _showPostpone = false;
|
||||
@state() private _showDetails = false;
|
||||
@state() private _showAdaptive = false;
|
||||
@state() private _skipReason = "";
|
||||
@state() private _resetDate = "";
|
||||
@state() private _postponeDate = "";
|
||||
@state() private _features: AdvancedFeatures = {
|
||||
adaptive: false, predictions: false, seasonal: false, environmental: false,
|
||||
adaptive: false, seasonal: false, environmental: false,
|
||||
budget: false, groups: false, checklists: false, schedule_time: false,
|
||||
completion_actions: false,
|
||||
};
|
||||
@@ -98,8 +104,10 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._showSkip = false;
|
||||
this._showReset = false;
|
||||
this._showPostpone = false;
|
||||
this._showAdaptive = false;
|
||||
this._skipReason = "";
|
||||
this._postponeDate = "";
|
||||
// Local calendar date — toISOString() is UTC and prefills YESTERDAY for
|
||||
// users east of UTC before their morning (bug audit 2026-08-22).
|
||||
this._resetDate = isoDateLocal(new Date());
|
||||
@@ -120,8 +128,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
this._access = settings.access;
|
||||
this._currencySymbol = currencySymbolOf(settings.budget ?? undefined);
|
||||
syncCurrencyDecimals(settings.budget ?? undefined);
|
||||
this._partsCostMode = settings.partsCostMode;
|
||||
}
|
||||
|
||||
/** #104: when spare parts count (the complete dialog explains them). */
|
||||
@state() private _partsCostMode: PartsCostMode = "purchase";
|
||||
|
||||
public close(): void {
|
||||
this._loadSeq++; // a load still in flight belongs to the closed dialog
|
||||
this._open = false;
|
||||
@@ -221,6 +233,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
// feedback exactly like the panel (DRY audit 2026-09-26).
|
||||
features: this._features,
|
||||
currencySymbol: this._currencySymbol,
|
||||
partsCostMode: this._partsCostMode,
|
||||
}),
|
||||
);
|
||||
if (ok) {
|
||||
@@ -258,6 +271,43 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Postpone and Snooze are household actions like Skip and Reset
|
||||
* (helpers/permissions HOUSEHOLD_ACTIONS) — the panel's ⋮ menu offered
|
||||
* them, the dashboard's dialog did not (audit 2026-09-29). */
|
||||
private async _onPostponeConfirm(): Promise<void> {
|
||||
if (!this._entryId || !this._taskId || !this._postponeDate) return;
|
||||
const ok = await this._runWs({
|
||||
type: "maintenance_supporter/task/postpone",
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
until: this._postponeDate,
|
||||
});
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("postpone");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async _onSnooze(): Promise<void> {
|
||||
if (!this._entryId || !this._taskId) return;
|
||||
const ok = await this._runWs<SnoozeResult>({
|
||||
type: "maintenance_supporter/task/snooze",
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("snooze");
|
||||
// #193: the dialog closes, so Home Assistant's own toast says for how
|
||||
// long (a card has no toast of its own).
|
||||
this.dispatchEvent(new CustomEvent("hass-notification", {
|
||||
detail: { message: snoozedMessage(ok, this._lang) },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private _onEdit(): void {
|
||||
if (!this._entryId || !this._taskId) return;
|
||||
import("../dialog-mount").then(({ openEditTaskDialog }) => {
|
||||
@@ -365,7 +415,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
this._showToast(r?.recommended_interval
|
||||
// The analyzer always works in DAYS (helpers/interval_analyzer.py), so
|
||||
// the unit is pinned here rather than taken from the task's own unit.
|
||||
? `${t("reanalyze_result", this._lang)}: ${formatInterval(r.recommended_interval, "days", this._lang)} (${r.data_points} pts)`
|
||||
? `${t("reanalyze_result", this._lang)}: ${formatInterval(r.recommended_interval, "days", this._lang)} (${r.data_points} ${t("data_points", this._lang)})`
|
||||
: t("reanalyze_insufficient_data", this._lang));
|
||||
await this._loadTask();
|
||||
}
|
||||
@@ -517,6 +567,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
const L = this._lang;
|
||||
const task = this._task;
|
||||
const writer = canWrite(this.hass?.user, this._access);
|
||||
// The server refuses to postpone an archived, disabled or paused task,
|
||||
// and such a task sends no reminders to snooze.
|
||||
const inert = !!task && (!!task.archived || task.enabled === false || task.status === "paused");
|
||||
|
||||
return renderModalShell(() => this.close(), html`
|
||||
${task
|
||||
@@ -550,6 +603,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
? html`<span><strong>${t("phase_current", L)}:</strong> ${phaseLabel(task)}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${task.notes
|
||||
? html`<div class="notes-body">${renderNotesMarkdown(task.notes)}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${this._error
|
||||
@@ -593,6 +649,28 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: this._showPostpone
|
||||
? html`
|
||||
<div class="inline-form">
|
||||
<label>${t("postpone_date_prompt", L)}</label>
|
||||
<ms-date-field
|
||||
kind="date"
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.value=${this._postponeDate}
|
||||
@value-changed=${(e: CustomEvent) => { this._postponeDate = e.detail.value as string; }}
|
||||
></ms-date-field>
|
||||
<div class="inline-actions">
|
||||
<button class="btn cancel" @click=${() => { this._showPostpone = false; }} ?disabled=${this._busy}>
|
||||
${t("cancel", L)}
|
||||
</button>
|
||||
<button class="btn primary qa-postpone-confirm" @click=${this._onPostponeConfirm}
|
||||
?disabled=${this._busy || !this._postponeDate}>
|
||||
${t("postpone", L)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="actions primary-row">
|
||||
<ha-button appearance="accent" variant="success" @click=${this._onComplete} .disabled=${this._busy}>
|
||||
@@ -626,6 +704,17 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
<ha-icon slot="start" icon="mdi:qrcode"></ha-icon>
|
||||
${t("qr_code", L)}
|
||||
</ha-button>
|
||||
${inert
|
||||
? nothing
|
||||
: html`<ha-button size="small" appearance="outlined" variant="neutral" class="qa-postpone"
|
||||
@click=${() => { this._showPostpone = true; }} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:calendar-arrow-right"></ha-icon>
|
||||
${t("postpone", L)}…
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="outlined" variant="neutral" class="qa-snooze" @click=${this._onSnooze} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:bell-sleep-outline"></ha-icon>
|
||||
${t("snooze", L)}
|
||||
</ha-button>`}
|
||||
${writer
|
||||
? html`<ha-button size="small" appearance="outlined" variant="neutral" class="qa-archive"
|
||||
@click=${task.archived ? this._onUnarchive : this._onArchive}
|
||||
@@ -706,6 +795,11 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
.actions.secondary-row {
|
||||
padding-top: 8px; border-top: 1px solid var(--divider-color);
|
||||
justify-content: flex-start;
|
||||
/* 360 px: Edit + QR + Archive + Delete were 388 px in a 342 px row —
|
||||
Delete sat past the dialog edge (audit 2026-09-29). Wrap instead;
|
||||
Delete keeps its margin-left:auto on the second line. */
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
}
|
||||
.actions.secondary-row .btn.danger,
|
||||
.actions.secondary-row ha-button.danger {
|
||||
@@ -734,6 +828,12 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
.btn.danger { color: var(--error-color); }
|
||||
.btn ha-icon { --mdc-icon-size: 18px; }
|
||||
.inline-form { display: flex; flex-direction: column; gap: 8px; }
|
||||
.notes-body {
|
||||
margin-top: 8px; padding: 8px; border-radius: 6px;
|
||||
background: var(--secondary-background-color);
|
||||
font-size: 13px; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.notes-body ha-markdown { white-space: normal; }
|
||||
.inline-form label { font-size: 13px; color: var(--secondary-text-color); }
|
||||
.inline-form input {
|
||||
padding: 8px; font-size: 14px;
|
||||
|
||||
Reference in New Issue
Block a user