Files
DriverVault/Phone App/lib/service_parts.dart
T
tajniak81andClaude Opus 5 35e6c511b7 Changed parts: the list is the car's, not the app's
The Changed parts section offered all three parts to every car. An EV changes no
oil, and a checkbox nobody will ever tick is one more thing to read past on every
service — so which parts a car records now belongs to the car, the same way its
tabs, its Information rows and its Service history columns already do.

It works the way those three do because a fourth mechanism for the same idea
would be a fourth to keep in step: hidden_service_parts on the car, validated by
the endpoint that already does this, stored as the hidden set so a part added in
a later release is on by default, and needing write access because the choice
belongs to the car and everyone it is shared with sees it.

There is no order beside it, which is the one place this departs from the other
three. Those arrange things whose position means something — a tab bar reads left
to right, a table's columns are read across. The parts are a checkbox list inside
a single column, and moving Cabin air filter above Oil says nothing. Adding one
later is the same shape as the others if that turns out to be wrong.

A part switched off leaves the form and the history together — the chips on the
phone's cards, the web column's summary and the panel it opens. "I don't record
this" means it stops taking up room, not that it takes up room saying nothing,
which is the rule a hidden column already follows. That is the judgment call
here: a car with five years of oil changes hides them all by switching the part
off. Nothing is written to the records, so switching it back on brings every one
of those chips back, which is what makes the call safe to reverse.

The part that would have been a silent data bug: the API rewrites all three
booleans from the body of a service update, so a form that simply stopped
sending a hidden part would set it false on the next edit of any old record.
Both forms therefore keep every part in their state and submit every one — only
the checkboxes are filtered. The mirror of that is a *new* record, where a hidden
part starts false rather than at its `initial`, since ticking a box nobody was
shown is not a default, it's a guess. Oil is the only part with initial: true, so
that case is live the moment anyone hides it.

Verified: go vet and go test ./... pass, with a new test covering that every part
is hideable (unlike the tabs and the columns — a service that changed nothing is
a real service), that the "parts" column key is refused as a part key and a part
key as a column key, and that no part is also a column. flutter analyze is clean
and flutter test passes 32 to 35, the new ones covering visibleParts, that a
hidden part's chips go while its stored boolean stays, and the picker's fourth
section. npm run build is clean.

Both apps were driven against throwaway stub APIs. Web: the picker saved
{"hiddenServiceParts":["oil"]}, the table's parts cell went from "Oil & Oil
filter +2" to "Engine air filter, Cabin air filter", the record whose only part
was oil went to an empty cell, the panel dropped to two rows, the add form
offered two unticked boxes where oil's initial: true would have ticked one, and
editing the three-part record sent changedOil:true back with a box that was never
on screen. Phone: the same car rendered chips "Engine air, Cabin air", "Changed
parts —" for the oil-only record, and an add sheet with exactly two unticked
boxes.

Not verified: no automated test guards the web behaviour — the web app still has
no test runner, so the above was read out of the live DOM and the outgoing
request bodies by hand. The phone's picker was checked by widget test and by
rendering, but its Save was not driven end to end. Neither app was run against
the real API Server: bootstrap appends the new field on the next start, and until
that start a client sending hiddenServiceParts takes a 400 — they deploy together
from this repo, but the server must go first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:30:08 +02:00

98 lines
3.9 KiB
Dart

import "models.dart";
/// The parts a service record can say were changed.
///
/// One list, because each part has to appear in three places at once — the
/// Changed parts section of the service form, the parts column of a service
/// card, and the panel that column opens — and three hand-kept copies would
/// drift the first time somebody adds a part. Adding one here (plus its boolean
/// on the API's service_records collection) is the whole job.
///
/// Mirrors SERVICE_PARTS in the web app's lib/serviceParts.js. The two lists
/// share the [label] keys on purpose: a part named one thing here and another
/// there would be the same service reading differently on the two screens.
class ServicePart {
/// What the part is known by wherever a part has to be identified. Not a
/// column key — every part shares the one "parts" column, which is what keeps
/// the table from widening as parts are added.
final String key;
/// The record's own boolean, as the API names it in JSON. Only the form needs
/// it, to build the payload it sends.
final String field;
/// The translation key, shared with the form so the card and the dialog
/// cannot end up wording the same part differently.
final String label;
/// The shorter wording, for the chip on a service card. A chip has room for a
/// phrase and not a sentence; the web's table cell has the same problem and
/// answers it by truncating the list instead.
final String chipLabel;
/// What a *new* record starts with: an oil change is the reason for most
/// services, the filters are the exception.
final bool initial;
/// Whether this record says the part was changed. A function rather than the
/// field name because Dart cannot index a class the way the web app indexes
/// the record object it got back as JSON.
final bool Function(ServiceRecord) changed;
const ServicePart({
required this.key,
required this.field,
required this.label,
required this.chipLabel,
required this.initial,
required this.changed,
});
}
final List<ServicePart> kServiceParts = [
ServicePart(
key: "oil",
field: "changedOil",
label: "forms.service.oil",
chipLabel: "car.services.chipOil",
initial: true,
changed: (s) => s.changedOil,
),
ServicePart(
key: "engineFilter",
field: "changedEngineAirFilter",
label: "forms.service.engineFilter",
chipLabel: "car.services.chipEngineFilter",
initial: false,
changed: (s) => s.changedEngineAirFilter,
),
ServicePart(
key: "cabinFilter",
field: "changedCabinAirFilter",
label: "forms.service.cabinFilter",
chipLabel: "car.services.chipCabinFilter",
initial: false,
changed: (s) => s.changedCabinAirFilter,
),
];
/// The parts a car actually records, which is every one it hasn't switched off.
/// An EV changes no oil, and a checkbox nobody will ever tick is one more thing
/// to read past on every service. The hidden set rather than the visible one, so
/// a part added in a later release is on by default.
List<ServicePart> visibleParts(Car car) =>
kServiceParts.where((part) => !car.hiddenServiceParts.contains(part.key)).toList();
/// The parts this record says were changed, among the ones [parts] still shows.
/// A record written before a part existed simply doesn't carry its field, which
/// the model reads as false — "not changed" rather than "unknown", because that
/// service genuinely didn't change it.
///
/// A part switched off disappears from the history as well as from the form, the
/// same way a hidden column does: what "I don't record this" means is that it
/// stops taking up room, not that it takes up room saying nothing. Its stored
/// boolean is left alone, so switching it back on brings the old records' chips
/// back with it.
List<ServicePart> changedParts(ServiceRecord service, List<ServicePart> parts) =>
parts.where((part) => part.changed(service)).toList();