278 files
This commit is contained in:
1 parent
d7a0a08372
commit
1b4f5f68c6
278 files changed
+36892
-7150
No files matched your search
+75
-90
@@ -12,13 +12,11 @@ import { property, state } from "lit/decorators.js";
|
||||
import { t, langOf, formatNumber } from "../styles";
|
||||
import type { HomeAssistant, ReadingSlot, ReadingValue } from "../types";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import {
|
||||
MAX_COMPLETION_PHOTOS,
|
||||
discardUploadedPhotos,
|
||||
uploadCompletionPhoto,
|
||||
} from "../helpers/photo-upload";
|
||||
import { PhotoUploadController } from "../helpers/photo-upload-controller";
|
||||
import "./ms-date-field";
|
||||
import "./history-photo";
|
||||
import "./ms-photo-picker";
|
||||
import { photoPickerStyles } from "./ms-photo-picker";
|
||||
|
||||
export interface HistoryEntryDraft {
|
||||
entry_id: string;
|
||||
@@ -79,11 +77,14 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
@state() private _partQty: Record<string, number> = {};
|
||||
private _partQtyOriginal = "";
|
||||
|
||||
// #161: the edited photo list; the original as a JSON key for the
|
||||
// "did it change" check, plus the docs THIS session uploaded so an
|
||||
// abandoned edit can drop them again (a removed pre-existing photo is
|
||||
// only detached — the file stays in the object's documents).
|
||||
@state() private _photos: string[] = [];
|
||||
// #161: the edited photo list (seeded with the entry's photos; the docs
|
||||
// THIS session uploaded are dropped again by an abandoned edit, a removed
|
||||
// pre-existing photo is only detached — the file stays in the object's
|
||||
// documents); the original as a JSON key for the "did it change" check.
|
||||
private readonly _photos = new PhotoUploadController(this, {
|
||||
entryId: () => this._draft?.entry_id ?? "",
|
||||
hass: () => this.hass,
|
||||
});
|
||||
private _photosOriginal = "";
|
||||
|
||||
// #161 phase 2: the reading rows (entry snapshot ∪ task slots) with the
|
||||
@@ -91,8 +92,6 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
@state() private _readingRows: ReadingSlot[] = [];
|
||||
@state() private _readingText: Record<string, string> = {};
|
||||
private _readingsOriginal = "";
|
||||
private _uploadedIds: string[] = [];
|
||||
@state() private _photoUploading = false;
|
||||
|
||||
/** Open the dialog with the given history-entry data. The caller must
|
||||
* pass `original_timestamp` (the entry's current timestamp before edit)
|
||||
@@ -105,10 +104,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
this._partOptions = null;
|
||||
this._partQty = {};
|
||||
this._partQtyOriginal = "";
|
||||
this._photos = [...(draft.photo_doc_ids ?? [])];
|
||||
this._photosOriginal = JSON.stringify(this._photos);
|
||||
this._uploadedIds = [];
|
||||
this._photoUploading = false;
|
||||
this._photos.reset(draft.photo_doc_ids ?? []);
|
||||
this._photosOriginal = JSON.stringify(this._photos.ids);
|
||||
this._seedReadings(draft);
|
||||
void this._loadPartOptions();
|
||||
}
|
||||
@@ -147,41 +144,6 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
return out;
|
||||
}
|
||||
|
||||
private async _onPhotoInput(e: Event): Promise<void> {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const files = Array.from(input.files ?? []);
|
||||
input.value = "";
|
||||
const draft = this._draft;
|
||||
if (files.length === 0 || !draft) return;
|
||||
const room = MAX_COMPLETION_PHOTOS - this._photos.length;
|
||||
const accepted = files.slice(0, Math.max(room, 0));
|
||||
this._photoUploading = true;
|
||||
this._error = "";
|
||||
try {
|
||||
for (const file of accepted) {
|
||||
const id = await uploadCompletionPhoto(this.hass, draft.entry_id, file);
|
||||
this._uploadedIds = [...this._uploadedIds, id];
|
||||
this._photos = [...this._photos, id];
|
||||
}
|
||||
if (files.length > accepted.length) {
|
||||
this._error = t("photos_limit", this._lang).replace("{max}", String(MAX_COMPLETION_PHOTOS));
|
||||
}
|
||||
} catch (err) {
|
||||
const key = err instanceof Error && err.message === "doc_too_large" ? "doc_too_large" : "doc_upload_failed";
|
||||
this._error = t(key, this._lang);
|
||||
} finally {
|
||||
this._photoUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _removePhoto(id: string): void {
|
||||
this._photos = this._photos.filter((x) => x !== id);
|
||||
if (this._uploadedIds.includes(id)) {
|
||||
this._uploadedIds = this._uploadedIds.filter((x) => x !== id);
|
||||
void discardUploadedPhotos(this.hass, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
/** The object's own parts + pooled parts this task draws on — from the
|
||||
* instance-wide overview so pooled owners resolve without extra calls. */
|
||||
private async _loadPartOptions(): Promise<void> {
|
||||
@@ -248,12 +210,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
this._error = "";
|
||||
this._draft = null;
|
||||
this._originalSnapshot = null;
|
||||
if (this._uploadedIds.length > 0) {
|
||||
// Cancelled after uploading: nothing references those files.
|
||||
const orphans = this._uploadedIds;
|
||||
this._uploadedIds = [];
|
||||
void discardUploadedPhotos(this.hass, orphans);
|
||||
}
|
||||
// Cancelled after uploading: nothing references those files.
|
||||
this._photos.discardOrphans();
|
||||
}
|
||||
|
||||
private _set<K extends keyof HistoryEntryDraft>(
|
||||
@@ -263,10 +221,42 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
this._draft = { ...this._draft, [key]: value };
|
||||
}
|
||||
|
||||
/** #170: remove the entry altogether (after a confirm). The backend
|
||||
* re-anchors last_performed on what remains; photos stay in the object's
|
||||
* documents, consumed parts are not restocked. */
|
||||
private async _delete(): Promise<void> {
|
||||
if (!this._draft || !this._originalSnapshot) return;
|
||||
const L = this._lang;
|
||||
if (!window.confirm(t("history_delete_confirm", L))) return;
|
||||
this._saving = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/task/history/delete",
|
||||
entry_id: this._draft.entry_id,
|
||||
task_id: this._draft.task_id,
|
||||
timestamp: this._originalSnapshot.original_timestamp,
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("history-entry-saved", {
|
||||
detail: { entry_id: this._draft.entry_id, task_id: this._draft.task_id, deleted: true },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, L);
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._draft || !this._originalSnapshot) return;
|
||||
this._saving = true;
|
||||
this._error = "";
|
||||
this._photos.clearError();
|
||||
try {
|
||||
const patch: Record<string, unknown> = {
|
||||
type: "maintenance_supporter/task/history/update",
|
||||
@@ -315,8 +305,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
patch.reading_values = map;
|
||||
}
|
||||
// #161: the photo list, only when it differs from what we opened with.
|
||||
if (JSON.stringify(this._photos) !== this._photosOriginal) {
|
||||
patch.photo_doc_ids = [...this._photos];
|
||||
if (JSON.stringify(this._photos.ids) !== this._photosOriginal) {
|
||||
patch.photo_doc_ids = this._photos.ids;
|
||||
}
|
||||
// Nothing changed → close without WS call
|
||||
const changedKeys = Object.keys(patch).filter(
|
||||
@@ -327,7 +317,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
await this.hass.connection.sendMessagePromise(patch);
|
||||
this._uploadedIds = []; // saved — they belong to the entry now
|
||||
this._photos.markAttached(); // saved — they belong to the entry now
|
||||
// Notify upstream so they can refresh
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("history-entry-saved", {
|
||||
@@ -352,10 +342,11 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
if (!this._open || !this._draft) return nothing;
|
||||
const L = this._lang;
|
||||
const d = this._draft;
|
||||
const error = this._error || this._photos.errorText(L);
|
||||
return html`
|
||||
<div class="backdrop" @click=${this.close}></div>
|
||||
<div class="dialog" role="dialog" aria-modal="true">
|
||||
<h2>${t("history_edit_title", L) || "Edit history entry"}</h2>
|
||||
<h2>${t("history_edit_title", L)}</h2>
|
||||
<div class="entry-type">
|
||||
<ha-icon icon="mdi:tag-outline"></ha-icon>
|
||||
<span>${t(d.type, L) || d.type}</span>
|
||||
@@ -365,7 +356,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
required
|
||||
.hass=${this.hass}
|
||||
.lang=${L}
|
||||
.label=${t("history_edit_timestamp", L) || "Timestamp"}
|
||||
.label=${t("history_edit_timestamp", L)}
|
||||
.value=${d.timestamp.slice(0, 19)}
|
||||
@value-changed=${(e: CustomEvent) => {
|
||||
// Always "YYYY-MM-DDTHH:MM:SS" from the field; the naive value
|
||||
@@ -386,7 +377,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
</label>
|
||||
<div class="row">
|
||||
<label>
|
||||
<span>${t("cost", L) || "Cost"}</span>
|
||||
<span>${t("cost", L)}</span>
|
||||
<input type="number" min="0" step="0.01"
|
||||
.value=${d.cost != null ? String(d.cost) : ""}
|
||||
@input=${(e: Event) => {
|
||||
@@ -395,7 +386,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
}} />
|
||||
</label>
|
||||
<label>
|
||||
<span>${t("duration", L) || "Duration (min)"}</span>
|
||||
<span>${t("duration", L)}</span>
|
||||
<input type="number" min="0"
|
||||
.value=${d.duration != null ? String(d.duration) : ""}
|
||||
@input=${(e: Event) => {
|
||||
@@ -434,32 +425,32 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
` : nothing}
|
||||
<div class="photos-block">
|
||||
<span class="parts-title">${t("completion_photos", L)}</span>
|
||||
${this._photos.length > 0 ? html`
|
||||
${this._photos.photos.length > 0 ? html`
|
||||
<div class="photo-strip">
|
||||
${this._photos.map((id) => html`
|
||||
${this._photos.photos.map((p) => html`
|
||||
<div class="photo-tile">
|
||||
<maintenance-history-photo .hass=${this.hass} .docId=${id}></maintenance-history-photo>
|
||||
<maintenance-history-photo .hass=${this.hass} .docId=${p.id}></maintenance-history-photo>
|
||||
<button type="button" class="photo-remove" title=${t("remove", L)}
|
||||
@click=${() => this._removePhoto(id)}>✕</button>
|
||||
@click=${() => this._photos.remove(p.id)}>✕</button>
|
||||
</div>`)}
|
||||
</div>` : nothing}
|
||||
${this._photos.length < MAX_COMPLETION_PHOTOS ? html`
|
||||
<label class="photo-add">
|
||||
<ha-icon icon="mdi:image-plus"></ha-icon>
|
||||
<span>${this._photoUploading ? t("uploading", L) : t("add_photos", L)}</span>
|
||||
<input type="file" accept="image/*" multiple
|
||||
?disabled=${this._photoUploading}
|
||||
@change=${this._onPhotoInput} />
|
||||
</label>` : html`<span class="photos-hint">${t("photos_limit", L).replace("{max}", String(MAX_COMPLETION_PHOTOS))}</span>`}
|
||||
${!this._photos.full
|
||||
? html`<ms-photo-picker .lang=${L} .busy=${this._photos.uploading} .remaining=${this._photos.remaining}
|
||||
@files-picked=${(e: CustomEvent<{ files: File[] }>) => this._photos.addFiles(e.detail.files)}
|
||||
></ms-photo-picker>`
|
||||
: html`<span class="photos-hint">${t("photos_limit", L).replace("{max}", String(this._photos.max))}</span>`}
|
||||
<span class="photos-hint">${t("history_edit_photos_hint", L)}</span>
|
||||
</div>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||
<div class="actions">
|
||||
<button class="delete-entry" @click=${this._delete} ?disabled=${this._saving} title=${t("history_delete_entry", L)}>
|
||||
<ha-icon icon="mdi:delete-outline"></ha-icon> ${t("history_delete_entry", L)}
|
||||
</button>
|
||||
<button class="cancel" @click=${this.close} ?disabled=${this._saving}>
|
||||
${t("cancel", L) || "Cancel"}
|
||||
${t("cancel", L)}
|
||||
</button>
|
||||
<button class="save" @click=${this._save} ?disabled=${this._saving}>
|
||||
${this._saving ? (t("saving", L) || "Saving…") : (t("save", L) || "Save")}
|
||||
${this._saving ? t("saving", L) : t("save", L)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,7 +490,7 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
</label>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
static styles = [photoPickerStyles, css`
|
||||
:host { display: contents; }
|
||||
.backdrop {
|
||||
position: fixed; inset: 0;
|
||||
@@ -536,6 +527,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
width: 100%; box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
.delete-entry { margin-right: auto; color: var(--error-color, #d32f2f); background: transparent; border: 1px solid var(--error-color, #d32f2f); border-radius: 6px; padding: 6px 10px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; }
|
||||
.delete-entry ha-icon { --mdc-icon-size: 18px; }
|
||||
.actions {
|
||||
display: flex; gap: 8px; justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
@@ -601,17 +594,9 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
background: var(--error-color, #db4437); color: #fff;
|
||||
cursor: pointer; font-size: 11px; line-height: 1; padding: 0;
|
||||
}
|
||||
.photo-add {
|
||||
display: inline-flex; flex-direction: row; align-items: center; gap: 8px;
|
||||
width: fit-content; padding: 6px 10px;
|
||||
border: 1px dashed var(--divider-color, #444); border-radius: 8px;
|
||||
cursor: pointer; font-size: 13px; color: var(--secondary-text-color);
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.photo-add:hover { border-color: var(--primary-color); }
|
||||
.photo-add input[type="file"] { display: none; }
|
||||
/* the camera / gallery pickers come from photoPickerStyles (ms-photo-picker) */
|
||||
.photos-hint { font-size: 12px; color: var(--secondary-text-color); }
|
||||
`;
|
||||
`];
|
||||
}
|
||||
|
||||
if (!customElements.get("maintenance-history-edit-dialog")) {
|
||||
|
||||
Reference in new issue
Block a user