feat(api): Introduce dedicated company management API and service

This commit is contained in:
ghost2023
2026-06-03 16:19:04 +03:00
parent e0473b2ffd
commit f9357ed1b2
9 changed files with 324 additions and 200 deletions

View File

@@ -79,6 +79,11 @@ export const URL_CONSTANTS = {
BY_USER_ID: (id: string) => `/api/customers/user/${id}`, BY_USER_ID: (id: string) => `/api/customers/user/${id}`,
}, },
COMPANIES_API: {
GET_INFO: "/api/companies/getInfo",
CREATE: "/api/companies/create",
},
BOOKINGS: { BOOKINGS: {
BASE: "/bookings", BASE: "/bookings",
BY_ID: (id: string | number) => `/bookings/${id}`, BY_ID: (id: string | number) => `/bookings/${id}`,

View File

@@ -35,9 +35,8 @@ const useAuth = () => {
}), }),
); );
const customerQuery = useQuery( const companyQuery = useQuery(
api.customers.getByUserId.queryOptions({ api.companies.getInfo.queryOptions({
input: { id: authQuery.data?.id ?? "" },
enabled: !!authQuery.data?.id, enabled: !!authQuery.data?.id,
retry: false, retry: false,
staleTime: 10 * 60 * 1000, staleTime: 10 * 60 * 1000,
@@ -48,12 +47,12 @@ const useAuth = () => {
useEffect(() => { useEffect(() => {
console.log({ console.log({
user: authQuery.data, user: authQuery.data,
customer: customerQuery.data, company: companyQuery.data,
isCustomer: !!customerQuery.data, isCompany: !!companyQuery.data,
isUserPending: authQuery.isPending, isUserPending: authQuery.isPending,
isCustomerPending: customerQuery.isPending, isCompanyPending: companyQuery.isPending,
}); });
}, [authQuery, customerQuery]); }, [authQuery, companyQuery]);
const hasToken = !!getCookie("auth-token"); const hasToken = !!getCookie("auth-token");
const isPending = authQuery.isPending && hasToken; const isPending = authQuery.isPending && hasToken;
@@ -180,7 +179,8 @@ const useAuth = () => {
return { return {
isPending, isPending,
user: authQuery.data ?? null, user: authQuery.data ?? null,
customer: customerQuery.data ?? null, company: companyQuery.data ?? null,
customer: companyQuery.data ?? null,
login, login,
signup, signup,
setPassword, setPassword,
@@ -189,7 +189,8 @@ const useAuth = () => {
generateVerificationCode, generateVerificationCode,
logout, logout,
authQuery, authQuery,
customerQuery, companyQuery,
customerQuery: companyQuery,
}; };
}; };

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -11,10 +12,12 @@ import {
CheckCircle2, CheckCircle2,
Loader2, Loader2,
ChevronLeft, ChevronLeft,
UploadCloud,
} from "lucide-react"; } from "lucide-react";
import type { OnboardingUserType } from "./types"; import type { OnboardingUserType } from "./types";
import type { AuthUser } from "@/types/auth"; import type { AuthUser } from "@/types/auth";
import type { CreateCustomerDto } from "@/types/customers"; import type { CreateCompanyPayload } from "@/services/companies.service";
import type { FileUploadSetting } from "@/types/fileUploadSettings";
import PhoneInput from "@/components/auth/PhoneInput"; import PhoneInput from "@/components/auth/PhoneInput";
import { import {
Button, Button,
@@ -23,9 +26,11 @@ import {
FieldLabel, FieldLabel,
FieldError, FieldError,
FieldGroup, FieldGroup,
SmartFileInput,
} from "@edr/ui-common"; } from "@edr/ui-common";
import { api } from "@/services/api";
type CompanyStep = "company" | "personnel" | "poa"; type CompanyStep = "company" | "personnel" | "poa" | "documents";
const onboardingSchema = z.object({ const onboardingSchema = z.object({
companyName: z.string().min(1, "Company name is required"), companyName: z.string().min(1, "Company name is required"),
@@ -79,55 +84,34 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
"generalManagerPhoneCountryCode", "generalManagerPhoneCountryCode",
], ],
poa: [], poa: [],
documents: [],
}; };
const POA_FIELDS: (keyof FormData)[] = [ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
"poaName",
"poaPhone",
"poaPhoneCountryCode",
"poaAddress",
"poaEmail",
"poaLocation",
];
const POA_LABELS: Record<string, string> = {
poaName: "PoA name",
poaPhone: "PoA phone",
poaPhoneCountryCode: "PoA country code",
poaAddress: "PoA address",
poaEmail: "PoA email",
poaLocation: "PoA location",
};
function buildPayload(data: FormData, user: AuthUser): CreateCustomerDto {
const nameParts = (user.name?.en ?? "").split(" ");
return { return {
userId: user.id,
firstName: nameParts[0] || "",
lastName: nameParts.slice(-1)[0] || "",
email: user.email,
phone: user.phoneNumber,
companyName: data.companyName, companyName: data.companyName,
companyEmail: data.companyEmail, companyEmail: data.companyEmail,
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`, companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
companyLocation: data.companyLocation, companyLocation: data.companyLocation,
companyAddress: data.companyAddress, companyAddress: data.companyAddress,
contactPersonName: data.contactPersonName, tin: data.tinNumber,
contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
tinNumber: data.tinNumber,
vatNumber: data.vatNumber, vatNumber: data.vatNumber,
fanNumber: data.fanNumber, fanNumber: data.fanNumber,
generalManagerName: data.generalManagerName, attributes: {
generalManagerEmail: data.generalManagerEmail, contactPersonName: data.contactPersonName,
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`, contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
poaName: data.poaName || undefined, generalManagerName: data.generalManagerName,
poaPhone: generalManagerEmail: data.generalManagerEmail,
data.poaPhone && data.poaPhoneCountryCode generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
? `${data.poaPhoneCountryCode}${data.poaPhone}` poaName: data.poaName || undefined,
: undefined, poaPhone:
poaAddress: data.poaAddress || undefined, data.poaPhone && data.poaPhoneCountryCode
poaEmail: data.poaEmail || undefined, ? `${data.poaPhoneCountryCode}${data.poaPhone}`
poaLocation: data.poaLocation || undefined, : undefined,
poaAddress: data.poaAddress || undefined,
poaEmail: data.poaEmail || undefined,
poaLocation: data.poaLocation || undefined,
},
}; };
} }
@@ -140,21 +124,26 @@ export default function CompanyProfileForm({
}: { }: {
userType: OnboardingUserType; userType: OnboardingUserType;
user: AuthUser; user: AuthUser;
onSubmit: (data: CreateCustomerDto) => void; onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean; isPending: boolean;
onBack: () => void; onBack: () => void;
}) { }) {
const requirePoA = userType === "freight-forwarder-et";
const [step, setStep] = useState<CompanyStep>("company"); const [step, setStep] = useState<CompanyStep>("company");
const [documentFiles, setDocumentFiles] = useState<
Record<string, File | File[] | null>
>({});
const { data: uploadSettings = [], isLoading: loadingDocuments } = useQuery(
api.fileUploadSettings.getByEntity.queryOptions({
input: { entity: "customer" },
refetchOnMount: false,
}),
);
const { const {
register, register,
handleSubmit, handleSubmit,
trigger, trigger,
setError,
clearErrors,
getValues,
formState: { errors }, formState: { errors },
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(onboardingSchema), resolver: zodResolver(onboardingSchema),
@@ -184,26 +173,18 @@ export default function CompanyProfileForm({
}, },
}); });
const hasDocuments = uploadSettings.length > 0;
const nextStep = async () => { const nextStep = async () => {
if (step === "poa") { if (step === "poa") {
if (requirePoA) { if (hasDocuments) {
clearErrors(POA_FIELDS); setStep("documents");
const values = getValues(); } else {
let hasError = false; handleSubmit((data) => onSubmit(buildPayload(data, user)))();
for (const field of POA_FIELDS) {
const val = values[field];
if (!val || val.toString().trim().length === 0) {
setError(field, {
message: `${
POA_LABELS[field].charAt(0).toUpperCase() +
POA_LABELS[field].slice(1)
} is required for Freight Forwarders`,
});
hasError = true;
}
}
if (hasError) return;
} }
return;
}
if (step === "documents") {
handleSubmit((data) => onSubmit(buildPayload(data, user)))(); handleSubmit((data) => onSubmit(buildPayload(data, user)))();
return; return;
} }
@@ -220,6 +201,8 @@ export default function CompanyProfileForm({
setStep("company"); setStep("company");
} else if (step === "poa") { } else if (step === "poa") {
setStep("personnel"); setStep("personnel");
} else {
setStep("poa");
} }
}; };
@@ -250,14 +233,21 @@ export default function CompanyProfileForm({
<StepIcon <StepIcon
icon={<FileText className="size-5" />} icon={<FileText className="size-5" />}
active={step === "poa"} active={step === "poa"}
completed={false} completed={hasDocuments ? step === "documents" : step === "personnel"}
/> />
{hasDocuments && (
<StepIcon
icon={<UploadCloud className="size-5" />}
active={step === "documents"}
completed={false}
/>
)}
</div> </div>
<p className="text-center text-sm text-muted-foreground mt-3"> <p className="text-center text-sm text-muted-foreground mt-3">
{step === "company" && "Step 1 of 3 — Company Information"} {step === "company" && `Step 1 of ${hasDocuments ? 4 : 3} — Company Information`}
{step === "personnel" && "Step 2 of 3 — Personnel Details"} {step === "personnel" && `Step 2 of ${hasDocuments ? 4 : 3} — Personnel Details`}
{step === "poa" && {step === "poa" && `Step 3 of ${hasDocuments ? 4 : 3} — Power of Attorney (Optional)`}
`Step 3 of 3Power of Attorney ${requirePoA ? "(Required)" : "(Optional)"}`} {step === "documents" && "Step 4 of 4Upload Documents (Optional)"}
</p> </p>
</div> </div>
@@ -449,16 +439,12 @@ export default function CompanyProfileForm({
{step === "poa" && ( {step === "poa" && (
<> <>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{requirePoA Power of Attorney details are optional. Fill them in if you have
? "Power of Attorney details are required for Freight Forwarder registration." them, or skip to continue.
: "Power of Attorney details are optional. Skip if not applicable."}
</p> </p>
<Field data-invalid={Boolean(errors.poaName)}> <Field data-invalid={Boolean(errors.poaName)}>
<FieldLabel> <FieldLabel>PoA Name</FieldLabel>
PoA Name
{requirePoA && <span className="text-destructive ml-1">*</span>}
</FieldLabel>
<Input <Input
placeholder="Authorized Representative Name" placeholder="Authorized Representative Name"
aria-invalid={Boolean(errors.poaName)} aria-invalid={Boolean(errors.poaName)}
@@ -469,10 +455,7 @@ export default function CompanyProfileForm({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.poaEmail)}> <Field data-invalid={Boolean(errors.poaEmail)}>
<FieldLabel> <FieldLabel>PoA Email</FieldLabel>
PoA Email
{requirePoA && <span className="text-destructive ml-1">*</span>}
</FieldLabel>
<Input <Input
type="email" type="email"
placeholder="poa@company.com" placeholder="poa@company.com"
@@ -485,7 +468,7 @@ export default function CompanyProfileForm({
<PhoneInput <PhoneInput
countryCode={{ ...register("poaPhoneCountryCode") }} countryCode={{ ...register("poaPhoneCountryCode") }}
phone={{ ...register("poaPhone"), placeholder: "912345678" }} phone={{ ...register("poaPhone"), placeholder: "912345678" }}
label={`PoA Phone${requirePoA ? " *" : ""}`} label="PoA Phone"
countryCodeError={errors.poaPhoneCountryCode} countryCodeError={errors.poaPhoneCountryCode}
phoneError={errors.poaPhone} phoneError={errors.poaPhone}
/> />
@@ -493,10 +476,7 @@ export default function CompanyProfileForm({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.poaLocation)}> <Field data-invalid={Boolean(errors.poaLocation)}>
<FieldLabel> <FieldLabel>PoA Location</FieldLabel>
PoA Location
{requirePoA && <span className="text-destructive ml-1">*</span>}
</FieldLabel>
<Input <Input
placeholder="City, Country" placeholder="City, Country"
aria-invalid={Boolean(errors.poaLocation)} aria-invalid={Boolean(errors.poaLocation)}
@@ -506,10 +486,7 @@ export default function CompanyProfileForm({
</Field> </Field>
<Field data-invalid={Boolean(errors.poaAddress)}> <Field data-invalid={Boolean(errors.poaAddress)}>
<FieldLabel> <FieldLabel>PoA Address</FieldLabel>
PoA Address
{requirePoA && <span className="text-destructive ml-1">*</span>}
</FieldLabel>
<Input <Input
placeholder="Full Address" placeholder="Full Address"
aria-invalid={Boolean(errors.poaAddress)} aria-invalid={Boolean(errors.poaAddress)}
@@ -520,6 +497,36 @@ export default function CompanyProfileForm({
</div> </div>
</> </>
)} )}
{step === "documents" && (
<>
<p className="text-sm text-muted-foreground">
Upload required documents for your registration. You can skip
this step and upload later from your account settings.
</p>
{loadingDocuments ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : uploadSettings.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
No document requirements found for your account type.
</p>
) : (
<div className="flex flex-col gap-6">
{uploadSettings.map((setting) => (
<SmartFileInput
key={setting.id}
file={setting}
value={documentFiles}
onChange={setDocumentFiles}
/>
))}
</div>
)}
</>
)}
</FieldGroup> </FieldGroup>
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2">
@@ -534,7 +541,7 @@ export default function CompanyProfileForm({
<Loader2 className="animate-spin" /> <Loader2 className="animate-spin" />
Submitting... Submitting...
</> </>
) : step === "poa" ? ( ) : step === "documents" ? (
"Complete Registration" "Complete Registration"
) : ( ) : (
<> <>
@@ -560,13 +567,12 @@ function StepIcon({
}) { }) {
return ( return (
<div <div
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${ className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${completed
completed
? "bg-primary border-primary text-primary-foreground" ? "bg-primary border-primary text-primary-foreground"
: active : active
? "bg-background border-primary text-primary shadow-md" ? "bg-background border-primary text-primary shadow-md"
: "bg-background border-border text-muted-foreground" : "bg-background border-border text-muted-foreground"
}`} }`}
> >
{completed ? <CheckCircle2 className="size-5" /> : icon} {completed ? <CheckCircle2 className="size-5" /> : icon}
</div> </div>

View File

@@ -12,7 +12,7 @@ import {
ChevronLeft, ChevronLeft,
} from "lucide-react"; } from "lucide-react";
import type { AuthUser } from "@/types/auth"; import type { AuthUser } from "@/types/auth";
import type { CreateCustomerDto } from "@/types/customers"; import type { CreateCompanyPayload } from "@/services/companies.service";
import PhoneInput from "@/components/auth/PhoneInput"; import PhoneInput from "@/components/auth/PhoneInput";
import { import {
Button, Button,
@@ -45,27 +45,21 @@ const stepLabels: Record<DjiboutiStep, string> = {
representative: "Step 2 of 2 — Representative Details", representative: "Step 2 of 2 — Representative Details",
}; };
function buildPayload(data: FormData, user: AuthUser): CreateCustomerDto { function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
const nameParts = (user.name?.en ?? "").split(" ");
return { return {
userId: user.id,
firstName: nameParts[0] || "",
lastName: nameParts.slice(-1)[0] || "",
email: user.email,
phone: user.phoneNumber,
companyName: data.companyName, companyName: data.companyName,
companyEmail: data.companyEmail, companyEmail: data.companyEmail,
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`, companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
companyLocation: data.companyLocation, companyLocation: data.companyLocation,
companyAddress: data.companyAddress, companyAddress: data.companyAddress,
contactPersonName: data.repName, tin: "",
contactPersonPhone: `${data.repPhoneCountryCode}${data.repPhone}`,
tinNumber: "",
vatNumber: "", vatNumber: "",
fanNumber: "", fanNumber: "",
generalManagerName: "", attributes: {
generalManagerEmail: "", repName: data.repName,
generalManagerPhone: "", repEmail: data.repEmail,
repPhone: `${data.repPhoneCountryCode}${data.repPhone}`,
},
}; };
} }
@@ -76,7 +70,7 @@ export default function DjiboutiAgentForm({
onBack, onBack,
}: { }: {
user: AuthUser; user: AuthUser;
onSubmit: (data: CreateCustomerDto) => void; onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean; isPending: boolean;
onBack: () => void; onBack: () => void;
}) { }) {

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
ArrowDownToLine, ArrowDownToLine,
ArrowUpFromLine, ArrowUpFromLine,
@@ -10,7 +10,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import { api } from "@/services/api"; import { api } from "@/services/api";
import type { CreateCustomerDto } from "@/types/customers"; import type { CreateCompanyPayload } from "@/services/companies.service";
import AuthLayout from "@/components/auth/AuthLayout"; import AuthLayout from "@/components/auth/AuthLayout";
import CompanyProfileForm from "./CompanyProfileForm"; import CompanyProfileForm from "./CompanyProfileForm";
import DjiboutiAgentForm from "./DjiboutiAgentForm"; import DjiboutiAgentForm from "./DjiboutiAgentForm";
@@ -23,40 +23,37 @@ const USER_TYPE_CARDS: {
description: string; description: string;
icon: React.ReactNode; icon: React.ReactNode;
}[] = [ }[] = [
{ {
id: "importer", id: "importer",
label: "Importer", label: "Importer",
description: "Import goods into Ethiopia via the railway corridor.", description: "Import goods into Ethiopia via the railway corridor.",
icon: <ArrowDownToLine className="size-6" />, icon: <ArrowDownToLine className="size-6" />,
}, },
{ {
id: "exporter", id: "exporter",
label: "Exporter", label: "Exporter",
description: "Export goods from Ethiopia via rail.", description: "Export goods from Ethiopia via rail.",
icon: <ArrowUpFromLine className="size-6" />, icon: <ArrowUpFromLine className="size-6" />,
}, },
{ {
id: "freight-forwarder-et", id: "freight-forwarder-et",
label: "Freight Forwarder (Ethiopia)", label: "Freight Forwarder (Ethiopia)",
description: description: "Ethiopian freight forwarding company handling client cargo.",
"Ethiopian freight forwarding company handling client cargo.", icon: <Building2 className="size-6" />,
icon: <Building2 className="size-6" />, },
}, {
{ id: "freight-forwarder-dj",
id: "freight-forwarder-dj", label: "FF Agent (Djibouti)",
label: "FF Agent (Djibouti)", description: "Djibouti-based agent coordinating cross-border logistics.",
description: icon: <Ship className="size-6" />,
"Djibouti-based agent coordinating cross-border logistics.", },
icon: <Ship className="size-6" />, {
}, id: "transporter",
{ label: "Transporter",
id: "transporter", description: "Trucking company providing first/last-mile services.",
label: "Transporter", icon: <Truck className="size-6" />,
description: },
"Trucking company providing first/last-mile services.", ];
icon: <Truck className="size-6" />,
},
];
const USER_TYPE_LEFT_MAP: Record< const USER_TYPE_LEFT_MAP: Record<
OnboardingUserType, OnboardingUserType,
@@ -121,21 +118,39 @@ export default function OnboardingPage() {
const { user } = useAuth(); const { user } = useAuth();
const [userType, setUserType] = useState<OnboardingUserType | null>(null); const [userType, setUserType] = useState<OnboardingUserType | null>(null);
const createCustomerMutation = useMutation({ useQuery(
mutationFn: (payload: CreateCustomerDto) => api.fileUploadSettings.getByEntity.queryOptions({
api.customers.create.call(payload), input: { entity: "customer" },
refetchOnMount: false,
}),
);
const COMPANY_TYPE_MAP: Record<OnboardingUserType, string> = {
importer: "customer",
exporter: "customer",
"freight-forwarder-et": "forwarder",
"freight-forwarder-dj": "forwarder",
transporter: "transporter",
};
const createCompanyMutation = useMutation({
mutationFn: (payload: CreateCompanyPayload) =>
api.companies.create.call(payload),
onSuccess: () => { onSuccess: () => {
if (user) queryClient.invalidateQueries({
queryClient.invalidateQueries({ queryKey: api.companies.getInfo.queryKey(),
queryKey: api.customers.getByUserId.queryKey({ id: user.id }), });
});
}, },
}); });
if (!user) return null; if (!user) return null;
const handleSubmit = (payload: CreateCustomerDto) => { const handleSubmit = (payload: CreateCompanyPayload) => {
createCustomerMutation.mutate(payload); const enriched: CreateCompanyPayload = {
...payload,
companyType: COMPANY_TYPE_MAP[userType!],
};
createCompanyMutation.mutate(enriched);
}; };
const handleSelectType = (type: OnboardingUserType) => { const handleSelectType = (type: OnboardingUserType) => {
@@ -172,9 +187,7 @@ export default function OnboardingPage() {
{card.icon} {card.icon}
</div> </div>
<div> <div>
<p className="font-semibold text-foreground"> <p className="font-semibold text-foreground">{card.label}</p>
{card.label}
</p>
<p className="mt-0.5 text-xs text-muted-foreground leading-relaxed"> <p className="mt-0.5 text-xs text-muted-foreground leading-relaxed">
{card.description} {card.description}
</p> </p>
@@ -197,21 +210,21 @@ export default function OnboardingPage() {
features: features:
userType === "transporter" userType === "transporter"
? [ ? [
"Vehicle & fleet registration", "Vehicle & fleet registration",
"TIN & FAN verification", "TIN & FAN verification",
"First-mile / Last-mile eligibility", "First-mile / Last-mile eligibility",
] ]
: userType === "freight-forwarder-dj" : userType === "freight-forwarder-dj"
? [ ? [
"Company details", "Company details",
"Representative information", "Representative information",
"Cross-border operations", "Cross-border operations",
] ]
: [ : [
"Company registration details", "Company registration details",
"Contact and management personnel", "Contact and management personnel",
"Power of Attorney (optional)", "Power of Attorney (optional)",
], ],
stats: { stats: {
label: "Active Customers", label: "Active Customers",
value: "500+", value: "500+",
@@ -226,14 +239,14 @@ export default function OnboardingPage() {
<TransporterForm <TransporterForm
user={user} user={user}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isPending={createCustomerMutation.isPending} isPending={createCompanyMutation.isPending}
onBack={handleBack} onBack={handleBack}
/> />
) : userType === "freight-forwarder-dj" ? ( ) : userType === "freight-forwarder-dj" ? (
<DjiboutiAgentForm <DjiboutiAgentForm
user={user} user={user}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isPending={createCustomerMutation.isPending} isPending={createCompanyMutation.isPending}
onBack={handleBack} onBack={handleBack}
/> />
) : ( ) : (
@@ -241,7 +254,7 @@ export default function OnboardingPage() {
userType={userType} userType={userType}
user={user} user={user}
onSubmit={handleSubmit} onSubmit={handleSubmit}
isPending={createCustomerMutation.isPending} isPending={createCompanyMutation.isPending}
onBack={handleBack} onBack={handleBack}
/> />
)} )}

View File

@@ -8,7 +8,7 @@ import {
Info, Info,
} from "lucide-react"; } from "lucide-react";
import type { AuthUser } from "@/types/auth"; import type { AuthUser } from "@/types/auth";
import type { CreateCustomerDto } from "@/types/customers"; import type { CreateCompanyPayload } from "@/services/companies.service";
import { import {
Button, Button,
Input, Input,
@@ -56,34 +56,23 @@ const transporterSchema = z
type FormData = z.infer<typeof transporterSchema>; type FormData = z.infer<typeof transporterSchema>;
function buildPayload(data: FormData, user: AuthUser): CreateCustomerDto { function buildPayload(data: FormData, user: AuthUser): CreateCompanyPayload {
const nameParts = (user.name?.en ?? "").split(" ");
return { return {
userId: user.id, companyName: user.name?.en ?? "",
firstName: nameParts[0] || "", companyEmail: user.email,
lastName: nameParts.slice(-1)[0] || "", companyPhone: user.phoneNumber,
email: user.email,
phone: user.phoneNumber,
companyName: "",
companyEmail: "",
companyPhone: "",
companyLocation: "", companyLocation: "",
companyAddress: "", companyAddress: "",
contactPersonName: "", tin: data.tinNumber,
contactPersonPhone: "",
tinNumber: data.tinNumber,
vatNumber: "", vatNumber: "",
fanNumber: data.fanNumber, fanNumber: data.fanNumber,
generalManagerName: "", attributes: {
generalManagerEmail: "",
generalManagerPhone: "",
notes: JSON.stringify({
truckType: data.truckType, truckType: data.truckType,
plateNumber: data.plateNumber, plateNumber: data.plateNumber,
plateNumber2: data.plateNumber2 || null, plateNumber2: data.plateNumber2 || null,
vehicleModel: data.vehicleModel, vehicleModel: data.vehicleModel,
yearOfManufacturing: data.yearOfManufacturing, yearOfManufacturing: data.yearOfManufacturing,
}), },
}; };
} }
@@ -94,7 +83,7 @@ export default function TransporterForm({
onBack, onBack,
}: { }: {
user: AuthUser; user: AuthUser;
onSubmit: (data: CreateCustomerDto) => void; onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean; isPending: boolean;
onBack: () => void; onBack: () => void;
}) { }) {

View File

@@ -15,6 +15,7 @@ import { fileUploadSettingsService } from "./fileUploadSettings.service";
import { dropdownSettingsService } from "./dropdownSettings.service"; import { dropdownSettingsService } from "./dropdownSettings.service";
import { authService } from "./auth.service"; import { authService } from "./auth.service";
import { customersService } from "./customers.service"; import { customersService } from "./customers.service";
import { companiesService } from "./companies.service";
import { import {
CreateDropdownOptionDto, CreateDropdownOptionDto,
CreateDropdownSettingDto, CreateDropdownSettingDto,
@@ -28,6 +29,10 @@ import {
Customer, Customer,
UpdateCustomerDto, UpdateCustomerDto,
} from "@/types/customers"; } from "@/types/customers";
import type {
CompanyInfoResponse,
CreateCompanyPayload,
} from "./companies.service";
import type { import type {
AuthUser, AuthUser,
GenerateVerificationCodePayload, GenerateVerificationCodePayload,
@@ -118,6 +123,20 @@ export const api = {
), ),
}, },
companies: {
getInfo: endpoint<void, CompanyInfoResponse | null>(
"companies",
"getInfo",
companiesService.getInfo,
),
create: endpoint<CreateCompanyPayload, CompanyInfoResponse>(
"companies",
"create",
companiesService.create,
),
},
bookings: { bookings: {
list: endpoint<void, PaginatedResponse<Freight.IBooking>>( list: endpoint<void, PaginatedResponse<Freight.IBooking>>(
"bookings", "bookings",
@@ -190,6 +209,12 @@ export const api = {
({ code }) => fileUploadSettingsService.getByCode(code), ({ code }) => fileUploadSettingsService.getByCode(code),
), ),
getByEntity: endpoint<{ entity: string }, FileUploadSetting[]>(
"file-upload-settings",
"getByEntity",
({ entity }) => fileUploadSettingsService.getByEntity(entity),
),
create: endpoint<CreateFileUploadSettingDto, FileUploadSetting>( create: endpoint<CreateFileUploadSettingDto, FileUploadSetting>(
"file-upload-settings", "file-upload-settings",
"create", "create",

View File

@@ -0,0 +1,83 @@
import { client } from "@/utils/api";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
import type { ApiResponse } from "@/types/apiResponse";
import { isAxiosError } from "axios";
export interface ExternalProfileResponse {
id: string;
userId: string;
companyId: string;
firstName: string;
lastName: string;
email: string;
phone: string | null;
nationalId: string | null;
jobTitle: string | null;
isPrimaryContact: boolean;
createdAt: string;
updatedAt: string;
}
export interface CompanyResponse {
id: string;
name: string;
type: string;
status: string;
tin: string;
vatNumber: string | null;
businessLicense: string | null;
fanNumber: string | null;
country: string;
address: string | null;
phone: string | null;
email: string | null;
website: string | null;
attributes: Record<string, any> | null;
createdAt: string;
updatedAt: string;
}
export interface CompanyInfoResponse {
profile: ExternalProfileResponse;
company: CompanyResponse;
}
export interface CreateCompanyPayload {
companyType?: string;
companyName: string;
companyEmail?: string;
companyPhone?: string;
companyLocation?: string;
companyAddress?: string;
tin?: string;
vatNumber?: string;
fanNumber?: string;
jobTitle?: string;
isPrimaryContact?: boolean;
attributes?: Record<string, any>;
}
export const companiesService = {
getInfo: async (): Promise<CompanyInfoResponse | null> => {
try {
const response = await client.get<ApiResponse<CompanyInfoResponse>>(
URL_CONSTANTS.COMPANIES_API.GET_INFO,
);
return unwrap(response.data);
} catch (e) {
if (isAxiosError(e) && e.response?.status === 404) {
return null;
}
throw e;
}
},
create: async (payload: CreateCompanyPayload): Promise<CompanyInfoResponse> => {
const response = await client.post<ApiResponse<CompanyInfoResponse>>(
URL_CONSTANTS.COMPANIES_API.CREATE,
payload,
);
return unwrap(response.data);
},
};

View File

@@ -43,6 +43,14 @@ export const fileUploadSettingsService = {
return unwrap(response.data); return unwrap(response.data);
}, },
// GET /file-upload-settings/by-entity/:entity
getByEntity: async (entity: string): Promise<FileUploadSetting[]> => {
const response = await client.get<ApiResponse<FileUploadSetting[]>>(
`${BASE}/by-entity/${encodeURIComponent(entity)}`,
);
return unwrap(response.data);
},
// GET /file-upload-settings/by-code/:code // GET /file-upload-settings/by-code/:code
getByCode: async (code: string): Promise<FileUploadSetting> => { getByCode: async (code: string): Promise<FileUploadSetting> => {
const response = await client.get<ApiResponse<FileUploadSetting>>( const response = await client.get<ApiResponse<FileUploadSetting>>(