Merge pull request #198 from Tria-plc/freight/fix/hot-fixes

Freight/fix/hot fixes
This commit is contained in:
Nathnael Wondisha
2026-06-17 13:43:58 +03:00
committed by GitHub
11 changed files with 409 additions and 383 deletions

View File

@@ -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
</Text>
<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>

View File

@@ -1,10 +1,5 @@
import { Box, SimpleGrid } from "@mantine/core";
import {
CheckCircle2,
Clock3,
Truck,
Wallet,
} from "lucide-react";
import { SimpleGrid } from "@mantine/core";
import { CheckCircle2, Clock3, Truck, Wallet } from "lucide-react";
import { memo } from "react";
import { formatCurrency } from "@/pages/billing/invoices.mock";
import { formatPct } from "../constants";
@@ -34,7 +29,6 @@ export const StatsSection = memo(function StatsSection({
completionRate,
spendYtd,
spendYtdChangePct,
dashboardLoading,
}: StatsSectionProps) {
return (
<Card className="rounded-2xl border border-edr-border bg-gradient-to-br from-white to-edr-soft px-7 py-5">

View File

@@ -9,7 +9,7 @@ import {
Wallet,
type LucideIcon,
} from "lucide-react";
import type { Currency, InvoiceStatus } from "@/pages/billing/invoices.mock";
import type { InvoiceStatus } from "@/pages/billing/invoices.mock";
export const cv = (token: string) => {
const [name, shade] = token.split(".");
@@ -319,12 +319,14 @@ export const STATUS_CONFIG: Record<string, StageConfig> = {
},
};
export const ACTION_PROPS: Record<string, { bg: string; c: string; bd?: string }> =
{
dark: { bg: "edr-ink", c: "white" },
amber: { bg: "edr-accent", c: "white" },
outline: { bg: "edr-card", c: "edr-text", bd: "1px solid edr-border" },
};
export const ACTION_PROPS: Record<
string,
{ bg: string; c: string; bd?: string }
> = {
dark: { bg: "edr-ink", c: "white" },
amber: { bg: "edr-accent", c: "white" },
outline: { bg: "edr-card", c: "edr-text", bd: "1px solid edr-border" },
};
export const INVOICE_BADGE: Record<
InvoiceStatus,

View File

@@ -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, useQueryClient } 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} /> },
@@ -50,71 +55,68 @@ const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
];
export default function SettingsPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const tab = (searchParams.get("tab") as SettingsTab) || "company";
const setTab = (t: SettingsTab) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set("tab", t);
return next;
},
{ replace: true },
);
};
const setTab = useCallback(
(t: SettingsTab) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set("tab", t);
return next;
},
{ 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: () => {
useEffect(() => {
if (profileQuery.dataUpdatedAt > 0) {
queryClient.invalidateQueries({
queryKey: api.companies.getProfile.queryKey(),
queryKey: api.companies.getInfo.queryKey(),
});
},
});
}
}, [profileQuery.dataUpdatedAt, queryClient]);
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"),
});
const [isOnboarding, setIsOnboarding] = useState<boolean | null>(null);
type OnboardingFormData = z.infer<typeof onboardingSchema>;
useEffect(() => {
if (profileQuery.isFetched && isOnboarding === null) {
setIsOnboarding(!profileQuery.data);
}
}, [profileQuery.isFetched, profileQuery.data, isOnboarding]);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<OnboardingFormData>({
resolver: zodResolver(onboardingSchema),
defaultValues: {
companyPhoneCountryCode: "+251",
},
});
const handleOnboardingSuccess = useCallback(() => {
setTab("contact");
}, [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 handleContactContinue = useCallback(() => {
setTab("gm");
}, [setTab]);
const handleGMContinue = useCallback(() => {
setTab("poa");
}, [setTab]);
const handlePOAContinue = useCallback(() => {
setTab("documents");
}, [setTab]);
const handleDocumentsContinue = useCallback(() => {
navigate("/portal");
}, [navigate]);
if (profileQuery.isPending) {
return (
@@ -124,25 +126,54 @@ export default function SettingsPage() {
);
}
const onboarding = isOnboarding === true;
const renderProfileContent = (children: React.ReactNode) => {
if (onboarding && tab !== "company" && !profile) {
return (
<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" py="xl">
<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 +183,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 +197,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")}
/>
<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
mode="create"
onCreateSuccess={handleOnboardingSuccess}
/>
) : (
<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>

View File

@@ -105,7 +105,6 @@ export function StatusHero({
function ProgressTracker({
current,
tone = "green",
negative,
}: {
current: number;
tone?: "green" | "ink";
@@ -114,7 +113,6 @@ function ProgressTracker({
const last = PROGRESS_STAGES.length - 1;
const activeFill = tone === "ink" ? "#0C1A2B" : "#0EA371";
const activeRing = tone === "ink" ? "#D9E0E7" : "#BFE8D4";
const activeSub = tone === "ink" ? "#475569" : "#0A6F4D";
return (
/* Scrollable on mobile so 5 stages never overflow */

View File

@@ -16,12 +16,18 @@ import {
Title,
} from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, Check, ChevronLeft, ChevronRight, Send, XCircle } from "lucide-react";
import {
AlertCircle,
Check,
ChevronLeft,
ChevronRight,
Send,
XCircle,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import useAuth from "@/hooks/useAuth";
import type { Freight } from "@/types";
import {
BookingFormInputValues,
STEPS,
@@ -191,8 +197,12 @@ export default function NewBookingPage() {
[docValues],
);
const [pricingPhase, setPricingPhase] = useState<"idle" | "generating" | "ready">("idle");
const [pricingData, setPricingData] = useState<GeneratePriceResponse | null>(null);
const [pricingPhase, setPricingPhase] = useState<
"idle" | "generating" | "ready"
>("idle");
const [pricingData, setPricingData] = useState<GeneratePriceResponse | null>(
null,
);
const [priceBookingId, setPriceBookingId] = useState<string | null>(null);
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
const [cancelReason, setCancelReason] = useState("");
@@ -444,7 +454,9 @@ export default function NewBookingPage() {
pricingData={pricingData}
onConfirm={() => confirmMutation.mutate()}
onContinueLater={
priceBookingId ? () => navigate(`/bookings/${priceBookingId}`) : undefined
priceBookingId
? () => navigate(`/bookings/${priceBookingId}`)
: undefined
}
onAbort={() => setCancelDialogOpen(true)}
confirmPending={confirmMutation.isPending}
@@ -502,7 +514,9 @@ export default function NewBookingPage() {
createMutation.isPending ? undefined : <Check size={16} />
}
>
{createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
{createMutation.isPending
? "Saving Draft..."
: "Save as Draft"}
</Button>
{hasDocuments && (
<Button
@@ -511,21 +525,20 @@ export default function NewBookingPage() {
radius="md"
loading={createAndPriceMutation.isPending}
leftSection={
createAndPriceMutation.isPending ? undefined : <Send size={16} />
createAndPriceMutation.isPending ? undefined : (
<Send size={16} />
)
}
onClick={() => handleGeneratePrice()}
>
{createAndPriceMutation.isPending ? "Generating price…" : "Submit"}
{createAndPriceMutation.isPending
? "Generating price…"
: "Submit"}
</Button>
)}
</Group>
) : pricingPhase === "generating" ? (
<Button
type="button"
color="edr-green"
radius="md"
loading
>
<Button type="button" color="edr-green" radius="md" loading>
Generating price estimate
</Button>
) : null}

View File

@@ -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,29 +30,52 @@ 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 => {
const phone = splitPhone(profile.companyPhone);
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,
companyEmail: profile.companyEmail ?? "",
companyPhone: phone.number,
companyPhoneCountryCode: phone.code,
companyLocation: profile.companyLocation,
companyAddress: profile.companyAddress ?? "",
tinNumber: profile.tinNumber,
fanNumber: profile.fanNumber ?? "",
};
}
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 ?? "",
companyName: "",
companyEmail: "",
companyPhone: "",
companyPhoneCountryCode: "+251",
companyLocation: "",
companyAddress: "",
tinNumber: "",
fanNumber: "",
};
}, [profile]);
@@ -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,34 +213,40 @@ 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">
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
{!isCreate && (
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
{isCreate ? "Continue" : "Save Changes"}
</Button>
</Group>
</Group>

View File

@@ -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,20 +123,22 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
)}
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
{mode === "edit" && (
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
{mode === "onboarding" ? "Continue" : "Save Changes"}
</Button>
</Group>
</Group>

View File

@@ -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,14 +94,37 @@ export default function TabDocuments({ profile }: { profile: ProfileResponse })
</Group>
)}
</Group>
<Button
type="button"
leftSection={<UploadCloud size={16} />}
loading={docUploadMutation.isPending}
onClick={() => docUploadMutation.mutate(documentFiles)}
>
Upload Documents
</Button>
{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} />}
loading={docUploadMutation.isPending}
onClick={() => docUploadMutation.mutate(documentFiles)}
>
Upload Documents
</Button>
)}
</Group>
)}
</Card>

View File

@@ -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,20 +140,22 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
)}
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
{mode === "edit" && (
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
{mode === "onboarding" ? "Continue" : "Save Changes"}
</Button>
</Group>
</Group>

View File

@@ -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,20 +179,22 @@ export default function TabPowerOfAttorney({
)}
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
{mode === "edit" && (
<Button
type="button"
variant="outline"
disabled={mutation.isPending || !isDirty}
onClick={() => reset()}
>
Reset
</Button>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
{mode === "onboarding" ? "Continue" : "Save Changes"}
</Button>
</Group>
</Group>