From 4994e19d57dd8a242236c10f18fce3d13d46a02b Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Sat, 6 Jun 2026 11:12:19 +0300
Subject: [PATCH 1/9] signup update
---
.../src/config/database.config.ts | 11 +-
.../portal/src/pages/accounts/SignupPage.tsx | 151 +++++++++++++++---
apps/edr-freight-web/portal/src/types/auth.ts | 2 +
3 files changed, 138 insertions(+), 26 deletions(-)
diff --git a/apps/edr-freight-api/src/config/database.config.ts b/apps/edr-freight-api/src/config/database.config.ts
index a511c1654..0e7375b19 100644
--- a/apps/edr-freight-api/src/config/database.config.ts
+++ b/apps/edr-freight-api/src/config/database.config.ts
@@ -17,7 +17,6 @@ import {
PositionType,
Position,
Project,
- UnitSetting,
GlobalUnitConfiguration,
Unit,
EmployeeSignature,
@@ -64,7 +63,6 @@ const iamEntities = [
PositionType,
Position,
Project,
- UnitSetting,
GlobalUnitConfiguration,
Unit,
EmployeeSignature,
@@ -98,10 +96,8 @@ const iamMigrationsGlob = join(
);
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
-export default registerAs(
- "database",
- (): TypeOrmModuleOptions => {
- return {
+export default registerAs("database", (): TypeOrmModuleOptions => {
+ return {
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5433", 10),
@@ -124,5 +120,4 @@ export default registerAs(
synchronize: false,
logging: process.env.NODE_ENV === "development",
};
- },
-);
+});
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
index b5289a5dc..8c305e0ef 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
@@ -3,12 +3,21 @@ import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
-import { ArrowRight, UserPlus, Loader2 } from "lucide-react";
+import {
+ ArrowRight,
+ Eye,
+ EyeOff,
+ UserPlus,
+ Loader2,
+ Check,
+ X,
+} from "lucide-react";
import { userType } from "@/enums/userType";
import useAuth from "@/hooks/useAuth";
import type { SignupPayload } from "@/types/auth";
import AuthLayout from "@/components/auth/AuthLayout";
import PhoneInput from "@/components/auth/PhoneInput";
+import { cn } from "@/lib/utils";
import {
Button,
Input,
@@ -18,23 +27,47 @@ import {
FieldGroup,
} from "@edr/ui-common";
-const userSchema = z.object({
- email: z.string().email("Invalid email address"),
- countryCode: z.string().min(1, "Country code is required"),
- phone: z
- .string()
- .min(9, "Phone number is too short")
- .max(9, "Phone number is too long"),
- userType: z.string(),
- firstName: z.object({
- en: z.string().min(2, "Name is required"),
- am: z.string().nullable(),
- }),
- lastName: z.object({
- en: z.string().min(2, "Name is required"),
- am: z.string().nullable(),
- }),
-});
+const passwordRequirements = [
+ { label: "At least 8 characters", test: (v: string) => v.length >= 8 },
+ { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
+ { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
+ { label: "One number", test: (v: string) => /\d/.test(v) },
+ {
+ label: "One special character",
+ test: (v: string) => /[^A-Za-z0-9]/.test(v),
+ },
+] as const;
+
+const userSchema = z
+ .object({
+ email: z.string().email("Invalid email address"),
+ countryCode: z.string().min(1, "Country code is required"),
+ phone: z
+ .string()
+ .min(9, "Phone number is too short")
+ .max(9, "Phone number is too long"),
+ userType: z.string(),
+ firstName: z.object({
+ en: z.string().min(2, "Name is required"),
+ am: z.string().nullable(),
+ }),
+ lastName: z.object({
+ en: z.string().min(2, "Name is required"),
+ am: z.string().nullable(),
+ }),
+ password: z
+ .string()
+ .min(8, "Password must be at least 8 characters")
+ .regex(/[A-Z]/, "Password must include an uppercase letter")
+ .regex(/[a-z]/, "Password must include a lowercase letter")
+ .regex(/\d/, "Password must include a number")
+ .regex(/[^A-Za-z0-9]/, "Password must include a special character"),
+ confirmPassword: z.string().min(1, "Please confirm your password"),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords do not match",
+ path: ["confirmPassword"],
+ });
type FormData = z.infer;
@@ -43,10 +76,13 @@ export default function SignupPage() {
const { signup } = useAuth();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const {
register,
handleSubmit,
+ watch,
formState: { errors },
} = useForm({
resolver: zodResolver(userSchema),
@@ -57,6 +93,8 @@ export default function SignupPage() {
userType: userType.individual,
firstName: { en: "", am: "" },
lastName: { en: "", am: "" },
+ password: "",
+ confirmPassword: "",
},
});
@@ -73,6 +111,8 @@ export default function SignupPage() {
phoneNumber: `${data.countryCode}${normalizedPhone}`,
userType: data.userType,
name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
+ password: data.password,
+ confirmPassword: data.confirmPassword,
};
const result = await signup(payload);
if (result.success) {
@@ -171,6 +211,81 @@ export default function SignupPage() {
countryCodeError={errors.countryCode}
phoneError={errors.phone}
/>
+
+
+ Password
+
+
+
+
+
+
+ {passwordRequirements.map((req) => {
+ const met = req.test(watch("password") ?? "");
+ return (
+
+ {met ? (
+
+ ) : (
+
+ )}
+ {req.label}
+
+ );
+ })}
+
+
+
+
+ Confirm Password
+
+
+
+
+
+
-
+
+
+
+
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx
new file mode 100644
index 000000000..68fd9c70f
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx
@@ -0,0 +1,374 @@
+import { useMemo, useState } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { useNavigate, useParams } from "react-router-dom";
+import {
+ AlertCircle,
+ Check,
+ ChevronLeft,
+ ChevronRight,
+ LoaderCircle,
+ Loader2,
+} from "lucide-react";
+import { Button } from "@edr/ui-common";
+import { api } from "@/services/api";
+import type {
+ CreateBookingPayload,
+} from "@/services/bookings.service";
+import {
+ BookingFormInputValues,
+ STEPS,
+ bookingFormSchema,
+ getRouteDirection,
+ initialBookingFormValues,
+ stepFields,
+ type BookingFormValues,
+ type RouteDirection,
+} from "./new-booking-form/schema";
+import { StepIndicator } from "./new-booking-form/StepIndicator";
+import {
+ Step1ContractType,
+ Step2ServiceType,
+ Step4Route,
+ Step5CargoDetails,
+ Step8Review,
+} from "./new-booking-form/steps";
+import type { Freight } from "@edr/types";
+
+function yardNameFromBooking(yard: { label?: string; code?: string; name?: string } | undefined | null): string {
+ return yard?.label ?? yard?.name ?? yard?.code ?? "";
+}
+
+function mapBookingToFormValues(
+ booking: Freight.IBooking,
+ referenceData: Freight.BookingReferenceData,
+): BookingFormInputValues {
+ const vals: BookingFormInputValues = {
+ ...initialBookingFormValues,
+ contractType: (booking.contractType?.toLowerCase() as "new" | "renewal") ?? "new",
+ previousContractRef: booking.previousContractId ?? "",
+ serviceType:
+ booking.serviceType === "RAIL_AND_FORWARDING" ? "rail_forwarding" : "rail",
+ firstMile: {
+ enabled: booking.firstMileEnabled ?? false,
+ pickUpAddress: booking.firstMilePickupAddress ?? "",
+ },
+ lastMile: {
+ enabled: booking.lastMileEnabled ?? false,
+ deliveryAddress: booking.lastMileDeliveryAddress ?? "",
+ },
+ equipmentReturn:
+ booking.equipmentReturn === "WITH_RETURN" ? "with_return" : "without_return",
+ originYard: yardNameFromBooking(booking.originYard),
+ destinationYard: yardNameFromBooking(booking.destinationYard),
+ cargoType: booking.freightType === "BULK" ? "bulk" : "container",
+ cargoWeight: String(booking.cargoTotalWeightVgm ?? ""),
+ isHazardous: booking.isHazardous ?? false,
+ isRefrigerated: booking.isRefrigerated ?? false,
+ shippingLine: (booking as any).shippingLine?.name ?? "",
+ consolidationEnabled: booking.allowConsolidation ?? false,
+ notes: "",
+ termsAccepted: false,
+ freightType: "",
+ bulkCommoditytype: "",
+ containers: [],
+ };
+
+ const bookingCargoTypeId = (booking as any).cargoTypeId as string | undefined;
+ if (booking.freightType === "BULK" && bookingCargoTypeId) {
+ for (const group of referenceData.cargo_type) {
+ const child = group.children?.find((c) => c.id === bookingCargoTypeId);
+ if (child) {
+ vals.freightType = group.code.toLowerCase();
+ vals.bulkCommoditytype = child.name;
+ break;
+ }
+ }
+ }
+
+ if (booking.freightType === "CONTAINER" && booking.containers && booking.containers.length > 0) {
+ vals.containers = booking.containers.map((c) => ({
+ type: c.type === "40ft" ? "40ft" : "20ft" as const,
+ containerType: "",
+ qty: String(c.qty),
+ vgm: String(c.vgm),
+ }));
+ }
+
+ return vals;
+}
+
+export default function EditBookingPage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const [step, setStep] = useState(1);
+
+ const bookingQuery = useQuery(
+ api.bookings.get.queryOptions({
+ input: { id: id! },
+ enabled: !!id,
+ }),
+ );
+
+ const { data: referenceData, isLoading: refDataLoading } = useQuery(
+ api.bookings.referenceData.queryOptions({
+ enabled: !!bookingQuery.data,
+ }),
+ );
+
+ const updateMutation = useMutation({
+ mutationFn: (payload: Partial) =>
+ api.bookings.update.call({ id: id!, dto: payload }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
+ queryClient.invalidateQueries({ queryKey: api.bookings.get.queryKey({ id: id! }) });
+ navigate(`/bookings/${id}`);
+ },
+ });
+
+ const booking = bookingQuery.data;
+
+ const formValues = useMemo((): BookingFormInputValues | undefined => {
+ if (!booking || !referenceData) return undefined;
+ return mapBookingToFormValues(booking, referenceData);
+ }, [booking, referenceData]);
+
+ const form = useForm({
+ defaultValues: initialBookingFormValues,
+ values: formValues,
+ resolver: zodResolver(bookingFormSchema),
+ mode: "onChange",
+ });
+
+ const originYard = form.watch("originYard");
+ const destinationYard = form.watch("destinationYard");
+
+ const direction: RouteDirection = useMemo(
+ () => getRouteDirection(originYard, destinationYard),
+ [originYard, destinationYard],
+ );
+
+ async function handleContinue() {
+ const valid = await form.trigger(stepFields[step], { shouldFocus: true });
+ if (!valid) return;
+ setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
+ }
+
+ const handleSubmit = form.handleSubmit((data) => {
+ const yards = referenceData?.yard ?? [];
+ const services = referenceData?.service ?? [];
+ const shippingLines = referenceData?.shipping_line ?? [];
+ const cargoTree = referenceData?.cargo_type ?? [];
+ const containerGroups = referenceData?.containers ?? [];
+
+ const findYardId = (name: string): string =>
+ yards.find((y) => y.name === name)?.id ?? "";
+
+ const findServiceTypeId = (): string => {
+ const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING";
+ return services.find((s) => s.code === code)?.id ?? services[0]?.id ?? "";
+ };
+
+ const findShippingLineId = (name: string): string | undefined =>
+ shippingLines.find((l) => l.name === name)?.id;
+
+ const selectedChild =
+ data.cargoType !== "container" && data.bulkCommoditytype
+ ? cargoTree
+ .find((g) => g.code.toLowerCase() === data.freightType)
+ ?.children?.find((c) => c.name === data.bulkCommoditytype)
+ : undefined;
+
+ const cargoTypeId =
+ data.cargoType === "container"
+ ? undefined
+ : selectedChild?.id ?? "";
+
+ const findContainerTypeId = (name: string): string => {
+ for (const group of containerGroups) {
+ const ct = group.types.find((t) => t.name === name);
+ if (ct) return ct.id;
+ }
+ return "";
+ };
+
+ const totalWeight =
+ data.cargoType === "container"
+ ? data.containers.reduce(
+ (acc, c) => acc + Number(c.vgm || 0) * Number(c.qty || 0),
+ 0,
+ )
+ : Number(data.cargoWeight || 0);
+
+ const apiPayload: Partial = {
+ scheduledDate: new Date().toISOString().slice(0, 10),
+ contractType:
+ data.contractType.toUpperCase() as CreateBookingPayload["contractType"],
+ serviceTypeId: findServiceTypeId(),
+ equipmentReturn:
+ data.equipmentReturn === "with_return"
+ ? "WITH_RETURN"
+ : "WITHOUT_RETURN",
+ originYardId: findYardId(data.originYard),
+ destinationYardId: findYardId(data.destinationYard),
+ tradeDirection:
+ direction === "export"
+ ? "EXPORT"
+ : direction === "domestic"
+ ? "DOMESTIC"
+ : "IMPORT",
+ cargoTypeId: data.cargoType === "container" ? undefined : cargoTypeId,
+ cargoTotalWeightVgm: totalWeight,
+ isHazardous: data.isHazardous,
+ paymentCurrency: "USD",
+ allowConsolidation: data.consolidationEnabled,
+ // @ts-ignore
+ freightType:
+ data.cargoType === "container"
+ ? ("CONTAINER" as const)
+ : ("BULK" as const),
+ containers:
+ data.cargoType === "container"
+ ? data.containers.map((c) => ({
+ containerTypeId: findContainerTypeId(c.containerType),
+ quantity: Number(c.qty || 1),
+ vgmPerUnitTons: Number(c.vgm || 0),
+ }))
+ : [],
+ ...(data.previousContractRef
+ ? { previousContractId: data.previousContractRef }
+ : {}),
+ ...(data.contractType === "renewal" && data.previousContractRef
+ ? { pnrCode: data.previousContractRef }
+ : {}),
+ ...(data.serviceType === "rail_forwarding" && data.firstMile.enabled
+ ? { firstMilePickupAddress: data.firstMile.pickUpAddress }
+ : {}),
+ ...(data.serviceType === "rail_forwarding" && data.lastMile.enabled
+ ? { lastMileDeliveryAddress: data.lastMile.deliveryAddress }
+ : {}),
+ ...(data.shippingLine
+ ? { shippingLineId: findShippingLineId(data.shippingLine) }
+ : {}),
+ };
+
+ updateMutation.mutate(apiPayload);
+ });
+
+ if (bookingQuery.isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (bookingQuery.isError || !booking) {
+ return (
+
+
+
+
Failed to load booking
+
+
+
+ );
+ }
+
+ if (!formValues) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
index 9b5a4cf73..78ba91af6 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
@@ -138,10 +138,7 @@ export default function NewBookingPage() {
const cargoTypeId =
data.cargoType === "container"
? findContainerCargoTypeId()
- : (findCargoTypeId(data.bulkCommoditytype) ??
- cargoTree.find((g) => g.code.toLowerCase() === data.freightType)
- ?.id ??
- "");
+ : selectedChild?.id ?? "";
const cargoFreeText =
data.cargoType === "container"
diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts
index 9b306b8b1..c49ebad0f 100644
--- a/apps/edr-freight-web/portal/src/services/api.ts
+++ b/apps/edr-freight-web/portal/src/services/api.ts
@@ -134,6 +134,11 @@ export const api = {
bookingsService.create,
),
+ update: endpoint<
+ { id: string; dto: Partial },
+ { booking: Freight.IBooking; warnings: string[] }
+ >("bookings", "update", ({ id, dto }) => bookingsService.update(id, dto)),
+
referenceData: endpoint(
"bookings",
"referenceData",
diff --git a/apps/edr-freight-web/portal/src/services/bookings.service.ts b/apps/edr-freight-web/portal/src/services/bookings.service.ts
index 95a3e5740..d9481d59e 100644
--- a/apps/edr-freight-web/portal/src/services/bookings.service.ts
+++ b/apps/edr-freight-web/portal/src/services/bookings.service.ts
@@ -74,6 +74,14 @@ export const bookingsService = {
const { data } = await client.get("/api/bookings/reference-data");
return data.data;
},
+ update: async (
+ id: string,
+ payload: Partial,
+ ): Promise<{ booking: Freight.IBooking; warnings: string[] }> => {
+ const { data } = await client.patch(`/api/bookings/${id}`, payload);
+ return data.data;
+ },
+
remove: async (id: string): Promise => {
await client.delete(`/api/bookings/${id}`);
},
From ea040a40cf215a76179574e4c573b74eda34e524 Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Sat, 6 Jun 2026 11:21:39 +0300
Subject: [PATCH 3/9] fix: wrong url
---
apps/edr-freight-web/portal/src/constants/URLS.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts
index 03123b672..578a1d97d 100644
--- a/apps/edr-freight-web/portal/src/constants/URLS.ts
+++ b/apps/edr-freight-web/portal/src/constants/URLS.ts
@@ -10,7 +10,7 @@ export const URL_CONSTANTS = {
USERS: {
BASE: "/users",
BY_ID: (id: string | number) => `/users/${id}`,
- SIGN_UP: "/api/auth/signup",
+ SIGN_UP: "/api/auth/signup-with-pwd",
SET_PASSWORD: "/api/auth/set-password",
ME: "/api/auth/me",
GENERATE_VERIFICATION_CODE: "/users/generate-verification-code",
From 5898634cd75cfb8a4ee068a545c5bcf1d1b50cac Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Sat, 6 Jun 2026 11:29:05 +0300
Subject: [PATCH 4/9] refactor(settings): decompose monolithic SettingsPage
into modular tab components
---
.../portal/src/pages/SettingsPage.tsx | 531 +-----------------
.../src/pages/settings/TabCompanyProfile.tsx | 219 ++++++++
.../src/pages/settings/TabContactPerson.tsx | 145 +++++
.../src/pages/settings/TabDocuments.tsx | 107 ++++
.../src/pages/settings/TabGeneralManager.tsx | 161 ++++++
.../src/pages/settings/TabPowerOfAttorney.tsx | 208 +++++++
6 files changed, 852 insertions(+), 519 deletions(-)
create mode 100644 apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/settings/TabContactPerson.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/settings/TabPowerOfAttorney.tsx
diff --git a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx
index faec1b5b2..2083fcd37 100644
--- a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx
@@ -1,41 +1,20 @@
-import { useState, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import { useQuery } from "@tanstack/react-query";
import {
Building2,
User,
Briefcase,
UserCheck,
FileCheck,
- Loader2,
- Save,
- UploadCloud,
- CheckCircle2,
- XCircle,
} from "lucide-react";
import { api } from "@/services/api";
-import { companiesService } from "@/services/companies.service";
-import PhoneInput from "@/components/auth/PhoneInput";
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
- CardFooter,
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
- SmartFileInput,
- Badge,
-} from "@edr/ui-common";
+import { Badge } from "@edr/ui-common";
import { cn } from "@/lib/utils";
+import TabCompanyProfile from "./settings/TabCompanyProfile";
+import TabContactPerson from "./settings/TabContactPerson";
+import TabGeneralManager from "./settings/TabGeneralManager";
+import TabPowerOfAttorney from "./settings/TabPowerOfAttorney";
+import TabDocuments from "./settings/TabDocuments";
type SettingsTab =
| "company"
@@ -44,32 +23,6 @@ type SettingsTab =
| "poa"
| "documents";
-const settingsSchema = z.object({
- companyName: z.string().min(1, "Company name is required"),
- companyEmail: z.string().email("Invalid email address"),
- companyPhone: z.string().min(1, "Company phone is required"),
- companyPhoneCountryCode: z.string().min(1, "Country code is required"),
- companyLocation: z.string().min(1, "Location is required"),
- companyAddress: z.string().min(1, "Address is required"),
- tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
- fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
- contactPersonName: z.string().min(1, "Contact person name is required"),
- contactPersonPhone: z.string().min(1, "Contact person phone is required"),
- contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
- generalManagerName: z.string().min(1, "GM name is required"),
- generalManagerEmail: z.string().email("Invalid GM email"),
- generalManagerPhone: z.string().min(1, "GM phone is required"),
- generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
- poaName: z.string().optional(),
- poaEmail: z.string().optional(),
- poaPhone: z.string().optional(),
- poaPhoneCountryCode: z.string().optional(),
- poaLocation: z.string().optional(),
- poaAddress: z.string().optional(),
-});
-
-type FormData = z.infer;
-
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
{ id: "company", label: "Company Profile", icon: },
{ id: "contact", label: "Contact Person", icon: },
@@ -78,15 +31,7 @@ const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
{ id: "documents", label: "Documents", icon: },
];
-function splitPhone(fullPhone?: string | null): { code: string; number: string } {
- if (!fullPhone) return { code: "+251", number: "" };
- const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
- if (match) return { code: match[1], number: match[2] };
- return { code: "+251", number: fullPhone };
-}
-
export default function SettingsPage() {
- const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const tab = (searchParams.get("tab") as SettingsTab) || "company";
const setTab = (t: SettingsTab) => {
@@ -96,130 +41,13 @@ export default function SettingsPage() {
return next;
}, { replace: true });
};
- const [documentFiles, setDocumentFiles] = useState<
- Record
- >({});
const profileQuery = useQuery(
api.companies.getProfile.queryOptions(),
);
- const docSettingQuery = useQuery(
- api.fileUploadSettings.getByCode.queryOptions({
- input: { code: "customer_documents" },
- enabled: tab === "documents",
- }),
- );
-
const profile = profileQuery.data;
- const defaultValues = useMemo((): FormData => {
- if (!profile) {
- return {
- companyName: "",
- companyEmail: "",
- companyPhone: "",
- companyPhoneCountryCode: "+251",
- companyLocation: "",
- companyAddress: "",
- tinNumber: "",
- fanNumber: "",
- contactPersonName: "",
- contactPersonPhone: "",
- contactPersonPhoneCountryCode: "+251",
- generalManagerName: "",
- generalManagerEmail: "",
- generalManagerPhone: "",
- generalManagerPhoneCountryCode: "+251",
- poaName: "",
- poaEmail: "",
- poaPhone: "",
- poaPhoneCountryCode: "+251",
- poaLocation: "",
- poaAddress: "",
- };
- }
- const contactPhone = splitPhone(profile.contactPersonPhone);
- const gmPhone = splitPhone(profile.generalManagerPhone);
- const poaPhone = splitPhone(profile.poaPhone);
- return {
- companyName: profile.companyName,
- companyEmail: profile.companyEmail ?? "",
- companyPhone: profile.companyPhone ?? "",
- companyPhoneCountryCode: splitPhone(profile.companyPhone).code,
- companyLocation: profile.companyLocation,
- companyAddress: profile.companyAddress ?? "",
- tinNumber: profile.tinNumber,
- fanNumber: profile.fanNumber ?? "",
- contactPersonName: profile.contactPersonName ?? "",
- contactPersonPhone: contactPhone.number,
- contactPersonPhoneCountryCode: contactPhone.code,
- generalManagerName: profile.generalManagerName ?? "",
- generalManagerEmail: profile.generalManagerEmail ?? "",
- generalManagerPhone: gmPhone.number,
- generalManagerPhoneCountryCode: gmPhone.code,
- poaName: profile.poaName ?? "",
- poaEmail: profile.poaEmail ?? "",
- poaPhone: poaPhone.number,
- poaPhoneCountryCode: poaPhone.code,
- poaLocation: profile.poaLocation ?? "",
- poaAddress: profile.poaAddress ?? "",
- };
- }, [profile]);
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors, isDirty },
- } = useForm({
- resolver: zodResolver(settingsSchema),
- values: defaultValues,
- });
-
- const updateMutation = useMutation({
- mutationFn: (data: FormData) =>
- api.companies.updateProfile.call({
- companyName: data.companyName,
- companyEmail: data.companyEmail,
- companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
- companyLocation: data.companyLocation,
- companyAddress: data.companyAddress,
- tin: data.tinNumber,
- fanNumber: data.fanNumber,
- contactPersonName: data.contactPersonName,
- contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
- generalManagerName: data.generalManagerName,
- generalManagerEmail: data.generalManagerEmail,
- generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
- poaName: data.poaName || undefined,
- poaPhone:
- data.poaPhone && data.poaPhoneCountryCode
- ? `${data.poaPhoneCountryCode}${data.poaPhone}`
- : undefined,
- poaEmail: data.poaEmail || undefined,
- poaLocation: data.poaLocation || undefined,
- poaAddress: data.poaAddress || undefined,
- }),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: api.companies.getProfile.queryKey(),
- });
- },
- });
-
- const docUploadMutation = useMutation({
- mutationFn: (files: Record) =>
- companiesService.uploadDocuments(profile!.companyId, files),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: api.companies.getProfile.queryKey(),
- });
- },
- });
-
- const isPending = profileQuery.isPending || updateMutation.isPending || docUploadMutation.isPending;
-
if (profileQuery.isPending) {
return (
@@ -236,10 +64,6 @@ export default function SettingsPage() {
);
}
- const onSubmit = (data: FormData) => {
- updateMutation.mutate(data);
- };
-
return (
@@ -276,342 +100,11 @@ export default function SettingsPage() {
))}
-
+ {tab === "company" &&
}
+ {tab === "contact" &&
}
+ {tab === "gm" &&
}
+ {tab === "poa" &&
}
+ {tab === "documents" &&
}
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
new file mode 100644
index 000000000..b64419565
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
@@ -0,0 +1,219 @@
+import { useMemo } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Building2, CheckCircle2, Loader2, Save, XCircle } from "lucide-react";
+import { api } from "@/services/api";
+import PhoneInput from "@/components/auth/PhoneInput";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ Button,
+ Input,
+ Field,
+ FieldLabel,
+ FieldError,
+ FieldGroup,
+} from "@edr/ui-common";
+import type { ProfileResponse } from "@/types/profile";
+
+const schema = z.object({
+ companyName: z.string().min(1, "Company name is required"),
+ companyEmail: z.string().email("Invalid email address"),
+ companyPhone: z.string().min(1, "Company phone is required"),
+ companyPhoneCountryCode: z.string().min(1, "Country code is required"),
+ companyLocation: z.string().min(1, "Location is required"),
+ companyAddress: z.string().min(1, "Address is required"),
+ tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
+ fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
+});
+
+type FormData = z.infer
;
+
+function splitPhone(fullPhone?: string | null) {
+ if (!fullPhone) return { code: "+251", number: "" };
+ const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
+ if (match) return { code: match[1], number: match[2] };
+ return { code: "+251", number: fullPhone };
+}
+
+export default function TabCompanyProfile({ profile }: { profile: ProfileResponse }) {
+ const queryClient = useQueryClient();
+
+ const defaultValues = useMemo((): FormData => {
+ const phone = splitPhone(profile.companyPhone);
+ return {
+ companyName: profile.companyName,
+ companyEmail: profile.companyEmail ?? "",
+ companyPhone: phone.number,
+ companyPhoneCountryCode: phone.code,
+ companyLocation: profile.companyLocation,
+ companyAddress: profile.companyAddress ?? "",
+ tinNumber: profile.tinNumber,
+ fanNumber: profile.fanNumber ?? "",
+ };
+ }, [profile]);
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty },
+ } = useForm({
+ resolver: zodResolver(schema),
+ values: defaultValues,
+ });
+
+ const mutation = useMutation({
+ mutationFn: (data: FormData) =>
+ api.companies.updateProfile.call({
+ companyName: data.companyName,
+ companyEmail: data.companyEmail,
+ companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
+ companyLocation: data.companyLocation,
+ companyAddress: data.companyAddress,
+ tin: data.tinNumber,
+ fanNumber: data.fanNumber,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
+ },
+ });
+
+ const onSubmit = (data: FormData) => mutation.mutate(data);
+
+ return (
+
+
+
+
+ Company Profile
+
+ Edit your company registration details
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabContactPerson.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabContactPerson.tsx
new file mode 100644
index 000000000..bb2401be8
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabContactPerson.tsx
@@ -0,0 +1,145 @@
+import { useMemo } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { CheckCircle2, Loader2, Save, User, XCircle } from "lucide-react";
+import { api } from "@/services/api";
+import PhoneInput from "@/components/auth/PhoneInput";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ Button,
+ Input,
+ Field,
+ FieldLabel,
+ FieldError,
+ FieldGroup,
+} from "@edr/ui-common";
+import type { ProfileResponse } from "@/types/profile";
+
+const schema = z.object({
+ contactPersonName: z.string().min(1, "Contact person name is required"),
+ contactPersonPhone: z.string().min(1, "Contact person phone is required"),
+ contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
+});
+
+type FormData = z.infer;
+
+function splitPhone(fullPhone?: string | null) {
+ if (!fullPhone) return { code: "+251", number: "" };
+ const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
+ if (match) return { code: match[1], number: match[2] };
+ return { code: "+251", number: fullPhone };
+}
+
+export default function TabContactPerson({ profile }: { profile: ProfileResponse }) {
+ const queryClient = useQueryClient();
+
+ const defaultValues = useMemo((): FormData => {
+ const phone = splitPhone(profile.contactPersonPhone);
+ return {
+ contactPersonName: profile.contactPersonName ?? "",
+ contactPersonPhone: phone.number,
+ contactPersonPhoneCountryCode: phone.code,
+ };
+ }, [profile]);
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty },
+ } = useForm({
+ resolver: zodResolver(schema),
+ values: defaultValues,
+ });
+
+ const mutation = useMutation({
+ mutationFn: (data: FormData) =>
+ api.companies.updateProfile.call({
+ contactPersonName: data.contactPersonName,
+ contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
+ },
+ });
+
+ const onSubmit = (data: FormData) => mutation.mutate(data);
+
+ return (
+
+
+
+
+ Contact Person
+
+ Manage the primary contact person for your account
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
new file mode 100644
index 000000000..298f7ad7c
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
@@ -0,0 +1,107 @@
+import { useState } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import {
+ CheckCircle2,
+ FileCheck,
+ Loader2,
+ UploadCloud,
+ XCircle,
+} from "lucide-react";
+import { api } from "@/services/api";
+import { companiesService } from "@/services/companies.service";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ Button,
+ SmartFileInput,
+} from "@edr/ui-common";
+import type { ProfileResponse } from "@/types/profile";
+
+export default function TabDocuments({ profile }: { profile: ProfileResponse }) {
+ const queryClient = useQueryClient();
+ const [documentFiles, setDocumentFiles] = useState>({});
+
+ const docSettingQuery = useQuery(
+ api.fileUploadSettings.getByCode.queryOptions({
+ input: { code: "customer_documents" },
+ }),
+ );
+
+ const docUploadMutation = useMutation({
+ mutationFn: (files: Record) =>
+ companiesService.uploadDocuments(profile.companyId, files),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
+ },
+ });
+
+ return (
+
+
+
+
+ Documents
+
+
+ Upload and manage required business documents
+
+
+
+ {docSettingQuery.isLoading ? (
+
+
+
+ ) : !docSettingQuery.data ? (
+
+ No document requirements configured for your account.
+
+ ) : (
+
+ )}
+
+ {docSettingQuery.data && (
+
+
+ {docUploadMutation.isSuccess && (
+
+
+ Documents uploaded successfully
+
+ )}
+ {docUploadMutation.isError && (
+
+
+ Upload failed
+
+ )}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx
new file mode 100644
index 000000000..7c7f0d4d3
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx
@@ -0,0 +1,161 @@
+import { useMemo } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Briefcase, CheckCircle2, Loader2, Save, XCircle } from "lucide-react";
+import { api } from "@/services/api";
+import PhoneInput from "@/components/auth/PhoneInput";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ Button,
+ Input,
+ Field,
+ FieldLabel,
+ FieldError,
+ FieldGroup,
+} from "@edr/ui-common";
+import type { ProfileResponse } from "@/types/profile";
+
+const schema = z.object({
+ generalManagerName: z.string().min(1, "GM name is required"),
+ generalManagerEmail: z.string().email("Invalid GM email"),
+ generalManagerPhone: z.string().min(1, "GM phone is required"),
+ generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
+});
+
+type FormData = z.infer;
+
+function splitPhone(fullPhone?: string | null) {
+ if (!fullPhone) return { code: "+251", number: "" };
+ const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
+ if (match) return { code: match[1], number: match[2] };
+ return { code: "+251", number: fullPhone };
+}
+
+export default function TabGeneralManager({ profile }: { profile: ProfileResponse }) {
+ const queryClient = useQueryClient();
+
+ const defaultValues = useMemo((): FormData => {
+ const phone = splitPhone(profile.generalManagerPhone);
+ return {
+ generalManagerName: profile.generalManagerName ?? "",
+ generalManagerEmail: profile.generalManagerEmail ?? "",
+ generalManagerPhone: phone.number,
+ generalManagerPhoneCountryCode: phone.code,
+ };
+ }, [profile]);
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty },
+ } = useForm({
+ resolver: zodResolver(schema),
+ values: defaultValues,
+ });
+
+ const mutation = useMutation({
+ mutationFn: (data: FormData) =>
+ api.companies.updateProfile.call({
+ generalManagerName: data.generalManagerName,
+ generalManagerEmail: data.generalManagerEmail,
+ generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
+ },
+ });
+
+ const onSubmit = (data: FormData) => mutation.mutate(data);
+
+ return (
+
+
+
+
+ General Manager
+
+ Manage the general manager information
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabPowerOfAttorney.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabPowerOfAttorney.tsx
new file mode 100644
index 000000000..e5d977e2d
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabPowerOfAttorney.tsx
@@ -0,0 +1,208 @@
+import { useMemo } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { CheckCircle2, Loader2, Save, UserCheck, XCircle } from "lucide-react";
+import { api } from "@/services/api";
+import PhoneInput from "@/components/auth/PhoneInput";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ Button,
+ Input,
+ Field,
+ FieldLabel,
+ FieldError,
+ FieldGroup,
+} from "@edr/ui-common";
+import type { ProfileResponse } from "@/types/profile";
+
+const schema = z.object({
+ poaName: z.string().optional(),
+ poaEmail: z.string().optional(),
+ poaPhone: z.string().optional(),
+ poaPhoneCountryCode: z.string().optional(),
+ poaLocation: z.string().optional(),
+ poaAddress: z.string().optional(),
+});
+
+type FormData = z.infer;
+
+function splitPhone(fullPhone?: string | null) {
+ if (!fullPhone) return { code: "+251", number: "" };
+ const match = fullPhone.match(/^(\+\d{1,3})(.*)$/);
+ if (match) return { code: match[1], number: match[2] };
+ return { code: "+251", number: fullPhone };
+}
+
+export default function TabPowerOfAttorney({
+ profile,
+}: {
+ profile: ProfileResponse;
+}) {
+ const queryClient = useQueryClient();
+
+ const defaultValues = useMemo((): FormData => {
+ const phone = splitPhone(profile.poaPhone);
+ return {
+ poaName: profile.poaName ?? "",
+ poaEmail: profile.poaEmail ?? "",
+ poaPhone: phone.number,
+ poaPhoneCountryCode: profile.poaPhone ? phone.code : "",
+ poaLocation: profile.poaLocation ?? "",
+ poaAddress: profile.poaAddress ?? "",
+ };
+ }, [profile]);
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isDirty },
+ } = useForm({
+ resolver: zodResolver(schema),
+
+ values: defaultValues,
+ });
+
+ const mutation = useMutation({
+ mutationFn: (data: FormData) =>
+ api.companies.updateProfile.call({
+ poaName: data.poaName || undefined,
+ poaPhone:
+ data.poaPhone && data.poaPhoneCountryCode
+ ? `${data.poaPhoneCountryCode}${data.poaPhone}`
+ : undefined,
+ poaEmail: data.poaEmail || undefined,
+ poaLocation: data.poaLocation || undefined,
+ poaAddress: data.poaAddress || undefined,
+ }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: api.companies.getProfile.queryKey(),
+ });
+ },
+ });
+
+ const onSubmit = (data: FormData) => mutation.mutate(data);
+
+ return (
+
+
+
+
+ Power of Attorney
+
+
+ Power of Attorney details are optional. Fill them in if you have an
+ authorized representative, or leave blank.
+
+
+
+
+ );
+}
From efdcb9a7c8f325c71dfddb313bced320e5cdeee8 Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Sat, 6 Jun 2026 11:29:14 +0300
Subject: [PATCH 5/9] fix: signup am
---
.../edr-freight-web/portal/src/pages/accounts/SignupPage.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
index 8c305e0ef..f8c8bc3c9 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
@@ -110,7 +110,10 @@ export default function SignupPage() {
username: data.email,
phoneNumber: `${data.countryCode}${normalizedPhone}`,
userType: data.userType,
- name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
+ name: {
+ en: `${data.firstName.en} ${data.lastName.en}`,
+ am: `${data.firstName.en} ${data.lastName.en}`,
+ },
password: data.password,
confirmPassword: data.confirmPassword,
};
From c76c6dbf90a8a210e29983e9934d2bcd20b8e69f Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Sat, 6 Jun 2026 11:43:59 +0300
Subject: [PATCH 6/9] feat(bookings): Revamp Edit Booking page to a single-form
experience with enhanced details
---
.../src/pages/bookings/EditBookingPage.tsx | 908 ++++++++++++++++--
1 file changed, 817 insertions(+), 91 deletions(-)
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx
index 68fd9c70f..b72d18ee1 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx
@@ -1,40 +1,51 @@
-import { useMemo, useState } from "react";
+import { useMemo } from "react";
+import { useFieldArray, Controller, useForm } from "react-hook-form";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
import { useNavigate, useParams } from "react-router-dom";
import {
AlertCircle,
Check,
- ChevronLeft,
- ChevronRight,
LoaderCircle,
Loader2,
+ Package,
+ Weight,
+ Plus,
+ Trash2,
+ MapPin,
+ Flame,
+ Snowflake,
+ Truck,
+ FileText,
} from "lucide-react";
-import { Button } from "@edr/ui-common";
+import {
+ Button,
+ Field,
+ FieldLabel,
+ FieldError,
+ Input,
+ Badge,
+ Switch,
+ Textarea,
+ Separator,
+ Skeleton,
+} from "@edr/ui-common";
+import type { Freight } from "@edr/types";
import { api } from "@/services/api";
-import type {
- CreateBookingPayload,
-} from "@/services/bookings.service";
+import type { CreateBookingPayload } from "@/services/bookings.service";
import {
BookingFormInputValues,
- STEPS,
bookingFormSchema,
getRouteDirection,
initialBookingFormValues,
- stepFields,
type BookingFormValues,
type RouteDirection,
} from "./new-booking-form/schema";
-import { StepIndicator } from "./new-booking-form/StepIndicator";
import {
- Step1ContractType,
- Step2ServiceType,
- Step4Route,
- Step5CargoDetails,
- Step8Review,
-} from "./new-booking-form/steps";
-import type { Freight } from "@edr/types";
+ SelectField,
+ SelectItem,
+ AlertBox,
+} from "./new-booking-form/shared";
function yardNameFromBooking(yard: { label?: string; code?: string; name?: string } | undefined | null): string {
return yard?.label ?? yard?.name ?? yard?.code ?? "";
@@ -103,7 +114,6 @@ export default function EditBookingPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
- const [step, setStep] = useState(1);
const bookingQuery = useQuery(
api.bookings.get.queryOptions({
@@ -112,7 +122,7 @@ export default function EditBookingPage() {
}),
);
- const { data: referenceData, isLoading: refDataLoading } = useQuery(
+ const { data: referenceData } = useQuery(
api.bookings.referenceData.queryOptions({
enabled: !!bookingQuery.data,
}),
@@ -144,17 +154,72 @@ export default function EditBookingPage() {
const originYard = form.watch("originYard");
const destinationYard = form.watch("destinationYard");
+ const serviceType = form.watch("serviceType");
+ const firstMileEnabled = form.watch("firstMile.enabled");
+ const lastMileEnabled = form.watch("lastMile.enabled");
+ const cargoType = form.watch("cargoType");
+ const freightType = form.watch("freightType");
+ const containers = form.watch("containers");
const direction: RouteDirection = useMemo(
() => getRouteDirection(originYard, destinationYard),
[originYard, destinationYard],
);
- async function handleContinue() {
- const valid = await form.trigger(stepFields[step], { shouldFocus: true });
- if (!valid) return;
- setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
- }
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: "containers",
+ });
+
+ const yardOptions = useMemo(() => {
+ if (!referenceData?.yard) return [];
+ return referenceData.yard.map((y) => ({
+ value: y.name,
+ label: y.name,
+ country: y.country,
+ }));
+ }, [referenceData]);
+
+ const shippingLineOptions = useMemo(() => {
+ if (!referenceData?.shipping_line) return [];
+ return referenceData.shipping_line.map((sl) => ({
+ value: sl.name,
+ label: sl.name,
+ }));
+ }, [referenceData]);
+
+ const freightTypeGroups = useMemo(() => {
+ if (!referenceData?.cargo_type) return [];
+ return referenceData.cargo_type.filter(
+ (g) => g.code !== "CONTAINER",
+ );
+ }, [referenceData]);
+
+ const commodityOptions = useMemo(() => {
+ if (!referenceData?.cargo_type || !freightType) return [];
+ const group = referenceData.cargo_type.find(
+ (g) => g.code.toLowerCase() === freightType,
+ );
+ return group?.children?.map((c) => c.name) ?? [];
+ }, [referenceData, freightType]);
+
+ const containerTypeOptions = useMemo(() => {
+ if (!referenceData?.containers) return [];
+ return referenceData.containers.flatMap((group) =>
+ group.types.map((t) => t.name),
+ );
+ }, [referenceData]);
+
+ const directionStyle: Record = {
+ export: "bg-sky-50 text-sky-800 border-sky-200",
+ import: "bg-amber-50 text-amber-800 border-amber-200",
+ domestic: "bg-muted text-muted-foreground border-border",
+ };
+ const directionLabel: Record = {
+ export: "Export workflow (inside country to outside country)",
+ import: "Import workflow (outside country to inside country)",
+ domestic: "Domestic corridor",
+ };
const handleSubmit = form.handleSubmit((data) => {
const yards = referenceData?.yard ?? [];
@@ -290,83 +355,744 @@ export default function EditBookingPage() {
return (