fix local merge conflict

This commit is contained in:
mengstabketemaw
2026-05-23 10:52:31 +03:00
parent 578f6f84ef
commit 3485ef299d
7 changed files with 1862 additions and 1511 deletions

View File

@@ -16,6 +16,7 @@ import { BillingModule } from "./modules/billing/billing.module";
import { NotificationsModule } from "./modules/notifications/notifications.module"; import { NotificationsModule } from "./modules/notifications/notifications.module";
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module"; import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module"; import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-settings.service";
@Module({ @Module({
imports: [ imports: [
@@ -42,9 +43,13 @@ import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-set
], ],
}) })
export class AppModule implements OnApplicationBootstrap { export class AppModule implements OnApplicationBootstrap {
constructor(private readonly seeder: DataSeeder) {} constructor(
private readonly seeder: DataSeeder,
private readonly dropdownSettingsService: DropdownSettingsService,
) {}
async onApplicationBootstrap() { async onApplicationBootstrap() {
await this.seeder.run(); await this.seeder.run();
await this.dropdownSettingsService.seedDefaultStations();
} }
} }

View File

@@ -16,6 +16,71 @@ import {
IDropdownSettingsRepository, IDropdownSettingsRepository,
} from "./interfaces/dropdown-settings.repository.interface"; } from "./interfaces/dropdown-settings.repository.interface";
const STATIONS_TER_CODE = "stations_ter";
const DEFAULT_STATION_OPTIONS: CreateDropdownOptionDto[] = [
{
value: "inside_addis_ababa",
label: "Addis Ababa",
note: "Inside country",
order: 1,
},
{
value: "inside_adama",
label: "Adama",
note: "Inside country",
order: 2,
},
{
value: "inside_mojo",
label: "Mojo",
note: "Inside country",
order: 3,
},
{
value: "inside_awash",
label: "Awash",
note: "Inside country",
order: 4,
},
{
value: "inside_mieso",
label: "Mieso",
note: "Inside country",
order: 5,
},
{
value: "inside_dire_dawa",
label: "Dire Dawa",
note: "Inside country",
order: 6,
},
{
value: "outside_ali_sabieh",
label: "Ali Sabieh",
note: "Outside country",
order: 7,
},
{
value: "outside_holhol",
label: "Holhol",
note: "Outside country",
order: 8,
},
{
value: "outside_djibouti_city",
label: "Djibouti City",
note: "Outside country",
order: 9,
},
{
value: "outside_doraleh_terminal",
label: "Doraleh Terminal",
note: "Outside country",
order: 10,
},
];
@Injectable() @Injectable()
export class DropdownSettingsService { export class DropdownSettingsService {
constructor( constructor(
@@ -62,6 +127,34 @@ export class DropdownSettingsService {
return this.getById(setting.id); return this.getById(setting.id);
} }
async seedDefaultStations(): Promise<void> {
const existing = await this.repository.findByCode(STATIONS_TER_CODE);
if (!existing) {
await this.create({
code: STATIONS_TER_CODE,
label: "Stations TER",
description:
"Temporary freight station list used by booking origin and destination yards.",
multiple: false,
meta: {
searchable: true,
clearable: true,
version: "temporary",
},
children: DEFAULT_STATION_OPTIONS,
});
return;
}
if ((existing.children?.length ?? 0) === 0) {
await this.repository.replaceOptions(
existing.id,
DEFAULT_STATION_OPTIONS,
);
}
}
async update( async update(
id: string, id: string,
dto: UpdateDropdownSettingDto, dto: UpdateDropdownSettingDto,

View File

@@ -18,6 +18,7 @@ import {
Settings, Settings,
UserCircle, UserCircle,
FileUp, FileUp,
MapPinned,
} from "lucide-react"; } from "lucide-react";
import BookingsPage from "./pages/bookings/BookingsPage"; import BookingsPage from "./pages/bookings/BookingsPage";
@@ -42,6 +43,7 @@ import DocumentsPage from "./pages/documents/DocumentsPage";
import DropdownSettingsPage from "./pages/admin/DropdownSettingsPage"; import DropdownSettingsPage from "./pages/admin/DropdownSettingsPage";
import FileUploadSettingsPage from "./pages/admin/FileUploadSettingsPage"; import FileUploadSettingsPage from "./pages/admin/FileUploadSettingsPage";
import MyPortalPage from "./pages/portal/MyPortalPage"; import MyPortalPage from "./pages/portal/MyPortalPage";
import Station from "./components/stations/Station";
const sidebarItems: SidebarItem[] = [ const sidebarItems: SidebarItem[] = [
{ label: "My Portal", href: "/portal", icon: <UserCircle /> }, { label: "My Portal", href: "/portal", icon: <UserCircle /> },
@@ -50,6 +52,7 @@ const sidebarItems: SidebarItem[] = [
{ label: "Bookings", href: "/bookings", icon: <CalendarCheck /> }, { label: "Bookings", href: "/bookings", icon: <CalendarCheck /> },
{ label: "Consignments", href: "/consignments", icon: <Package /> }, { label: "Consignments", href: "/consignments", icon: <Package /> },
{ label: "Tracking", href: "/tracking", icon: <MapPin /> }, { label: "Tracking", href: "/tracking", icon: <MapPin /> },
{ label: "Stations", href: "/stations", icon: <MapPinned /> },
{ label: "Trains", href: "/trains", icon: <Train /> }, { label: "Trains", href: "/trains", icon: <Train /> },
{ label: "Billing", href: "/billing", icon: <Receipt /> }, { label: "Billing", href: "/billing", icon: <Receipt /> },
{ label: "Documents", href: "/documents", icon: <FileText /> }, { label: "Documents", href: "/documents", icon: <FileText /> },
@@ -121,6 +124,7 @@ const App = () => {
<Route path="/consignments" element={<ConsignmentsPage />} /> <Route path="/consignments" element={<ConsignmentsPage />} />
<Route path="/consignments/:id" element={<ConsignmentDetailPage />} /> <Route path="/consignments/:id" element={<ConsignmentDetailPage />} />
<Route path="/tracking" element={<TrackingPage />} /> <Route path="/tracking" element={<TrackingPage />} />
<Route path="/stations" element={<Station />} />
<Route path="/trains" element={<TrainsPage />} /> <Route path="/trains" element={<TrainsPage />} />
<Route path="/billing" element={<BillingPage />} /> <Route path="/billing" element={<BillingPage />} />
<Route path="/documents" element={<DocumentsPage />} /> <Route path="/documents" element={<DocumentsPage />} />

View File

@@ -0,0 +1,228 @@
import { useMemo, useState } from "react";
import {
AlertCircle,
CircleOff,
Loader2,
MapPin,
Search,
TrainFront,
} from "lucide-react";
import Breadcrumbs from "@/components/Breadcrumbs";
import { useDropdownSettingByCode } from "@/hooks/useDropdownSettings";
import type { DropdownOption } from "@/types/dropdownSettings";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
DataTable,
DataTableFooter,
Input,
type ColumnDef,
usePagination,
} from "@edr/ui-common";
const STATION_DROPDOWN_CODE = "stations_ter";
export default function Station() {
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [query, setQuery] = useState("");
const { data, isLoading, isError, error } = useDropdownSettingByCode(
STATION_DROPDOWN_CODE,
);
const stations = useMemo<DropdownOption[]>(
() => [...(data?.children ?? [])].sort((a, b) => a.order - b.order),
[data?.children],
);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return stations;
return stations.filter(
(station) =>
station.label.toLowerCase().includes(q) ||
station.value.toLowerCase().includes(q) ||
(station.note ?? "").toLowerCase().includes(q),
);
}, [query, stations]);
const total = filtered.length;
const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize));
const start = pagination.pageIndex * pagination.pageSize;
const end = Math.min(start + pagination.pageSize, total);
const paginatedData = useMemo(
() => filtered.slice(start, end),
[end, filtered, start],
);
const activeCount = stations.filter((station) => !station.disabled).length;
const disabledCount = stations.length - activeCount;
const status: "loading" | "error" | "success" = isLoading
? "loading"
: isError
? "error"
: "success";
const columns: ColumnDef<DropdownOption>[] = [
{
id: "station",
header: "Station",
cell: ({ row }) => {
const station = row.original;
return (
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-foreground">
<MapPin />
</div>
<div>
<p className="font-medium text-slate-900">{station.label}</p>
<p className="text-xs text-slate-500">
{station.note ?? "No station note"}
</p>
</div>
</div>
);
},
},
{
id: "value",
header: "Code",
cell: ({ row }) => (
<span className="rounded-md bg-slate-100 px-2 py-1 font-mono text-xs text-slate-700">
{row.original.value}
</span>
),
},
{
accessorKey: "order",
header: "Order",
},
{
id: "status",
header: "Status",
cell: ({ row }) =>
row.original.disabled ? (
<span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">
<CircleOff className="h-3 w-3" />
Disabled
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
<TrainFront className="h-3 w-3" />
Active
</span>
),
},
];
return (
<div className="min-h-screen p-6">
<div className="space-y-6">
<Breadcrumbs items={[{ label: "Stations" }]} />
<Card className="p-6 flex-row justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
Stations
</h1>
<p className="mt-1 text-sm text-secondary-foreground">
Station options loaded from dropdown code{" "}
<span className="font-mono">stations_ter</span>.
</p>
</div>
<div className="relative w-full sm:w-80">
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="search"
value={query}
onChange={(event) => {
setQuery(event.target.value);
setPagination({
pageIndex: 0,
pageSize: pagination.pageSize,
});
}}
placeholder="Search stations..."
className="pl-8!"
/>
</div>
</Card>
<div className="grid gap-4 md:grid-cols-3">
<StationStat label="Stations" value={stations.length} />
<StationStat label="Active" value={activeCount} />
<StationStat label="Disabled" value={disabledCount} />
</div>
{isError ? (
<Card>
<CardContent className="flex items-center gap-3 py-6 text-sm text-red-600">
<AlertCircle className="h-5 w-5" />
Failed to load stations.{" "}
{error instanceof Error ? error.message : "Unknown error."}
</CardContent>
</Card>
) : null}
<Card className="gap-0">
<CardHeader className="border-b">
<CardTitle>Station List</CardTitle>
<CardDescription>
All configured freight stations from the dropdown service.
</CardDescription>
</CardHeader>
<CardContent className="px-0">
{isLoading ? (
<div className="flex items-center justify-center py-12 text-sm text-slate-500">
<Loader2 className="mr-2 h-4 w-4 animate-spin text-primary" />
Loading stations...
</div>
) : (
<DataTable
columns={columns}
data={paginatedData}
status={status}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-b shadow-none"
footer={DataTableFooter}
/>
)}
</CardContent>
</Card>
</div>
</div>
);
}
function StationStat({ label, value }: { label: string; value: number }) {
return (
<Card>
<CardContent className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">{label}</p>
<h3 className="mt-2 text-3xl font-bold text-slate-900">{value}</h3>
</div>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
<MapPin />
</div>
</CardContent>
</Card>
);
}

View File

@@ -471,6 +471,12 @@ export function getRouteDirection(
dest: string, dest: string,
): RouteDirection { ): RouteDirection {
if (!origin || !dest) return null; if (!origin || !dest) return null;
const oLocation = getStationLocation(origin);
const dLocation = getStationLocation(dest);
if (oLocation === "inside" && dLocation === "outside") return "export";
if (oLocation === "outside" && dLocation === "inside") return "import";
if (oLocation === "inside" && dLocation === "inside") return "domestic";
const oEth = ETHIOPIA_STATIONS.has(origin); const oEth = ETHIOPIA_STATIONS.has(origin);
const dEth = ETHIOPIA_STATIONS.has(dest); const dEth = ETHIOPIA_STATIONS.has(dest);
if (oEth && !dEth) return "export"; if (oEth && !dEth) return "export";
@@ -489,6 +495,20 @@ export function calcWagons(containers: ContainerConfig[]): WagonCalcResult {
const wagonLayout: WagonConfig[] = []; const wagonLayout: WagonConfig[] = [];
let hasOddUnit = Ft20Wagons % 2 === 1; let hasOddUnit = Ft20Wagons % 2 === 1;
let sharedWagons = Math.floor(Ft20Wagons / 2); let sharedWagons = Math.floor(Ft20Wagons / 2);
function getStationLocation(value: string): "inside" | "outside" | null {
const normalized = value.trim().toLowerCase();
if (normalized.startsWith("inside")) return "inside";
if (normalized.startsWith("outside")) return "outside";
return null;
}
export function calcWagons(
type: BookingFormValues["containerType"],
qty: number,
): WagonCalcResult {
if (type === "40ft") {
return { totalWagons: qty, hasOddUnit: false, sharedWagons: qty };
}
return { return {
totalWagons: sharedWagons + Ft40Wagons, totalWagons: sharedWagons + Ft40Wagons,

View File

@@ -129,18 +129,24 @@ export function SelectField({
error, error,
label, label,
placeholder, placeholder,
disabled,
children, children,
}: { }: {
field: ControllerRenderProps<BookingFormValues>; field: ControllerRenderProps<BookingFormValues>;
error?: RhfFieldError; error?: RhfFieldError;
label: string; label: string;
placeholder: string; placeholder: string;
disabled?: boolean;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<Field data-invalid={Boolean(error)}> <Field data-invalid={Boolean(error)}>
<FieldLabel>{label}</FieldLabel> <FieldLabel>{label}</FieldLabel>
<Select value={String(field.value)} onValueChange={field.onChange}> <Select
value={String(field.value)}
onValueChange={field.onChange}
disabled={disabled}
>
<SelectTrigger <SelectTrigger
className={cn("w-full ", error ? "border-destructive!" : "")} className={cn("w-full ", error ? "border-destructive!" : "")}
aria-invalid={Boolean(error)} aria-invalid={Boolean(error)}

3013
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff