mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
feat: complete train scheduling integration
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@edr/ui-common';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useContainers, useAssignContainerToWagon } from '@/hooks/useContainers';
|
||||
import { useContainers, useAssignContainerToWagon } from './use-containers';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@edr/ui-common';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCargoTypes } from './use-cargo-types';
|
||||
import { useCargoMutations } from './use-cargoes';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface Cargo {
|
||||
id: string;
|
||||
cargoNumber: string;
|
||||
cargoTypeId: string;
|
||||
weight: number;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
interface CargoFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
cargo?: Cargo | null;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export default function CargoFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
cargo,
|
||||
onSuccess,
|
||||
}: CargoFormDialogProps) {
|
||||
const { data: cargoTypes } = useCargoTypes();
|
||||
const { createCargo, updateCargo } = useCargoMutations();
|
||||
const [formData, setFormData] = useState<Partial<Cargo>>({
|
||||
cargoNumber: '',
|
||||
cargoTypeId: '',
|
||||
weight: 0,
|
||||
remarks: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cargo) {
|
||||
setFormData(cargo);
|
||||
} else {
|
||||
setFormData({
|
||||
cargoNumber: '',
|
||||
cargoTypeId: '',
|
||||
weight: 0,
|
||||
remarks: '',
|
||||
});
|
||||
}
|
||||
}, [cargo, open]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (cargo?.id) {
|
||||
updateCargo.mutate(
|
||||
{ id: cargo.id, data: formData },
|
||||
{ onSuccess: () => { onOpenChange(false); onSuccess?.(); } }
|
||||
);
|
||||
} else {
|
||||
createCargo.mutate(formData, {
|
||||
onSuccess: () => { onOpenChange(false); onSuccess?.(); }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = createCargo.isPending || updateCargo.isPending;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader><DialogTitle>{cargo ? 'Edit Cargo' : 'Create New Cargo'}</DialogTitle></DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Cargo Number *</Label>
|
||||
<Input value={formData.cargoNumber} onChange={e => setFormData({...formData, cargoNumber: e.target.value})} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Cargo Type *</Label>
|
||||
<Select
|
||||
value={formData.cargoTypeId || ''}
|
||||
onValueChange={(val) => setFormData({ ...formData, cargoTypeId: val })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select cargo type..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cargoTypes?.map((type: any) => (
|
||||
<SelectItem key={type.id} value={type.id}>{type.cargo_type_name || type.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Weight (kg) *</Label>
|
||||
<Input type="number" value={formData.weight} onChange={e => setFormData({...formData, weight: parseFloat(e.target.value)})} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Remarks</Label>
|
||||
<Textarea value={formData.remarks} onChange={e => setFormData({...formData, remarks: e.target.value})} rows={3} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isLoading}>{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}{cargo ? 'Update' : 'Create'}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -12,7 +10,16 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@edr/ui-common';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useContainerTypes } from './use-container-types';
|
||||
import { useContainerMutations } from './use-containers';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface Container {
|
||||
@@ -39,8 +46,6 @@ interface ContainerFormDialogProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
|
||||
export default function ContainerFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -48,7 +53,8 @@ export default function ContainerFormDialog({
|
||||
wagons = [],
|
||||
onSuccess,
|
||||
}: ContainerFormDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: containerTypes } = useContainerTypes();
|
||||
const { createContainer, updateContainer } = useContainerMutations();
|
||||
const [formData, setFormData] = useState<Partial<Container>>({
|
||||
containerNumber: '',
|
||||
containerTypeId: '',
|
||||
@@ -73,40 +79,6 @@ export default function ContainerFormDialog({
|
||||
}
|
||||
}, [container, open]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<Container>) =>
|
||||
axios.post(`${API_BASE_URL}/api/containers`, data),
|
||||
onSuccess: () => {
|
||||
toast.success('Container created successfully');
|
||||
onOpenChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['containers'] });
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = axios.isAxiosError(error)
|
||||
? error.response?.data?.message || 'Failed to create container'
|
||||
: 'Failed to create container';
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: Partial<Container>) =>
|
||||
axios.patch(`${API_BASE_URL}/api/containers/${container?.id}`, data),
|
||||
onSuccess: () => {
|
||||
toast.success('Container updated successfully');
|
||||
onOpenChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['containers'] });
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = axios.isAxiosError(error)
|
||||
? error.response?.data?.message || 'Failed to update container'
|
||||
: 'Failed to update container';
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.containerNumber || !formData.containerTypeId) {
|
||||
@@ -114,13 +86,18 @@ export default function ContainerFormDialog({
|
||||
return;
|
||||
}
|
||||
if (container?.id) {
|
||||
updateMutation.mutate(formData);
|
||||
updateContainer.mutate(
|
||||
{ id: container.id, data: formData },
|
||||
{ onSuccess: () => { onOpenChange(false); onSuccess?.(); } }
|
||||
);
|
||||
} else {
|
||||
createMutation.mutate(formData);
|
||||
createContainer.mutate(formData, {
|
||||
onSuccess: () => { onOpenChange(false); onSuccess?.(); }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = createMutation.isPending || updateMutation.isPending;
|
||||
const isLoading = createContainer.isPending || updateContainer.isPending;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -145,53 +122,59 @@ export default function ContainerFormDialog({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="containerTypeId">Type *</Label>
|
||||
<Input
|
||||
id="containerTypeId"
|
||||
value={formData.containerTypeId || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, containerTypeId: e.target.value })
|
||||
}
|
||||
placeholder="e.g., 20ft Box"
|
||||
required
|
||||
/>
|
||||
<Label htmlFor="containerTypeId">Container Type *</Label>
|
||||
<Select
|
||||
value={formData.containerTypeId || ''}
|
||||
onValueChange={(val:any) => setFormData({ ...formData, containerTypeId: val })}
|
||||
>
|
||||
<SelectTrigger id="containerTypeId">
|
||||
<SelectValue placeholder="Select container type..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{containerTypes?.map((type: any) => (
|
||||
<SelectItem key={type.id} value={type.id}>{type.name || type.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="wagonId">Wagon (Optional)</Label>
|
||||
<select
|
||||
id="wagonId"
|
||||
value={formData.wagonId || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, wagonId: e.target.value || undefined })
|
||||
}
|
||||
className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
<Select
|
||||
value={formData.wagonId || 'none'}
|
||||
onValueChange={(val:any) => setFormData({ ...formData, wagonId: val === 'none' ? undefined : val })}
|
||||
>
|
||||
<option value="">Select a wagon...</option>
|
||||
{wagons.map(wagon => (
|
||||
<option key={wagon.id} value={wagon.id}>
|
||||
{wagon.wagonNumber}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger id="wagonId">
|
||||
<SelectValue placeholder="Select a wagon..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
{wagons.map((wagon) => (
|
||||
<SelectItem key={wagon.id} value={wagon.id}>
|
||||
{wagon.wagonNumber}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
<Select
|
||||
value={formData.status || 'AVAILABLE'}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, status: e.target.value as any })
|
||||
}
|
||||
className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onValueChange={(val:any) => setFormData({ ...formData, status: val })}
|
||||
>
|
||||
<option value="AVAILABLE">Available</option>
|
||||
<option value="IN_USE">In Use</option>
|
||||
<option value="MAINTENANCE">Maintenance</option>
|
||||
<option value="RETIRED">Retired</option>
|
||||
</select>
|
||||
<SelectTrigger id="status">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AVAILABLE">Available</SelectItem>
|
||||
<SelectItem value="IN_USE">In Use</SelectItem>
|
||||
<SelectItem value="MAINTENANCE">Maintenance</SelectItem>
|
||||
<SelectItem value="RETIRED">Retired</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContainersByWagon, useUnassignContainer } from '@/hooks/useContainers';
|
||||
import { useContainersByWagon, useUnassignContainer } from './use-containers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import type { Container } from '@/services/containerService';
|
||||
import type { Container } from './container.service';
|
||||
|
||||
export function ContainersTable({ wagonId }: { wagonId: string }) {
|
||||
const { data: containers, refetch } = useContainersByWagon(wagonId);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { api } from '../../auth/http';
|
||||
|
||||
type ListResponse<T> = T[] | { data: T[] };
|
||||
|
||||
const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
Array.isArray(payload) ? payload : payload.data;
|
||||
|
||||
export const cargoTypesService = {
|
||||
async getCargoTypes() {
|
||||
const response = await api.get<ListResponse<unknown>>('/cargo-types', {
|
||||
params: { isActive: true, pageSize: 500 },
|
||||
});
|
||||
return asList(response.data);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { api } from "../../auth/http";
|
||||
|
||||
export const cargoService = {
|
||||
async getCargoes() {
|
||||
const response = await api.get('/cargoes');
|
||||
return response.data;
|
||||
},
|
||||
async createCargo(data: any) {
|
||||
const response = await api.post('/cargoes', data);
|
||||
return response.data;
|
||||
},
|
||||
async updateCargo(id: string, data: any) {
|
||||
const response = await api.patch(`/cargoes/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
async deleteCargo(id: string) {
|
||||
await api.delete(`/cargoes/${id}`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { api } from '../../auth/http';
|
||||
|
||||
type ListResponse<T> = T[] | { data: T[] };
|
||||
|
||||
const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
Array.isArray(payload) ? payload : payload.data;
|
||||
|
||||
export const containerTypesService = {
|
||||
async getContainerTypes() {
|
||||
const response = await api.get<ListResponse<unknown>>('/container-types', {
|
||||
params: { isActive: true, pageSize: 500 },
|
||||
});
|
||||
return asList(response.data);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { api } from "../../auth/http";
|
||||
|
||||
export const containerService = {
|
||||
async getContainers() {
|
||||
const response = await api.get('/containers');
|
||||
return response.data;
|
||||
},
|
||||
async getContainersByWagon(wagonId: string) {
|
||||
const response = await api.get('/containers', { params: { wagonId } });
|
||||
return response.data;
|
||||
},
|
||||
async createContainer(data: any) {
|
||||
const response = await api.post('/containers', data);
|
||||
return response.data;
|
||||
},
|
||||
async updateContainer(id: string, data: any) {
|
||||
const response = await api.patch(`/containers/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
async deleteContainer(id: string) {
|
||||
await api.delete(`/containers/${id}`);
|
||||
},
|
||||
async assignToWagon(containerId: string, wagonId: string, position?: number) {
|
||||
const response = await api.post(`/containers/${containerId}/assign-wagon`, { wagonId, position });
|
||||
return response.data;
|
||||
},
|
||||
async unassignFromWagon(containerId: string) {
|
||||
const response = await api.post(`/containers/${containerId}/unassign-wagon`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { cargoTypesService } from './cargo-types.service';
|
||||
|
||||
export const CARGO_TYPES_QUERY_KEY = ['cargo-types'];
|
||||
|
||||
export function useCargoTypes() {
|
||||
return useQuery({
|
||||
queryKey: CARGO_TYPES_QUERY_KEY,
|
||||
queryFn: () => cargoTypesService.getCargoTypes(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { cargoService } from './cargo.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const CARGOES_QUERY_KEY = ['cargoes'];
|
||||
|
||||
export function useCargoes() {
|
||||
return useQuery({
|
||||
queryKey: CARGOES_QUERY_KEY,
|
||||
queryFn: () => cargoService.getCargoes(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCargoMutations() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createCargo = useMutation({
|
||||
mutationFn: (data: any) => cargoService.createCargo(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CARGOES_QUERY_KEY });
|
||||
toast.success('Cargo created successfully');
|
||||
},
|
||||
});
|
||||
|
||||
const updateCargo = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) => cargoService.updateCargo(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CARGOES_QUERY_KEY });
|
||||
toast.success('Cargo updated successfully');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCargo = useMutation({
|
||||
mutationFn: (id: string) => cargoService.deleteCargo(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CARGOES_QUERY_KEY });
|
||||
toast.success('Cargo deleted successfully');
|
||||
},
|
||||
});
|
||||
|
||||
return { createCargo, updateCargo, deleteCargo };
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { containerTypesService } from './container-types.service';
|
||||
|
||||
export const CONTAINER_TYPES_QUERY_KEY = ['container-types'];
|
||||
|
||||
export function useContainerTypes() {
|
||||
return useQuery({
|
||||
queryKey: CONTAINER_TYPES_QUERY_KEY,
|
||||
queryFn: () => containerTypesService.getContainerTypes(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { containerService } from './container.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const CONTAINERS_QUERY_KEY = ['containers'];
|
||||
|
||||
export function useContainers() {
|
||||
return useQuery({
|
||||
queryKey: CONTAINERS_QUERY_KEY,
|
||||
queryFn: () => containerService.getContainers(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useContainersByWagon(wagonId: string) {
|
||||
return useQuery({
|
||||
queryKey: [...CONTAINERS_QUERY_KEY, 'wagon', wagonId],
|
||||
queryFn: () => containerService.getContainersByWagon(wagonId),
|
||||
enabled: !!wagonId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useContainerMutations() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createContainer = useMutation({
|
||||
mutationFn: (data: any) => containerService.createContainer(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CONTAINERS_QUERY_KEY });
|
||||
toast.success('Container created successfully');
|
||||
},
|
||||
});
|
||||
|
||||
const updateContainer = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) => containerService.updateContainer(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CONTAINERS_QUERY_KEY });
|
||||
toast.success('Container updated successfully');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteContainer = useMutation({
|
||||
mutationFn: (id: string) => containerService.deleteContainer(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CONTAINERS_QUERY_KEY });
|
||||
toast.success('Container deleted successfully');
|
||||
},
|
||||
});
|
||||
|
||||
return { createContainer, updateContainer, deleteContainer };
|
||||
}
|
||||
|
||||
export function useUnassignContainer() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => containerService.unassignFromWagon(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CONTAINERS_QUERY_KEY });
|
||||
toast.success('Container unassigned from wagon');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignContainerToWagon() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ containerId, wagonId, position }: { containerId: string; wagonId: string; position?: number }) =>
|
||||
containerService.assignToWagon(containerId, wagonId, position),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CONTAINERS_QUERY_KEY });
|
||||
toast.success('Container assigned to wagon');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { wagonTypesService } from './wagon-types.service';
|
||||
|
||||
export const WAGON_TYPES_QUERY_KEY = ['wagon-types'];
|
||||
|
||||
export function useWagonTypes() {
|
||||
return useQuery({
|
||||
queryKey: WAGON_TYPES_QUERY_KEY,
|
||||
queryFn: () => wagonTypesService.getWagonTypes(),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { wagonService } from './wagon.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const WAGONS_QUERY_KEY = ['wagons'];
|
||||
|
||||
export function useWagons() {
|
||||
return useQuery({
|
||||
queryKey: WAGONS_QUERY_KEY,
|
||||
queryFn: () => wagonService.getWagons(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useWagonMutations() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createWagon = useMutation({
|
||||
mutationFn: (data: any) => wagonService.createWagon(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: WAGONS_QUERY_KEY });
|
||||
toast.success('Wagon created successfully');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to create wagon');
|
||||
},
|
||||
});
|
||||
|
||||
const updateWagon = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: any }) => wagonService.updateWagon(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: WAGONS_QUERY_KEY });
|
||||
toast.success('Wagon updated successfully');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to update wagon');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteWagon = useMutation({
|
||||
mutationFn: (id: string) => wagonService.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: WAGONS_QUERY_KEY });
|
||||
toast.success('Wagon deleted successfully');
|
||||
},
|
||||
});
|
||||
|
||||
return { createWagon, updateWagon, deleteWagon };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { api } from '../../auth/http';
|
||||
|
||||
type ListResponse<T> = T[] | { data: T[] };
|
||||
|
||||
const asList = <T>(payload: ListResponse<T>): T[] =>
|
||||
Array.isArray(payload) ? payload : payload.data;
|
||||
|
||||
export const wagonTypesService = {
|
||||
async getWagonTypes() {
|
||||
const response = await api.get<ListResponse<unknown>>('/wagon-types');
|
||||
return asList(response.data);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { api } from "../../auth/http";
|
||||
|
||||
export const wagonService = {
|
||||
async getWagons() {
|
||||
const response = await api.get('/wagons');
|
||||
return response.data;
|
||||
},
|
||||
async getWagonById(id: string) {
|
||||
const response = await api.get(`/wagons/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
async createWagon(data: any) {
|
||||
const response = await api.post('/wagons', data);
|
||||
return response.data;
|
||||
},
|
||||
async updateWagon(id: string, data: any) {
|
||||
const response = await api.patch(`/wagons/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
async deleteWagon(id: string) {
|
||||
await api.delete(`/wagons/${id}`);
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||
import { useWagonTypes } from '@/hooks/use-wagon-types';
|
||||
import { useCreateWagon, useUpdateWagon } from '@/hooks/useWagons';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
@@ -25,6 +27,7 @@ export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogPr
|
||||
});
|
||||
const createWagon = useCreateWagon();
|
||||
const updateWagon = useUpdateWagon();
|
||||
const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useWagonTypes();
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,6 +43,10 @@ export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogPr
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.wagonNumber || !form.wagonTypeId) {
|
||||
toast({ title: 'Missing required field', description: 'Please select a wagon type.', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (wagon) await updateWagon.mutateAsync({ id: wagon.id, data: form });
|
||||
else await createWagon.mutateAsync(form);
|
||||
@@ -57,10 +64,37 @@ export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogPr
|
||||
<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>Wagon Number*</Label><Input value={form.wagonNumber} onChange={e => setForm({...form, wagonNumber: e.target.value})} /></div>
|
||||
<div>
|
||||
<Label>Wagon Type*</Label>
|
||||
<Select
|
||||
value={form.wagonTypeId}
|
||||
disabled={wagonTypesLoading}
|
||||
onValueChange={(value) => {
|
||||
const selectedType = wagonTypes.find((type: any) => type.id === value);
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
wagonTypeId: value,
|
||||
maxPayloadWeight: current.maxPayloadWeight > 0
|
||||
? current.maxPayloadWeight
|
||||
: Number(selectedType?.capacityTons ?? current.maxPayloadWeight),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select wagon type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{wagonTypes.map((type: any) => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.code} - {type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>Tare Weight (kg)*</Label><Input type="number" value={form.tareWeight} onChange={e => setForm({...form, tareWeight: Number(e.target.value)})} /></div>
|
||||
<div><Label>Max Payload (kg)*</Label><Input type="number" value={form.maxPayloadWeight} onChange={e => setForm({...form, maxPayloadWeight: Number(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>
|
||||
@@ -68,4 +102,4 @@ export function WagonFormDialog({ trigger, wagon, onSuccess }: WagonFormDialogPr
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useWagonTypes } from '@/hooks/use-wagon-types';
|
||||
|
||||
interface Wagon {
|
||||
id: string;
|
||||
@@ -49,6 +51,7 @@ export default function WagonFormDialog({
|
||||
onSuccess,
|
||||
}: WagonFormDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: wagonTypes = [], isLoading: wagonTypesLoading } = useWagonTypes();
|
||||
const [formData, setFormData] = useState<Partial<Wagon>>({
|
||||
wagonNumber: '',
|
||||
wagonTypeId: '',
|
||||
@@ -146,15 +149,31 @@ export default function WagonFormDialog({
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="wagonTypeId">Type *</Label>
|
||||
<Input
|
||||
id="wagonTypeId"
|
||||
<Select
|
||||
value={formData.wagonTypeId || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, wagonTypeId: e.target.value })
|
||||
}
|
||||
placeholder="e.g., Flat Bed"
|
||||
required
|
||||
/>
|
||||
disabled={wagonTypesLoading}
|
||||
onValueChange={(value) => {
|
||||
const selectedType = wagonTypes.find((type: any) => type.id === value);
|
||||
setFormData({
|
||||
...formData,
|
||||
wagonTypeId: value,
|
||||
capacity: formData.capacity && formData.capacity > 0
|
||||
? formData.capacity
|
||||
: Number(selectedType?.capacityTons ?? 0),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="wagonTypeId">
|
||||
<SelectValue placeholder="Select wagon type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{wagonTypes.map((type: any) => (
|
||||
<SelectItem key={type.id} value={type.id}>
|
||||
{type.code} - {type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user