113 files
This commit is contained in:
+6
-1
@@ -48,8 +48,13 @@ describe("budget KPI tiles: spent-only display (#104)", () => {
|
||||
const tiles = sr(el).querySelectorAll(".budget-tile");
|
||||
expect(tiles.length).to.equal(2);
|
||||
expect(sr(el).querySelectorAll(".budget-tile-bar").length).to.equal(1);
|
||||
expect(tiles[0].textContent).to.contain("9.00 / 150 €");
|
||||
// 2026-08-24: the spent amount carries the full stat-value typography
|
||||
// (same size as the other KPI chips); the "/ max" ratio is its own
|
||||
// small line so it can't overflow the grid cell.
|
||||
expect(tiles[0].querySelector(".stat-value")!.textContent).to.contain("9.00 €");
|
||||
expect(tiles[0].querySelector(".budget-tile-max")!.textContent).to.contain("/ 150 €");
|
||||
expect(tiles[1].textContent).to.contain("429.60 €");
|
||||
expect(tiles[1].querySelector(".budget-tile-max"), "spent-only tile has no ratio line").to.equal(null);
|
||||
});
|
||||
|
||||
it("tiles live INSIDE the stats strip (#125)", async () => {
|
||||
|
||||
@@ -99,6 +99,25 @@ describe("trigger-chart", () => {
|
||||
const marks = el.shadowRoot!.querySelectorAll('rect[fill="var(--success-color, #4caf50)"]');
|
||||
expect(marks.length).to.equal(1);
|
||||
});
|
||||
|
||||
it("renders the production-shaped projection with real horizontal extent", async () => {
|
||||
// sparkline.ts builds the projection as [last sample, last sample + 30d].
|
||||
// Before the domain fix (2026-08-24) the data-only time domain put that
|
||||
// start on the right plot edge and the x2 clamp collapsed the dashed
|
||||
// line to zero width — the degradation projection never rendered.
|
||||
const el = await mount();
|
||||
const last = POINTS[POINTS.length - 1];
|
||||
el.projection = [last, { ts: last.ts + 30 * DAY, val: last.val + 15 }];
|
||||
await el.updateComplete;
|
||||
const line = el.shadowRoot!.querySelector('line[stroke-dasharray="4,3"]');
|
||||
expect(line, "projection line rendered").to.exist;
|
||||
const x1 = Number(line!.getAttribute("x1"));
|
||||
const x2 = Number(line!.getAttribute("x2"));
|
||||
expect(x2 - x1, `projection width (${x1} -> ${x2})`).to.be.greaterThan(50);
|
||||
// still clamped inside the plot
|
||||
const svgW = Number(el.shadowRoot!.querySelector("svg")!.getAttribute("width"));
|
||||
expect(x2).to.be.at.most(svgW);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chart-utils", () => {
|
||||
|
||||
+5
-2
@@ -76,8 +76,11 @@ export class MaintenanceBudgetSectionCard extends LitElement {
|
||||
this._busy = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const m = parseFloat(this._localMonthly);
|
||||
const y = parseFloat(this._localYearly);
|
||||
// An emptied field means "remove this budget" and must SEND 0 (the
|
||||
// backend's off-state) — omitting the key kept the old value, so a
|
||||
// budget could never be cleared from this card (bug audit 2026-08-22).
|
||||
const m = this._localMonthly.trim() === "" ? 0 : parseFloat(this._localMonthly);
|
||||
const y = this._localYearly.trim() === "" ? 0 : parseFloat(this._localYearly);
|
||||
const settings: Record<string, number> = {};
|
||||
if (!isNaN(m) && m >= 0) settings.budget_monthly = m;
|
||||
if (!isNaN(y) && y >= 0) settings.budget_yearly = y;
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
|
||||
private _onAddTask(): void {
|
||||
if (!this._entryId) return;
|
||||
import("../dialog-mount").then(({ openCreateTaskDialog }) => {
|
||||
openCreateTaskDialog();
|
||||
openCreateTaskDialog(this._entryId!);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
+32
-6
@@ -16,6 +16,8 @@ import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatInterval, formatRecurrence, langOf } from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { partsForCompletion } from "../helpers/shared-parts";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||
@@ -75,7 +77,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
this._showReset = false;
|
||||
this._showAdaptive = false;
|
||||
this._skipReason = "";
|
||||
this._resetDate = new Date().toISOString().slice(0, 10);
|
||||
// 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());
|
||||
this._open = true;
|
||||
await Promise.all([this._loadTask(), this._loadFeatures()]);
|
||||
}
|
||||
@@ -146,14 +150,36 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
|
||||
private _onComplete(): void {
|
||||
if (!this._entryId || !this._taskId || !this._task) return;
|
||||
// Reuse the existing rich complete-dialog by mounting it on body
|
||||
import("../dialog-mount").then(({ openCompleteDialog }) => {
|
||||
// Reuse the existing rich complete-dialog by mounting it on body.
|
||||
// Pass EVERYTHING the card's direct path passes — omitting
|
||||
// required_completion_fields let a mandatory note be skipped, and a
|
||||
// reading task without type+unit never rendered its value field
|
||||
// (bug audit 2026-08-22).
|
||||
import("../dialog-mount").then(async ({ openCompleteDialog }) => {
|
||||
const task = this._task!;
|
||||
const isBuy = !!(task as { part_ref?: string }).part_ref;
|
||||
let parts: Parameters<typeof openCompleteDialog>[0]["parts"] = [];
|
||||
if (!isBuy) {
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
objects: MaintenanceObjectResponse[];
|
||||
}>({ type: "maintenance_supporter/objects", compact: true });
|
||||
parts = partsForCompletion(task, this._entryId!, r.objects || [], this._lang);
|
||||
} catch {
|
||||
// Parts stay empty — the dialog still completes without them.
|
||||
}
|
||||
}
|
||||
const ok = openCompleteDialog({
|
||||
entry_id: this._entryId!,
|
||||
task_id: this._taskId!,
|
||||
task_name: this._task!.name,
|
||||
checklist: this._task!.checklist || [],
|
||||
adaptive_enabled: !!this._task!.adaptive_config?.enabled,
|
||||
task_name: task.name,
|
||||
checklist: task.checklist || [],
|
||||
adaptive_enabled: !!task.adaptive_config?.enabled,
|
||||
required_completion_fields: task.required_completion_fields || [],
|
||||
task_type: task.type || "",
|
||||
reading_unit: (task as { reading_unit?: string }).reading_unit || "",
|
||||
parts,
|
||||
consumes_parts: isBuy ? [] : (task.consumes_parts || []),
|
||||
});
|
||||
if (ok) {
|
||||
this._notifyChanged("complete");
|
||||
|
||||
@@ -156,7 +156,14 @@ export class MaintenanceTriggerChart extends LitElement {
|
||||
}
|
||||
|
||||
const tsMin = pts[0].ts;
|
||||
const tsMax = pts[pts.length - 1].ts;
|
||||
// The dashed projection extends the TIME domain. sparkline.ts builds it
|
||||
// as [last sample, last sample + 30 d]; with a data-only domain its
|
||||
// start sat exactly on the right plot edge and the x2 clamp below
|
||||
// collapsed the line to zero width — the production degradation
|
||||
// projection never rendered (found via the design-system previews,
|
||||
// 2026-08-24).
|
||||
const projEnd = this.projection && this.projection.length === 2 ? this.projection[1].ts : null;
|
||||
const tsMax = projEnd != null ? Math.max(pts[pts.length - 1].ts, projEnd) : pts[pts.length - 1].ts;
|
||||
const tsSpan = tsMax - tsMin || 1;
|
||||
const withYear = needsYear(tsMin, tsMax);
|
||||
|
||||
|
||||
@@ -167,7 +167,10 @@ export function openEditObjectDialog(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function openCreateTaskDialog(): boolean {
|
||||
export function openCreateTaskDialog(
|
||||
entryId = "",
|
||||
objects?: Array<{ entry_id: string; object: { name: string } }>,
|
||||
): boolean {
|
||||
const dlg = getOrCreate<MaintenanceTaskDialog>(TASK_DIALOG_TAG);
|
||||
if (!syncHass(dlg)) return false;
|
||||
const hass = getHass();
|
||||
@@ -179,13 +182,20 @@ export function openCreateTaskDialog(): boolean {
|
||||
scheduleTimeEnabled: boolean;
|
||||
completionActionsEnabled: boolean;
|
||||
defaultWarningDays: number;
|
||||
openCreate: (entryId?: string) => void;
|
||||
openCreate: (
|
||||
entryId: string,
|
||||
objects?: Array<{ entry_id: string; object: { name: string } }>,
|
||||
) => void;
|
||||
};
|
||||
dlgFull.checklistsEnabled = settings.features.checklists;
|
||||
dlgFull.scheduleTimeEnabled = settings.features.schedule_time;
|
||||
dlgFull.completionActionsEnabled = settings.features.completion_actions;
|
||||
dlgFull.defaultWarningDays = settings.defaultWarningDays;
|
||||
dlgFull.openCreate();
|
||||
// openCreate NEEDS a target: a bare call left _entryId undefined and
|
||||
// _objectChoices empty — no object picker, and save failed on the
|
||||
// backend's required entry_id (bug audit 2026-08-22). Callers pass the
|
||||
// entry (quick-actions) or their object list (card header button).
|
||||
dlgFull.openCreate(entryId, objects);
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
@@ -273,6 +283,12 @@ export function openCompleteDialog(args: {
|
||||
adaptive_enabled?: boolean;
|
||||
/** Details the task demands before it counts as done (v2.44). */
|
||||
required_completion_fields?: string[];
|
||||
/** Reading tasks need type+unit or the value field never renders. */
|
||||
task_type?: string;
|
||||
reading_unit?: string;
|
||||
/** #99/#111: per-completion parts selection incl. shared pools. */
|
||||
parts?: MaintenanceCompleteDialog["parts"];
|
||||
consumes_parts?: MaintenanceCompleteDialog["consumesParts"];
|
||||
}): boolean {
|
||||
const dlg = getOrCreate<MaintenanceCompleteDialog>(COMPLETE_DIALOG_TAG);
|
||||
if (!syncHass(dlg)) return false;
|
||||
@@ -282,6 +298,12 @@ export function openCompleteDialog(args: {
|
||||
dlg.checklist = args.checklist ?? [];
|
||||
dlg.adaptiveEnabled = !!args.adaptive_enabled;
|
||||
dlg.requiredFields = args.required_completion_fields ?? [];
|
||||
// Always assign — the dialog is a singleton, so an omitted field must not
|
||||
// leak the previous task's value.
|
||||
dlg.taskType = args.task_type ?? "";
|
||||
dlg.readingUnit = args.reading_unit ?? "";
|
||||
dlg.parts = args.parts ?? [];
|
||||
dlg.consumesParts = args.consumes_parts ?? [];
|
||||
dlg.lang = (getHass()?.language) || "en";
|
||||
dlg.open();
|
||||
return true;
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Výchozí filtr uživatele",
|
||||
"cal_editor_my_tasks": "Moje úkoly (aktuální uživatel)",
|
||||
"cal_editor_show_object_filter": "Zobrazit filtr objektu",
|
||||
"cal_editor_object_hint": "Předvyberte objekt přes YAML: object_filter: \"<název>\" — nebo seznam názvů pro omezení karty na více objektů."
|
||||
"cal_editor_object_hint": "Předvyberte objekt přes YAML: object_filter: \"<název>\" — nebo seznam názvů pro omezení karty na více objektů.",
|
||||
"object_history_section": "Historie (všechny úkoly)",
|
||||
"object_history_all_tasks": "Všechny úkoly",
|
||||
"object_history_empty": "V tomto období nejsou žádné záznamy.",
|
||||
"object_history_cap_note": "Historie uchovává až 500 záznamů na úkol — velmi staré záznamy mohou chybět.",
|
||||
"service_record_title": "Servisní knížka",
|
||||
"service_record_print": "Servisní knížka (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "záznamů",
|
||||
"completed_by": "Dokončil",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Standard brugerfilter",
|
||||
"cal_editor_my_tasks": "Mine opgaver (aktuel bruger)",
|
||||
"cal_editor_show_object_filter": "Vis objektfilter",
|
||||
"cal_editor_object_hint": "Forvælg et objekt via YAML: object_filter: \"<navn>\" — eller en liste af navne for at begrænse kortet til flere objekter."
|
||||
"cal_editor_object_hint": "Forvælg et objekt via YAML: object_filter: \"<navn>\" — eller en liste af navne for at begrænse kortet til flere objekter.",
|
||||
"object_history_section": "Historik (alle opgaver)",
|
||||
"object_history_all_tasks": "Alle opgaver",
|
||||
"object_history_empty": "Ingen poster i denne periode.",
|
||||
"object_history_cap_note": "Historikken gemmer op til 500 poster pr. opgave — meget gamle poster kan mangle.",
|
||||
"service_record_title": "Servicebog",
|
||||
"service_record_print": "Servicebog (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "poster",
|
||||
"completed_by": "Udført af",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Standard-Benutzerfilter",
|
||||
"cal_editor_my_tasks": "Meine Aufgaben (aktueller Benutzer)",
|
||||
"cal_editor_show_object_filter": "Objektfilter-Dropdown anzeigen",
|
||||
"cal_editor_object_hint": "Ein Objekt per YAML vorauswählen: object_filter: \"<Objektname>\" — oder eine Namensliste, um die Karte auf mehrere Objekte zu beschränken."
|
||||
"cal_editor_object_hint": "Ein Objekt per YAML vorauswählen: object_filter: \"<Objektname>\" — oder eine Namensliste, um die Karte auf mehrere Objekte zu beschränken.",
|
||||
"object_history_section": "Verlauf (alle Aufgaben)",
|
||||
"object_history_all_tasks": "Alle Aufgaben",
|
||||
"object_history_empty": "Keine Einträge in diesem Zeitraum.",
|
||||
"object_history_cap_note": "Der Verlauf umfasst bis zu 500 Einträge pro Aufgabe — sehr alte Einträge können fehlen.",
|
||||
"service_record_title": "Serviceheft",
|
||||
"service_record_print": "Serviceheft (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "Einträge",
|
||||
"completed_by": "Erledigt von",
|
||||
"date_from": "Von",
|
||||
"date_to": "Bis"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Default user filter",
|
||||
"cal_editor_my_tasks": "My tasks (current user)",
|
||||
"cal_editor_show_object_filter": "Show object filter dropdown",
|
||||
"cal_editor_object_hint": "Pre-select one object via YAML: object_filter: \"<object name>\" — or a list of names to restrict the card to several objects."
|
||||
"cal_editor_object_hint": "Pre-select one object via YAML: object_filter: \"<object name>\" — or a list of names to restrict the card to several objects.",
|
||||
"object_history_section": "History (all tasks)",
|
||||
"object_history_all_tasks": "All tasks",
|
||||
"object_history_empty": "No entries in this range.",
|
||||
"object_history_cap_note": "History keeps up to 500 entries per task — very old entries may be missing.",
|
||||
"service_record_title": "Service record",
|
||||
"service_record_print": "Service record (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entries",
|
||||
"completed_by": "Completed by",
|
||||
"date_from": "From",
|
||||
"date_to": "To"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Filtro de usuario predeterminado",
|
||||
"cal_editor_my_tasks": "Mis tareas (usuario actual)",
|
||||
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
|
||||
"cal_editor_object_hint": "Preselecciona un objeto por YAML: object_filter: \"<nombre>\" — o una lista de nombres para limitar la tarjeta a varios objetos."
|
||||
"cal_editor_object_hint": "Preselecciona un objeto por YAML: object_filter: \"<nombre>\" — o una lista de nombres para limitar la tarjeta a varios objetos.",
|
||||
"object_history_section": "Historial (todas las tareas)",
|
||||
"object_history_all_tasks": "Todas las tareas",
|
||||
"object_history_empty": "No hay entradas en este periodo.",
|
||||
"object_history_cap_note": "El historial conserva hasta 500 entradas por tarea; las entradas muy antiguas pueden faltar.",
|
||||
"service_record_title": "Registro de mantenimiento",
|
||||
"service_record_print": "Registro de mantenimiento (PDF)",
|
||||
"date": "Fecha",
|
||||
"service_record_entries": "entradas",
|
||||
"completed_by": "Realizado por",
|
||||
"date_from": "Desde",
|
||||
"date_to": "Hasta"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Oletuskäyttäjäsuodatin",
|
||||
"cal_editor_my_tasks": "Omat tehtävät (nykyinen käyttäjä)",
|
||||
"cal_editor_show_object_filter": "Näytä kohdesuodatin",
|
||||
"cal_editor_object_hint": "Esivalitse kohde YAML:lla: object_filter: \"<nimi>\" — tai nimilista rajataksesi kortin useisiin kohteisiin."
|
||||
"cal_editor_object_hint": "Esivalitse kohde YAML:lla: object_filter: \"<nimi>\" — tai nimilista rajataksesi kortin useisiin kohteisiin.",
|
||||
"object_history_section": "Historia (kaikki tehtävät)",
|
||||
"object_history_all_tasks": "Kaikki tehtävät",
|
||||
"object_history_empty": "Ei merkintöjä tällä aikavälillä.",
|
||||
"object_history_cap_note": "Historia säilyttää enintään 500 merkintää tehtävää kohden — hyvin vanhat merkinnät voivat puuttua.",
|
||||
"service_record_title": "Huoltokirja",
|
||||
"service_record_print": "Huoltokirja (PDF)",
|
||||
"date": "Päivämäärä",
|
||||
"service_record_entries": "merkintää",
|
||||
"completed_by": "Suorittanut",
|
||||
"date_from": "Alkaen",
|
||||
"date_to": "Asti"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Filtre utilisateur par défaut",
|
||||
"cal_editor_my_tasks": "Mes tâches (utilisateur actuel)",
|
||||
"cal_editor_show_object_filter": "Afficher le filtre d'objet",
|
||||
"cal_editor_object_hint": "Présélectionnez un objet via YAML : object_filter : \"<nom>\" — ou une liste de noms pour limiter la carte à plusieurs objets."
|
||||
"cal_editor_object_hint": "Présélectionnez un objet via YAML : object_filter : \"<nom>\" — ou une liste de noms pour limiter la carte à plusieurs objets.",
|
||||
"object_history_section": "Historique (toutes les tâches)",
|
||||
"object_history_all_tasks": "Toutes les tâches",
|
||||
"object_history_empty": "Aucune entrée sur cette période.",
|
||||
"object_history_cap_note": "L'historique conserve jusqu'à 500 entrées par tâche — les entrées très anciennes peuvent manquer.",
|
||||
"service_record_title": "Carnet d'entretien",
|
||||
"service_record_print": "Carnet d'entretien (PDF)",
|
||||
"date": "Date",
|
||||
"service_record_entries": "entrées",
|
||||
"completed_by": "Réalisé par",
|
||||
"date_from": "Du",
|
||||
"date_to": "Au"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "डिफ़ॉल्ट उपयोगकर्ता फ़िल्टर",
|
||||
"cal_editor_my_tasks": "मेरे कार्य (वर्तमान उपयोगकर्ता)",
|
||||
"cal_editor_show_object_filter": "ऑब्जेक्ट फ़िल्टर दिखाएँ",
|
||||
"cal_editor_object_hint": "YAML से एक ऑब्जेक्ट पहले से चुनें: object_filter: \"<नाम>\" — या कार्ड को कई ऑब्जेक्ट तक सीमित करने हेतु नामों की सूची।"
|
||||
"cal_editor_object_hint": "YAML से एक ऑब्जेक्ट पहले से चुनें: object_filter: \"<नाम>\" — या कार्ड को कई ऑब्जेक्ट तक सीमित करने हेतु नामों की सूची।",
|
||||
"object_history_section": "इतिहास (सभी कार्य)",
|
||||
"object_history_all_tasks": "सभी कार्य",
|
||||
"object_history_empty": "इस अवधि में कोई प्रविष्टि नहीं है।",
|
||||
"object_history_cap_note": "इतिहास प्रति कार्य अधिकतम 500 प्रविष्टियाँ रखता है — बहुत पुरानी प्रविष्टियाँ अनुपस्थित हो सकती हैं।",
|
||||
"service_record_title": "सेवा रिकॉर्ड",
|
||||
"service_record_print": "सेवा रिकॉर्ड (PDF)",
|
||||
"date": "दिनांक",
|
||||
"service_record_entries": "प्रविष्टियाँ",
|
||||
"completed_by": "द्वारा पूर्ण",
|
||||
"date_from": "से",
|
||||
"date_to": "तक"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Alapértelmezett felhasználószűrő",
|
||||
"cal_editor_my_tasks": "Saját feladatok (aktuális felhasználó)",
|
||||
"cal_editor_show_object_filter": "Objektumszűrő megjelenítése",
|
||||
"cal_editor_object_hint": "Előválasztás YAML-lel: object_filter: \"<név>\" — vagy névlista, hogy a kártya több objektumra korlátozódjon."
|
||||
"cal_editor_object_hint": "Előválasztás YAML-lel: object_filter: \"<név>\" — vagy névlista, hogy a kártya több objektumra korlátozódjon.",
|
||||
"object_history_section": "Előzmények (összes feladat)",
|
||||
"object_history_all_tasks": "Összes feladat",
|
||||
"object_history_empty": "Nincs bejegyzés ebben az időszakban.",
|
||||
"object_history_cap_note": "Az előzmények feladatonként legfeljebb 500 bejegyzést őriznek meg — a nagyon régiek hiányozhatnak.",
|
||||
"service_record_title": "Szervizkönyv",
|
||||
"service_record_print": "Szervizkönyv (PDF)",
|
||||
"date": "Dátum",
|
||||
"service_record_entries": "bejegyzés",
|
||||
"completed_by": "Elvégezte",
|
||||
"date_from": "Ettől",
|
||||
"date_to": "Eddig"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Filtro utente predefinito",
|
||||
"cal_editor_my_tasks": "Le mie attività (utente attuale)",
|
||||
"cal_editor_show_object_filter": "Mostra il filtro oggetto",
|
||||
"cal_editor_object_hint": "Preseleziona un oggetto via YAML: object_filter: \"<nome>\" — o un elenco di nomi per limitare la scheda a più oggetti."
|
||||
"cal_editor_object_hint": "Preseleziona un oggetto via YAML: object_filter: \"<nome>\" — o un elenco di nomi per limitare la scheda a più oggetti.",
|
||||
"object_history_section": "Cronologia (tutte le attività)",
|
||||
"object_history_all_tasks": "Tutte le attività",
|
||||
"object_history_empty": "Nessuna voce in questo periodo.",
|
||||
"object_history_cap_note": "La cronologia conserva fino a 500 voci per attività — le voci molto vecchie potrebbero mancare.",
|
||||
"service_record_title": "Libretto di manutenzione",
|
||||
"service_record_print": "Libretto di manutenzione (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "voci",
|
||||
"completed_by": "Completato da",
|
||||
"date_from": "Dal",
|
||||
"date_to": "Al"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "既定のユーザーフィルター",
|
||||
"cal_editor_my_tasks": "自分のタスク(現在のユーザー)",
|
||||
"cal_editor_show_object_filter": "オブジェクトフィルターを表示",
|
||||
"cal_editor_object_hint": "YAML でオブジェクトを事前選択:object_filter: \"<名前>\" — 複数指定はカードを複数オブジェクトに限定します。"
|
||||
"cal_editor_object_hint": "YAML でオブジェクトを事前選択:object_filter: \"<名前>\" — 複数指定はカードを複数オブジェクトに限定します。",
|
||||
"object_history_section": "履歴(全タスク)",
|
||||
"object_history_all_tasks": "すべてのタスク",
|
||||
"object_history_empty": "この期間の記録はありません。",
|
||||
"object_history_cap_note": "履歴はタスクごとに最大500件まで保持されます。非常に古い記録は含まれない場合があります。",
|
||||
"service_record_title": "整備記録",
|
||||
"service_record_print": "整備記録(PDF)",
|
||||
"date": "日付",
|
||||
"service_record_entries": "件",
|
||||
"completed_by": "実施者",
|
||||
"date_from": "開始",
|
||||
"date_to": "終了"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "기본 사용자 필터",
|
||||
"cal_editor_my_tasks": "내 작업 (현재 사용자)",
|
||||
"cal_editor_show_object_filter": "객체 필터 표시",
|
||||
"cal_editor_object_hint": "YAML로 객체를 미리 선택: object_filter: \"<이름>\" — 이름 목록으로 카드를 여러 객체로 제한할 수 있습니다."
|
||||
"cal_editor_object_hint": "YAML로 객체를 미리 선택: object_filter: \"<이름>\" — 이름 목록으로 카드를 여러 객체로 제한할 수 있습니다.",
|
||||
"object_history_section": "기록(전체 작업)",
|
||||
"object_history_all_tasks": "모든 작업",
|
||||
"object_history_empty": "이 기간에 기록이 없습니다.",
|
||||
"object_history_cap_note": "기록은 작업당 최대 500건까지 보관됩니다. 아주 오래된 기록은 없을 수 있습니다.",
|
||||
"service_record_title": "정비 기록",
|
||||
"service_record_print": "정비 기록(PDF)",
|
||||
"date": "날짜",
|
||||
"service_record_entries": "건",
|
||||
"completed_by": "수행자",
|
||||
"date_from": "시작",
|
||||
"date_to": "종료"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Standard brukerfilter",
|
||||
"cal_editor_my_tasks": "Mine oppgaver (gjeldende bruker)",
|
||||
"cal_editor_show_object_filter": "Vis objektfilter",
|
||||
"cal_editor_object_hint": "Forhåndsvelg et objekt via YAML: object_filter: \"<navn>\" — eller en liste med navn for å begrense kortet til flere objekter."
|
||||
"cal_editor_object_hint": "Forhåndsvelg et objekt via YAML: object_filter: \"<navn>\" — eller en liste med navn for å begrense kortet til flere objekter.",
|
||||
"object_history_section": "Historikk (alle oppgaver)",
|
||||
"object_history_all_tasks": "Alle oppgaver",
|
||||
"object_history_empty": "Ingen oppføringer i denne perioden.",
|
||||
"object_history_cap_note": "Historikken beholder opptil 500 oppføringer per oppgave — svært gamle oppføringer kan mangle.",
|
||||
"service_record_title": "Servicehefte",
|
||||
"service_record_print": "Servicehefte (PDF)",
|
||||
"date": "Dato",
|
||||
"service_record_entries": "oppføringer",
|
||||
"completed_by": "Utført av",
|
||||
"date_from": "Fra",
|
||||
"date_to": "Til"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Standaard gebruikersfilter",
|
||||
"cal_editor_my_tasks": "Mijn taken (huidige gebruiker)",
|
||||
"cal_editor_show_object_filter": "Objectfilter tonen",
|
||||
"cal_editor_object_hint": "Selecteer een object vooraf via YAML: object_filter: \"<naam>\" — of een lijst met namen om de kaart tot meerdere objecten te beperken."
|
||||
"cal_editor_object_hint": "Selecteer een object vooraf via YAML: object_filter: \"<naam>\" — of een lijst met namen om de kaart tot meerdere objecten te beperken.",
|
||||
"object_history_section": "Geschiedenis (alle taken)",
|
||||
"object_history_all_tasks": "Alle taken",
|
||||
"object_history_empty": "Geen items in deze periode.",
|
||||
"object_history_cap_note": "De geschiedenis bewaart maximaal 500 items per taak — zeer oude items kunnen ontbreken.",
|
||||
"service_record_title": "Onderhoudsboekje",
|
||||
"service_record_print": "Onderhoudsboekje (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "items",
|
||||
"completed_by": "Voltooid door",
|
||||
"date_from": "Van",
|
||||
"date_to": "Tot"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Domyślny filtr użytkownika",
|
||||
"cal_editor_my_tasks": "Moje zadania (bieżący użytkownik)",
|
||||
"cal_editor_show_object_filter": "Pokaż filtr obiektu",
|
||||
"cal_editor_object_hint": "Wybierz obiekt w YAML: object_filter: \"<nazwa>\" — lub listę nazw, aby ograniczyć kartę do kilku obiektów."
|
||||
"cal_editor_object_hint": "Wybierz obiekt w YAML: object_filter: \"<nazwa>\" — lub listę nazw, aby ograniczyć kartę do kilku obiektów.",
|
||||
"object_history_section": "Historia (wszystkie zadania)",
|
||||
"object_history_all_tasks": "Wszystkie zadania",
|
||||
"object_history_empty": "Brak wpisów w tym okresie.",
|
||||
"object_history_cap_note": "Historia przechowuje do 500 wpisów na zadanie — bardzo stare wpisy mogą brakować.",
|
||||
"service_record_title": "Książka serwisowa",
|
||||
"service_record_print": "Książka serwisowa (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "wpisów",
|
||||
"completed_by": "Wykonane przez",
|
||||
"date_from": "Od",
|
||||
"date_to": "Do"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Filtro de usuário padrão",
|
||||
"cal_editor_my_tasks": "Minhas tarefas (usuário atual)",
|
||||
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
|
||||
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos."
|
||||
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos.",
|
||||
"object_history_section": "Histórico (todas as tarefas)",
|
||||
"object_history_all_tasks": "Todas as tarefas",
|
||||
"object_history_empty": "Sem registros neste período.",
|
||||
"object_history_cap_note": "O histórico guarda até 500 registros por tarefa — registros muito antigos podem faltar.",
|
||||
"service_record_title": "Registro de manutenção",
|
||||
"service_record_print": "Registro de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registros",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Filtro de utilizador predefinido",
|
||||
"cal_editor_my_tasks": "As minhas tarefas (utilizador atual)",
|
||||
"cal_editor_show_object_filter": "Mostrar filtro de objeto",
|
||||
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos."
|
||||
"cal_editor_object_hint": "Pré-selecione um objeto via YAML: object_filter: \"<nome>\" — ou uma lista de nomes para limitar o cartão a vários objetos.",
|
||||
"object_history_section": "Histórico (todas as tarefas)",
|
||||
"object_history_all_tasks": "Todas as tarefas",
|
||||
"object_history_empty": "Sem registos neste período.",
|
||||
"object_history_cap_note": "O histórico guarda até 500 registos por tarefa — registos muito antigos podem faltar.",
|
||||
"service_record_title": "Registo de manutenção",
|
||||
"service_record_print": "Registo de manutenção (PDF)",
|
||||
"date": "Data",
|
||||
"service_record_entries": "registos",
|
||||
"completed_by": "Concluído por",
|
||||
"date_from": "De",
|
||||
"date_to": "Até"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Фильтр пользователя по умолчанию",
|
||||
"cal_editor_my_tasks": "Мои задачи (текущий пользователь)",
|
||||
"cal_editor_show_object_filter": "Показывать фильтр объекта",
|
||||
"cal_editor_object_hint": "Предварительный выбор объекта через YAML: object_filter: \"<имя>\" — или список имён, чтобы ограничить карточку несколькими объектами."
|
||||
"cal_editor_object_hint": "Предварительный выбор объекта через YAML: object_filter: \"<имя>\" — или список имён, чтобы ограничить карточку несколькими объектами.",
|
||||
"object_history_section": "История (все задачи)",
|
||||
"object_history_all_tasks": "Все задачи",
|
||||
"object_history_empty": "Нет записей за этот период.",
|
||||
"object_history_cap_note": "История хранит до 500 записей на задачу — очень старые записи могут отсутствовать.",
|
||||
"service_record_title": "Сервисная книжка",
|
||||
"service_record_print": "Сервисная книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записей",
|
||||
"completed_by": "Выполнил",
|
||||
"date_from": "С",
|
||||
"date_to": "По"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Standardanvändarfilter",
|
||||
"cal_editor_my_tasks": "Mina uppgifter (aktuell användare)",
|
||||
"cal_editor_show_object_filter": "Visa objektfilter",
|
||||
"cal_editor_object_hint": "Förvälj ett objekt via YAML: object_filter: \"<namn>\" — eller en lista med namn för att begränsa kortet till flera objekt."
|
||||
"cal_editor_object_hint": "Förvälj ett objekt via YAML: object_filter: \"<namn>\" — eller en lista med namn för att begränsa kortet till flera objekt.",
|
||||
"object_history_section": "Historik (alla uppgifter)",
|
||||
"object_history_all_tasks": "Alla uppgifter",
|
||||
"object_history_empty": "Inga poster under denna period.",
|
||||
"object_history_cap_note": "Historiken sparar upp till 500 poster per uppgift — mycket gamla poster kan saknas.",
|
||||
"service_record_title": "Servicebok",
|
||||
"service_record_print": "Servicebok (PDF)",
|
||||
"date": "Datum",
|
||||
"service_record_entries": "poster",
|
||||
"completed_by": "Utförd av",
|
||||
"date_from": "Från",
|
||||
"date_to": "Till"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Varsayılan kullanıcı filtresi",
|
||||
"cal_editor_my_tasks": "Görevlerim (geçerli kullanıcı)",
|
||||
"cal_editor_show_object_filter": "Nesne filtresini göster",
|
||||
"cal_editor_object_hint": "YAML ile bir nesne önceden seçin: object_filter: \"<ad>\" — veya kartı birden çok nesneyle sınırlamak için ad listesi."
|
||||
"cal_editor_object_hint": "YAML ile bir nesne önceden seçin: object_filter: \"<ad>\" — veya kartı birden çok nesneyle sınırlamak için ad listesi.",
|
||||
"object_history_section": "Geçmiş (tüm görevler)",
|
||||
"object_history_all_tasks": "Tüm görevler",
|
||||
"object_history_empty": "Bu aralıkta kayıt yok.",
|
||||
"object_history_cap_note": "Geçmiş, görev başına en fazla 500 kayıt tutar — çok eski kayıtlar eksik olabilir.",
|
||||
"service_record_title": "Servis kaydı",
|
||||
"service_record_print": "Servis kaydı (PDF)",
|
||||
"date": "Tarih",
|
||||
"service_record_entries": "kayıt",
|
||||
"completed_by": "Tamamlayan",
|
||||
"date_from": "Başlangıç",
|
||||
"date_to": "Bitiş"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "Типовий фільтр користувача",
|
||||
"cal_editor_my_tasks": "Мої завдання (поточний користувач)",
|
||||
"cal_editor_show_object_filter": "Показувати фільтр об'єкта",
|
||||
"cal_editor_object_hint": "Попередній вибір об'єкта через YAML: object_filter: \"<назва>\" — або список назв, щоб обмежити картку кількома об'єктами."
|
||||
"cal_editor_object_hint": "Попередній вибір об'єкта через YAML: object_filter: \"<назва>\" — або список назв, щоб обмежити картку кількома об'єктами.",
|
||||
"object_history_section": "Історія (усі завдання)",
|
||||
"object_history_all_tasks": "Усі завдання",
|
||||
"object_history_empty": "Немає записів за цей період.",
|
||||
"object_history_cap_note": "Історія зберігає до 500 записів на завдання — дуже старі записи можуть бути відсутні.",
|
||||
"service_record_title": "Сервісна книжка",
|
||||
"service_record_print": "Сервісна книжка (PDF)",
|
||||
"date": "Дата",
|
||||
"service_record_entries": "записів",
|
||||
"completed_by": "Виконав",
|
||||
"date_from": "Від",
|
||||
"date_to": "До"
|
||||
}
|
||||
|
||||
@@ -878,5 +878,16 @@
|
||||
"cal_editor_default_user": "默认用户筛选",
|
||||
"cal_editor_my_tasks": "我的任务(当前用户)",
|
||||
"cal_editor_show_object_filter": "显示对象筛选",
|
||||
"cal_editor_object_hint": "通过 YAML 预选对象:object_filter: \"<对象名>\" — 或名称列表,将卡片限定为多个对象。"
|
||||
"cal_editor_object_hint": "通过 YAML 预选对象:object_filter: \"<对象名>\" — 或名称列表,将卡片限定为多个对象。",
|
||||
"object_history_section": "历史(全部任务)",
|
||||
"object_history_all_tasks": "全部任务",
|
||||
"object_history_empty": "此时间段内没有记录。",
|
||||
"object_history_cap_note": "每个任务的历史最多保留 500 条记录,很早的记录可能缺失。",
|
||||
"service_record_title": "维护记录",
|
||||
"service_record_print": "维护记录(PDF)",
|
||||
"date": "日期",
|
||||
"service_record_entries": "条记录",
|
||||
"completed_by": "完成人",
|
||||
"date_from": "从",
|
||||
"date_to": "至"
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ export class MaintenanceSupporterCard extends LitElement {
|
||||
<mwc-icon-button
|
||||
class="hdr-add"
|
||||
title="${t("add_task", L)}"
|
||||
@click=${() => openCreateTaskDialog()}
|
||||
@click=${() => openCreateTaskDialog("", this._objects)}
|
||||
>
|
||||
<ha-icon icon="mdi:playlist-plus"></ha-icon>
|
||||
</mwc-icon-button>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { UserService } from "./user-service";
|
||||
import type { MaintenanceObjectDialog } from "./components/object-dialog";
|
||||
import "./components/documents-section";
|
||||
import "./components/parts-section";
|
||||
import "./components/object-history-section";
|
||||
import "./components/task-documents";
|
||||
import type { MaintenanceTaskDialog } from "./components/task-dialog";
|
||||
import type { MaintenanceCompleteDialog } from "./components/complete-dialog";
|
||||
@@ -3078,7 +3079,8 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
const color = pct >= 100 ? "var(--error-color, #f44336)" : pct >= b.alert_threshold_pct ? "var(--warning-color, #ff9800)" : "var(--success-color, #4caf50)";
|
||||
return html`
|
||||
<div class="stat-item budget-tile" title="${label}: ${spent.toFixed(2)} / ${budget.toFixed(2)} ${cs}">
|
||||
<span class="stat-value budget-tile-value">${spent.toFixed(2)} / ${budget.toFixed(0)} ${cs}</span>
|
||||
<span class="stat-value budget-tile-value">${spent.toFixed(2)} ${cs}</span>
|
||||
<span class="budget-tile-max">/ ${budget.toFixed(0)} ${cs}</span>
|
||||
<div class="budget-tile-bar"><div style="width:${pct}%; background:${color}"></div></div>
|
||||
<span class="stat-label">${label}</span>
|
||||
</div>
|
||||
@@ -3341,6 +3343,16 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
.currencySymbol=${this._currencySymbol}
|
||||
@parts-changed=${() => this._loadData()}
|
||||
></maintenance-parts-section>
|
||||
|
||||
<maintenance-object-history-section
|
||||
.hass=${this.hass}
|
||||
.entryId=${obj.entry_id}
|
||||
.object=${o}
|
||||
.tasks=${obj.tasks}
|
||||
.currencySymbol=${this._currencySymbol}
|
||||
.userName=${(id: string) => this._userService?.getUserName(id) ?? null}
|
||||
@open-task=${(e: CustomEvent<{ taskId: string }>) => this._showTask(obj.entry_id, e.detail.taskId)}
|
||||
></maintenance-object-history-section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -151,6 +151,13 @@ function progressSpec(task: MaintenanceTask, unit: string, ctx: SparklineContext
|
||||
case "counter": {
|
||||
const target = tc.trigger_target_value;
|
||||
if (target == null || target <= 0) return null;
|
||||
if (!tc.trigger_delta_mode) {
|
||||
// Non-delta counters count from zero since the last reset — the raw
|
||||
// value IS the progress. Subtracting a baseline here showed a fresh
|
||||
// cycle as stuck at 0 (progress.ts branches the same way; bug audit
|
||||
// 2026-08-22).
|
||||
return { progress: Math.max(0, cur), target, unit, meter: null };
|
||||
}
|
||||
const base = counterBaseline(task, rawStatsPoints(task, ctx));
|
||||
return { progress: Math.max(0, cur - (base?.value ?? cur)), target, unit, meter: cur };
|
||||
}
|
||||
@@ -296,13 +303,17 @@ function renderChart(task: MaintenanceTask, unit: string, ctx: SparklineContext)
|
||||
let forceZero = false;
|
||||
if (triggerType === "counter" && tc.trigger_target_value != null && points.length) {
|
||||
// Progress domain: cumulative since the last service, never negative.
|
||||
const base = counterBaseline(task, points);
|
||||
if (base) {
|
||||
if (base.ts != null) {
|
||||
const kept = points.filter((p) => p.ts >= base.ts!);
|
||||
if (kept.length >= 2) points = kept;
|
||||
// Baseline subtraction is a DELTA-mode concept — a non-delta counter's
|
||||
// raw value already is the cycle progress (bug audit 2026-08-22).
|
||||
if (tc.trigger_delta_mode) {
|
||||
const base = counterBaseline(task, points);
|
||||
if (base) {
|
||||
if (base.ts != null) {
|
||||
const kept = points.filter((p) => p.ts >= base.ts!);
|
||||
if (kept.length >= 2) points = kept;
|
||||
}
|
||||
points = points.map((p) => ({ ...p, val: Math.max(0, p.val - base.value) }));
|
||||
}
|
||||
points = points.map((p) => ({ ...p, val: Math.max(0, p.val - base.value) }));
|
||||
}
|
||||
targetValue = tc.trigger_target_value;
|
||||
forceZero = true;
|
||||
|
||||
@@ -954,10 +954,20 @@ export const sharedStyles = css`
|
||||
}
|
||||
|
||||
/* Budget KPI tiles in the stats strip (#125) — replaced the full-width
|
||||
budget-bars row. */
|
||||
budget-bars row. The spent amount inherits .stat-value's full 24px bold
|
||||
so the budget tiles read exactly like the other KPI chips (user report
|
||||
2026-08-24: the old 15px override made them visibly smaller); only the
|
||||
"/ max" suffix stays secondary. */
|
||||
.stat-item.budget-tile .budget-tile-value {
|
||||
font-size: 15px;
|
||||
padding-top: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* The "/ max" ratio is its OWN small line between value and bar — inline
|
||||
it overflowed the ~150px grid cell into the neighbouring tile once the
|
||||
value took the full 24px. */
|
||||
.budget-tile-max {
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.budget-tile-bar {
|
||||
@@ -1440,9 +1450,10 @@ export const sharedStyles = css`
|
||||
|
||||
.weibull-info-row { flex-direction: column; gap: 8px; }
|
||||
|
||||
/* Budget tiles on narrow screens (#125): slightly smaller value so the
|
||||
"x / y €" pair fits the wrapped grid cell. */
|
||||
.stat-item.budget-tile .budget-tile-value { font-size: 13px; }
|
||||
/* Budget tiles on narrow screens (#125): the spent amount keeps the
|
||||
full chip size (consistency, user report 2026-08-24); the "/ max"
|
||||
suffix is hidden instead — the bar and the title carry the ratio. */
|
||||
.budget-tile-max { display: none; }
|
||||
|
||||
.group-card { min-width: 0; max-width: 100%; }
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { t } from "./styles";
|
||||
* the raw key as-is so the user can still tell which input was rejected.
|
||||
*/
|
||||
const FIELD_LABEL_KEYS: Record<string, string> = {
|
||||
entry_id: "object",
|
||||
name: "name",
|
||||
task_type: "maintenance_type",
|
||||
schedule_type: "schedule_type",
|
||||
|
||||
Reference in New Issue
Block a user