diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts
index dc68f1785..fe1bc5598 100644
--- a/apps/edr-freight-api/src/modules/companies/companies.service.ts
+++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts
@@ -92,6 +92,34 @@ export class CompaniesService {
isPrimaryContact: dto.isPrimaryContact ?? true,
});
+ // Persist the operational role(s) chosen during onboarding. Types are
+ // already constrained to the company type on the client; any that don't
+ // match are skipped defensively rather than failing the whole signup.
+ if (dto.companyProfiles?.length) {
+ const allowedTypes = this.getProfileTypeForCompanyType(company.type);
+ for (const input of dto.companyProfiles) {
+ if (!allowedTypes.includes(input.type)) continue;
+ const existing = await this.companyProfilesRepo.findByType(
+ company.id,
+ input.type,
+ );
+ if (existing) continue;
+ const reference = await this.companyProfilesRepo.generateReference(
+ input.type,
+ );
+ await this.companyProfilesRepo.create({
+ companyId: company.id,
+ type: input.type,
+ reference,
+ businessLicense: input.businessLicense ?? null,
+ status: ProfileStatus.Active,
+ });
+ }
+ company.companyProfiles = await this.companyProfilesRepo.findByCompanyId(
+ company.id,
+ );
+ }
+
return { company, profile };
}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx b/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
index 8c448bfbd..000dcc25e 100644
--- a/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
+++ b/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
@@ -1,65 +1,18 @@
import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { Building2, CheckCircle2, Save, XCircle } from "lucide-react";
import {
- ArrowDownToLine,
- ArrowUpFromLine,
- Building2,
- Check,
- CheckCircle2,
- Save,
- XCircle,
-} from "lucide-react";
-import {
- Box,
Button,
Card,
Group,
SimpleGrid,
Text,
- ThemeIcon,
Title,
- UnstyledButton,
} from "@mantine/core";
import { api } from "@/services/api";
import type { ProfileResponse } from "@/types/profile";
-
-interface RoleOption {
- type: string;
- label: string;
- description: string;
- icon: React.ReactNode;
-}
-
-const CUSTOMER_ROLES: RoleOption[] = [
- {
- type: "importer",
- label: "Importer",
- description: "Import goods into Ethiopia via the railway corridor.",
- icon: ,
- },
- {
- type: "exporter",
- label: "Exporter",
- description: "Export goods from Ethiopia via rail.",
- icon: ,
- },
-];
-
-const FORWARDER_ROLES: RoleOption[] = [
- {
- type: "freight_forwarder",
- label: "Freight Forwarder",
- description: "Handle cargo on behalf of importers and exporters.",
- icon: ,
- },
-];
-
-// dj_freight_forwarder and transporter are intentionally not exposed yet.
-function rolesForCompanyType(companyType: string): RoleOption[] {
- if (companyType === "customer") return CUSTOMER_ROLES;
- if (companyType === "freight_forwarder") return FORWARDER_ROLES;
- return [];
-}
+import RoleCard from "./RoleCard";
+import { rolesForCompanyType } from "./companyRoles";
interface CompanyRolesCardProps {
profile: ProfileResponse;
@@ -131,49 +84,19 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
{options.map((opt) => {
const isActive = activeByType.has(opt.type);
- const isSelected = selected.has(opt.type);
- const highlighted = isActive || isSelected;
return (
- toggle(opt.type)}
- className={`group block rounded-lg border! p-5! text-left transition-all duration-200 ${
- highlighted
- ? "border-[var(--mantine-color-edr-green-5)]! bg-edr-soft!"
- : "border-edr-border! bg-edr-card! hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft"
- } ${isActive ? "cursor-default" : ""}`}
- >
-
-
- {opt.icon}
-
-
-
- {opt.label}
-
-
- {opt.description}
-
- {isActive && (
-
- Active · {activeByType.get(opt.type)}
-
- )}
-
- {highlighted && (
-
- )}
-
-
+ />
);
})}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx b/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx
new file mode 100644
index 000000000..faa9e7377
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx
@@ -0,0 +1,77 @@
+import { Card, Divider, Group, SimpleGrid, Text, Title } from "@mantine/core";
+import { Building2 } from "lucide-react";
+import RoleCard from "./RoleCard";
+import { CUSTOMER_ROLES, FREIGHT_FORWARDER } from "./companyRoles";
+
+interface OnboardingRoleSelectProps {
+ /** Currently selected profile types (e.g. ["importer"], ["importer","exporter"], ["freight_forwarder"]). */
+ value: string[];
+ onChange: (next: string[]) => void;
+}
+
+/**
+ * First (and only) thing shown in the Company Profile tab during onboarding.
+ * Importer / Exporter sit side by side and can both be picked; Freight
+ * Forwarder is a separate, mutually-exclusive choice below them. A valid
+ * selection reveals the company-profile fields.
+ */
+export default function OnboardingRoleSelect({
+ value,
+ onChange,
+}: OnboardingRoleSelectProps) {
+ const selected = new Set(value);
+ const isForwarder = selected.has(FREIGHT_FORWARDER.type);
+
+ // Toggling a customer role drops any forwarder selection (mutually exclusive).
+ const toggleCustomerRole = (type: string) => {
+ const next = new Set(value.filter((t) => t !== FREIGHT_FORWARDER.type));
+ if (next.has(type)) next.delete(type);
+ else next.add(type);
+ onChange([...next]);
+ };
+
+ const toggleForwarder = () => {
+ onChange(isForwarder ? [] : [FREIGHT_FORWARDER.type]);
+ };
+
+ return (
+
+
+
+ What does your company do?
+
+
+ Pick Importer, Exporter, or both — or register as a Freight Forwarder.
+
+
+
+ {CUSTOMER_ROLES.map((role) => (
+ toggleCustomerRole(role.type)}
+ />
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx b/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx
new file mode 100644
index 000000000..2e507ff83
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx
@@ -0,0 +1,75 @@
+import { Box, Group, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
+import { Check } from "lucide-react";
+
+export interface RoleCardProps {
+ label: string;
+ description: string;
+ icon: React.ReactNode;
+ /** Highlighted because the user just selected it (toggleable). */
+ selected?: boolean;
+ /** Highlighted and non-interactive because it is already persisted. */
+ locked?: boolean;
+ /** Small note under the description, e.g. "Active · IM-00001". */
+ lockedNote?: string;
+ onClick?: () => void;
+}
+
+/**
+ * The selectable company-role card used by both the onboarding role picker and
+ * the add-only roles card in settings. Visual mirror of the onboarding
+ * account-type cards.
+ */
+export default function RoleCard({
+ label,
+ description,
+ icon,
+ selected = false,
+ locked = false,
+ lockedNote,
+ onClick,
+}: RoleCardProps) {
+ const highlighted = selected || locked;
+
+ return (
+
+
+
+ {icon}
+
+
+
+ {label}
+
+
+ {description}
+
+ {lockedNote && (
+
+ {lockedNote}
+
+ )}
+
+ {highlighted && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
index 80e8d6ab7..f02740664 100644
--- a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -17,8 +17,12 @@ import {
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import type { ProfileResponse } from "@/types/profile";
-import type { CreateCompanyPayload } from "@/services/companies.service";
+import type {
+ CreateCompanyPayload,
+ CompanyProfileInput,
+} from "@/services/companies.service";
import CompanyRolesCard from "./CompanyRolesCard";
+import OnboardingRoleSelect from "./OnboardingRoleSelect";
export const COMPANY_PROFILE_SCHEMA = z.object({
companyName: z.string().min(1, "Company name is required"),
@@ -53,6 +57,7 @@ export default function TabCompanyProfile({
}: TabCompanyProfileProps) {
const queryClient = useQueryClient();
const isCreate = mode === "create";
+ const [selectedRoles, setSelectedRoles] = useState([]);
const defaultValues = useMemo((): CompanyProfileFormData => {
if (profile) {
@@ -92,8 +97,7 @@ export default function TabCompanyProfile({
const mutation = useMutation({
mutationFn: async (data: CompanyProfileFormData) => {
- const payload: CreateCompanyPayload = {
- companyType: "customer",
+ const base = {
companyName: data.companyName,
companyEmail: data.companyEmail,
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
@@ -104,10 +108,21 @@ export default function TabCompanyProfile({
};
if (isCreate) {
+ // Importer/Exporter -> a "customer" company; Freight Forwarder is its
+ // own company type. Both drive the persisted CompanyProfile rows.
+ const companyType = selectedRoles.includes("freight_forwarder")
+ ? "freight_forwarder"
+ : "customer";
+ const payload: CreateCompanyPayload = {
+ ...base,
+ companyType,
+ companyProfiles: selectedRoles.map((type) => ({
+ type: type as CompanyProfileInput["type"],
+ })),
+ };
return api.companies.create.call(payload);
- } else {
- return api.companies.updateProfile.call(payload);
}
+ return api.companies.updateProfile.call(base);
},
onSuccess: () => {
queryClient.invalidateQueries({
@@ -119,11 +134,26 @@ export default function TabCompanyProfile({
},
});
- const onSubmit = (data: CompanyProfileFormData) => mutation.mutate(data);
+ const onSubmit = (data: CompanyProfileFormData) => {
+ if (isCreate && selectedRoles.length === 0) return;
+ mutation.mutate(data);
+ };
+
+ // During onboarding the role selection gates the form: nothing else shows
+ // until the user picks Importer/Exporter or Freight Forwarder.
+ const showForm = !isCreate || selectedRoles.length > 0;
return (
- {!isCreate && profile && }
+ {isCreate ? (
+
+ ) : (
+ profile &&
+ )}
+ {showForm && (
@@ -255,6 +285,7 @@ export default function TabCompanyProfile({
+ )}
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/settings/companyRoles.tsx b/apps/edr-freight-web/portal/src/pages/settings/companyRoles.tsx
new file mode 100644
index 000000000..d875d68af
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/settings/companyRoles.tsx
@@ -0,0 +1,39 @@
+import { ArrowDownToLine, ArrowUpFromLine, Building2 } from "lucide-react";
+
+export interface RoleMeta {
+ type: string;
+ label: string;
+ description: string;
+ icon: React.ReactNode;
+}
+
+export const IMPORTER: RoleMeta = {
+ type: "importer",
+ label: "Importer",
+ description: "Import goods into Ethiopia via the railway corridor.",
+ icon: ,
+};
+
+export const EXPORTER: RoleMeta = {
+ type: "exporter",
+ label: "Exporter",
+ description: "Export goods from Ethiopia via rail.",
+ icon: ,
+};
+
+export const FREIGHT_FORWARDER: RoleMeta = {
+ type: "freight_forwarder",
+ label: "Freight Forwarder",
+ description: "Handle cargo on behalf of importers and exporters.",
+ icon: ,
+};
+
+/** Importer / Exporter — the two roles a "customer" company can hold. */
+export const CUSTOMER_ROLES: RoleMeta[] = [IMPORTER, EXPORTER];
+
+// dj_freight_forwarder and transporter are intentionally not exposed yet.
+export function rolesForCompanyType(companyType: string): RoleMeta[] {
+ if (companyType === "customer") return CUSTOMER_ROLES;
+ if (companyType === "freight_forwarder") return [FREIGHT_FORWARDER];
+ return [];
+}