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:
tajniak81
2026-07-17 11:54:26 +02:00
co-authored by Claude Opus 4.8
parent 03738f08dc
commit 21c99ad762
11 changed files with 713 additions and 81 deletions
+110 -62
View File
@@ -41,78 +41,81 @@ func formatPBDate(t time.Time) string {
// carRecord is the PocketBase-facing shape of a car (snake_case fields).
type carRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Make string `json:"make"`
Model string `json:"model"`
Year int `json:"year"`
Registration string `json:"registration"`
RegistrationCountry string `json:"registration_country"`
VIN string `json:"vin"`
ServiceIntervalDays int `json:"service_interval_days"`
ServiceIntervalKm int `json:"service_interval_km"`
OilSpec string `json:"oil_spec"`
TransmissionOilSpec string `json:"transmission_oil_spec"`
DifferentialOilSpec string `json:"differential_oil_spec"`
BrakeFluidSpec string `json:"brake_fluid_spec"`
CoolantSpec string `json:"coolant_spec"`
CurrentKm int `json:"current_km"`
FuelType string `json:"fuel_type"`
BuildDate string `json:"build_date"`
FirstRegistrationDate string `json:"first_registration_date"`
Owner string `json:"owner"`
Created string `json:"created"`
Updated string `json:"updated"`
ID string `json:"id"`
Name string `json:"name"`
Make string `json:"make"`
Model string `json:"model"`
Year int `json:"year"`
Registration string `json:"registration"`
RegistrationCountry string `json:"registration_country"`
VIN string `json:"vin"`
ServiceIntervalDays int `json:"service_interval_days"`
ServiceIntervalKm int `json:"service_interval_km"`
TechnicalCheckIntervalDays int `json:"technical_check_interval_days"`
OilSpec string `json:"oil_spec"`
TransmissionOilSpec string `json:"transmission_oil_spec"`
DifferentialOilSpec string `json:"differential_oil_spec"`
BrakeFluidSpec string `json:"brake_fluid_spec"`
CoolantSpec string `json:"coolant_spec"`
CurrentKm int `json:"current_km"`
FuelType string `json:"fuel_type"`
BuildDate string `json:"build_date"`
FirstRegistrationDate string `json:"first_registration_date"`
Owner string `json:"owner"`
Created string `json:"created"`
Updated string `json:"updated"`
}
func (rec carRecord) toModel() models.Car {
return models.Car{
ID: rec.ID,
Name: rec.Name,
Make: rec.Make,
Model: rec.Model,
Year: rec.Year,
Registration: rec.Registration,
RegistrationCountry: rec.RegistrationCountry,
VIN: rec.VIN,
ServiceIntervalDays: rec.ServiceIntervalDays,
ServiceIntervalKm: rec.ServiceIntervalKm,
OilSpec: rec.OilSpec,
TransmissionOilSpec: rec.TransmissionOilSpec,
DifferentialOilSpec: rec.DifferentialOilSpec,
BrakeFluidSpec: rec.BrakeFluidSpec,
CoolantSpec: rec.CoolantSpec,
CurrentKm: rec.CurrentKm,
FuelType: rec.FuelType,
BuildDate: rec.BuildDate,
FirstRegistrationDate: rec.FirstRegistrationDate,
Owner: rec.Owner,
Created: rec.Created,
Updated: rec.Updated,
ID: rec.ID,
Name: rec.Name,
Make: rec.Make,
Model: rec.Model,
Year: rec.Year,
Registration: rec.Registration,
RegistrationCountry: rec.RegistrationCountry,
VIN: rec.VIN,
ServiceIntervalDays: rec.ServiceIntervalDays,
ServiceIntervalKm: rec.ServiceIntervalKm,
TechnicalCheckIntervalDays: rec.TechnicalCheckIntervalDays,
OilSpec: rec.OilSpec,
TransmissionOilSpec: rec.TransmissionOilSpec,
DifferentialOilSpec: rec.DifferentialOilSpec,
BrakeFluidSpec: rec.BrakeFluidSpec,
CoolantSpec: rec.CoolantSpec,
CurrentKm: rec.CurrentKm,
FuelType: rec.FuelType,
BuildDate: rec.BuildDate,
FirstRegistrationDate: rec.FirstRegistrationDate,
Owner: rec.Owner,
Created: rec.Created,
Updated: rec.Updated,
}
}
// carPayload builds the write payload for create/update from a domain Car.
func carPayload(c models.Car) map[string]any {
return map[string]any{
"name": c.Name,
"make": c.Make,
"model": c.Model,
"year": c.Year,
"registration": c.Registration,
"registration_country": c.RegistrationCountry,
"vin": c.VIN,
"service_interval_days": c.ServiceIntervalDays,
"service_interval_km": c.ServiceIntervalKm,
"oil_spec": c.OilSpec,
"transmission_oil_spec": c.TransmissionOilSpec,
"differential_oil_spec": c.DifferentialOilSpec,
"brake_fluid_spec": c.BrakeFluidSpec,
"coolant_spec": c.CoolantSpec,
"current_km": c.CurrentKm,
"fuel_type": c.FuelType,
"build_date": c.BuildDate,
"first_registration_date": c.FirstRegistrationDate,
"name": c.Name,
"make": c.Make,
"model": c.Model,
"year": c.Year,
"registration": c.Registration,
"registration_country": c.RegistrationCountry,
"vin": c.VIN,
"service_interval_days": c.ServiceIntervalDays,
"service_interval_km": c.ServiceIntervalKm,
"technical_check_interval_days": c.TechnicalCheckIntervalDays,
"oil_spec": c.OilSpec,
"transmission_oil_spec": c.TransmissionOilSpec,
"differential_oil_spec": c.DifferentialOilSpec,
"brake_fluid_spec": c.BrakeFluidSpec,
"coolant_spec": c.CoolantSpec,
"current_km": c.CurrentKm,
"fuel_type": c.FuelType,
"build_date": c.BuildDate,
"first_registration_date": c.FirstRegistrationDate,
}
}
@@ -161,6 +164,51 @@ func servicePayload(r models.ServiceRecord) map[string]any {
}
}
// --- technical checks ---
type technicalCheckRecord struct {
ID string `json:"id"`
Car string `json:"car"`
Date string `json:"date"`
Result string `json:"result"`
Cost float64 `json:"cost"`
Station string `json:"station"`
ValidUntil string `json:"valid_until"`
Notes string `json:"notes"`
File string `json:"file"`
Created string `json:"created"`
Updated string `json:"updated"`
}
func (rec technicalCheckRecord) toModel() models.TechnicalCheck {
return models.TechnicalCheck{
ID: rec.ID,
Car: rec.Car,
Date: parsePBDate(rec.Date),
Result: rec.Result,
Cost: rec.Cost,
Station: rec.Station,
ValidUntil: parsePBDatePtr(rec.ValidUntil),
Notes: rec.Notes,
Attachment: attachmentOf(rec.File),
Created: rec.Created,
Updated: rec.Updated,
}
}
// technicalCheckPayload omits the file field — see attachmentOf.
func technicalCheckPayload(t models.TechnicalCheck) map[string]any {
return map[string]any{
"car": t.Car,
"date": formatPBDate(t.Date),
"result": t.Result,
"cost": t.Cost,
"station": t.Station,
"valid_until": formatPBDatePtr(t.ValidUntil),
"notes": t.Notes,
}
}
// --- attachments ---
// attachmentOf renders a PocketBase file field into the model's attachment pair.