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:
co-authored by
Claude Opus 4.8
parent
03738f08dc
commit
21c99ad762
@@ -24,11 +24,11 @@ type Attachment struct {
|
||||
|
||||
// Car corresponds to one worksheet in the original spreadsheet.
|
||||
type Car struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"` // e.g. "Toyota Yaris"
|
||||
Make string `json:"make"` // e.g. "Toyota"
|
||||
Model string `json:"model"` // e.g. "Yaris"
|
||||
Year int `json:"year"` // optional
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"` // e.g. "Toyota Yaris"
|
||||
Make string `json:"make"` // e.g. "Toyota"
|
||||
Model string `json:"model"` // e.g. "Yaris"
|
||||
Year int `json:"year"` // optional
|
||||
Registration string `json:"registration"` // optional plate
|
||||
RegistrationCountry string `json:"registrationCountry"` // optional (country of registration)
|
||||
VIN string `json:"vin"` // optional
|
||||
@@ -38,6 +38,12 @@ type Car struct {
|
||||
ServiceIntervalDays int `json:"serviceIntervalDays"`
|
||||
ServiceIntervalKm int `json:"serviceIntervalKm"`
|
||||
|
||||
// TechnicalCheckIntervalDays is the roadworthiness inspection cycle. It only
|
||||
// prefills the next date — the interval is set by law, not by the car, and
|
||||
// changes as the car ages, so any check can override it with the date its
|
||||
// certificate actually carries.
|
||||
TechnicalCheckIntervalDays int `json:"technicalCheckIntervalDays"`
|
||||
|
||||
// CurrentKm is the car's present odometer reading, updated by the user. Used
|
||||
// to flag km-based overdue service (current_km >= last service km + interval).
|
||||
CurrentKm int `json:"currentKm"`
|
||||
@@ -87,6 +93,40 @@ type ServiceRecord struct {
|
||||
Updated string `json:"updated,omitempty"`
|
||||
}
|
||||
|
||||
// TechnicalCheck is one mandatory roadworthiness inspection in a car's history —
|
||||
// przegląd techniczny, MOT, TÜV, contrôle technique, depending on where the car
|
||||
// is registered.
|
||||
//
|
||||
// It is shaped like a ServiceRecord, but recurs on time alone: an inspection
|
||||
// falls due on a date whatever the odometer says. The cycle is a legal one
|
||||
// rather than a property of the car, and it is not constant — a new car's first
|
||||
// check falls due years later than its third — so the car's interval is only a
|
||||
// default, and ValidUntil overrides it per record.
|
||||
type TechnicalCheck struct {
|
||||
ID string `json:"id"`
|
||||
Car string `json:"car"` // relation -> Car.ID
|
||||
Date time.Time `json:"date"` // date of the inspection
|
||||
|
||||
Result string `json:"result"` // passed | failed
|
||||
Cost float64 `json:"cost"`
|
||||
Station string `json:"station,omitempty"` // inspection station / inspector
|
||||
Notes string `json:"notes,omitempty"`
|
||||
|
||||
// ValidUntil is the expiry printed on the certificate. When set it wins over
|
||||
// the car's interval, because it is the date that actually governs.
|
||||
ValidUntil *time.Time `json:"validUntil,omitempty"`
|
||||
|
||||
// The certificate itself.
|
||||
Attachment
|
||||
|
||||
// Derived (not stored): filled in by the API on read.
|
||||
NextCheckDate *time.Time `json:"nextCheckDate,omitempty"`
|
||||
Expiry ExpiryAssessment `json:"expiry"`
|
||||
|
||||
Created string `json:"created,omitempty"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
}
|
||||
|
||||
// Part is one entry in a car's parts catalog (Excel cols M/N).
|
||||
type Part struct {
|
||||
ID string `json:"id"`
|
||||
@@ -262,7 +302,7 @@ type Reminder struct {
|
||||
Status string `json:"status"` // done | overdue | due_soon | upcoming | no_trigger
|
||||
DaysLeft *int `json:"daysLeft,omitempty"` // nil when there is no due date
|
||||
KmLeft *int `json:"kmLeft,omitempty"` // nil when there is no due km / no odometer
|
||||
Auto bool `json:"auto"` // true = derived from a document/service, read-only
|
||||
Auto bool `json:"auto"` // true = derived from a document/service, read-only
|
||||
SourceRef string `json:"sourceRef,omitempty"` // id of the record an auto reminder came from
|
||||
|
||||
Created string `json:"created,omitempty"`
|
||||
@@ -325,6 +365,50 @@ func (r *ServiceRecord) ComputeDerived(c *Car) {
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTechnicalCheckIntervalDays is the annual cycle most of Europe settles
|
||||
// into once a car is a few years old. It is only a starting point: see
|
||||
// Car.TechnicalCheckIntervalDays.
|
||||
const DefaultTechnicalCheckIntervalDays = 365
|
||||
|
||||
// ComputeTechnicalCheckDerived fills NextCheckDate and the expiry assessment.
|
||||
//
|
||||
// The date on the certificate wins over the car's interval when present; the
|
||||
// interval only covers records entered without one. A failed inspection
|
||||
// certifies nothing, so it yields no next date at all — reading one off a
|
||||
// failure would put a reassuring "valid until" on a car that just flunked.
|
||||
func (t *TechnicalCheck) ComputeTechnicalCheckDerived(c *Car, now time.Time) {
|
||||
t.NextCheckDate = nil
|
||||
t.Expiry = ExpiryAssessment{State: "no_expiry"}
|
||||
if t.Result == "failed" {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case t.ValidUntil != nil && !t.ValidUntil.IsZero():
|
||||
d := *t.ValidUntil
|
||||
t.NextCheckDate = &d
|
||||
case !t.Date.IsZero():
|
||||
days := DefaultTechnicalCheckIntervalDays
|
||||
if c != nil && c.TechnicalCheckIntervalDays > 0 {
|
||||
days = c.TechnicalCheckIntervalDays
|
||||
}
|
||||
d := t.Date.AddDate(0, 0, days)
|
||||
t.NextCheckDate = &d
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
days := daysBetween(now, *t.NextCheckDate)
|
||||
state := "valid"
|
||||
switch {
|
||||
case days < 0:
|
||||
state = "expired"
|
||||
case days <= SoonDays:
|
||||
state = "expiring_soon"
|
||||
}
|
||||
t.Expiry = ExpiryAssessment{State: state, Days: &days}
|
||||
}
|
||||
|
||||
// SoonDays is the window within which an upcoming expiry or reminder is
|
||||
// surfaced as "due soon" rather than merely upcoming.
|
||||
const SoonDays = 30
|
||||
|
||||
Reference in New Issue
Block a user