feat(fleet): validate vehicle plate numbers against a letters-and-digits format

Plate, power-plate and trailer accepted any free text — a vehicle could be
saved with a plate of "assadasd". They must be letters, a hyphen, then digits,
like ET-9875 or AA-8642.

The server now enforces it on CreateVehicleDto (and UpdateVehicleDto via
PartialType): each plate is trimmed and upper-cased, then matched against
^[A-Z]{2,3}-\d{2,6}$, so "et-9875" is accepted and stored as ET-9875 while an
empty optional trailer/power plate still passes.

The fleet form gains the same check inline: FleetFormFieldDef takes an optional
pattern, the dialog tests it on submit against the upper-cased value, and the
vehicle config points plate and trailer at a regex that mirrors the server's.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-21 09:39:49 +00:00
parent 2349f62247
commit b616f520ae
5 changed files with 116 additions and 3 deletions

View File

@@ -249,6 +249,16 @@ const FleetFormDialog = ({
}
}
}
// Format check (e.g. plate numbers). Skipped for an empty optional field —
// "required" above already owns the empty case. Upper-cased to match the
// server, which stores plates upper-case.
if (field.pattern && stringValue && stringValue !== FLEET_SELECT_NONE) {
const candidate = field.pattern.uppercase === false ? stringValue : stringValue.toUpperCase();
if (!field.pattern.regex.test(candidate)) {
next[field.name] = field.pattern.message;
}
}
});
setErrors(next);
return Object.keys(next).length === 0;

View File

@@ -69,6 +69,11 @@ export interface FleetFormFieldDef extends FormFieldDef {
* (e.g. a license expiry); "past" (default) = cannot be in the future.
*/
dateBound?: "past" | "future";
/**
* Format the value must match, checked on submit. The value is upper-cased and
* trimmed before the test, matching the server. Empty optional fields skip it.
*/
pattern?: { regex: RegExp; message: string; uppercase?: boolean };
}
export interface FleetListFilterDef {

View File

@@ -1,5 +1,15 @@
import type { FleetResourceConfig } from "./resources";
/**
* A plate is two or three letters, a hyphen, then two to six digits — ET-9875,
* AA-8642. Mirrors VEHICLE_PLATE_REGEX on the API so the form and the server
* agree on what a plate looks like.
*/
const PLATE_PATTERN = {
regex: /^[A-Z]{2,3}-\d{2,6}$/,
message: "Use letters and numbers like ET-9875 or AA-8642",
};
const VEHICLE_TYPE_OPTIONS = [
{ label: "Truck", value: "TRUCK" },
{ label: "Van", value: "VAN" },
@@ -77,9 +87,9 @@ export const vehiclesConfig: FleetResourceConfig = {
],
formFields: [
{ name: "code", label: "Code", type: "text" },
{ name: "plateNumber", label: "Power Plate No", type: "text", required: true },
{ name: "plateNumber", label: "Power Plate No", type: "text", required: true, pattern: PLATE_PATTERN },
// { name: "powerPlateNo", label: "Power Plate No", type: "text" },
{ name: "trailerPlateNo", label: "Trailer Plate No", type: "text" },
{ name: "trailerPlateNo", label: "Trailer Plate No", type: "text", pattern: PLATE_PATTERN },
{ name: "vehicleType", label: "Vehicle Type", type: "select", required: true, options: VEHICLE_TYPE_OPTIONS },
{ name: "manufacturer", label: "Manufacturer", type: "text", required: true },
{ name: "model", label: "Model", type: "text", required: true },