mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: improve the setting onboarding flow
This commit is contained in:
@@ -1,31 +1,61 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { ArrowRight, Truck } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, Truck, AlertTriangle } from "lucide-react";
|
||||
import { memo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "@/services/api";
|
||||
import type { ProfileResponse } from "@/types/profile";
|
||||
import { cv } from "../constants";
|
||||
|
||||
const REQUIRED_FIELDS: (keyof ProfileResponse)[] = [
|
||||
"companyEmail",
|
||||
"companyPhone",
|
||||
"companyAddress",
|
||||
"fanNumber",
|
||||
"contactPersonName",
|
||||
"contactPersonPhone",
|
||||
"generalManagerName",
|
||||
"generalManagerEmail",
|
||||
"generalManagerPhone",
|
||||
];
|
||||
|
||||
function isProfileIncomplete(profile?: ProfileResponse | null): boolean {
|
||||
if (!profile) return true;
|
||||
return REQUIRED_FIELDS.some((field) => !profile[field]);
|
||||
}
|
||||
|
||||
interface SetupPromptProps {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const SetupPrompt = memo(function SetupPrompt({ show }: SetupPromptProps) {
|
||||
if (!show) return null;
|
||||
const profileQuery = useQuery(
|
||||
api.companies.getProfile.queryOptions({ retry: false }),
|
||||
);
|
||||
|
||||
const incomplete = !profileQuery.isPending && isProfileIncomplete(profileQuery.data);
|
||||
|
||||
if (!show && !incomplete) return null;
|
||||
|
||||
return (
|
||||
<Box className="rounded-2xl border border-edr-border bg-gradient-to-r from-edr-blue/5 to-edr-green/5 px-7 py-6">
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Box className="flex-1">
|
||||
<Text fz={15} fw={700} c="edr-text" mb={6}>
|
||||
Setup your Company Profile
|
||||
<Group gap={6} align="center" mb={6}>
|
||||
{incomplete && <AlertTriangle size={16} color={cv("edr-orange")} />}
|
||||
<Text fz={15} fw={700} c="edr-text">
|
||||
{incomplete ? "Complete Your Profile" : "Setup your Company Profile"}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz={13} c="edr-muted" mb={12}>
|
||||
Complete your company information to unlock all features and start
|
||||
booking shipments.
|
||||
{incomplete
|
||||
? "Your company profile is incomplete. Fill in the missing details to unlock all features."
|
||||
: "Complete your company information to unlock all features and start booking shipments."}
|
||||
</Text>
|
||||
<Link to="/settings" className="no-underline">
|
||||
<Group gap={8} align="center" className="w-fit">
|
||||
<Text fz={13} fw={600} c="edr-green.7">
|
||||
Complete Setup
|
||||
{incomplete ? "Complete Profile" : "Complete Setup"}
|
||||
</Text>
|
||||
<ArrowRight size={16} color={cv("edr-green.7")} />
|
||||
</Group>
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/services/api";
|
||||
import type { ProfileResponse } from "@/types/profile";
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
Text,
|
||||
Tabs,
|
||||
Card,
|
||||
TextInput,
|
||||
Button,
|
||||
Badge,
|
||||
Alert,
|
||||
Center,
|
||||
Loader,
|
||||
Grid,
|
||||
Tabs,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Briefcase,
|
||||
CheckCircle2,
|
||||
Building2,
|
||||
FileCheck,
|
||||
Save,
|
||||
User,
|
||||
UserCheck,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { api } from "@/services/api";
|
||||
import type { CreateCompanyPayload } from "@/services/companies.service";
|
||||
import PhoneInput from "@/components/auth/PhoneInput";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import TabCompanyProfile from "./settings/TabCompanyProfile";
|
||||
import TabContactPerson from "./settings/TabContactPerson";
|
||||
import TabDocuments from "./settings/TabDocuments";
|
||||
import TabGeneralManager from "./settings/TabGeneralManager";
|
||||
import TabPowerOfAttorney from "./settings/TabPowerOfAttorney";
|
||||
import TabDocuments from "./settings/TabDocuments";
|
||||
|
||||
type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents";
|
||||
|
||||
function tabIncomplete(tabId: SettingsTab, profile?: ProfileResponse | null): boolean {
|
||||
if (!profile) return false;
|
||||
switch (tabId) {
|
||||
case "company":
|
||||
return !profile.companyEmail || !profile.companyPhone || !profile.companyAddress || !profile.fanNumber;
|
||||
case "contact":
|
||||
return !profile.contactPersonName || !profile.contactPersonPhone;
|
||||
case "gm":
|
||||
return !profile.generalManagerName || !profile.generalManagerEmail || !profile.generalManagerPhone;
|
||||
case "poa":
|
||||
return false;
|
||||
case "documents":
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "company", label: "Company Profile", icon: <Building2 size={16} /> },
|
||||
{ id: "contact", label: "Contact Person", icon: <User size={16} /> },
|
||||
@@ -49,11 +54,20 @@ const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: "documents", label: "Documents", icon: <FileCheck size={16} /> },
|
||||
];
|
||||
|
||||
const TAB_ORDER: SettingsTab[] = [
|
||||
"company",
|
||||
"contact",
|
||||
"gm",
|
||||
"poa",
|
||||
"documents",
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tab = (searchParams.get("tab") as SettingsTab) || "company";
|
||||
const setTab = (t: SettingsTab) => {
|
||||
const setTab = useCallback(
|
||||
(t: SettingsTab) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
@@ -62,59 +76,46 @@ export default function SettingsPage() {
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
},
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const profileQuery = useQuery(api.companies.getProfile.queryOptions());
|
||||
const profileQuery = useQuery(
|
||||
api.companies.getProfile.queryOptions({
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const profile = profileQuery.data;
|
||||
|
||||
const createCompanyMutation = useMutation({
|
||||
mutationFn: (payload: CreateCompanyPayload) =>
|
||||
api.companies.create.call(payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: api.companies.getProfile.queryKey(),
|
||||
});
|
||||
},
|
||||
});
|
||||
const [isOnboarding, setIsOnboarding] = useState<boolean | null>(null);
|
||||
|
||||
const onboardingSchema = 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"),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (profileQuery.isFetched && isOnboarding === null) {
|
||||
setIsOnboarding(!profileQuery.data);
|
||||
}
|
||||
}, [profileQuery.isFetched, profileQuery.data, isOnboarding]);
|
||||
|
||||
type OnboardingFormData = z.infer<typeof onboardingSchema>;
|
||||
const handleOnboardingSuccess = useCallback(() => {
|
||||
setTab("contact");
|
||||
}, [setTab]);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<OnboardingFormData>({
|
||||
resolver: zodResolver(onboardingSchema),
|
||||
defaultValues: {
|
||||
companyPhoneCountryCode: "+251",
|
||||
},
|
||||
});
|
||||
const handleContactContinue = useCallback(() => {
|
||||
setTab("gm");
|
||||
}, [setTab]);
|
||||
|
||||
const onSubmitOnboarding = (data: OnboardingFormData) => {
|
||||
const payload: CreateCompanyPayload = {
|
||||
companyType: "customer",
|
||||
companyName: data.companyName,
|
||||
companyEmail: data.companyEmail,
|
||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
||||
companyLocation: data.companyLocation,
|
||||
companyAddress: data.companyAddress,
|
||||
tin: data.tinNumber,
|
||||
fanNumber: data.fanNumber,
|
||||
};
|
||||
createCompanyMutation.mutate(payload);
|
||||
};
|
||||
const handleGMContinue = useCallback(() => {
|
||||
setTab("poa");
|
||||
}, [setTab]);
|
||||
|
||||
const handlePOAContinue = useCallback(() => {
|
||||
setTab("documents");
|
||||
}, [setTab]);
|
||||
|
||||
const handleDocumentsContinue = useCallback(() => {
|
||||
navigate("/portal");
|
||||
}, [navigate]);
|
||||
|
||||
if (profileQuery.isPending) {
|
||||
return (
|
||||
@@ -124,25 +125,54 @@ export default function SettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const onboarding = isOnboarding === true;
|
||||
|
||||
const renderProfileContent = (children: React.ReactNode) => {
|
||||
if (onboarding && tab !== "company" && !profile) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Center h={200}>
|
||||
<Loader color="edr-green" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
if (!profile) {
|
||||
return (
|
||||
<Card padding="xl">
|
||||
<Center>
|
||||
<Alert
|
||||
icon={<AlertCircle size={24} />}
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
Please complete the company profile first.
|
||||
</Alert>
|
||||
</Center>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="xl" px="lg">
|
||||
<Group justify="space-between" mb="xl">
|
||||
<div>
|
||||
<Title order={1} size="h2">
|
||||
Account Settings
|
||||
{onboarding ? "Complete Your Profile" : "Account Settings"}
|
||||
</Title>
|
||||
<Text c="edr-muted" size="sm" mt={4}>
|
||||
Manage your company profile, personnel, and documents
|
||||
{onboarding
|
||||
? "Set up your company profile, personnel, and documents to get started"
|
||||
: "Manage your company profile, personnel, and documents"}
|
||||
</Text>
|
||||
</div>
|
||||
{profile && <Badge color="edr-green">Verified</Badge>}
|
||||
</Group>
|
||||
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(value) => {
|
||||
if (!value) return;
|
||||
if (!profile && value !== "company") return;
|
||||
// if (onboarding) return;
|
||||
setTab(value as SettingsTab);
|
||||
}}
|
||||
>
|
||||
@@ -152,7 +182,12 @@ export default function SettingsPage() {
|
||||
key={t.id}
|
||||
value={t.id}
|
||||
leftSection={t.icon}
|
||||
disabled={!profile && t.id !== "company"}
|
||||
disabled={!onboarding && !profile && t.id !== "company"}
|
||||
rightSection={
|
||||
!onboarding && profile && tabIncomplete(t.id, profile) ? (
|
||||
<AlertCircle size={14} color="red" />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{t.label}
|
||||
</Tabs.Tab>
|
||||
@@ -161,201 +196,52 @@ export default function SettingsPage() {
|
||||
|
||||
<Tabs.Panel value="company">
|
||||
{!profile ? (
|
||||
<Card padding="lg">
|
||||
<Stack gap="md">
|
||||
<Group gap="sm">
|
||||
<Building2 size={20} />
|
||||
<Title order={3}>Company Profile</Title>
|
||||
</Group>
|
||||
<Text c="edr-muted" size="sm">
|
||||
Enter your company registration details to get started
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmitOnboarding)}>
|
||||
<Stack gap="md" mt="lg">
|
||||
<TextInput
|
||||
label="Company Name"
|
||||
placeholder="Global Logistics Ltd"
|
||||
error={errors.companyName?.message}
|
||||
{...register("companyName")}
|
||||
<TabCompanyProfile
|
||||
mode="create"
|
||||
onCreateSuccess={handleOnboardingSuccess}
|
||||
/>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label="Company Email"
|
||||
type="email"
|
||||
placeholder="ops@company.com"
|
||||
error={errors.companyEmail?.message}
|
||||
{...register("companyEmail")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<PhoneInput
|
||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
||||
phone={{
|
||||
...register("companyPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={errors.companyPhoneCountryCode}
|
||||
phoneError={errors.companyPhone}
|
||||
label="Company Phone"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label="Location"
|
||||
placeholder="Addis Ababa, Ethiopia"
|
||||
error={errors.companyLocation?.message}
|
||||
{...register("companyLocation")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label="Address"
|
||||
placeholder="Bole Subcity, Woreda 03"
|
||||
error={errors.companyAddress?.message}
|
||||
{...register("companyAddress")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label="TIN Number (10 digits)"
|
||||
placeholder="1234567890"
|
||||
maxLength={10}
|
||||
error={errors.tinNumber?.message}
|
||||
{...register("tinNumber")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label="FAN Number (16 digits)"
|
||||
placeholder="1234567890123456"
|
||||
maxLength={16}
|
||||
error={errors.fanNumber?.message}
|
||||
{...register("fanNumber")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
<Group
|
||||
justify="space-between"
|
||||
mt="xl"
|
||||
pt="md"
|
||||
style={{
|
||||
borderTop: "1px solid var(--mantine-color-edr-border-0)",
|
||||
}}
|
||||
>
|
||||
<Group gap="xs">
|
||||
{createCompanyMutation.isSuccess && (
|
||||
<Group gap={6} c="green">
|
||||
<CheckCircle2 size={16} />
|
||||
<Text size="sm" fw={500}>
|
||||
Profile created successfully
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{createCompanyMutation.isError && (
|
||||
<Group gap={6} c="red">
|
||||
<XCircle size={16} />
|
||||
<Text size="sm" fw={500}>
|
||||
Failed to create profile
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
<Button
|
||||
type="submit"
|
||||
leftSection={<Save size={16} />}
|
||||
loading={createCompanyMutation.isPending}
|
||||
>
|
||||
Create Profile
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Card>
|
||||
) : (
|
||||
<TabCompanyProfile profile={profile} />
|
||||
<TabCompanyProfile mode="edit" profile={profile} />
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="contact">
|
||||
{profile ? (
|
||||
<TabContactPerson profile={profile} />
|
||||
) : (
|
||||
<Card padding="xl">
|
||||
<Center>
|
||||
<Alert
|
||||
icon={<AlertCircle size={24} />}
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
Please complete the company profile first.
|
||||
</Alert>
|
||||
</Center>
|
||||
</Card>
|
||||
{renderProfileContent(
|
||||
<TabContactPerson
|
||||
profile={profile!}
|
||||
mode={onboarding ? "onboarding" : "edit"}
|
||||
onContinue={handleContactContinue}
|
||||
/>,
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="gm">
|
||||
{profile ? (
|
||||
<TabGeneralManager profile={profile} />
|
||||
) : (
|
||||
<Card padding="xl">
|
||||
<Center>
|
||||
<Alert
|
||||
icon={<AlertCircle size={24} />}
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
Please complete the company profile first.
|
||||
</Alert>
|
||||
</Center>
|
||||
</Card>
|
||||
{renderProfileContent(
|
||||
<TabGeneralManager
|
||||
profile={profile!}
|
||||
mode={onboarding ? "onboarding" : "edit"}
|
||||
onContinue={handleGMContinue}
|
||||
/>,
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="poa">
|
||||
{profile ? (
|
||||
<TabPowerOfAttorney profile={profile} />
|
||||
) : (
|
||||
<Card padding="xl">
|
||||
<Center>
|
||||
<Alert
|
||||
icon={<AlertCircle size={24} />}
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
Please complete the company profile first.
|
||||
</Alert>
|
||||
</Center>
|
||||
</Card>
|
||||
{renderProfileContent(
|
||||
<TabPowerOfAttorney
|
||||
profile={profile!}
|
||||
mode={onboarding ? "onboarding" : "edit"}
|
||||
onContinue={handlePOAContinue}
|
||||
/>,
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="documents">
|
||||
{profile ? (
|
||||
<TabDocuments profile={profile} />
|
||||
) : (
|
||||
<Card padding="xl">
|
||||
<Center>
|
||||
<Alert
|
||||
icon={<AlertCircle size={24} />}
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
Please complete the company profile first.
|
||||
</Alert>
|
||||
</Center>
|
||||
</Card>
|
||||
{renderProfileContent(
|
||||
<TabDocuments
|
||||
profile={profile!}
|
||||
mode={onboarding ? "onboarding" : "edit"}
|
||||
onContinue={handleDocumentsContinue}
|
||||
/>,
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
@@ -17,8 +17,9 @@ 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";
|
||||
|
||||
const schema = z.object({
|
||||
export const COMPANY_PROFILE_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"),
|
||||
@@ -29,19 +30,31 @@ const schema = z.object({
|
||||
fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
export type CompanyProfileFormData = z.infer<typeof COMPANY_PROFILE_SCHEMA>;
|
||||
|
||||
function splitPhone(fullPhone?: string | null) {
|
||||
export 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();
|
||||
interface TabCompanyProfileProps {
|
||||
profile?: ProfileResponse;
|
||||
mode?: "edit" | "create";
|
||||
onCreateSuccess?: () => void;
|
||||
}
|
||||
|
||||
const defaultValues = useMemo((): FormData => {
|
||||
export default function TabCompanyProfile({
|
||||
profile,
|
||||
mode = "edit",
|
||||
onCreateSuccess,
|
||||
}: TabCompanyProfileProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isCreate = mode === "create";
|
||||
|
||||
const defaultValues = useMemo((): CompanyProfileFormData => {
|
||||
if (profile) {
|
||||
const phone = splitPhone(profile.companyPhone);
|
||||
return {
|
||||
companyName: profile.companyName,
|
||||
@@ -53,6 +66,17 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
tinNumber: profile.tinNumber,
|
||||
fanNumber: profile.fanNumber ?? "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
companyName: "",
|
||||
companyEmail: "",
|
||||
companyPhone: "",
|
||||
companyPhoneCountryCode: "+251",
|
||||
companyLocation: "",
|
||||
companyAddress: "",
|
||||
tinNumber: "",
|
||||
fanNumber: "",
|
||||
};
|
||||
}, [profile]);
|
||||
|
||||
const {
|
||||
@@ -60,14 +84,15 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isDirty },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
} = useForm<CompanyProfileFormData>({
|
||||
resolver: zodResolver(COMPANY_PROFILE_SCHEMA),
|
||||
values: defaultValues,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: FormData) =>
|
||||
api.companies.updateProfile.call({
|
||||
mutationFn: async (data: CompanyProfileFormData) => {
|
||||
const payload: CreateCompanyPayload = {
|
||||
companyType: "customer",
|
||||
companyName: data.companyName,
|
||||
companyEmail: data.companyEmail,
|
||||
companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
|
||||
@@ -75,13 +100,25 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
companyAddress: data.companyAddress,
|
||||
tin: data.tinNumber,
|
||||
fanNumber: data.fanNumber,
|
||||
}),
|
||||
};
|
||||
|
||||
if (isCreate) {
|
||||
return api.companies.create.call(payload);
|
||||
} else {
|
||||
return api.companies.updateProfile.call(payload);
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: api.companies.getProfile.queryKey(),
|
||||
});
|
||||
if (isCreate) {
|
||||
onCreateSuccess?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: FormData) => mutation.mutate(data);
|
||||
const onSubmit = (data: CompanyProfileFormData) => mutation.mutate(data);
|
||||
|
||||
return (
|
||||
<Card padding="lg">
|
||||
@@ -90,7 +127,9 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
<Title order={3}>Company Profile</Title>
|
||||
</Group>
|
||||
<Text c="edr-muted" size="sm" mb="lg">
|
||||
Edit your company registration details
|
||||
{isCreate
|
||||
? "Enter your company registration details to get started"
|
||||
: "Edit your company registration details"}
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -115,7 +154,10 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
<Grid.Col span={6}>
|
||||
<PhoneInput
|
||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
||||
phone={{ ...register("companyPhone"), placeholder: "912345678" }}
|
||||
phone={{
|
||||
...register("companyPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={errors.companyPhoneCountryCode}
|
||||
phoneError={errors.companyPhone}
|
||||
label="Company Phone"
|
||||
@@ -171,20 +213,25 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
style={{ borderTop: "1px solid var(--mantine-color-edr-border-0)" }}
|
||||
>
|
||||
<Group gap="xs">
|
||||
{mutation.isSuccess && (
|
||||
{mutation.isSuccess && !isCreate && (
|
||||
<Group gap={6} c="green">
|
||||
<CheckCircle2 size={16} />
|
||||
<Text size="sm" fw={500}>Saved successfully</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
Saved successfully
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{mutation.isError && (
|
||||
<Group gap={6} c="red">
|
||||
<XCircle size={16} />
|
||||
<Text size="sm" fw={500}>Save failed</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
{isCreate ? "Failed to create profile" : "Save failed"}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="md">
|
||||
{!isCreate && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -193,12 +240,13 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
leftSection={<Save size={16} />}
|
||||
loading={mutation.isPending}
|
||||
>
|
||||
Save Changes
|
||||
{isCreate ? "Continue" : "Save Changes"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -32,7 +32,13 @@ function splitPhone(fullPhone?: string | null) {
|
||||
return { code: "+251", number: fullPhone };
|
||||
}
|
||||
|
||||
export default function TabContactPerson({ profile }: { profile: ProfileResponse }) {
|
||||
interface TabContactPersonProps {
|
||||
profile: ProfileResponse;
|
||||
mode?: "edit" | "onboarding";
|
||||
onContinue?: () => void;
|
||||
}
|
||||
|
||||
export default function TabContactPerson({ profile, mode = "edit", onContinue }: TabContactPersonProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const defaultValues = useMemo((): FormData => {
|
||||
@@ -62,6 +68,7 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||
if (mode === "onboarding") onContinue?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -116,6 +123,7 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="md">
|
||||
{mode === "edit" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -124,12 +132,13 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
leftSection={<Save size={16} />}
|
||||
loading={mutation.isPending}
|
||||
>
|
||||
Save Changes
|
||||
{mode === "onboarding" ? "Continue" : "Save Changes"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
FileCheck,
|
||||
Loader2,
|
||||
@@ -20,7 +21,13 @@ import { companiesService } from "@/services/companies.service";
|
||||
import { SmartFileInput } from "@edr/ui-common";
|
||||
import type { ProfileResponse } from "@/types/profile";
|
||||
|
||||
export default function TabDocuments({ profile }: { profile: ProfileResponse }) {
|
||||
interface TabDocumentsProps {
|
||||
profile: ProfileResponse;
|
||||
mode?: "edit" | "onboarding";
|
||||
onContinue?: () => void;
|
||||
}
|
||||
|
||||
export default function TabDocuments({ profile, mode = "edit", onContinue }: TabDocumentsProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [documentFiles, setDocumentFiles] = useState<Record<string, File | File[] | null>>({});
|
||||
|
||||
@@ -75,7 +82,9 @@ export default function TabDocuments({ profile }: { profile: ProfileResponse })
|
||||
{docUploadMutation.isSuccess && (
|
||||
<Group gap={6} c="green">
|
||||
<CheckCircle2 size={16} />
|
||||
<Text size="sm" fw={500}>Documents uploaded successfully</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
{mode === "onboarding" ? "Saved successfully" : "Documents uploaded successfully"}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{docUploadMutation.isError && (
|
||||
@@ -85,6 +94,28 @@ export default function TabDocuments({ profile }: { profile: ProfileResponse })
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
{mode === "onboarding" ? (
|
||||
<Button
|
||||
type="button"
|
||||
leftSection={<ArrowRight size={16} />}
|
||||
loading={docUploadMutation.isPending}
|
||||
onClick={() => {
|
||||
const hasFiles = Object.values(documentFiles).some((f) => f !== null);
|
||||
if (hasFiles) {
|
||||
docUploadMutation.mutate(documentFiles, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||
onContinue?.();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
onContinue?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
leftSection={<UploadCloud size={16} />}
|
||||
@@ -93,6 +124,7 @@ export default function TabDocuments({ profile }: { profile: ProfileResponse })
|
||||
>
|
||||
Upload Documents
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -34,7 +34,13 @@ function splitPhone(fullPhone?: string | null) {
|
||||
return { code: "+251", number: fullPhone };
|
||||
}
|
||||
|
||||
export default function TabGeneralManager({ profile }: { profile: ProfileResponse }) {
|
||||
interface TabGeneralManagerProps {
|
||||
profile: ProfileResponse;
|
||||
mode?: "edit" | "onboarding";
|
||||
onContinue?: () => void;
|
||||
}
|
||||
|
||||
export default function TabGeneralManager({ profile, mode = "edit", onContinue }: TabGeneralManagerProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const defaultValues = useMemo((): FormData => {
|
||||
@@ -66,6 +72,7 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
|
||||
if (mode === "onboarding") onContinue?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -133,6 +140,7 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="md">
|
||||
{mode === "edit" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -141,12 +149,13 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
leftSection={<Save size={16} />}
|
||||
loading={mutation.isPending}
|
||||
>
|
||||
Save Changes
|
||||
{mode === "onboarding" ? "Continue" : "Save Changes"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -36,11 +36,17 @@ function splitPhone(fullPhone?: string | null) {
|
||||
return { code: "+251", number: fullPhone };
|
||||
}
|
||||
|
||||
interface TabPowerOfAttorneyProps {
|
||||
profile: ProfileResponse;
|
||||
mode?: "edit" | "onboarding";
|
||||
onContinue?: () => void;
|
||||
}
|
||||
|
||||
export default function TabPowerOfAttorney({
|
||||
profile,
|
||||
}: {
|
||||
profile: ProfileResponse;
|
||||
}) {
|
||||
mode = "edit",
|
||||
onContinue,
|
||||
}: TabPowerOfAttorneyProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const defaultValues = useMemo((): FormData => {
|
||||
@@ -49,7 +55,7 @@ export default function TabPowerOfAttorney({
|
||||
poaName: profile.poaName ?? "",
|
||||
poaEmail: profile.poaEmail ?? "",
|
||||
poaPhone: phone.number,
|
||||
poaPhoneCountryCode: profile.poaPhone ? phone.code : "",
|
||||
poaPhoneCountryCode: profile.poaPhone ? phone.code : "+251",
|
||||
poaLocation: profile.poaLocation ?? "",
|
||||
poaAddress: profile.poaAddress ?? "",
|
||||
};
|
||||
@@ -81,6 +87,7 @@ export default function TabPowerOfAttorney({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: api.companies.getProfile.queryKey(),
|
||||
});
|
||||
if (mode === "onboarding") onContinue?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,11 +106,6 @@ export default function TabPowerOfAttorney({
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Text c="edr-muted" size="sm">
|
||||
Power of Attorney details are optional. Fill them in if you have an
|
||||
authorized representative, or leave blank.
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label="PoA Full Name"
|
||||
placeholder="Authorized Representative Name"
|
||||
@@ -177,6 +179,7 @@ export default function TabPowerOfAttorney({
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="md">
|
||||
{mode === "edit" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -185,12 +188,13 @@ export default function TabPowerOfAttorney({
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
leftSection={<Save size={16} />}
|
||||
loading={mutation.isPending}
|
||||
>
|
||||
Save Changes
|
||||
{mode === "onboarding" ? "Continue" : "Save Changes"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
Reference in New Issue
Block a user