Service history: columns you can switch off and rearrange
A car's page has let you choose and arrange two things for a while - which tabs
it shows, and which rows the Information tab lists, both dragged into whatever
order you like. The Service history table was left out of that: nine columns,
hardcoded, in one order, for every car. An EV shows Oil & Oil filter and Engine
air filter on every row of a history that will never record either, and a reader
who mostly wants Notes has to look past four columns of dates and distances to
reach it.
It works the way the other two do, because a third mechanism for the same idea
would be one to keep in step. Both lists are properties of the car, so everyone
it is shared with sees the same table, and both need write access to set. The
columns are stored as the hidden set rather than the visible one, so a column
added in a later release is on by default. The arrangement covers the hidden
columns too, which is what makes a column switched back on return to where it
was instead of reappearing at the end - verified below, since that is the part
of this shape that is easy to get wrong and invisible until somebody hits it.
Date cannot be switched off. Every row of that table is work done on a day, and
a history with the day taken out stops being a history; it can still be dragged
anywhere, which is exactly the rule Information already follows in the tab bar.
That is a judgment call and the annotation that prompted this only circled the
other eight columns - moving "date" into hideableServiceColumns and dropping the
filter in the picker would reverse it in two lines if it turns out to be wrong.
Server: hidden_service_columns and service_column_order on the car, validated
against their own key sets by the endpoint that already does this for tabs,
fields and readings. The arrangeable set is derived from the hideable one plus
the date rather than written out again, so the two cannot drift as columns are
added. Bootstrap appends missing fields to existing collections, so the two
columns appear on the next server start with no migration to run.
Web: the table stopped being nine hardcoded th/td pairs and is now driven by one
list of columns, head and body from the same source, which is what stops a moved
or hidden column from shifting the headings out of line with the cells. The
cells are built a row at a time rather than a call per cell, so a long history
doesn't rebuild every cell three times to read its text, its classes and whether
it is the file column. The column headings kept their existing car.services.col*
translations - the keys are mapped rather than derived, because renaming a dozen
strings in three languages to save a lookup table would be the wrong trade. Four
new strings in all three languages.
Verified: go vet and go test ./... pass, with new tests covering both key sets -
that hiding the date is refused, that a field key is not a column key, and that
the arrangeable set is the hideable one plus the date. npm run build is clean.
The page itself was driven in a browser against a throwaway stub API: the
rewritten table renders identically to the hardcoded one, switching two columns
off removed exactly those two from head and body with the rest still aligned and
sent {"hiddenServiceColumns":["oil","engineFilter"]}, dragging Notes onto Km
reordered head and body live and saved an order with the hidden columns still
holding their places, switching Oil back on returned it between Next km and
Cabin air filter rather than to the end, and a read-only share gets no gear
button, no draggable headings and no drag hint.
Not verified: the drag was exercised by dispatching drag events at the
component's own handlers, not by a pointer - the browser pane was not
compositing, which rules out both screenshots and a real drag - so the native
drag image and cursor are unchecked. No automated test guards any of the web
behaviour; the web app still has no test runner. The API rejects unknown JSON
fields, so this web build against an older API Server would take a 400 when
saving the picker: they deploy together from this repo, but one must not ship
without the other. The phone app is deliberately untouched, having no column
table to arrange, and ignores both new fields.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
caf4d2996d
commit
b60d929ed6
@@ -380,7 +380,10 @@
|
||||
"tabsHeading": "Faner",
|
||||
"fieldsHeading": "Oplysninger",
|
||||
"alwaysOn": "{tab} er altid tilgængelig.",
|
||||
"fieldsOrderHint": "Træk felterne på fanen Oplysninger for at ændre deres rækkefølge."
|
||||
"fieldsOrderHint": "Træk felterne på fanen Oplysninger for at ændre deres rækkefølge.",
|
||||
"serviceColumnsHeading": "Kolonner i servicehistorik",
|
||||
"columnAlwaysOn": "{column} vises altid.",
|
||||
"serviceColumnsOrderHint": "Træk kolonneoverskrifterne på fanen Servicehistorik for at ændre deres rækkefølge."
|
||||
},
|
||||
|
||||
"provider": {
|
||||
@@ -462,6 +465,7 @@
|
||||
"colCabinFilter": "Kabinefilter",
|
||||
"colNotes": "Noter",
|
||||
"colFile": "Fil",
|
||||
"dragHint": "Træk en kolonne for at ændre rækkefølgen i bilens servicehistorik.",
|
||||
"confirmDelete": "Slet denne servicepost?"
|
||||
},
|
||||
|
||||
|
||||
@@ -379,7 +379,10 @@
|
||||
"tabsHeading": "Tabs",
|
||||
"fieldsHeading": "Information fields",
|
||||
"alwaysOn": "{tab} is always available.",
|
||||
"fieldsOrderHint": "Drag the fields on the Information tab to change the order they appear in."
|
||||
"fieldsOrderHint": "Drag the fields on the Information tab to change the order they appear in.",
|
||||
"serviceColumnsHeading": "Service history columns",
|
||||
"columnAlwaysOn": "{column} is always shown.",
|
||||
"serviceColumnsOrderHint": "Drag the column headings on the Service history tab to change the order they appear in."
|
||||
},
|
||||
|
||||
"provider": {
|
||||
@@ -461,6 +464,7 @@
|
||||
"colCabinFilter": "Cabin air filter",
|
||||
"colNotes": "Notes",
|
||||
"colFile": "File",
|
||||
"dragHint": "Drag a column to rearrange this car's service history.",
|
||||
"confirmDelete": "Delete this service record?"
|
||||
},
|
||||
|
||||
|
||||
@@ -384,7 +384,10 @@
|
||||
"tabsHeading": "Zakładki",
|
||||
"fieldsHeading": "Pola informacji",
|
||||
"alwaysOn": "Zakładka {tab} jest zawsze dostępna.",
|
||||
"fieldsOrderHint": "Przeciągnij pola na zakładce Informacje, aby zmienić ich kolejność."
|
||||
"fieldsOrderHint": "Przeciągnij pola na zakładce Informacje, aby zmienić ich kolejność.",
|
||||
"serviceColumnsHeading": "Kolumny historii serwisowej",
|
||||
"columnAlwaysOn": "Kolumna {column} jest zawsze widoczna.",
|
||||
"serviceColumnsOrderHint": "Przeciągnij nagłówki kolumn na zakładce Historia serwisowa, aby zmienić ich kolejność."
|
||||
},
|
||||
|
||||
"provider": {
|
||||
@@ -466,6 +469,7 @@
|
||||
"colCabinFilter": "Filtr kabinowy",
|
||||
"colNotes": "Notatki",
|
||||
"colFile": "Plik",
|
||||
"dragHint": "Przeciągnij kolumnę, aby zmienić układ historii serwisowej tego samochodu.",
|
||||
"confirmDelete": "Usunąć ten wpis serwisowy?"
|
||||
},
|
||||
|
||||
|
||||
@@ -241,14 +241,28 @@ const INFO_FIELD_KEYS = [
|
||||
"odometer", "serviceInterval", "nextDue", "registrationPlate",
|
||||
"registrationCountry", "vin", "fuelType", "buildDate", "firstRegistration",
|
||||
];
|
||||
// The Service history columns, in their default order. Keys mirror
|
||||
// hideableServiceColumns/arrangeableServiceColumns in the API's cars.go — the
|
||||
// server rejects anything else. Date is in the list because it can be moved,
|
||||
// but not in HIDEABLE_SERVICE_COLUMNS: a service is the day it happened, and a
|
||||
// table of them with the day taken out stops being a history.
|
||||
const ALL_SERVICE_COLUMN_KEYS = [
|
||||
"date", "km", "nextDate", "nextKm", "oil", "engineFilter", "cabinFilter",
|
||||
"notes", "file",
|
||||
];
|
||||
const HIDEABLE_SERVICE_COLUMNS = ALL_SERVICE_COLUMN_KEYS.filter((key) => key !== "date");
|
||||
const tabDraft = ref([]); // tab keys that stay visible
|
||||
const fieldDraft = ref([]); // Information keys that stay visible
|
||||
const columnDraft = ref([]); // Service history columns that stay visible
|
||||
const viewSaving = ref(false);
|
||||
const viewError = ref("");
|
||||
|
||||
function openViewPicker() {
|
||||
tabDraft.value = HIDEABLE_TABS.filter((key) => !hiddenTabs.value.includes(key));
|
||||
fieldDraft.value = fieldKeys.value.filter((key) => !hiddenFields.value.includes(key));
|
||||
columnDraft.value = serviceColumnKeys.value.filter(
|
||||
(key) => key !== "date" && !hiddenServiceColumns.value.includes(key)
|
||||
);
|
||||
viewError.value = "";
|
||||
showViewPicker.value = true;
|
||||
}
|
||||
@@ -262,6 +276,9 @@ function toggleTabDraft(key, on) {
|
||||
function toggleFieldDraft(key, on) {
|
||||
fieldDraft.value = on ? [...fieldDraft.value, key] : fieldDraft.value.filter((k) => k !== key);
|
||||
}
|
||||
function toggleColumnDraft(key, on) {
|
||||
columnDraft.value = on ? [...columnDraft.value, key] : columnDraft.value.filter((k) => k !== key);
|
||||
}
|
||||
|
||||
async function saveView() {
|
||||
viewSaving.value = true;
|
||||
@@ -270,6 +287,7 @@ async function saveView() {
|
||||
const updated = await api.updateCarView(props.id, {
|
||||
hiddenTabs: HIDEABLE_TABS.filter((key) => !tabDraft.value.includes(key)),
|
||||
hiddenFields: INFO_FIELD_KEYS.filter((key) => !fieldDraft.value.includes(key)),
|
||||
hiddenServiceColumns: HIDEABLE_SERVICE_COLUMNS.filter((key) => !columnDraft.value.includes(key)),
|
||||
});
|
||||
car.value = { ...updated, access: car.value.access };
|
||||
showViewPicker.value = false;
|
||||
@@ -291,6 +309,17 @@ function tabPickerLabel(key) {
|
||||
function infoFieldLabel(key) {
|
||||
return t(`car.info.${key}`);
|
||||
}
|
||||
// The column headings were translated as car.services.col* long before they
|
||||
// became keys, so the two are mapped rather than derived: renaming a dozen
|
||||
// strings in three languages to save this table would be the wrong trade.
|
||||
const SERVICE_COLUMN_LABELS = {
|
||||
date: "colDate", km: "colKm", nextDate: "colNextDate", nextKm: "colNextKm",
|
||||
oil: "colOil", engineFilter: "colEngineFilter", cabinFilter: "colCabinFilter",
|
||||
notes: "colNotes", file: "colFile",
|
||||
};
|
||||
function serviceColumnLabel(key) {
|
||||
return t(`car.services.${SERVICE_COLUMN_LABELS[key]}`);
|
||||
}
|
||||
|
||||
// --- The arrangement of the Information rows ---
|
||||
//
|
||||
@@ -405,6 +434,142 @@ const infoFields = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
// --- The Service history columns ---
|
||||
//
|
||||
// The same three pieces as the Information rows, on the same reasoning: a
|
||||
// hidden set so a column added in a later release is on by default, an
|
||||
// arrangement that covers the hidden columns so one switched back on returns to
|
||||
// where it was, and both properties of the car, so everyone it is shared with
|
||||
// sees the same table.
|
||||
const hiddenServiceColumns = computed(() => car.value?.hiddenServiceColumns || []);
|
||||
const serviceColumnKeys = ref([...ALL_SERVICE_COLUMN_KEYS]);
|
||||
watch(
|
||||
() => car.value?.serviceColumnOrder,
|
||||
(order) => {
|
||||
const arranged = [];
|
||||
for (const key of order || []) {
|
||||
if (ALL_SERVICE_COLUMN_KEYS.includes(key) && !arranged.includes(key)) arranged.push(key);
|
||||
}
|
||||
// A column the stored arrangement doesn't mention follows the arranged ones,
|
||||
// so it joins at the right-hand end rather than in the middle of somebody's
|
||||
// table — the rule the tabs, the Information rows and the garage all use.
|
||||
serviceColumnKeys.value = [
|
||||
...arranged,
|
||||
...ALL_SERVICE_COLUMN_KEYS.filter((k) => !arranged.includes(k)),
|
||||
];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// The visible columns in their arranged order, as data, so the head and the body
|
||||
// are driven by one list and cannot drift apart when a column moves or goes
|
||||
// away. `center` is for the three yes/no columns, whose heading sits over a
|
||||
// column of two-letter answers.
|
||||
const CENTERED_SERVICE_COLUMNS = ["oil", "engineFilter", "cabinFilter"];
|
||||
const serviceColumns = computed(() =>
|
||||
serviceColumnKeys.value
|
||||
.filter((key) => !hiddenServiceColumns.value.includes(key))
|
||||
.map((key) => ({
|
||||
key,
|
||||
label: serviceColumnLabel(key),
|
||||
center: CENTERED_SERVICE_COLUMNS.includes(key),
|
||||
}))
|
||||
);
|
||||
|
||||
// One cell of that table. Returns the text and the classes it carries beyond the
|
||||
// shared padding; the file column is the one whose cell is a button, and says so
|
||||
// rather than returning text the template would have to special-case by key.
|
||||
function serviceCell(s, key) {
|
||||
switch (key) {
|
||||
case "date":
|
||||
return { text: formatDate(s.date), classes: "whitespace-nowrap data font-medium text-strong" };
|
||||
case "km":
|
||||
return { text: formatKm(s.km), classes: "whitespace-nowrap data text-body" };
|
||||
case "nextDate":
|
||||
return { text: formatDate(s.nextServiceDate), classes: "whitespace-nowrap data text-muted" };
|
||||
case "nextKm":
|
||||
return { text: formatKm(s.nextServiceKm), classes: "whitespace-nowrap data text-muted" };
|
||||
case "oil":
|
||||
return yesNoCell(s.changedOil);
|
||||
case "engineFilter":
|
||||
return yesNoCell(s.changedEngineAirFilter);
|
||||
case "cabinFilter":
|
||||
return yesNoCell(s.changedCabinAirFilter);
|
||||
case "notes":
|
||||
return { text: s.notes || t("common.empty"), classes: "text-body" };
|
||||
default: // file
|
||||
return { file: true, classes: "whitespace-nowrap" };
|
||||
}
|
||||
}
|
||||
function yesNoCell(on) {
|
||||
return {
|
||||
text: yn(on),
|
||||
classes: `text-center text-xs font-semibold ${on ? "text-success" : "text-muted"}`,
|
||||
};
|
||||
}
|
||||
|
||||
// One row's cells, already in the arranged order. A row at a time rather than a
|
||||
// call per cell, so a table of twenty services doesn't rebuild every cell three
|
||||
// times over to read its classes, its text and whether it is the file column.
|
||||
function serviceRow(s) {
|
||||
return serviceColumns.value.map((col) => ({ ...col, ...serviceCell(s, col.key) }));
|
||||
}
|
||||
|
||||
// Dragging a column header, on the same native drag events as the tabs, the
|
||||
// Information rows and the garage — so pointer-only, as touch browsers don't
|
||||
// fire these. Needs write access, since the arrangement belongs to the car, and
|
||||
// there is nothing to rearrange with one column showing.
|
||||
const canArrangeServiceColumns = computed(
|
||||
() => canWrite.value && !prefs.dragLocked && serviceColumns.value.length > 1
|
||||
);
|
||||
const dragColumn = ref(""); // column being dragged
|
||||
const dropColumn = ref(""); // column it is currently hovering over
|
||||
const columnOrderError = ref("");
|
||||
let columnsMoved = false; // the table changed during this drag and isn't saved yet
|
||||
|
||||
function onColumnDragStart(key, e) {
|
||||
dragColumn.value = key;
|
||||
columnsMoved = false;
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
// Firefox only starts a drag once something is on the transfer.
|
||||
e.dataTransfer.setData("text/plain", key);
|
||||
}
|
||||
|
||||
// Reorder live as the pointer crosses headers, so the table shows the
|
||||
// arrangement you are about to get. The splice works on the full list, hidden
|
||||
// columns included, which keeps a hidden column anchored between the same two
|
||||
// visible neighbours.
|
||||
function onColumnDragEnter(key) {
|
||||
if (!dragColumn.value || key === dragColumn.value || dropColumn.value === key) return;
|
||||
dropColumn.value = key;
|
||||
const list = serviceColumnKeys.value;
|
||||
const from = list.indexOf(dragColumn.value);
|
||||
const to = list.indexOf(key);
|
||||
if (from < 0 || to < 0) return;
|
||||
list.splice(to, 0, ...list.splice(from, 1));
|
||||
columnsMoved = true;
|
||||
}
|
||||
|
||||
// Save whatever the table now shows. Called from both drop and dragend: a header
|
||||
// released past the end of the row never produces a drop, and leaving that
|
||||
// arrangement unsaved would quietly undo itself on the next load.
|
||||
async function commitServiceColumnOrder() {
|
||||
dragColumn.value = "";
|
||||
dropColumn.value = "";
|
||||
if (!columnsMoved) return;
|
||||
columnsMoved = false;
|
||||
columnOrderError.value = "";
|
||||
try {
|
||||
const updated = await api.updateCarView(props.id, { serviceColumnOrder: serviceColumnKeys.value });
|
||||
car.value = { ...updated, access: car.value.access };
|
||||
} catch (e) {
|
||||
// The arrangement didn't stick; say so and put the stored one back rather
|
||||
// than leaving the page showing an order the server doesn't have.
|
||||
columnOrderError.value = e.message;
|
||||
await load();
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
@@ -901,8 +1066,11 @@ onMounted(load);
|
||||
<p v-if="canArrangeFields" class="mt-2 text-xs text-muted">{{ t("car.info.dragHint") }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Service history -->
|
||||
<!-- Service history. The columns can be switched off in the view picker and
|
||||
dragged into any order with write access; both belong to the car, like
|
||||
which tabs and Information rows it shows. -->
|
||||
<section v-else-if="activeTab === 'services'">
|
||||
<p v-if="columnOrderError" class="mb-4 rounded-control bg-danger-soft px-4 py-3 text-sm font-medium text-danger">{{ columnOrderError }}</p>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-lg font-bold tracking-[-0.02em] text-strong">{{ t("car.services.title") }}</h2>
|
||||
<button v-if="canWrite" class="dh-btn dh-btn-primary !px-3 !py-1.5" @click="openAddService">
|
||||
@@ -919,33 +1087,41 @@ onMounted(load);
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-sunken text-left">
|
||||
<tr class="[&>th]:eyebrow [&>th]:px-4 [&>th]:py-3">
|
||||
<th>{{ t("car.services.colDate") }}</th>
|
||||
<th>{{ t("car.services.colKm") }}</th>
|
||||
<th>{{ t("car.services.colNextDate") }}</th>
|
||||
<th>{{ t("car.services.colNextKm") }}</th>
|
||||
<th class="!text-center">{{ t("car.services.colOil") }}</th>
|
||||
<th class="!text-center">{{ t("car.services.colEngineFilter") }}</th>
|
||||
<th class="!text-center">{{ t("car.services.colCabinFilter") }}</th>
|
||||
<th>{{ t("car.services.colNotes") }}</th>
|
||||
<th>{{ t("car.services.colFile") }}</th>
|
||||
<th
|
||||
v-for="col in serviceColumns"
|
||||
:key="col.key"
|
||||
:draggable="canArrangeServiceColumns"
|
||||
:title="canArrangeServiceColumns ? t('car.services.dragHint') : ''"
|
||||
:class="[
|
||||
col.center ? '!text-center' : '',
|
||||
canArrangeServiceColumns ? 'cursor-grab active:cursor-grabbing' : '',
|
||||
dragColumn === col.key ? 'opacity-50' : '',
|
||||
dropColumn === col.key ? 'ring-2 ring-inset ring-accent' : '',
|
||||
]"
|
||||
@dragstart="onColumnDragStart(col.key, $event)"
|
||||
@dragenter.prevent="onColumnDragEnter(col.key)"
|
||||
@dragover.prevent
|
||||
@drop.prevent="commitServiceColumnOrder"
|
||||
@dragend="commitServiceColumnOrder"
|
||||
>{{ col.label }}</th>
|
||||
<th v-if="canWrite"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-subtle">
|
||||
<tr v-for="s in services" :key="s.id" class="transition-colors hover:bg-sunken">
|
||||
<td class="whitespace-nowrap px-4 py-3 data font-medium text-strong">{{ formatDate(s.date) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 data text-body">{{ formatKm(s.km) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 data text-muted">{{ formatDate(s.nextServiceDate) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 data text-muted">{{ formatKm(s.nextServiceKm) }}</td>
|
||||
<td class="px-4 py-3 text-center text-xs font-semibold" :class="s.changedOil ? 'text-success' : 'text-muted'">{{ yn(s.changedOil) }}</td>
|
||||
<td class="px-4 py-3 text-center text-xs font-semibold" :class="s.changedEngineAirFilter ? 'text-success' : 'text-muted'">{{ yn(s.changedEngineAirFilter) }}</td>
|
||||
<td class="px-4 py-3 text-center text-xs font-semibold" :class="s.changedCabinAirFilter ? 'text-success' : 'text-muted'">{{ yn(s.changedCabinAirFilter) }}</td>
|
||||
<td class="px-4 py-3 text-body">{{ s.notes || t("common.empty") }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<button v-if="s.hasFile" class="text-xs font-medium text-brandtext hover:underline" @click="downloadAttachment('services', s)">
|
||||
{{ t("common.download") }}
|
||||
</button>
|
||||
<span v-else class="text-xs text-muted">{{ t("common.empty") }}</span>
|
||||
<td
|
||||
v-for="cell in serviceRow(s)"
|
||||
:key="cell.key"
|
||||
class="px-4 py-3"
|
||||
:class="cell.classes"
|
||||
>
|
||||
<template v-if="cell.file">
|
||||
<button v-if="s.hasFile" class="text-xs font-medium text-brandtext hover:underline" @click="downloadAttachment('services', s)">
|
||||
{{ t("common.download") }}
|
||||
</button>
|
||||
<span v-else class="text-xs text-muted">{{ t("common.empty") }}</span>
|
||||
</template>
|
||||
<template v-else>{{ cell.text }}</template>
|
||||
</td>
|
||||
<td v-if="canWrite" class="whitespace-nowrap px-4 py-3 text-right">
|
||||
<button class="text-xs font-medium text-brandtext hover:underline" @click="openEditService(s)">{{ t("common.edit") }}</button>
|
||||
@@ -955,6 +1131,7 @@ onMounted(load);
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-if="canArrangeServiceColumns" class="mt-2 text-xs text-muted">{{ t("car.services.dragHint") }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Technical check history -->
|
||||
@@ -1579,6 +1756,27 @@ onMounted(load);
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted">{{ t("car.viewPicker.fieldsOrderHint") }}</p>
|
||||
|
||||
<p class="eyebrow mb-2 mt-5">{{ t("car.viewPicker.serviceColumnsHeading") }}</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<label
|
||||
v-for="key in serviceColumnKeys.filter((k) => k !== 'date')"
|
||||
:key="key"
|
||||
class="flex items-center gap-2 text-sm font-medium text-body"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-subtle text-accent focus:ring-accent"
|
||||
:checked="columnDraft.includes(key)"
|
||||
@change="toggleColumnDraft(key, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ serviceColumnLabel(key) }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
{{ t("car.viewPicker.columnAlwaysOn", { column: t("car.services.colDate") }) }}
|
||||
{{ t("car.viewPicker.serviceColumnsOrderHint") }}
|
||||
</p>
|
||||
|
||||
<p v-if="viewError" class="mt-3 text-sm text-danger">{{ viewError }}</p>
|
||||
|
||||
<div class="mt-5 flex justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user