Merge branch 'freight/develop' into freight/feat/ui-migration

This commit is contained in:
Nathnael Wondisha
2026-06-09 12:13:03 +03:00
committed by GitHub
451 changed files with 47082 additions and 6320 deletions

View File

@@ -1,18 +0,0 @@
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
WORKDIR /app
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/edr-freight-web/backoffice/package.json ./apps/edr-freight-web/backoffice/
COPY packages ./packages
RUN pnpm install --frozen-lockfile --filter @edr/freight-backoffice...
FROM deps AS build
COPY apps/edr-freight-web/backoffice ./apps/edr-freight-web/backoffice
RUN pnpm --filter @edr/freight-backoffice build
FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/apps/edr-freight-web/backoffice/dist /usr/share/nginx/html
EXPOSE 5183
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,18 +0,0 @@
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
WORKDIR /app
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/edr-freight-web/portal/package.json ./apps/edr-freight-web/portal/
COPY packages ./packages
RUN pnpm install --frozen-lockfile --filter @edr/freight-portal...
FROM deps AS build
COPY apps/edr-freight-web/portal ./apps/edr-freight-web/portal
RUN pnpm --filter @edr/freight-portal build
FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/apps/edr-freight-web/portal/dist /usr/share/nginx/html
EXPOSE 5173
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -34,17 +34,18 @@ import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
import TrainsPage from "./pages/trains/TrainsPage";
import {
CargoesCrudPage,
ContainersCrudPage,
LocomotivesCrudPage,
TrainMasterDataPage,
WagonsCrudPage,
} from "./pages/fleet/FleetCrudPages";
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
import TrainDetailPage from "./pages/trains/TrainDetailPage";
import RoutesPage from "./pages/fleet/RoutesPage";
import TrainsPage from "./pages/trains/TrainsPage";
import {
CargoesCrudPage,
ContainersCrudPage,
LocomotivesCrudPage,
TrainMasterDataPage,
WagonTypesCrudPage,
WagonsCrudPage,
} from "./pages/fleet/FleetCrudPages";
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
import TrainDetailPage from "./pages/trains/TrainDetailPage";
import RoutesPage from "./pages/fleet/RoutesPage";
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
{
@@ -92,10 +93,15 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
href: "/dashboard/trains",
icon: <Train />,
},
{
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
{
label: "Wagon types",
href: "/dashboard/wagon-types",
icon: <Boxes />,
},
{
label: "Wagons",
href: "/dashboard/wagons",
icon: <Truck />,
},
{
label: "Containers",
@@ -236,20 +242,19 @@ const App = () => {
<Route path="booking-requests" element={<BookingRequestsPage />} />
<Route path="booking-requests/:id" element={<BookingRequestDetailPage />} />
<Route
path="booking-requests/:id/contract"
element={<BookingContractPage />}
/>
<Route path="operations/train-scheduling" element={<TrainsPage />} />
<Route path="trains" element={<TrainMasterDataPage />} />
<Route path="operations/train-scheduling" element={<TrainsPage />} />
<Route path="routes" element={<RoutesPage />} />
<Route path="locomotives" element={<LocomotivesCrudPage />} />
<Route path="trains" element={<TrainMasterDataPage />} />
<Route path="trains/:id" element={<TrainDetailPage />} />
<Route path="wagons" element={<WagonsCrudPage />} />
<Route path="containers" element={<ContainersCrudPage />} />
<Route path="cargoes" element={<CargoesCrudPage />} />
<Route
path="booking-requests/:id/contract"
element={<BookingContractPage />}
/>
<Route path="operations/train-scheduling" element={<TrainsPage />} />
<Route path="trains" element={<TrainMasterDataPage />} />
<Route path="trains/:id" element={<TrainDetailPage />} />
<Route path="routes" element={<RoutesPage />} />
<Route path="locomotives" element={<LocomotivesCrudPage />} />
<Route path="wagon-types" element={<WagonTypesCrudPage />} />
<Route path="wagons" element={<WagonsCrudPage />} />
<Route path="containers" element={<ContainersCrudPage />} />
<Route path="cargoes" element={<CargoesCrudPage />} />
<Route path="user-management" element={<UserManagementPage />} />
<Route path="user-management/users" element={<UsersPage />} />

View File

@@ -107,6 +107,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
},
},
...rulesRouteMeta,
{
prefix: "/dashboard/wagon-types",
meta: {
title: "Wagon Types",
subtitle: "Manage wagon type capacity and supported load configuration",
},
},
{
prefix: "/dashboard/user1",
meta: {

View File

@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { wagonTypesService } from '@/services/wagon-types.service';
export const WAGON_TYPES_QUERY_KEY = ['wagon-types'];
@@ -7,6 +7,30 @@ export function useWagonTypes() {
return useQuery({
queryKey: WAGON_TYPES_QUERY_KEY,
queryFn: () => wagonTypesService.getWagonTypes(),
staleTime: Infinity,
});
}
}
export function useCreateWagonType() {
const qc = useQueryClient();
return useMutation({
mutationFn: wagonTypesService.create,
onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }),
});
}
export function useUpdateWagonType() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
wagonTypesService.update(id, data),
onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }),
});
}
export function useDeleteWagonType() {
const qc = useQueryClient();
return useMutation({
mutationFn: wagonTypesService.delete,
onSuccess: () => qc.invalidateQueries({ queryKey: WAGON_TYPES_QUERY_KEY }),
});
}

View File

