trains, wagons,containers and cargoes schema and API

This commit is contained in:
hagiye
2026-06-04 15:56:29 +03:00
parent 18015ceaff
commit 71ac89edc7
66 changed files with 2077 additions and 90 deletions

View File

@@ -0,0 +1,44 @@
import { useCargoesByContainer, useDeliverCargo, useUnloadCargo } from '@/hooks/useCargoes';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { LoadCargoDialog } from './LoadCargoDialog';
export function CargoesTable({ containerId }: { containerId: string }) {
const { data: cargoes, refetch } = useCargoesByContainer(containerId);
const deliver = useDeliverCargo();
const unload = useUnloadCargo();
if (!cargoes?.length) return <div className="text-muted-foreground">No cargoes for this container.</div>;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Reference</TableHead>
<TableHead>Description</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Weight (kg)</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{cargoes.map(cargo => (
<TableRow key={cargo.id}>
<TableCell>{cargo.cargoReference}</TableCell>
<TableCell>{cargo.description || '-'}</TableCell>
<TableCell>{cargo.quantity}</TableCell>
<TableCell>{cargo.weight}</TableCell>
<TableCell><Badge variant="outline">{cargo.status}</Badge></TableCell>
<TableCell className="space-x-2">
{cargo.status === 'PENDING' && <LoadCargoDialog cargoId={cargo.id} onSuccess={() => refetch()} />}
{cargo.status === 'LOADED' && <Button size="sm" onClick={() => deliver.mutateAsync(cargo.id).then(() => refetch())}>Deliver</Button>}
{cargo.status === 'LOADED' && <Button size="sm" variant="outline" onClick={() => unload.mutateAsync(cargo.id).then(() => refetch())}>Unload</Button>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useLoadCargo } from '@/hooks/useCargoes';
import { useToast } from '@/hooks/use-toast';
export function LoadCargoDialog({ cargoId, onSuccess }: { cargoId: string; onSuccess?: () => void }) {
const [open, setOpen] = useState(false);
const [quantity, setQuantity] = useState(0);
const [weight, setWeight] = useState(0);
const [volume, setVolume] = useState<number>();
const load = useLoadCargo();
const { toast } = useToast();
const handleLoad = async () => {
await load.mutateAsync({ id: cargoId, quantity, weight, volume });
toast({ title: 'Loaded', description: 'Cargo loaded into container.' });
setOpen(false);
onSuccess?.();
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild><Button size="sm">Load Cargo</Button></DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Load Cargo</DialogTitle></DialogHeader>
<div className="space-y-4">
<div><Label>Quantity*</Label><Input type="number" required value={quantity} onChange={e => setQuantity(parseFloat(e.target.value))} /></div>
<div><Label>Weight (kg)*</Label><Input type="number" required value={weight} onChange={e => setWeight(parseFloat(e.target.value))} /></div>
<div><Label>Volume (m³)</Label><Input type="number" value={volume ?? ''} onChange={e => setVolume(parseFloat(e.target.value) || undefined)} /></div>
<Button onClick={handleLoad} disabled={load.isPending}>Confirm Load</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useContainers, useAssignContainerToWagon } from '@/hooks/useContainers';
import { useToast } from '@/hooks/use-toast';
import { Plus } from 'lucide-react';
export function AssignContainerDialog({ wagonId }: { wagonId: string }) {
const [open, setOpen] = useState(false);
const [containerId, setContainerId] = useState('');
const [position, setPosition] = useState<number>();
const { data: containers } = useContainers();
const assign = useAssignContainerToWagon();
const { toast } = useToast();
const available = containers?.filter(c => c.status === 'AVAILABLE' && !c.wagonId);
const handleAssign = async () => {
if (!containerId) return;
await assign.mutateAsync({ containerId, wagonId, position });
toast({ title: 'Assigned', description: 'Container placed on wagon.' });
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild><Button size="sm"><Plus className="mr-2 h-4 w-4" />Assign Container</Button></DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Assign Container to Wagon</DialogTitle></DialogHeader>
<div className="space-y-4">
<div><Label>Container</Label><Select value={containerId} onValueChange={setContainerId}><SelectTrigger><SelectValue placeholder="Select container" /></SelectTrigger><SelectContent>{available?.map(c => <SelectItem key={c.id} value={c.id}>{c.containerNumber}</SelectItem>)}</SelectContent></Select></div>
<div><Label>Position (optional)</Label><Input type="number" value={position ?? ''} onChange={e => setPosition(parseInt(e.target.value) || undefined)} /></div>
<Button onClick={handleAssign} disabled={assign.isPending}>Assign</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,40 @@
import { useContainersByWagon, useUnassignContainer } from '@/hooks/useContainers';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Trash2 } from 'lucide-react';
export function ContainersTable({ wagonId }: { wagonId: string }) {
const { data: containers, refetch } = useContainersByWagon(wagonId);
const unassign = useUnassignContainer();
if (!containers?.length) return <div className="text-muted-foreground">No containers assigned.</div>;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Number</TableHead>
<TableHead>Type</TableHead>
<TableHead>Position</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{containers.map(container => (
<TableRow key={container.id}>
<TableCell>{container.containerNumber}</TableCell>
<TableCell>{container.containerTypeId}</TableCell>
<TableCell>{container.position}</TableCell>
<TableCell>{container.status}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => unassign.mutateAsync(container.id).then(() => refetch())}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,19 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Train } from '@/services/trainService';
export function TrainDetailCard({ train }: { train: Train }) {
return (
<Card>
<CardHeader><CardTitle>{train.trainNumber || train.code} - {train.trainName || 'Unnamed'}</CardTitle></CardHeader>
<CardContent className="grid md:grid-cols-2 gap-4">
<div><span className="font-medium">Status:</span> {train.status}</div>
<div><span className="font-medium">Capacity:</span> {train.capacityTons} tons</div>
<div><span className="font-medium">Origin:</span> {train.originStationId || '-'}</div>
<div><span className="font-medium">Destination:</span> {train.destinationStationId || '-'}</div>
<div><span className="font-medium">Departure:</span> {train.departureTime ? new Date(train.departureTime).toLocaleString() : '-'}</div>
<div><span className="font-medium">Arrival:</span> {train.arrivalTime ? new Date(train.arrivalTime).toLocaleString() : '-'}</div>
{train.remarks && <div className="col-span-2"><span className="font-medium">Remarks:</span> {train.remarks}</div>}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,59 @@
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useCreateTrain, useUpdateTrain } from '@/hooks/useTrains';
import { useToast } from '@/hooks/use-toast';
interface TrainFormDialogProps {
trigger?: React.ReactNode;
train?: any;
onSuccess?: () => void;
}
export function TrainFormDialog({ trigger, train, onSuccess }: TrainFormDialogProps) {
const [open, setOpen] = useState(false);
const [form, setForm] = useState({ code: '', capacityTons: 0, trainNumber: '', trainName: '' });
const createTrain = useCreateTrain();
const updateTrain = useUpdateTrain();
const { toast } = useToast();
useEffect(() => {
if (train) setForm({
code: train.code,
capacityTons: train.capacityTons,
trainNumber: train.trainNumber || '',
trainName: train.trainName || '',
});
}, [train]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (train) await updateTrain.mutateAsync({ id: train.id, data: form });
else await createTrain.mutateAsync(form);
toast({ title: train ? 'Train updated' : 'Train created', description: `${form.code} saved.` });
setOpen(false);
onSuccess?.();
} catch {
toast({ title: 'Error', description: `Failed to ${train ? 'update' : 'create'} train.`, variant: 'destructive' });
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger || <Button>New Train</Button>}</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>{train ? 'Edit Train' : 'Create Train'}</DialogTitle></DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div><Label>Code*</Label><Input required value={form.code} onChange={e => setForm({...form, code: e.target.value})} /></div>
<div><Label>Capacity (tons)*</Label><Input type="number" required value={form.capacityTons} onChange={e => setForm({...form, capacityTons: parseFloat(e.target.value)})} /></div>
<div><Label>Train Number</Label><Input value={form.trainNumber} onChange={e => setForm({...form, trainNumber: e.target.value})} /></div>
<div><Label>Train Name</Label><Input value={form.trainName} onChange={e => setForm({...form, trainName: e.target.value})} /></div>
<Button type="submit" disabled={createTrain.isPending || updateTrain.isPending}>Save</Button>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,45 @@
import { useTrains, useDeleteTrain } from '@/hooks/useTrains';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Eye, Trash2 } from 'lucide-react';
import { Link } from 'react-router-dom';
export function TrainsTable() {
const { data: trains, isLoading } = useTrains();
const deleteTrain = useDeleteTrain();
if (isLoading) return <div>Loading trains...</div>;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Number</TableHead>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Capacity (tons)</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{trains?.map(train => (
<TableRow key={train.id}>
<TableCell>{train.trainNumber || train.code}</TableCell>
<TableCell>{train.trainName || '-'}</TableCell>
<TableCell><Badge variant="outline">{train.status}</Badge></TableCell>
<TableCell>{train.capacityTons}</TableCell>
<TableCell className="flex space-x-2">
<Link to={`/trains/${train.id}`}>
<Button variant="ghost" size="icon"><Eye className="h-4 w-4" /></Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => deleteTrain.mutate(train.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,52 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useWagons, useAssignWagonToTrain } from '@/hooks/useWagons';
import { useToast } from '@/hooks/use-toast';
import { Plus } from 'lucide-react';
export function AssignWagonDialog({ trainId }: { trainId: string }) {
const [open, setOpen] = useState(false);
const [wagonId, setWagonId] = useState('');
const [sequence, setSequence] = useState<number>();
const { data: wagons } = useWagons();
const assign = useAssignWagonToTrain();
const { toast } = useToast();
const available = wagons?.filter(w => w.status === 'AVAILABLE' || !w.trainId);
const handleAssign = async () => {
if (!wagonId) return;
await assign.mutateAsync({ wagonId, trainId, sequenceNumber: sequence });
toast({ title: 'Assigned', description: 'Wagon attached to train.' });
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild><Button size="sm"><Plus className="mr-2 h-4 w-4" />Assign Wagon</Button></DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Assign Wagon to Train</DialogTitle></DialogHeader>
<div className="space-y-4">
<div>
<Label>Wagon</Label>
<Select value={wagonId} onValueChange={setWagonId}>
<SelectTrigger><SelectValue placeholder="Select wagon" /></SelectTrigger>
<SelectContent>
{available?.map(w => <SelectItem key={w.id} value={w.id}>{w.wagonNumber}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div>
<Label>Sequence (optional)</Label>
<Input type="number" value={sequence ?? ''} onChange={e => setSequence(parseInt(e.target.value) || undefined)} />
</div>
<Button onClick={handleAssign} disabled={assign.isPending}>Assign</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,71 @@
// src/components/wagons/WagonFormDialog.tsx
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useCreateWagon, useUpdateWagon } from '@/hooks/useWagons';
import { useToast } from '@/hooks/use-toast';
interface WagonFormDialogProps {
trigger?: React.ReactNode;
wagon?: any;
onSuccess?: () => void;
}
export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogProps) {
const [open, setOpen] = useState(false);
const [form, setForm] = useState({
wagonNumber: '',
wagonTypeId: '',
tareWeight: 0,
maxPayloadWeight: 0,
status: 'AVAILABLE',
notes: ''
});
const createWagon = useCreateWagon();
const updateWagon = useUpdateWagon();
const { toast } = useToast();
useEffect(() => {
if (wagon) setForm({
wagonNumber: wagon.wagonNumber,
wagonTypeId: wagon.wagonTypeId,
tareWeight: wagon.tareWeight,
maxPayloadWeight: wagon.maxPayloadWeight,
status: wagon.status,
notes: wagon.notes || ''
});
}, [wagon]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (wagon) await updateWagon.mutateAsync({ id: wagon.id, data: form });
else await createWagon.mutateAsync(form);
toast({ title: wagon ? 'Wagon updated' : 'Wagon created', description: `${form.wagonNumber} saved.` });
setOpen(false);
onSuccess?.();
} catch {
toast({ title: 'Error', description: `Failed to ${wagon ? 'update' : 'create'} wagon.`, variant: 'destructive' });
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger || <Button>New Wagon</Button>}</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>{wagon ? 'Edit Wagon' : 'Create Wagon'}</DialogTitle></DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div><Label>Wagon Number*</Label><Input required value={form.wagonNumber} onChange={e => setForm({...form, wagonNumber: e.target.value})} /></div>
<div><Label>Wagon Type ID*</Label><Input required value={form.wagonTypeId} onChange={e => setForm({...form, wagonTypeId: e.target.value})} /></div>
<div><Label>Tare Weight (kg)*</Label><Input type="number" required value={form.tareWeight} onChange={e => setForm({...form, tareWeight: parseFloat(e.target.value)})} /></div>
<div><Label>Max Payload (kg)*</Label><Input type="number" required value={form.maxPayloadWeight} onChange={e => setForm({...form, maxPayloadWeight: parseFloat(e.target.value)})} /></div>
<div><Label>Status</Label><Input value={form.status} onChange={e => setForm({...form, status: e.target.value})} /></div>
<div><Label>Notes</Label><Input value={form.notes} onChange={e => setForm({...form, notes: e.target.value})} /></div>
<Button type="submit" disabled={createWagon.isPending || updateWagon.isPending}>Save</Button>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,63 @@
import { useWagonsByTrain, useUnassignWagon, useReorderWagons } from '@/hooks/useWagons';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Trash2, GripVertical } from 'lucide-react';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
export function WagonsTable({ trainId }: { trainId: string }) {
const { data: wagons, refetch } = useWagonsByTrain(trainId);
const unassign = useUnassignWagon();
const reorder = useReorderWagons();
const onDragEnd = (result: any) => {
if (!result.destination) return;
const items = Array.from(wagons || []);
const [removed] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, removed);
reorder.mutate({ trainId, wagonIds: items.map(w => w.id) });
};
if (!wagons?.length) return <div className="text-muted-foreground">No wagons assigned.</div>;
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="wagons">
{(provided) => (
<Table {...provided.droppableProps} ref={provided.innerRef}>
<TableHeader>
<TableRow>
<TableHead className="w-10"></TableHead>
<TableHead>Number</TableHead>
<TableHead>Type</TableHead>
<TableHead>Sequence</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{wagons.map((wagon, idx) => (
<Draggable key={wagon.id} draggableId={wagon.id} index={idx}>
{(provided) => (
<TableRow ref={provided.innerRef} {...provided.draggableProps}>
<TableCell {...provided.dragHandleProps}><GripVertical className="h-4 w-4 cursor-grab" /></TableCell>
<TableCell>{wagon.wagonNumber}</TableCell>
<TableCell>{wagon.wagonTypeId}</TableCell>
<TableCell>{wagon.sequenceNumber}</TableCell>
<TableCell>{wagon.status}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => unassign.mutateAsync(wagon.id).then(() => refetch())}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
)}
</Draggable>
))}
{provided.placeholder}
</TableBody>
</Table>
)}
</Droppable>
</DragDropContext>
);
}