mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
feat: complete train scheduling integration
This commit is contained in:
@@ -12,7 +12,11 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { useCargoTypes } from '@/hooks/use-cargo-types';
|
||||
import { useContainerTypes } from '@/hooks/use-container-types';
|
||||
import { useWagonTypes } from '@/hooks/use-wagon-types';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useCreateCargo, useDeleteCargo, useCargoes, useUpdateCargo } from '@/hooks/useCargoes';
|
||||
import {
|
||||
@@ -28,11 +32,19 @@ import type { Container } from '@/services/containerService';
|
||||
import type { Train } from '@/services/trains.service';
|
||||
import type { Wagon } from '@/services/wagon.service';
|
||||
|
||||
type FormValue = string | number;
|
||||
|
||||
type Field = {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: 'text' | 'number';
|
||||
type?: 'text' | 'number' | 'select';
|
||||
required?: boolean;
|
||||
options?: { value: string; label: string }[];
|
||||
placeholder?: string;
|
||||
onValueChange?: (
|
||||
value: string,
|
||||
current: Record<string, FormValue>,
|
||||
) => Partial<Record<string, FormValue>>;
|
||||
};
|
||||
|
||||
type Column<T> = {
|
||||
@@ -49,20 +61,62 @@ type FleetCrudPageProps<T extends { id: string }> = {
|
||||
isLoading: boolean;
|
||||
columns: Column<T>[];
|
||||
fields: Field[];
|
||||
emptyValues: Record<string, string | number>;
|
||||
emptyValues: Record<string, FormValue>;
|
||||
searchText: (item: T) => string;
|
||||
create: { mutateAsync: (data: Record<string, unknown>) => Promise<unknown>; isPending: boolean };
|
||||
update: { mutateAsync: (data: { id: string; data: Record<string, unknown> }) => Promise<unknown>; isPending: boolean };
|
||||
remove: { mutateAsync: (id: string) => Promise<unknown>; isPending: boolean };
|
||||
};
|
||||
|
||||
const normalizePayload = (values: Record<string, string | number>) =>
|
||||
const normalizePayload = (values: Record<string, FormValue>) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(values)
|
||||
.map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
|
||||
.filter(([, value]) => value !== ''),
|
||||
);
|
||||
|
||||
const extractBackendErrors = (error: unknown) => {
|
||||
const responseData = (error as { response?: { data?: unknown } })?.response?.data;
|
||||
const data = responseData && typeof responseData === 'object' ? responseData as Record<string, unknown> : undefined;
|
||||
const rawMessage = data?.message ?? data?.error ?? (error as Error)?.message;
|
||||
const rawErrors = data?.errors;
|
||||
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
if (rawErrors && typeof rawErrors === 'object' && !Array.isArray(rawErrors)) {
|
||||
Object.entries(rawErrors as Record<string, unknown>).forEach(([field, value]) => {
|
||||
fieldErrors[field] = Array.isArray(value) ? value.join(', ') : String(value);
|
||||
});
|
||||
}
|
||||
|
||||
const message = Array.isArray(rawMessage)
|
||||
? rawMessage.join(', ')
|
||||
: rawMessage
|
||||
? String(rawMessage)
|
||||
: 'Save failed';
|
||||
|
||||
return { message, fieldErrors };
|
||||
};
|
||||
|
||||
const validateForm = (fields: Field[], values: Record<string, FormValue>) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
const value = values[field.key];
|
||||
const stringValue = typeof value === 'string' ? value.trim() : String(value ?? '');
|
||||
|
||||
if (field.required && stringValue === '') {
|
||||
errors[field.key] = `${field.label} is required`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.type === 'number' && stringValue !== '' && !Number.isFinite(Number(value))) {
|
||||
errors[field.key] = `${field.label} must be a valid number`;
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
function FleetCrudPage<T extends { id: string }>({
|
||||
title,
|
||||
description,
|
||||
@@ -85,6 +139,7 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
const [editing, setEditing] = useState<T | null>(null);
|
||||
const [viewing, setViewing] = useState<T | null>(null);
|
||||
const [form, setForm] = useState(emptyValues);
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
const { toast } = useToast();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
@@ -118,6 +173,7 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
setForm(emptyValues);
|
||||
setFieldErrors({});
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
@@ -128,6 +184,7 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
Object.keys(emptyValues).map((key) => [key, (item as Record<string, string | number | null | undefined>)[key] ?? '']),
|
||||
),
|
||||
);
|
||||
setFieldErrors({});
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
@@ -135,11 +192,24 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
setFormOpen(false);
|
||||
setEditing(null);
|
||||
setForm(emptyValues);
|
||||
setFieldErrors({});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const validationErrors = validateForm(fields, form);
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setFieldErrors(validationErrors);
|
||||
toast({
|
||||
title: 'Save failed',
|
||||
description: Object.values(validationErrors)[0],
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = normalizePayload(form);
|
||||
setFieldErrors({});
|
||||
|
||||
try {
|
||||
if (editing) {
|
||||
@@ -150,8 +220,10 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
toast({ title: `${title.slice(0, -1)} created` });
|
||||
}
|
||||
closeForm();
|
||||
} catch {
|
||||
toast({ title: 'Save failed', description: 'Please check the fields and try again.', variant: 'destructive' });
|
||||
} catch (error) {
|
||||
const { message, fieldErrors: backendFieldErrors } = extractBackendErrors(error);
|
||||
setFieldErrors(backendFieldErrors);
|
||||
toast({ title: 'Save failed', description: message, variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,23 +345,57 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
<DialogTitle>{editing ? `Edit ${title.slice(0, -1)}` : addLabel}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label htmlFor={field.key}>{field.label}</Label>
|
||||
<Input
|
||||
id={field.key}
|
||||
type={field.type ?? 'text'}
|
||||
required={field.required}
|
||||
value={form[field.key] ?? ''}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: field.type === 'number' ? Number(event.target.value) : event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{fields.map((field) => {
|
||||
const value = form[field.key] ?? '';
|
||||
const inputValue = field.type === 'number' && value !== '' && !Number.isFinite(Number(value))
|
||||
? ''
|
||||
: value;
|
||||
return (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label htmlFor={field.key}>{field.label}</Label>
|
||||
{field.type === 'select' ? (
|
||||
<Select
|
||||
value={String(value)}
|
||||
onValueChange={(selectedValue) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: selectedValue,
|
||||
...(field.onValueChange?.(selectedValue, current) ?? {}),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={field.key}>
|
||||
<SelectValue placeholder={field.placeholder ?? `Select ${field.label.toLowerCase()}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={field.key}
|
||||
type={field.type ?? 'text'}
|
||||
value={inputValue}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[field.key]: field.type === 'number' && event.target.value !== ''
|
||||
? Number(event.target.value)
|
||||
: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{fieldErrors[field.key] ? (
|
||||
<p className="text-sm text-destructive">{fieldErrors[field.key]}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={closeForm}>
|
||||
Cancel
|
||||
@@ -325,6 +431,9 @@ function FleetCrudPage<T extends { id: string }>({
|
||||
|
||||
const statusBadge = (status?: string) => <Badge variant="outline">{status ?? '-'}</Badge>;
|
||||
|
||||
const optionLabel = (options: { value: string; label: string }[], value?: string | null) =>
|
||||
options.find((option) => option.value === value)?.label ?? value ?? '-';
|
||||
|
||||
export function TrainMasterDataPage() {
|
||||
const query = useTrains();
|
||||
return (
|
||||
@@ -362,6 +471,11 @@ export function TrainMasterDataPage() {
|
||||
|
||||
export function WagonsCrudPage() {
|
||||
const query = useWagons();
|
||||
const { data: wagonTypes = [] } = useWagonTypes();
|
||||
const wagonTypeOptions = wagonTypes.map((type: any) => ({
|
||||
value: type.id,
|
||||
label: `${type.code} - ${type.name}`,
|
||||
}));
|
||||
return (
|
||||
<FleetCrudPage<Wagon>
|
||||
title="Wagons"
|
||||
@@ -375,13 +489,24 @@ export function WagonsCrudPage() {
|
||||
searchText={(wagon) => [wagon.wagonNumber, wagon.wagonTypeId, wagon.trainId, wagon.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'wagonNumber', label: 'Number' },
|
||||
{ key: 'wagonTypeId', label: 'Type ID' },
|
||||
{ key: 'wagonTypeId', label: 'Type', render: (wagon) => optionLabel(wagonTypeOptions, wagon.wagonTypeId) },
|
||||
{ key: 'maxPayloadWeight', label: 'Max payload' },
|
||||
{ key: 'status', label: 'Status', render: (wagon) => statusBadge(wagon.status) },
|
||||
]}
|
||||
fields={[
|
||||
{ key: 'wagonNumber', label: 'Wagon number', required: true },
|
||||
{ key: 'wagonTypeId', label: 'Wagon type ID', required: true },
|
||||
{
|
||||
key: 'wagonTypeId',
|
||||
label: 'Wagon type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: wagonTypeOptions,
|
||||
onValueChange: (value, current) => {
|
||||
const selectedType = wagonTypes.find((type: any) => type.id === value);
|
||||
if (!selectedType || Number(current.maxPayloadWeight) > 0) return {};
|
||||
return { maxPayloadWeight: Number(selectedType.capacityTons) };
|
||||
},
|
||||
},
|
||||
{ key: 'tareWeight', label: 'Tare weight', type: 'number', required: true },
|
||||
{ key: 'maxPayloadWeight', label: 'Max payload weight', type: 'number', required: true },
|
||||
{ key: 'status', label: 'Status' },
|
||||
@@ -394,6 +519,16 @@ export function WagonsCrudPage() {
|
||||
|
||||
export function ContainersCrudPage() {
|
||||
const query = useContainers();
|
||||
const { data: containerTypes = [] } = useContainerTypes();
|
||||
const { data: wagons = [] } = useWagons();
|
||||
const containerTypeOptions = containerTypes.map((type: any) => ({
|
||||
value: type.id,
|
||||
label: type.label ?? type.name ?? type.code,
|
||||
}));
|
||||
const wagonOptions = wagons.map((wagon: Wagon) => ({
|
||||
value: wagon.id,
|
||||
label: wagon.wagonNumber,
|
||||
}));
|
||||
return (
|
||||
<FleetCrudPage<Container>
|
||||
title="Containers"
|
||||
@@ -407,15 +542,27 @@ export function ContainersCrudPage() {
|
||||
searchText={(container) => [container.containerNumber, container.containerTypeId, container.wagonId, container.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'containerNumber', label: 'Number' },
|
||||
{ key: 'containerTypeId', label: 'Type ID' },
|
||||
{ key: 'wagonId', label: 'Wagon', render: (container) => container.wagonId || 'Unassigned' },
|
||||
{ key: 'containerTypeId', label: 'Type', render: (container) => optionLabel(containerTypeOptions, container.containerTypeId) },
|
||||
{ key: 'wagonId', label: 'Wagon', render: (container) => optionLabel(wagonOptions, container.wagonId) },
|
||||
{ key: 'maxGrossWeight', label: 'Max gross' },
|
||||
{ key: 'status', label: 'Status', render: (container) => statusBadge(container.status) },
|
||||
]}
|
||||
fields={[
|
||||
{ key: 'containerNumber', label: 'Container number', required: true },
|
||||
{ key: 'containerTypeId', label: 'Container type ID', required: true },
|
||||
{ key: 'wagonId', label: 'Wagon ID' },
|
||||
{
|
||||
key: 'containerTypeId',
|
||||
label: 'Container type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: containerTypeOptions,
|
||||
},
|
||||
{
|
||||
key: 'wagonId',
|
||||
label: 'Wagon',
|
||||
type: 'select',
|
||||
options: [{ value: 'none', label: 'Unassigned' }, ...wagonOptions],
|
||||
onValueChange: (value) => (value === 'none' ? { wagonId: '' } : {}),
|
||||
},
|
||||
{ key: 'position', label: 'Position', type: 'number' },
|
||||
{ key: 'tareWeight', label: 'Tare weight', type: 'number', required: true },
|
||||
{ key: 'maxGrossWeight', label: 'Max gross weight', type: 'number', required: true },
|
||||
@@ -429,6 +576,16 @@ export function ContainersCrudPage() {
|
||||
|
||||
export function CargoesCrudPage() {
|
||||
const query = useCargoes();
|
||||
const { data: cargoTypes = [] } = useCargoTypes();
|
||||
const { data: containers = [] } = useContainers();
|
||||
const cargoTypeOptions = cargoTypes.map((type: any) => ({
|
||||
value: type.id,
|
||||
label: type.cargoTypeName ?? type.cargo_type_name ?? type.name ?? type.code,
|
||||
}));
|
||||
const containerOptions = containers.map((container: Container) => ({
|
||||
value: container.id,
|
||||
label: container.containerNumber,
|
||||
}));
|
||||
return (
|
||||
<FleetCrudPage<Cargo>
|
||||
title="Cargoes"
|
||||
@@ -442,7 +599,8 @@ export function CargoesCrudPage() {
|
||||
searchText={(cargo) => [cargo.cargoReference, cargo.description, cargo.containerId, cargo.status].join(' ')}
|
||||
columns={[
|
||||
{ key: 'cargoReference', label: 'Reference' },
|
||||
{ key: 'containerId', label: 'Container ID' },
|
||||
{ key: 'cargoTypeId', label: 'Cargo type', render: (cargo) => optionLabel(cargoTypeOptions, cargo.cargoTypeId) },
|
||||
{ key: 'containerId', label: 'Container', render: (cargo) => optionLabel(containerOptions, cargo.containerId) },
|
||||
{ key: 'quantity', label: 'Quantity' },
|
||||
{ key: 'weight', label: 'Weight' },
|
||||
{ key: 'status', label: 'Status', render: (cargo) => statusBadge(cargo.status) },
|
||||
@@ -450,8 +608,19 @@ export function CargoesCrudPage() {
|
||||
fields={[
|
||||
{ key: 'cargoReference', label: 'Cargo reference', required: true },
|
||||
{ key: 'shipmentId', label: 'Shipment ID', required: true },
|
||||
{ key: 'containerId', label: 'Container ID', required: true },
|
||||
{ key: 'cargoTypeId', label: 'Cargo type ID' },
|
||||
{
|
||||
key: 'containerId',
|
||||
label: 'Container',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: containerOptions,
|
||||
},
|
||||
{
|
||||
key: 'cargoTypeId',
|
||||
label: 'Cargo type',
|
||||
type: 'select',
|
||||
options: cargoTypeOptions,
|
||||
},
|
||||
{ key: 'description', label: 'Description' },
|
||||
{ key: 'quantity', label: 'Quantity', type: 'number', required: true },
|
||||
{ key: 'weight', label: 'Weight', type: 'number', required: true },
|
||||
|
||||
@@ -80,7 +80,7 @@ const deriveFromBooking = (
|
||||
|
||||
const TrainsPage = () => {
|
||||
const qc = useQueryClient();
|
||||
const [filters, setFilters] = useState<TrainScheduleFilters>({ status: 'PAID' });
|
||||
const [filters, setFilters] = useState<TrainScheduleFilters>({});
|
||||
const [selectedBookingIds, setSelectedBookingIds] = useState<string[]>([]);
|
||||
const [preview, setPreview] = useState<TrainSchedulePreviewResponse | null>(null);
|
||||
const [selectedLocomotiveId, setSelectedLocomotiveId] = useState('');
|
||||
@@ -361,7 +361,7 @@ const TrainsPage = () => {
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Booking status</label>
|
||||
<Select
|
||||
value={filters.status ?? 'PAID'}
|
||||
value={filters.status ?? '__all__'}
|
||||
onValueChange={(value) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
@@ -370,9 +370,10 @@ const TrainsPage = () => {
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Paid bookings" />
|
||||
<SelectValue placeholder="All statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">All eligible statuses</SelectItem>
|
||||
<SelectItem value="PAID">Paid</SelectItem>
|
||||
<SelectItem value="FULLY_EXECUTED">Fully executed</SelectItem>
|
||||
<SelectItem value="APPROVED">Approved</SelectItem>
|
||||
|
||||
109
apps/edr-freight-web/backoffice/src/pages/wagons/WagonForm.tsx
Normal file
109
apps/edr-freight-web/backoffice/src/pages/wagons/WagonForm.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||
import { useWagonTypes } from '@/hooks/use-wagon-types';
|
||||
|
||||
const wagonSchema = z.object({
|
||||
wagonNumber: z.string().min(1, 'Required'),
|
||||
wagonTypeId: z.string().min(1, 'Required'),
|
||||
maxPayloadWeight: z.coerce.number().min(0),
|
||||
});
|
||||
|
||||
type WagonFormValues = z.infer<typeof wagonSchema>;
|
||||
|
||||
interface WagonFormProps {
|
||||
initialValues?: Partial<WagonFormValues>;
|
||||
onSubmit: (values: WagonFormValues) => void;
|
||||
}
|
||||
|
||||
export function WagonForm({ initialValues, onSubmit }: WagonFormProps) {
|
||||
const { data: wagonTypes, isLoading: loadingTypes } = useWagonTypes();
|
||||
|
||||
const form = useForm<WagonFormValues>({
|
||||
resolver: zodResolver(wagonSchema),
|
||||
defaultValues: {
|
||||
wagonNumber: initialValues?.wagonNumber || '',
|
||||
wagonTypeId: initialValues?.wagonTypeId || '',
|
||||
maxPayloadWeight: initialValues?.maxPayloadWeight || 0,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedTypeId = form.watch('wagonTypeId');
|
||||
|
||||
// Autofill maxPayloadWeight when type changes
|
||||
useEffect(() => {
|
||||
if (selectedTypeId && wagonTypes) {
|
||||
const type = wagonTypes.find((t) => t.id === selectedTypeId);
|
||||
if (type) {
|
||||
// Only autofill if it's a new selection and field is at default or empty
|
||||
const currentWeight = form.getValues('maxPayloadWeight');
|
||||
if (!initialValues?.wagonTypeId || selectedTypeId !== initialValues.wagonTypeId) {
|
||||
form.setValue('maxPayloadWeight', Number(type.capacityTons));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedTypeId, wagonTypes, form, initialValues?.wagonTypeId]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wagonNumber"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wagon Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g. W12345" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wagonTypeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wagon Type</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={loadingTypes}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select wagon type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{wagonTypes?.map((type) => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.code} - {type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxPayloadWeight"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max Payload Weight (Tons)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" step="0.001" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user