@@ -16,7 +16,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
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 {
useCreateWagonType,
useDeleteWagonType,
useUpdateWagonType,
useWagonTypes,
} from '@/hooks/use-wagon-types';
import { useToast } from '@/hooks/use-toast';
import { useCreateCargo, useDeleteCargo, useCargoes, useUpdateCargo } from '@/hooks/useCargoes';
import {
@@ -38,8 +43,9 @@ import type { Container } from '@/services/containerService';
import type { Locomotive } from '@/services/locomotives.service';
import type { Train } from '@/services/trains.service';
import type { Wagon } from '@/services/wagon.service';
import type { WagonType } from '@/services/wagon-types.service';
type FormValue = string | number;
type FormValue = string | number | boolean | string[];
type Field = {
key: string;
@@ -83,8 +89,20 @@ type FleetCrudPageProps<T extends { id: string }> = {
const normalizePayload = (values: Record<string, FormValue>) =>
Object.fromEntries(
Object.entries(values)
.map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
.filter(([, value]) => value !== ''),
.map(([key, value]) => [
key,
key === 'supportedLoadTypes' && typeof value === 'string'
? value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
: Array.isArray(value)
? value
: typeof value === 'string'
? value.trim()
: value,
])
.filter(([, value]) => value !== '' && !(Array.isArray(value) && value.length === 0)),
);
const extractBackendErrors = (error: unknown) => {
@@ -198,7 +216,10 @@ function FleetCrudPage<T extends { id: string }>({
setEditing(item);
setForm(
Object.fromEntries(
Object.keys(emptyValues).map((key) => [key, (item as Record<string, string | number | null | undefined>)[key] ?? '']),
Object.keys(emptyValues).map((key) => [
key,
(item as Record<string, FormValue | null | undefined>)[key] ?? '',
]),
),
);
setFieldErrors({});
@@ -369,7 +390,11 @@ function FleetCrudPage<T extends { id: string }>({
const value = form[field.key] ?? '';
const inputValue = field.type === 'number' && value !== '' && !Number.isFinite(Number(value))
? ''
: value;
: Array.isArray(value)
? value.join(', ')
: typeof value === 'boolean'
? String(value)
: value;
return (
<div key={field.key} className="space-y-2">
<Label htmlFor={field.key}>{field.label}</Label>
@@ -451,6 +476,12 @@ function FleetCrudPage<T extends { id: string }>({
const statusBadge = (status?: string) => <Badge variant="outline">{status ?? '-'}</Badge>;
const activeBadge = (isActive?: boolean) => (
<Badge variant={isActive === false ? 'secondary' : 'outline'}>
{isActive === false ? 'Inactive' : 'Active'}
</Badge>
);
const optionLabel = (options: { value: string; label: string }[], value?: string | null) =>
options.find((option) => option.value === value)?.label ?? value ?? '-';
@@ -489,6 +520,69 @@ export function TrainMasterDataPage() {
);
}
export function WagonTypesCrudPage() {
const query = useWagonTypes();
return (
<FleetCrudPage<WagonType>
title="Wagon Types"
description="Manage wagon type capacities and load compatibility used by wagon master data."
addLabel="Add Wagon Type"
data={query.data}
isLoading={query.isLoading}
create={useCreateWagonType()}
update={useUpdateWagonType()}
remove={useDeleteWagonType()}
searchText={(type) =>
[type.code, type.name, type.supportedLoadTypes?.join(' '), String(type.isActive)].join(' ')
}
columns={[
{ key: 'code', label: 'Code' },
{ key: 'name', label: 'Name' },
{ key: 'capacityTons', label: 'Capacity (tons)' },
{ key: 'lengthMeters', label: 'Length (m)' },
{
key: 'supportedLoadTypes',
label: 'Load types',
render: (type) => type.supportedLoadTypes?.join(', ') || '-',
},
{ key: 'isActive', label: 'Status', render: (type) => activeBadge(type.isActive) },
]}
fields={[
{ key: 'code', label: 'Code', required: true },
{ key: 'name', label: 'Name', required: true },
{ key: 'capacityTons', label: 'Capacity (tons)', type: 'number', required: true },
{ key: 'lengthMeters', label: 'Length (meters)', type: 'number', required: true },
{ key: 'maxWagonsPerTrain', label: 'Max wagons per train', type: 'number' },
{
key: 'supportedLoadTypes',
label: 'Supported load types',
placeholder: 'container, break-bulk',
},
{
key: 'isActive',
label: 'Status',
type: 'select',
options: [
{ value: 'true', label: 'Active' },
{ value: 'false', label: 'Inactive' },
],
onValueChange: (value) => ({ isActive: value === 'true' }),
},
]}
emptyValues={{
code: '',
name: '',
capacityTons: 0,
lengthMeters: 0,
maxWagonsPerTrain: '',
supportedLoadTypes: '',
isActive: true,
}}
/>
);
}
export function WagonsCrudPage() {
const query = useWagons();
const { data: wagonTypes = [] } = useWagonTypes();

View File

@@ -2,14 +2,28 @@ import { api } from "../auth/http";
type ListResponse<T> = T[] | { data: T[] };
export interface WagonType {
id: string;
code: string;
name: string;
capacityTons: number;
lengthMeters: number;
maxWagonsPerTrain?: number | null;
supportedLoadTypes: string[];
isActive: boolean;
}
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', {
params: { isActive: true, pageSize: 500 },
const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
params: { isActive: 'all', pageSize: 500 },
});
return asList(response.data);
},
create: (data: Partial<WagonType>) => api.post('/wagon-types', data),
update: (id: string, data: Partial<WagonType>) => api.patch(`/wagon-types/${id}`, data),
delete: (id: string) => api.delete(`/wagon-types/${id}`),
};