Add technical check history
A car's mandatory roadworthiness inspections — przegląd techniczny, MOT, TÜV — had nowhere to live. Log them behind Service history, which this mirrors minus the odometer: an inspection falls due on a date whatever the mileage reads. The cycle is legal rather than mechanical, and it is not constant — a new car's first check falls due years later than its third. So the car's technical_check_interval_days only prefills the next date, and any check overrides it with the valid-until printed on its own certificate. A failed check derives no next date at all: reading one off a failure would stamp a reassuring "valid until" on a car that just flunked. The next-due date and its status are derived on every read rather than stored, for the same reason documents are — a stored verdict goes stale as the date passes. The response carries the same expiry shape documents use, so the web app reuses the existing badge rather than growing a second one. Each check records result, cost, station and notes, and holds the certificate as its single attachment on the same terms as every other record. Needs `node scripts/setup-pocketbase.mjs` to create technical_checks and add the interval to cars. The reformatting in records.go is gofmt realigning the car struct around a longer field name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
03738f08dc
commit
21c99ad762
11 files changed
+713
-81
No files matched your search
@@ -15,6 +15,7 @@ import {
|
||||
} from "../lib/format.js";
|
||||
import CarFormModal from "../components/CarFormModal.vue";
|
||||
import ServiceFormModal from "../components/ServiceFormModal.vue";
|
||||
import TechnicalCheckFormModal from "../components/TechnicalCheckFormModal.vue";
|
||||
import PartFormModal from "../components/PartFormModal.vue";
|
||||
import FuelFormModal from "../components/FuelFormModal.vue";
|
||||
import MaintenanceFormModal from "../components/MaintenanceFormModal.vue";
|
||||
@@ -27,6 +28,7 @@ const router = useRouter();
|
||||
|
||||
const car = ref(null);
|
||||
const services = ref([]);
|
||||
const technicalChecks = ref([]);
|
||||
const parts = ref([]);
|
||||
const fuel = ref([]);
|
||||
const fuelStats = ref(null);
|
||||
@@ -40,6 +42,8 @@ const error = ref("");
|
||||
const showCarEdit = ref(false);
|
||||
const showService = ref(false);
|
||||
const editingService = ref(null);
|
||||
const showTechnicalCheck = ref(false);
|
||||
const editingTechnicalCheck = ref(null);
|
||||
const showPart = ref(false);
|
||||
const editingPart = ref(null);
|
||||
const showFuel = ref(false);
|
||||
@@ -80,6 +84,7 @@ const dueReminders = computed(
|
||||
const TABS = [
|
||||
{ key: "info", label: "Information" },
|
||||
{ key: "services", label: "Service history" },
|
||||
{ key: "technical", label: "Technical check history" },
|
||||
{ key: "maintenance", label: "Maintenance" },
|
||||
{ key: "fuel", label: "Fuel" },
|
||||
{ key: "documents", label: "Documents" },
|
||||
@@ -94,6 +99,7 @@ async function load() {
|
||||
[
|
||||
car.value,
|
||||
services.value,
|
||||
technicalChecks.value,
|
||||
parts.value,
|
||||
fuel.value,
|
||||
fuelStats.value,
|
||||
@@ -103,6 +109,7 @@ async function load() {
|
||||
] = await Promise.all([
|
||||
api.getCar(props.id),
|
||||
api.listCarServices(props.id),
|
||||
api.listCarTechnicalChecks(props.id),
|
||||
api.listCarParts(props.id),
|
||||
api.listCarFuel(props.id),
|
||||
api.getCarFuelStats(props.id),
|
||||
@@ -141,6 +148,30 @@ async function deleteService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- technical checks ---
|
||||
function openAddTechnicalCheck() {
|
||||
editingTechnicalCheck.value = null;
|
||||
showTechnicalCheck.value = true;
|
||||
}
|
||||
function openEditTechnicalCheck(t) {
|
||||
editingTechnicalCheck.value = t;
|
||||
showTechnicalCheck.value = true;
|
||||
}
|
||||
async function onTechnicalCheckSaved() {
|
||||
showTechnicalCheck.value = false;
|
||||
editingTechnicalCheck.value = null;
|
||||
await load();
|
||||
}
|
||||
async function deleteTechnicalCheck(id) {
|
||||
if (!confirm("Delete this technical check?")) return;
|
||||
try {
|
||||
await api.deleteTechnicalCheck(id);
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function openAddPart() {
|
||||
editingPart.value = null;
|
||||
showPart.value = true;
|
||||
@@ -538,6 +569,71 @@ onMounted(load);
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Technical check history -->
|
||||
<section v-else-if="activeTab === 'technical'">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold tracking-[-0.02em] text-strong">Technical check history</h2>
|
||||
<p class="text-sm text-muted">
|
||||
Mandatory roadworthiness inspections. Recurs on time alone, whatever the odometer reads.
|
||||
</p>
|
||||
</div>
|
||||
<button v-if="canWrite" class="dh-btn dh-btn-primary !px-3 !py-1.5" @click="openAddTechnicalCheck">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-4 w-4"><path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" /></svg>
|
||||
Add check
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="technicalChecks.length === 0" class="rounded-card border border-dashed border-default p-8 text-center text-muted">
|
||||
No technical checks yet.
|
||||
</div>
|
||||
|
||||
<div v-else class="dh-card overflow-x-auto p-0">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-sunken text-left">
|
||||
<tr class="[&>th]:eyebrow [&>th]:px-4 [&>th]:py-3">
|
||||
<th>Date</th>
|
||||
<th>Result</th>
|
||||
<th>Next check</th>
|
||||
<th>Status</th>
|
||||
<th>Station</th>
|
||||
<th class="!text-right">Cost</th>
|
||||
<th>Notes</th>
|
||||
<th>File</th>
|
||||
<th v-if="canWrite"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-subtle">
|
||||
<tr v-for="t in technicalChecks" :key="t.id" class="transition-colors hover:bg-sunken">
|
||||
<td class="whitespace-nowrap px-4 py-3 data font-medium text-strong">{{ formatDate(t.date) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<span :class="t.result === 'failed' ? 'dh-badge dh-badge-danger' : 'dh-badge dh-badge-success'">
|
||||
{{ t.result === 'failed' ? 'Failed' : 'Passed' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 data text-muted">{{ formatDate(t.nextCheckDate) }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<span :class="expiryStatus(t).classes">{{ expiryStatus(t).label }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-body">{{ t.station || '—' }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right data text-body">{{ t.cost ? formatMoney(t.cost) : '—' }}</td>
|
||||
<td class="px-4 py-3 text-body">{{ t.notes || '—' }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
<button v-if="t.hasFile" class="text-xs font-medium text-brandtext hover:underline" @click="downloadAttachment('technical', t)">
|
||||
Download
|
||||
</button>
|
||||
<span v-else class="text-xs text-muted">—</span>
|
||||
</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="openEditTechnicalCheck(t)">Edit</button>
|
||||
<button class="ml-3 text-xs font-medium text-danger hover:underline" @click="deleteTechnicalCheck(t.id)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Maintenance -->
|
||||
<section v-else-if="activeTab === 'maintenance'">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
@@ -892,6 +988,14 @@ onMounted(load);
|
||||
@saved="onServiceSaved"
|
||||
@close="showService = false"
|
||||
/>
|
||||
<TechnicalCheckFormModal
|
||||
v-if="showTechnicalCheck"
|
||||
:car-id="id"
|
||||
:car="car"
|
||||
:check="editingTechnicalCheck"
|
||||
@saved="onTechnicalCheckSaved"
|
||||
@close="showTechnicalCheck = false"
|
||||
/>
|
||||
<PartFormModal
|
||||
v-if="showPart"
|
||||
:car-id="id"
|
||||
|
||||
Reference in new issue
Block a user