331 files
This commit is contained in:
+42
-13
@@ -444,7 +444,9 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
: b.no_sensor
|
||||
? html`<span class="bf-offline bf-nosensor">${t("battery_fleet_no_sensor", L)}</span>`
|
||||
: nothing;
|
||||
const type = html`<span class="bf-type">${b.quantity}× ${b.battery_type}</span>`;
|
||||
// The full type as a tooltip: a long one ends in an ellipsis (D#162).
|
||||
const typeText = `${b.quantity}× ${b.battery_type}`;
|
||||
const type = html`<span class="bf-type" title=${typeText}>${typeText}</span>`;
|
||||
const writer = this._canWrite;
|
||||
const showMark = writer && (o.mark === "always" || b.no_sensor || b.can_mark_replaced);
|
||||
return html`
|
||||
@@ -659,7 +661,10 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
/* The query container of the row layout (see "In a narrow list" below):
|
||||
* the list's own width, not the screen's. */
|
||||
.bf-card {
|
||||
container: bf-fleet / inline-size;
|
||||
background: var(--card-background-color, #fff);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 10px;
|
||||
@@ -817,9 +822,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
/* Every row shares the list's columns, so ONE long type ("1× BATTERY
|
||||
* PACK") widened the type column of all of them (D#162): it stops at
|
||||
* 12em and ends in an ellipsis, the full text is the tooltip. */
|
||||
.bf-type {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 13px;
|
||||
max-width: 12em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bf-recharge {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -835,18 +847,37 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
flex: 0 0 auto;
|
||||
cursor: help;
|
||||
}
|
||||
/* On phones the row cannot fit name + chips + curve + bar + date in ONE
|
||||
* line: the decorations yield (the percentage still carries the number)
|
||||
* and the row wraps to two lines - the name spans the full width, the
|
||||
* status chip moves under it (left, into the name column) and the rest
|
||||
* keeps its pinned subgrid column, so type / percentage / date / eye
|
||||
* stay aligned across rows. Without this the fixed max-content columns
|
||||
* overflowed 400 px and the chips overlapped the wrapped names. */
|
||||
@media (max-width: 640px) {
|
||||
/* In a narrow list the row cannot fit name + chips + curve + bar + date
|
||||
* in ONE line: the decorations yield (the percentage still carries the
|
||||
* number) and the row wraps to two lines - the name spans the full
|
||||
* width, the status chip moves under it (left, into the name column)
|
||||
* and the rest keeps its pinned subgrid column, so type / percentage /
|
||||
* date / eye stay aligned across rows. Without this the fixed
|
||||
* max-content columns overflowed 400 px and the chips overlapped the
|
||||
* wrapped names.
|
||||
*
|
||||
* The LIST's width decides, not the screen's (D#162): the Lovelace card
|
||||
* sits in a dashboard column of 400-500 px on a wide screen, kept the
|
||||
* one-line layout there and cut off the badge, the date and the hide
|
||||
* button. 680 px of list is where the one-line rows stop fitting;
|
||||
* desktop task pages (700 px and more) keep them. */
|
||||
@container bf-fleet (max-width: 680px) {
|
||||
.bf-spark,
|
||||
.bf-bar {
|
||||
display: none;
|
||||
}
|
||||
/* No fixed gaps: a column a phone leaves empty (status chip moved,
|
||||
* curve and bar hidden) cost its 8 px gap all the same, and together
|
||||
* with one long type the rows ran 25 px past a 402 px phone. Each
|
||||
* occupied column brings its own 6 px instead, and the type column
|
||||
* may shrink - its text ends in an ellipsis. */
|
||||
.bf-rows {
|
||||
column-gap: 0;
|
||||
grid-template-columns: minmax(0, 1fr) max-content minmax(0, max-content) repeat(7, max-content);
|
||||
}
|
||||
.bf-row > :not(.bf-dev) {
|
||||
margin-inline-start: 6px;
|
||||
}
|
||||
.bf-row {
|
||||
row-gap: 2px;
|
||||
}
|
||||
@@ -864,12 +895,10 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
/* The column shrinks, the ellipsis comes from the base rule (a cap in
|
||||
* vw measured the screen, not the list). */
|
||||
.bf-type {
|
||||
grid-row: 2;
|
||||
max-width: 44vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bf-recharge,
|
||||
.bf-level,
|
||||
|
||||
+28
-14
@@ -14,6 +14,7 @@ import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
|
||||
import { PhotoUploadController } from "../helpers/photo-upload-controller";
|
||||
import { parseDurationMinutes } from "../helpers/duration";
|
||||
import "./ms-date-field";
|
||||
import "./ms-cost-input";
|
||||
import "./ms-photo-picker";
|
||||
import { photoPickerStyles } from "./ms-photo-picker";
|
||||
|
||||
@@ -173,10 +174,14 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
entry_id: this.entryId,
|
||||
task_id: this.taskId,
|
||||
};
|
||||
if (this._notes) data.notes = this._notes;
|
||||
// Trimmed like the required check reads it: a text area easily keeps a
|
||||
// trailing line break, which the history would show as an empty line.
|
||||
const notes = this._notes.trim();
|
||||
if (notes) data.notes = notes;
|
||||
if (this._cost) {
|
||||
// #200: a credit is negative; the server holds the bounds.
|
||||
const cost = parseFloat(this._cost);
|
||||
if (!isNaN(cost) && cost >= 0) data.cost = cost;
|
||||
if (!isNaN(cost)) data.cost = cost;
|
||||
}
|
||||
// Whole minutes — the server coerces to int (helpers/duration).
|
||||
const dur = parseDurationMinutes(this._duration);
|
||||
@@ -309,9 +314,15 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
return this.requiredFields.filter((f) => !filled[f]);
|
||||
}
|
||||
|
||||
/** Does the task demand this detail? Its label then drops "(optional)" —
|
||||
* "Notes (optional) *" contradicted itself. */
|
||||
private _demands(field: string): boolean {
|
||||
return this.requiredFields.includes(field);
|
||||
}
|
||||
|
||||
/** Marker appended to a required field's label. */
|
||||
private _req(field: string) {
|
||||
return this.requiredFields.includes(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
|
||||
return this._demands(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
|
||||
}
|
||||
|
||||
/** A buy task's price: restock qty (packages) × the part's price per
|
||||
@@ -485,20 +496,22 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
only sees the title + Cancel/Complete buttons — the original
|
||||
bug report. Native inputs always render. -->
|
||||
<label class="field">
|
||||
<span class="field-label">${t("notes_optional", L)}${this._req("notes")}</span>
|
||||
<input type="text" class="field-input"
|
||||
<span class="field-label">${this._demands("notes") ? t("notes_label", L) : t("notes_optional", L)}${this._req("notes")}</span>
|
||||
<!-- Several lines (#202): what was done, one line each. The
|
||||
history and the printouts keep the line breaks; they show
|
||||
notes as plain text, so no Markdown hint here. -->
|
||||
<textarea class="field-input notes-input" rows="3"
|
||||
.value=${this._notes}
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)} />
|
||||
@input=${(e: Event) => (this._notes = (e.target as HTMLTextAreaElement).value)}></textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">${t("cost_optional", L)}${this._req("cost")}</span>
|
||||
<input type="number" step="0.01" min="0" class="field-input"
|
||||
.value=${this._cost}
|
||||
@input=${(e: Event) => (this._cost = (e.target as HTMLInputElement).value)} />
|
||||
<div class="field">
|
||||
<span class="field-label">${this._demands("cost") ? t("cost", L) : t("cost_optional", L)}${this._req("cost")}</span>
|
||||
<ms-cost-input .lang=${L} .value=${this._cost}
|
||||
@value-changed=${(e: CustomEvent<{ value: string }>) => (this._cost = e.detail.value)}></ms-cost-input>
|
||||
${this._renderCostSuggestion(L)}
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">${t("duration_minutes", L)}${this._req("duration")}</span>
|
||||
<span class="field-label">${this._demands("duration") ? t("quick_complete_defaults_duration", L) : t("duration_minutes", L)}${this._req("duration")}</span>
|
||||
<input type="number" step="1" min="0" inputmode="numeric" class="field-input"
|
||||
.value=${this._duration}
|
||||
@input=${(e: Event) => (this._duration = (e.target as HTMLInputElement).value)} />
|
||||
@@ -519,7 +532,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
</button>`}
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-label">${t("completion_photos_optional", L)}${this._req("photo")}</span>
|
||||
<span class="field-label">${this._demands("photo") ? t("completion_photos", L) : t("completion_photos_optional", L)}${this._req("photo")}</span>
|
||||
${this._photos.photos.length > 0
|
||||
? html`<div class="photo-strip">
|
||||
${this._photos.photos.map((p) => html`
|
||||
@@ -582,6 +595,7 @@ export class MaintenanceCompleteDialog extends LitElement {
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.notes-input { resize: vertical; line-height: 1.4; }
|
||||
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
|
||||
.cost-note {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -106,8 +106,10 @@ export class MaintenanceGroupDialog extends LitElement {
|
||||
const L = this._lang;
|
||||
const title = this._groupId ? t("edit_group", L) : t("new_group", L);
|
||||
|
||||
// Our own title element: Home Assistant's dialog draws no `heading`.
|
||||
return html`
|
||||
<ha-dialog open @closed=${this._close} heading="${title}">
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${title}</div>
|
||||
<div class="content">
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
<ms-textfield
|
||||
@@ -169,6 +171,7 @@ export class MaintenanceGroupDialog extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+9
-7
@@ -18,6 +18,7 @@ import { PhotoUploadController } from "../helpers/photo-upload-controller";
|
||||
import { parseDurationMinutes } from "../helpers/duration";
|
||||
import { confirmAction } from "../helpers/confirm";
|
||||
import "./ms-date-field";
|
||||
import "./ms-cost-input";
|
||||
import "./history-photo";
|
||||
import "./ms-photo-picker";
|
||||
import { photoPickerStyles } from "./ms-photo-picker";
|
||||
@@ -404,15 +405,14 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
.value=${d.notes ?? ""}></textarea>
|
||||
</label>
|
||||
<div class="row">
|
||||
<label>
|
||||
<div class="cost-field">
|
||||
<span>${t("cost", L)}</span>
|
||||
<input type="number" min="0" step="0.01"
|
||||
.value=${d.cost != null ? String(d.cost) : ""}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
<ms-cost-input .lang=${L} .value=${d.cost != null ? String(d.cost) : ""}
|
||||
@value-changed=${(e: CustomEvent<{ value: string }>) => {
|
||||
const v = e.detail.value;
|
||||
this._set("cost", v ? Number(v) : null);
|
||||
}} />
|
||||
</label>
|
||||
}}></ms-cost-input>
|
||||
</div>
|
||||
<label>
|
||||
<span>${t("duration", L)}</span>
|
||||
<input type="number" min="0" step="1" inputmode="numeric"
|
||||
@@ -531,6 +531,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
|
||||
}
|
||||
label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
||||
label span { color: var(--secondary-text-color); }
|
||||
.cost-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
|
||||
.cost-field > span { color: var(--secondary-text-color); }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
input, textarea {
|
||||
padding: 8px; font-size: 14px;
|
||||
|
||||
+4
-1
@@ -100,8 +100,10 @@ export class SeasonalOverridesDialog extends LitElement {
|
||||
render() {
|
||||
if (!this._open) return html``;
|
||||
const L = this._lang;
|
||||
// Our own title element: Home Assistant's dialog draws no `heading`.
|
||||
return html`
|
||||
<ha-dialog open @closed=${this._close} heading="${t("seasonal_overrides_title", L)}">
|
||||
<ha-dialog open @closed=${this._close}>
|
||||
<div class="dialog-title">${t("seasonal_overrides_title", L)}</div>
|
||||
<div class="content">
|
||||
<p class="hint">${t("seasonal_overrides_hint", L)}</p>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
|
||||
@@ -138,6 +140,7 @@ export class SeasonalOverridesDialog extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
|
||||
.content {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
|
||||
@@ -1240,6 +1240,14 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<span class="setting-label">${t("settings_max_per_day", L)}</span>
|
||||
${this._intSetting("max_notifications_per_day", n.max_per_day)}
|
||||
</label>
|
||||
<!-- Every Snooze uses it — the task menu, a card, voice, the phone's
|
||||
button — so it is not tied to the Snooze action button (D#193:
|
||||
it only showed while that button was on). -->
|
||||
<label class="setting-row snooze-duration">
|
||||
<span class="setting-label">${t("settings_snooze_hours", L)}</span>
|
||||
${this._intSetting("snooze_duration_hours", a.snooze_duration_hours)}
|
||||
</label>
|
||||
<div class="setting-hint">${t("settings_snooze_hours_hint", L)}</div>
|
||||
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_bundling", L)}</span>
|
||||
@@ -1336,12 +1344,6 @@ export class MaintenanceSettingsView extends LitElement {
|
||||
<input type="checkbox" .checked=${a.snooze_enabled}
|
||||
@change=${(e: Event) => this._updateSetting("action_snooze_enabled", (e.target as HTMLInputElement).checked)} />
|
||||
</label>
|
||||
${a.snooze_enabled ? html`
|
||||
<label class="setting-row sub-row">
|
||||
<span class="setting-desc">${t("settings_snooze_hours", L)}</span>
|
||||
${this._intSetting("snooze_duration_hours", a.snooze_duration_hours)}
|
||||
</label>
|
||||
` : nothing}
|
||||
<label class="setting-row">
|
||||
<span class="setting-label">${t("settings_weekly_digest", L)}</span>
|
||||
<input type="checkbox" .checked=${a.weekly_digest_enabled}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required
|
||||
import "./ms-textfield";
|
||||
import { defaultNotifyIcon } from "../helpers/notify-icons";
|
||||
import "./ms-date-field";
|
||||
import "./ms-cost-input";
|
||||
|
||||
const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"];
|
||||
const PRIORITY_KEYS = ["low", "normal", "high"];
|
||||
@@ -1034,12 +1035,11 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
.value=${this._qcNotes}
|
||||
@input=${(e: Event) => { this._qcNotes = (e.target as HTMLInputElement).value; }}
|
||||
></ms-textfield>
|
||||
<ms-textfield
|
||||
label="${t("quick_complete_defaults_cost", L)}"
|
||||
type="number" min="0" step="0.01"
|
||||
.value=${this._qcCost}
|
||||
@input=${(e: Event) => { this._qcCost = (e.target as HTMLInputElement).value; }}
|
||||
></ms-textfield>
|
||||
<div class="qc-cost">
|
||||
<span class="qc-cost-label">${t("quick_complete_defaults_cost", L)}</span>
|
||||
<ms-cost-input .lang=${L} .value=${this._qcCost}
|
||||
@value-changed=${(e: CustomEvent<{ value: string }>) => { this._qcCost = e.detail.value; }}></ms-cost-input>
|
||||
</div>
|
||||
<ms-textfield
|
||||
label="${t("quick_complete_defaults_duration", L)}"
|
||||
type="number" min="0" step="1"
|
||||
@@ -1788,7 +1788,7 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
const qcd: Record<string, unknown> = {};
|
||||
if (this._qcNotes.trim()) qcd.notes = this._qcNotes.trim();
|
||||
const cost = parseFloat(this._qcCost);
|
||||
if (!isNaN(cost) && cost >= 0) qcd.cost = cost;
|
||||
if (!isNaN(cost)) qcd.cost = cost;
|
||||
const dur = parseDurationMinutes(this._qcDuration);
|
||||
if (dur !== null) qcd.duration = dur;
|
||||
if (this._qcFeedback) qcd.feedback = this._qcFeedback;
|
||||
@@ -3588,6 +3588,8 @@ export class MaintenanceTaskDialog extends LitElement {
|
||||
font-weight: 500;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.qc-cost { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
|
||||
.qc-cost-label { font-size: 12px; color: var(--secondary-text-color); }
|
||||
.field-help {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
Reference in New Issue
Block a user