Files
edr-platform/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx

292 lines
8.3 KiB
TypeScript

import { api } from "@/services/api";
import type { ProfileResponse } from "@/types/profile";
import {
Badge,
Box,
Card,
Center,
Container,
Group,
Loader,
Stack,
Tabs,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertCircle,
BadgeCheck,
Briefcase,
Building2,
FileCheck,
Globe,
ShieldCheck,
User,
UserCheck,
} from "lucide-react";
import { useCallback, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { rolesForCompanyType } from "./settings/companyRoles";
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";
type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents";
/** A section is "incomplete" when its required fields aren't filled in yet. */
function tabIncomplete(
tabId: SettingsTab,
profile: ProfileResponse,
): boolean {
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":
case "documents":
return false;
}
}
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
{ id: "company", label: "Company", icon: <Building2 size={16} /> },
{ id: "contact", label: "Contact Person", icon: <User size={16} /> },
{ id: "gm", label: "General Manager", icon: <Briefcase size={16} /> },
{ id: "poa", label: "Power of Attorney", icon: <UserCheck size={16} /> },
{ id: "documents", label: "Documents", icon: <FileCheck size={16} /> },
];
/**
* Polished identity banner shown above the editor tabs — company name, its
* registered operating roles, location and verification status at a glance.
*/
function ProfileHeader({ profile }: { profile: ProfileResponse }) {
const refByType = new Map(profile.companyProfiles.map((p) => [p.type, p]));
const roleOptions = rolesForCompanyType(profile.companyType);
const activeRoles = roleOptions.filter((o) => refByType.has(o.type));
return (
<Card
padding="xl"
radius="lg"
style={{
background:
"linear-gradient(135deg, var(--mantine-color-edr-ink-6) 0%, var(--mantine-color-edr-ink-8, var(--mantine-color-edr-ink-6)) 100%)",
position: "relative",
overflow: "hidden",
}}
>
<Box
style={{ position: "absolute", top: -24, right: -16, opacity: 0.08 }}
>
<Building2 size={180} color="white" />
</Box>
<Group
justify="space-between"
align="flex-start"
wrap="nowrap"
style={{ position: "relative", zIndex: 1 }}
>
<Group gap="lg" align="center" wrap="nowrap">
<ThemeIcon variant="white" color="edr-green" size={72} radius="lg">
<Building2 size={36} />
</ThemeIcon>
<Stack gap={8}>
<Group gap="sm" align="center">
<Title order={1} size="h2" c="white">
{profile.companyName}
</Title>
<Badge
color="edr-green"
variant="filled"
leftSection={<BadgeCheck size={13} />}
>
Verified
</Badge>
</Group>
{activeRoles.length > 0 ? (
<Group gap="xs">
{activeRoles.map((opt) => (
<Badge
key={opt.type}
variant="white"
color="edr-ink"
radius="sm"
size="lg"
>
{opt.label} · {refByType.get(opt.type)!.reference}
</Badge>
))}
</Group>
) : (
<Text c="gray.4" fw={500} tt="capitalize">
{profile.companyType.replace(/_/g, " ")}
</Text>
)}
<Group gap="lg" mt={4}>
{profile.companyLocation && (
<Group gap={6} c="gray.4">
<Globe size={15} />
<Text size="sm">{profile.companyLocation}</Text>
</Group>
)}
{profile.tinNumber && (
<Group gap={6} c="gray.4">
<ShieldCheck size={15} />
<Text size="sm" ff="monospace">
TIN {profile.tinNumber}
</Text>
</Group>
)}
</Group>
</Stack>
</Group>
</Group>
</Card>
);
}
export default function SettingsPage() {
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const tab = (searchParams.get("tab") as SettingsTab) || "company";
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({
retry: false,
refetchOnWindowFocus: false,
}),
);
const profile = profileQuery.data;
// Keep the cached company info in sync whenever the profile changes, so the
// header (and the rest of the app) reflect edits immediately.
useEffect(() => {
if (profileQuery.dataUpdatedAt > 0) {
queryClient.invalidateQueries({
queryKey: api.companies.getInfo.queryKey(),
});
}
}, [profileQuery.dataUpdatedAt, queryClient]);
if (profileQuery.isPending) {
return (
<Center h="100%">
<Loader color="edr-green" size="lg" />
</Center>
);
}
if (!profile) {
return (
<Container size="xl" px="lg" py="xl">
<Card padding="xl" radius="lg">
<Center>
<Group gap="sm" c="edr-muted">
<AlertCircle size={20} />
<Text>No company profile found.</Text>
</Group>
</Center>
</Card>
</Container>
);
}
return (
<Container size="xl" px="lg" py="xl">
<Stack gap="xl">
<ProfileHeader profile={profile} />
<div>
<Title order={2} size="h3">
Account Settings
</Title>
<Text c="edr-muted" size="sm" mt={4}>
Manage your company profile, personnel, and documents.
</Text>
</div>
<Tabs
value={tab}
onChange={(value) => value && setTab(value as SettingsTab)}
variant="pills"
radius="md"
>
<Tabs.List mb="lg">
{TABS.map((t) => (
<Tabs.Tab
key={t.id}
value={t.id}
leftSection={t.icon}
rightSection={
tabIncomplete(t.id, profile) ? (
<Box
w={7}
h={7}
style={{
borderRadius: "50%",
background: "var(--mantine-color-red-6)",
}}
/>
) : undefined
}
>
{t.label}
</Tabs.Tab>
))}
</Tabs.List>
<Tabs.Panel value="company">
<TabCompanyProfile mode="edit" profile={profile} />
</Tabs.Panel>
<Tabs.Panel value="contact">
<TabContactPerson profile={profile} mode="edit" />
</Tabs.Panel>
<Tabs.Panel value="gm">
<TabGeneralManager profile={profile} mode="edit" />
</Tabs.Panel>
<Tabs.Panel value="poa">
<TabPowerOfAttorney profile={profile} mode="edit" />
</Tabs.Panel>
<Tabs.Panel value="documents">
<TabDocuments profile={profile} mode="edit" />
</Tabs.Panel>
</Tabs>
</Stack>
</Container>
);
}