Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement

This commit is contained in:
Marshal
2026-07-21 13:24:21 +00:00
62 changed files with 4398 additions and 429 deletions

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 },

View File

@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import {
Alert,
Badge,
@@ -139,7 +140,13 @@ function Rows({ rows }: { rows: TruckOnSite[] }) {
export default function TrucksOnSitePage() {
const { data: trucks = [], isLoading } = useTrucksOnSite();
const [scope, setScope] = useState<"ALL" | "ON_SITE" | "INBOUND">("ALL");
// The dashboard's "Trucks on-site" card counts only arrived trucks, so it
// deep-links here with ?scope=ON_SITE to land on the matching tab.
const [searchParams] = useSearchParams();
const scopeParam = searchParams.get("scope");
const [scope, setScope] = useState<"ALL" | "ON_SITE" | "INBOUND">(
scopeParam === "ON_SITE" || scopeParam === "INBOUND" ? scopeParam : "ALL",
);
const [source, setSource] = useState<"ALL" | "CUSTOMER" | "EDR">("ALL");
const [search, setSearch] = useState("");

View File

@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Button, Card, Group, Select, Stack, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { PackageOpen, Search, Truck } from 'lucide-react';
import { PackageOpen, Search, Truck, X } from 'lucide-react';
import { PageContainer, PageHeader } from '@/components/page';
import {
@@ -25,14 +25,36 @@ export default function WarehouseInventoryPage() {
const [filter, setFilter] = useState<InventoryFilter>(
initialStatus ? { status: initialStatus } : {},
);
// KPI drill-downs arriving from the ops dashboard cards; each shows as a
// dismissible chip so the list can be widened back out in place.
const [quick, setQuick] = useState<
Pick<InventoryFilter, 'receivedToday' | 'pendingInspection' | 'agingOverDays'>
>(() => {
const aging = Number(searchParams.get('agingOverDays'));
return {
receivedToday: searchParams.get('receivedToday') ? true : undefined,
pendingInspection: searchParams.get('pendingInspection') ? true : undefined,
agingOverDays: Number.isFinite(aging) && aging > 0 ? aging : undefined,
};
});
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 300);
const queryFilter = useMemo<InventoryFilter>(
() => ({ ...filter, direction, search: debouncedSearch || undefined }),
[filter, direction, debouncedSearch],
() => ({ ...filter, ...quick, direction, search: debouncedSearch || undefined }),
[filter, quick, direction, debouncedSearch],
);
const quickChips: Array<{ key: keyof typeof quick; label: string }> = [
...(quick.receivedToday ? [{ key: 'receivedToday' as const, label: 'Received today' }] : []),
...(quick.pendingInspection
? [{ key: 'pendingInspection' as const, label: 'Pending inspection' }]
: []),
...(quick.agingOverDays
? [{ key: 'agingOverDays' as const, label: `In warehouse >${quick.agingOverDays}d` }]
: []),
];
const warehousesQuery = useWarehouses();
const yardsQuery = useWarehouseYards(filter.warehouseId);
const zonesQuery = useWarehouseZones(filter.yardId);
@@ -136,6 +158,17 @@ export default function WarehouseInventoryPage() {
}
w={200}
/>
{quickChips.map((chip) => (
<Button
key={chip.key}
size="compact-xs"
variant="light"
rightSection={<X size={12} />}
onClick={() => setQuick((q) => ({ ...q, [chip.key]: undefined }))}
>
{chip.label}
</Button>
))}
</Group>
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />