257 files
This commit is contained in:
@@ -113,6 +113,8 @@ interface SettingsResponse {
|
||||
/** #173 follow-up: completion notifications — off | automatic | all. */
|
||||
completed?: string;
|
||||
extra_data?: string;
|
||||
/** #44: default | object_name | task_name — was only in the Configure dialog. */
|
||||
title_style?: string;
|
||||
};
|
||||
actions: {
|
||||
complete_enabled: boolean;
|
||||
@@ -132,6 +134,8 @@ interface SettingsResponse {
|
||||
currency_symbol: string;
|
||||
/** Decimal places for every displayed amount (0 = whole numbers). */
|
||||
currency_decimals?: number;
|
||||
/** #104: spare parts count when bought ("purchase") or used ("use"). */
|
||||
parts_cost_mode?: string;
|
||||
};
|
||||
// v2.10.0 archive automation (panel-managed). Optional for forward-compat
|
||||
// with a backend that predates the feature.
|
||||
@@ -183,6 +187,9 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
@state() private _settings: SettingsResponse | null = null;
|
||||
@state() private _loading = true;
|
||||
@state() private _importCsv = "";
|
||||
/** People an import's tasks pointed at that match nobody on this instance
|
||||
* (by name) — their assignments were cleared; shown until the next import. */
|
||||
@state() private _importUnmatched: string[] = [];
|
||||
@state() private _importLoading = false;
|
||||
@state() private _includeHistory = true;
|
||||
@state() private _toast = "";
|
||||
@@ -689,7 +696,6 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
// "groups" target also lands here as the closest match.
|
||||
const items: { key: keyof AdvancedFeatures; settingKey: string; label: string; desc: string }[] = [
|
||||
{ key: "adaptive", settingKey: "advanced_adaptive_visible", label: t("feat_adaptive", L), desc: t("feat_adaptive_desc", L) },
|
||||
{ key: "predictions", settingKey: "advanced_predictions_visible", label: t("feat_predictions", L), desc: t("feat_predictions_desc", L) },
|
||||
{ key: "seasonal", settingKey: "advanced_seasonal_visible", label: t("feat_seasonal", L), desc: t("feat_seasonal_desc", L) },
|
||||
{ key: "environmental", settingKey: "advanced_environmental_visible", label: t("feat_environmental", L), desc: t("feat_environmental_desc", L) },
|
||||
{ key: "budget", settingKey: "advanced_budget_visible", label: t("feat_budget", L), desc: t("feat_budget_desc", L) },
|
||||
@@ -1056,6 +1062,15 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</select>
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_currency_decimals_hint", L)}</div>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_parts_cost_mode", L)}</span>
|
||||
<select class="parts-cost-mode" .value=${live(b.parts_cost_mode === "use" ? "use" : "purchase")}
|
||||
@change=${(e: Event) => this._updateSetting("parts_cost_mode", (e.target as HTMLSelectElement).value)}>
|
||||
<option value="purchase" ?selected=${b.parts_cost_mode !== "use"}>${t("settings_parts_cost_purchase", L)}</option>
|
||||
<option value="use" ?selected=${b.parts_cost_mode === "use"}>${t("settings_parts_cost_use", L)}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_parts_cost_mode_hint", L)}</div>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_panel_enabled", L)}</span>
|
||||
<input type="checkbox" .checked=${g.panel_enabled}
|
||||
@@ -1093,7 +1108,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">${t("test_notification", L)}</span>
|
||||
<button class="ha-button secondary"
|
||||
?disabled=${!g.notify_service || this._testingNotification}
|
||||
?disabled=${(!g.notify_service && !this._settings?.notifications?.event_only) || this._testingNotification}
|
||||
@click=${() => this._sendTestNotification()}>
|
||||
${this._testingNotification ? t("testing", L) : t("send_test", L)}
|
||||
</button>
|
||||
@@ -1273,6 +1288,16 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
</select>
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_notify_completed_hint", L)}</div>
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_title_style", L)}</span>
|
||||
<select class="notify-title-style" .value=${live(n.title_style || "default")}
|
||||
@change=${(e: Event) => this._updateSetting("notification_title_style", (e.target as HTMLSelectElement).value)}>
|
||||
<option value="default">${t("title_style_default", L)}</option>
|
||||
<option value="object_name">${t("title_style_object_name", L)}</option>
|
||||
<option value="task_name">${t("title_style_task_name", L)}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_title_style_hint", L)}</div>
|
||||
|
||||
<div class="notify-rule">
|
||||
<h4 style="margin: 16px 0 8px; font-size: 14px;">${t("settings_notify_rule", L)}</h4>
|
||||
@@ -2015,6 +2040,11 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
${this._importLoading ? "…" : t("settings_import_btn", L)}
|
||||
</button>
|
||||
</div>
|
||||
${this._importUnmatched.length
|
||||
? html`<div class="import-notice">
|
||||
${t("settings_import_unmatched_users", L).replace("{names}", this._importUnmatched.join(", "))}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -2101,7 +2131,8 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
// (JSON `{`/`[` or YAML `version:`) goes to the structured importer,
|
||||
// which parses JSON and YAML alike.
|
||||
const isCsv = content.startsWith("object_name");
|
||||
const result = await this._ws<{ created: number }>(
|
||||
this._importUnmatched = [];
|
||||
const result = await this._ws<{ created: number; unmatched_users?: string[] }>(
|
||||
isCsv
|
||||
? { type: "maintenance_supporter/csv/import", csv_content: content }
|
||||
: { type: "maintenance_supporter/json/import", json_content: content },
|
||||
@@ -2111,6 +2142,7 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
if (!result) return;
|
||||
const count = result.created ?? 0;
|
||||
this._showToast(t("settings_import_success", this._lang).replace("{count}", String(count)));
|
||||
this._importUnmatched = Array.isArray(result.unmatched_users) ? result.unmatched_users : [];
|
||||
this._importCsv = "";
|
||||
this.dispatchEvent(new CustomEvent("settings-changed"));
|
||||
}
|
||||
@@ -2143,13 +2175,18 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
const result = await postMultipart<{ blobs_written: number; documents_created: number }>(
|
||||
this.hass, "/api/maintenance_supporter/documents/archive", form, "docs_archive_too_large",
|
||||
const result = await postMultipart<{ blobs_written: number; documents_created: number; files_missing?: number }>(
|
||||
this.hass, "/api/maintenance_supporter/documents/archive", form, "docs_archive_too_large", ["docs_archive_no_manifest"],
|
||||
);
|
||||
const restored = t("settings_docs_import_success", this._lang)
|
||||
.replace("{blobs}", String(result.blobs_written ?? 0))
|
||||
.replace("{docs}", String(result.documents_created ?? 0));
|
||||
// Documents whose file the archive did not carry are not created —
|
||||
// said, so a file left out when zipping again is noticed.
|
||||
this._showToast(
|
||||
t("settings_docs_import_success", this._lang)
|
||||
.replace("{blobs}", String(result.blobs_written ?? 0))
|
||||
.replace("{docs}", String(result.documents_created ?? 0))
|
||||
result.files_missing
|
||||
? `${restored} · ${t("settings_docs_import_missing", this._lang).replace("{n}", String(result.files_missing))}`
|
||||
: restored,
|
||||
);
|
||||
this.dispatchEvent(new CustomEvent("settings-changed"));
|
||||
} catch (e) {
|
||||
@@ -2158,7 +2195,9 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
e instanceof Error && e.message === "docs_archive_too_large"
|
||||
? t("docs_archive_too_large", this._lang)
|
||||
.replace("{max}", `${formatNumber(DOCS_ARCHIVE_MAX_BYTES / (1024 * 1024), this._lang)} MB`)
|
||||
: t("action_error", this._lang),
|
||||
: e instanceof Error && e.message === "docs_archive_no_manifest"
|
||||
? t("docs_archive_no_manifest", this._lang)
|
||||
: t("action_error", this._lang),
|
||||
);
|
||||
}
|
||||
input.value = "";
|
||||
@@ -2168,6 +2207,12 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
// --- Styles ---
|
||||
|
||||
static styles = [personStyles, css`
|
||||
.import-notice {
|
||||
margin-top: 8px; padding: 8px 12px; border-radius: 8px;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid rgba(255, 152, 0, 0.35);
|
||||
font-size: 13.5px; line-height: 1.45;
|
||||
}
|
||||
.bn-note {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
margin: 6px 0 10px; padding: 10px 12px; border-radius: 8px;
|
||||
@@ -2331,6 +2376,13 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
.settings-actions {
|
||||
display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;
|
||||
}
|
||||
/* Heading, hint and buttons stacked — as a row the hint landed between
|
||||
the heading and the buttons, reading like a caption for neither. */
|
||||
.docs-archive-block {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.docs-archive-block h4, .docs-archive-block .section-desc { margin: 0; }
|
||||
.settings-actions button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
@@ -2443,7 +2495,9 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
}
|
||||
.vac-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
/* The date fields carry a typing toggle and a clear button (2 × 36px)
|
||||
next to the picker — 160px left the date itself cut to "11/1". */
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user