The Service history columns became a property of the car two commits ago, and
the phone was left out of it on the grounds that it has no table to arrange.
But the arrangement is not the table's — it belongs to the car, and everyone it
is shared with sees it. A reader who switched Oil off on the web still had it
on every card here, which makes the setting look broken rather than absent.
A card is not a table, so the columns cannot be cells. Consecutive short ones
share a wrapping line, which flows left to right and then down and so keeps the
arrangement intact; the parts, the notes and the file each take a line of their
own. That means the grouping follows the car's order rather than the
catalogue's — move Notes between Km and Next date and the short columns split
around it — which is the part a hand-written card gets wrong by collecting the
short columns first and appending the blocks after them, quietly undoing the
arrangement it was asked to honour. serviceColumnRuns is a function for exactly
that reason: it is the piece worth a test.
The date carries no heading where every other column does. A card list is read
down its dates, and "Date" in front of one says nothing the date doesn't — the
same judgment the server makes by refusing to hide it. It is offered in the
picker anyway, ticked and locked, because a row missing from that list is a row
nothing on this screen can drag; the web drags the column headings themselves,
which on a touch screen is the scroll's gesture. A column that is on but empty
says so ("Notes —") rather than vanishing: it was switched on deliberately, and
a card that silently drops it reads as a record that failed to load.
Changed parts arrives with it. Every part shares the one column — they are a
growing list and a column apiece would widen the web's table without end — and
lib/service_parts.dart is the twin of the web's lib/serviceParts.js, so the
form's checkboxes and the card's chips come from one list and adding a part is
one entry plus its boolean on service_records. The chips keep their own shorter
wording; the form keeps the web's, which is what stops the two apps naming the
same part differently.
Verified: flutter analyze is clean and flutter test passes, 22 tests to 32 —
the new ones cover that the arrangeable set is the hideable one plus the date,
that a field key is not a column key, that adding a part adds no column, the
run grouping, and a widget test of the picker showing the date's is the only
locked checkbox. The screen itself was driven against a throwaway stub API: a
default car renders date, Km, Next date, Next km, chips, notes and file in that
order, and a car hiding km and file with the order [notes, date, nextKm, parts,
km, nextDate] rendered exactly that — notes first, both hidden columns gone, the
short columns split around the chips. updateCarView round-trips both new fields
under the names records.go decodes.
Not verified: the picker's own Save button — the tap landed in the harness but
the request never reached the stub, which reads as the fire-and-forget future
being cut off at teardown, since the same call made directly worked. Drag was
exercised through the reorder callback, not by a finger. Nothing here needs the
API Server to change: both fields already ship, and a phone running against an
older one simply reads empty lists and shows every column.
Two commits needed nothing: the web's masked date box answers <input
type="date"> rendering in the browser's locale, which a picker-only field
cannot have, and the garage card's width answers a badge that wrapped, which
this badge cannot. car.services.next goes, its prose replaced by the columns
that now say it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.1 KiB
Dart
85 lines
3.1 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 this record says were changed. 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.
|
|
List<ServicePart> changedParts(ServiceRecord service) =>
|
|
kServiceParts.where((part) => part.changed(service)).toList();
|