style: use mantine in settings page

This commit is contained in:
ghost2023
2026-06-16 14:16:55 +03:00
parent b883c2d878
commit d6ad094d5a
6 changed files with 619 additions and 618 deletions

View File

@@ -1,12 +1,27 @@
import { useSearchParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Container,
Group,
Stack,
Title,
Text,
Tabs,
Card,
TextInput,
Button,
Badge,
Alert,
Center,
Loader,
Grid,
} from "@mantine/core";
import {
AlertCircle,
Building2,
Briefcase,
CheckCircle2,
FileCheck,
Loader2,
Save,
User,
UserCheck,
@@ -16,43 +31,22 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { api } from "@/services/api";
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Field,
FieldError,
FieldGroup,
FieldLabel,
Input,
} from "@edr/ui-common";
import { cn } from "@/lib/utils";
import PhoneInput from "@/components/auth/PhoneInput";
import type { CreateCompanyPayload } from "@/services/companies.service";
import PhoneInput from "@/components/auth/PhoneInput";
import TabCompanyProfile from "./settings/TabCompanyProfile";
import TabContactPerson from "./settings/TabContactPerson";
import TabGeneralManager from "./settings/TabGeneralManager";
import TabPowerOfAttorney from "./settings/TabPowerOfAttorney";
import TabDocuments from "./settings/TabDocuments";
type SettingsTab =
| "company"
| "contact"
| "gm"
| "poa"
| "documents";
type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents";
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
{ id: "company", label: "Company Profile", icon: <Building2 className="size-4" /> },
{ id: "contact", label: "Contact Person", icon: <User className="size-4" /> },
{ id: "gm", label: "General Manager", icon: <Briefcase className="size-4" /> },
{ id: "poa", label: "Power of Attorney", icon: <UserCheck className="size-4" /> },
{ id: "documents", label: "Documents", icon: <FileCheck className="size-4" /> },
{ id: "company", label: "Company Profile", 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} /> },
];
export default function SettingsPage() {
@@ -60,16 +54,17 @@ export default function SettingsPage() {
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 });
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set("tab", t);
return next;
},
{ replace: true },
);
};
const profileQuery = useQuery(
api.companies.getProfile.queryOptions(),
);
const profileQuery = useQuery(api.companies.getProfile.queryOptions());
const profile = profileQuery.data;
@@ -123,93 +118,80 @@ export default function SettingsPage() {
if (profileQuery.isPending) {
return (
<div className="flex h-full items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-primary" />
</div>
<Center h="100%">
<Loader color="edr-green" size="lg" />
</Center>
);
}
return (
<div className="px-4 py-8">
<div className="mb-8 flex items-center justify-between">
<Container size="xl" py="xl">
<Group justify="space-between" mb="xl">
<div>
<h1 className="text-2xl font-black tracking-tight text-foreground">
<Title order={1} size="h2">
Account Settings
</h1>
<p className="mt-1 text-sm text-muted-foreground">
</Title>
<Text c="edr-muted" size="sm" mt={4}>
Manage your company profile, personnel, and documents
</p>
</Text>
</div>
{profile && (
<Badge variant="secondary" className="font-bold uppercase tracking-wider">
Verified
</Badge>
)}
</div>
{profile && <Badge color="edr-green">Verified</Badge>}
</Group>
{/* Tab Bar */}
<div className="mb-6 flex flex-wrap gap-1 border-b border-border">
{TABS.map((t) => (
<button
key={t.id}
type="button"
onClick={() => {
if (!profile && t.id !== "company") return;
setTab(t.id);
}}
className={cn(
"flex items-center gap-2 border-b-2 px-4 py-3 text-sm font-semibold transition-colors",
tab === t.id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground",
!profile && t.id !== "company" && "cursor-not-allowed opacity-50 hover:text-muted-foreground",
)}
>
{t.icon}
{t.label}
</button>
))}
</div>
<Tabs
value={tab}
onChange={(value) => {
if (!value) return;
if (!profile && value !== "company") return;
setTab(value as SettingsTab);
}}
>
<Tabs.List mb="md">
{TABS.map((t) => (
<Tabs.Tab
key={t.id}
value={t.id}
leftSection={t.icon}
disabled={!profile && t.id !== "company"}
>
{t.label}
</Tabs.Tab>
))}
</Tabs.List>
{/* Tab Content */}
{!profile ? (
<>
{tab === "company" ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="size-5 text-primary" />
Company Profile
</CardTitle>
<CardDescription>
<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
</CardDescription>
</CardHeader>
</Text>
</Stack>
<form onSubmit={handleSubmit(onSubmitOnboarding)}>
<CardContent>
<FieldGroup className="gap-4">
<Field data-invalid={Boolean(errors.companyName)}>
<FieldLabel>Company Name</FieldLabel>
<Input
placeholder="Global Logistics Ltd"
aria-invalid={Boolean(errors.companyName)}
{...register("companyName")}
<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")}
/>
<FieldError errors={[errors.companyName]} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.companyEmail)}>
<FieldLabel>Company Email</FieldLabel>
<Input
type="email"
placeholder="ops@company.com"
aria-invalid={Boolean(errors.companyEmail)}
{...register("companyEmail")}
/>
<FieldError errors={[errors.companyEmail]} />
</Field>
</Grid.Col>
<Grid.Col span={6}>
<PhoneInput
countryCode={{ ...register("companyPhoneCountryCode") }}
phone={{
@@ -220,103 +202,163 @@ export default function SettingsPage() {
phoneError={errors.companyPhone}
label="Company Phone"
/>
</div>
</Grid.Col>
</Grid>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.companyLocation)}>
<FieldLabel>Location</FieldLabel>
<Input
placeholder="Addis Ababa, Ethiopia"
aria-invalid={Boolean(errors.companyLocation)}
{...register("companyLocation")}
/>
<FieldError errors={[errors.companyLocation]} />
</Field>
<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>
<Field data-invalid={Boolean(errors.companyAddress)}>
<FieldLabel>Address</FieldLabel>
<Input
placeholder="Bole Subcity, Woreda 03"
aria-invalid={Boolean(errors.companyAddress)}
{...register("companyAddress")}
/>
<FieldError errors={[errors.companyAddress]} />
</Field>
</div>
<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>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.tinNumber)}>
<FieldLabel>TIN Number (10 digits)</FieldLabel>
<Input
placeholder="1234567890"
maxLength={10}
aria-invalid={Boolean(errors.tinNumber)}
{...register("tinNumber")}
/>
<FieldError errors={[errors.tinNumber]} />
</Field>
<Field data-invalid={Boolean(errors.fanNumber)}>
<FieldLabel>FAN Number (16 digits)</FieldLabel>
<Input
placeholder="1234567890123456"
maxLength={16}
aria-invalid={Boolean(errors.fanNumber)}
{...register("fanNumber")}
/>
<FieldError errors={[errors.fanNumber]} />
</Field>
</div>
</FieldGroup>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 border-t border-border px-6 py-4">
<div className="flex items-center gap-2">
<Group
justify="space-between"
mt="xl"
pt="md"
style={{
borderTop: "1px solid var(--mantine-color-edr-border-0)",
}}
>
<Group gap="xs">
{createCompanyMutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Profile created successfully
</span>
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>
Profile created successfully
</Text>
</Group>
)}
{createCompanyMutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Failed to create profile
</span>
)}
</div>
<Button type="submit" disabled={createCompanyMutation.isPending}>
{createCompanyMutation.isPending ? (
<><Loader2 className="size-4 animate-spin" /> Creating...</>
) : (
<><Save className="size-4" /> Create Profile</>
<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>
</CardFooter>
</Group>
</form>
</Card>
) : (
<Card>
<CardContent className="py-12">
<div className="flex flex-col items-center gap-3 text-center">
<AlertCircle className="size-8 text-muted-foreground" />
<p className="text-muted-foreground">
Please complete the company profile first.
</p>
</div>
</CardContent>
<TabCompanyProfile 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>
)}
</>
) : (
<>
{tab === "company" && <TabCompanyProfile profile={profile} />}
{tab === "contact" && <TabContactPerson profile={profile} />}
{tab === "gm" && <TabGeneralManager profile={profile} />}
{tab === "poa" && <TabPowerOfAttorney profile={profile} />}
{tab === "documents" && <TabDocuments profile={profile} />}
</>
)}
</div>
</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>
)}
</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>
)}
</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>
)}
</Tabs.Panel>
</Tabs>
</Container>
);
}
//

View File

@@ -3,23 +3,19 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Building2, CheckCircle2, Loader2, Save, XCircle } from "lucide-react";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import { Building2, CheckCircle2, Save, XCircle } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Group,
Stack,
Title,
Text,
TextInput,
Button,
Input,
Field,
FieldLabel,
FieldError,
FieldGroup,
} from "@edr/ui-common";
Grid,
} from "@mantine/core";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import type { ProfileResponse } from "@/types/profile";
const schema = z.object({
@@ -88,114 +84,107 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
const onSubmit = (data: FormData) => mutation.mutate(data);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="size-5 text-primary" />
Company Profile
</CardTitle>
<CardDescription>Edit your company registration details</CardDescription>
</CardHeader>
<Card padding="lg">
<Group gap="sm" mb="xs">
<Building2 size={20} />
<Title order={3}>Company Profile</Title>
</Group>
<Text c="edr-muted" size="sm" mb="lg">
Edit your company registration details
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent>
<FieldGroup className="gap-4">
<Field data-invalid={Boolean(errors.companyName)}>
<FieldLabel>Company Name</FieldLabel>
<Input
placeholder="Global Logistics Ltd"
aria-invalid={Boolean(errors.companyName)}
{...register("companyName")}
<Stack gap="md">
<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")}
/>
<FieldError errors={[errors.companyName]} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.companyEmail)}>
<FieldLabel>Company Email</FieldLabel>
<Input
type="email"
placeholder="ops@company.com"
aria-invalid={Boolean(errors.companyEmail)}
{...register("companyEmail")}
/>
<FieldError errors={[errors.companyEmail]} />
</Field>
</Grid.Col>
<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"
/>
</div>
</Grid.Col>
</Grid>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.companyLocation)}>
<FieldLabel>Location</FieldLabel>
<Input
placeholder="Addis Ababa, Ethiopia"
aria-invalid={Boolean(errors.companyLocation)}
{...register("companyLocation")}
/>
<FieldError errors={[errors.companyLocation]} />
</Field>
<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>
<Field data-invalid={Boolean(errors.companyAddress)}>
<FieldLabel>Address</FieldLabel>
<Input
placeholder="Bole Subcity, Woreda 03"
aria-invalid={Boolean(errors.companyAddress)}
{...register("companyAddress")}
/>
<FieldError errors={[errors.companyAddress]} />
</Field>
</div>
<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>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.tinNumber)}>
<FieldLabel>TIN Number (10 digits)</FieldLabel>
<Input
placeholder="1234567890"
maxLength={10}
aria-invalid={Boolean(errors.tinNumber)}
{...register("tinNumber")}
/>
<FieldError errors={[errors.tinNumber]} />
</Field>
<Field data-invalid={Boolean(errors.fanNumber)}>
<FieldLabel>FAN Number (16 digits)</FieldLabel>
<Input
placeholder="1234567890123456"
maxLength={16}
aria-invalid={Boolean(errors.fanNumber)}
{...register("fanNumber")}
/>
<FieldError errors={[errors.fanNumber]} />
</Field>
</div>
</FieldGroup>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 border-t border-border px-6 py-4">
<div className="flex items-center gap-2">
<Group
justify="space-between"
mt="xl"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-edr-border-0)" }}
>
<Group gap="xs">
{mutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Saved successfully
</span>
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>Saved successfully</Text>
</Group>
)}
{mutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Save failed
</span>
<Group gap={6} c="red">
<XCircle size={16} />
<Text size="sm" fw={500}>Save failed</Text>
</Group>
)}
</div>
<div className="flex items-center gap-3">
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
@@ -204,15 +193,15 @@ export default function TabCompanyProfile({ profile }: { profile: ProfileRespons
>
Reset
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? (
<><Loader2 className="size-4 animate-spin" /> Saving...</>
) : (
<><Save className="size-4" /> Save Changes</>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
</Button>
</div>
</CardFooter>
</Group>
</Group>
</form>
</Card>
);

View File

@@ -3,23 +3,18 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { CheckCircle2, Loader2, Save, User, XCircle } from "lucide-react";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import { CheckCircle2, Save, User, XCircle } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Group,
Stack,
Title,
Text,
TextInput,
Button,
Input,
Field,
FieldLabel,
FieldError,
FieldGroup,
} from "@edr/ui-common";
} from "@mantine/core";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import type { ProfileResponse } from "@/types/profile";
const schema = z.object({
@@ -73,55 +68,54 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
const onSubmit = (data: FormData) => mutation.mutate(data);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="size-5 text-primary" />
Contact Person
</CardTitle>
<CardDescription>Manage the primary contact person for your account</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent>
<FieldGroup className="gap-4">
<Field data-invalid={Boolean(errors.contactPersonName)}>
<FieldLabel>Full Name</FieldLabel>
<Input
placeholder="Jane Smith"
aria-invalid={Boolean(errors.contactPersonName)}
{...register("contactPersonName")}
/>
<FieldError errors={[errors.contactPersonName]} />
</Field>
<Card padding="lg">
<Group gap="sm" mb="xs">
<User size={20} />
<Title order={3}>Contact Person</Title>
</Group>
<Text c="edr-muted" size="sm" mb="lg">
Manage the primary contact person for your account
</Text>
<PhoneInput
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
phone={{
...register("contactPersonPhone"),
placeholder: "912345678",
}}
countryCodeError={errors.contactPersonPhoneCountryCode}
phoneError={errors.contactPersonPhone}
label="Phone Number"
/>
</FieldGroup>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 border-t border-border px-6 py-4">
<div className="flex items-center gap-2">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label="Full Name"
placeholder="Jane Smith"
error={errors.contactPersonName?.message}
{...register("contactPersonName")}
/>
<PhoneInput
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
phone={{ ...register("contactPersonPhone"), placeholder: "912345678" }}
countryCodeError={errors.contactPersonPhoneCountryCode}
phoneError={errors.contactPersonPhone}
label="Phone Number"
/>
</Stack>
<Group
justify="space-between"
mt="xl"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-edr-border-0)" }}
>
<Group gap="xs">
{mutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Saved successfully
</span>
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>Saved successfully</Text>
</Group>
)}
{mutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Save failed
</span>
<Group gap={6} c="red">
<XCircle size={16} />
<Text size="sm" fw={500}>Save failed</Text>
</Group>
)}
</div>
<div className="flex items-center gap-3">
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
@@ -130,15 +124,15 @@ export default function TabContactPerson({ profile }: { profile: ProfileResponse
>
Reset
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? (
<><Loader2 className="size-4 animate-spin" /> Saving...</>
) : (
<><Save className="size-4" /> Save Changes</>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
</Button>
</div>
</CardFooter>
</Group>
</Group>
</form>
</Card>
);

View File

@@ -7,17 +7,17 @@ import {
UploadCloud,
XCircle,
} from "lucide-react";
import { api } from "@/services/api";
import { companiesService } from "@/services/companies.service";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Group,
Title,
Text,
Button,
SmartFileInput,
} from "@edr/ui-common";
Center,
} from "@mantine/core";
import { api } from "@/services/api";
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 }) {
@@ -39,69 +39,62 @@ export default function TabDocuments({ profile }: { profile: ProfileResponse })
});
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileCheck className="size-5 text-primary" />
Documents
</CardTitle>
<CardDescription>
Upload and manage required business documents
</CardDescription>
</CardHeader>
<CardContent>
{docSettingQuery.isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : !docSettingQuery.data ? (
<p className="text-sm text-muted-foreground text-center py-4">
No document requirements configured for your account.
</p>
) : (
<SmartFileInput
file={docSettingQuery.data}
value={documentFiles}
onChange={setDocumentFiles}
/>
)}
<Card padding="lg">
<Group gap="sm" mb="xs">
<FileCheck size={20} />
<Title order={3}>Documents</Title>
</Group>
<Text c="edr-muted" size="sm" mb="lg">
Upload and manage required business documents
</Text>
{docSettingQuery.data && (
<div className="flex items-center justify-between pt-4">
<div className="flex items-center gap-2">
{docUploadMutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Documents uploaded successfully
</span>
)}
{docUploadMutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Upload failed
</span>
)}
</div>
<Button
type="button"
onClick={() => docUploadMutation.mutate(documentFiles)}
disabled={docUploadMutation.isPending}
>
{docUploadMutation.isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Uploading...
</>
) : (
<>
<UploadCloud className="size-4" />
Upload Documents
</>
)}
</Button>
</div>
)}
</CardContent>
{docSettingQuery.isLoading ? (
<Center py="xl">
<Loader2 size={24} className="animate-spin" />
</Center>
) : !docSettingQuery.data ? (
<Text c="edr-muted" size="sm" ta="center" py="md">
No document requirements configured for your account.
</Text>
) : (
<SmartFileInput
file={docSettingQuery.data}
value={documentFiles}
onChange={setDocumentFiles}
/>
)}
{docSettingQuery.data && (
<Group
justify="space-between"
mt="lg"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-edr-border-0)" }}
>
<Group gap="xs">
{docUploadMutation.isSuccess && (
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>Documents uploaded successfully</Text>
</Group>
)}
{docUploadMutation.isError && (
<Group gap={6} c="red">
<XCircle size={16} />
<Text size="sm" fw={500}>Upload failed</Text>
</Group>
)}
</Group>
<Button
type="button"
leftSection={<UploadCloud size={16} />}
loading={docUploadMutation.isPending}
onClick={() => docUploadMutation.mutate(documentFiles)}
>
Upload Documents
</Button>
</Group>
)}
</Card>
);
}

View File

@@ -3,23 +3,19 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Briefcase, CheckCircle2, Loader2, Save, XCircle } from "lucide-react";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import { Briefcase, CheckCircle2, Save, XCircle } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Group,
Stack,
Title,
Text,
TextInput,
Button,
Input,
Field,
FieldLabel,
FieldError,
FieldGroup,
} from "@edr/ui-common";
Grid,
} from "@mantine/core";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import type { ProfileResponse } from "@/types/profile";
const schema = z.object({
@@ -76,68 +72,67 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
const onSubmit = (data: FormData) => mutation.mutate(data);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Briefcase className="size-5 text-primary" />
General Manager
</CardTitle>
<CardDescription>Manage the general manager information</CardDescription>
</CardHeader>
<Card padding="lg">
<Group gap="sm" mb="xs">
<Briefcase size={20} />
<Title order={3}>General Manager</Title>
</Group>
<Text c="edr-muted" size="sm" mb="lg">
Manage the general manager information
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent>
<FieldGroup className="gap-4">
<Field data-invalid={Boolean(errors.generalManagerName)}>
<FieldLabel>Full Name</FieldLabel>
<Input
placeholder="Abebe Bikila"
aria-invalid={Boolean(errors.generalManagerName)}
{...register("generalManagerName")}
<Stack gap="md">
<TextInput
label="Full Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
<Grid>
<Grid.Col span={6}>
<TextInput
label="Email Address"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
{...register("generalManagerEmail")}
/>
<FieldError errors={[errors.generalManagerName]} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.generalManagerEmail)}>
<FieldLabel>Email Address</FieldLabel>
<Input
type="email"
placeholder="gm@company.com"
aria-invalid={Boolean(errors.generalManagerEmail)}
{...register("generalManagerEmail")}
/>
<FieldError errors={[errors.generalManagerEmail]} />
</Field>
</Grid.Col>
<Grid.Col span={6}>
<PhoneInput
countryCode={{ ...register("generalManagerPhoneCountryCode") }}
phone={{
...register("generalManagerPhone"),
placeholder: "912345678",
}}
phone={{ ...register("generalManagerPhone"), placeholder: "912345678" }}
countryCodeError={errors.generalManagerPhoneCountryCode}
phoneError={errors.generalManagerPhone}
label="Phone Number"
/>
</div>
</FieldGroup>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 border-t border-border px-6 py-4">
<div className="flex items-center gap-2">
</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">
{mutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Saved successfully
</span>
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>Saved successfully</Text>
</Group>
)}
{mutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Save failed
</span>
<Group gap={6} c="red">
<XCircle size={16} />
<Text size="sm" fw={500}>Save failed</Text>
</Group>
)}
</div>
<div className="flex items-center gap-3">
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
@@ -146,15 +141,15 @@ export default function TabGeneralManager({ profile }: { profile: ProfileRespons
>
Reset
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? (
<><Loader2 className="size-4 animate-spin" /> Saving...</>
) : (
<><Save className="size-4" /> Save Changes</>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
</Button>
</div>
</CardFooter>
</Group>
</Group>
</form>
</Card>
);

View File

@@ -4,22 +4,18 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { CheckCircle2, Loader2, Save, UserCheck, XCircle } from "lucide-react";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Group,
Stack,
Title,
Text,
TextInput,
Button,
Input,
Field,
FieldLabel,
FieldError,
FieldGroup,
} from "@edr/ui-common";
Grid,
} from "@mantine/core";
import { api } from "@/services/api";
import PhoneInput from "@/components/auth/PhoneInput";
import type { ProfileResponse } from "@/types/profile";
const schema = z.object({
@@ -66,7 +62,6 @@ export default function TabPowerOfAttorney({
formState: { errors, isDirty },
} = useForm<FormData>({
resolver: zodResolver(schema),
values: defaultValues,
});
@@ -92,47 +87,41 @@ export default function TabPowerOfAttorney({
const onSubmit = (data: FormData) => mutation.mutate(data);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserCheck className="size-5 text-accent" />
Power of Attorney
</CardTitle>
<CardDescription>
Power of Attorney details are optional. Fill them in if you have an
authorized representative, or leave blank.
</CardDescription>
</CardHeader>
<Card padding="lg">
<Group gap="sm" mb="xs">
<UserCheck size={20} />
<Title order={3}>Power of Attorney</Title>
</Group>
<Text c="edr-muted" size="sm" mb="lg">
Power of Attorney details are optional. Fill them in if you have an
authorized representative, or leave blank.
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent>
<FieldGroup className="gap-4">
<p className="text-sm text-muted-foreground">
Power of Attorney details are optional. Fill them in if you have
an authorized representative, or leave blank.
</p>
<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>
<Field data-invalid={Boolean(errors.poaName)}>
<FieldLabel>PoA Full Name</FieldLabel>
<Input
placeholder="Authorized Representative Name"
aria-invalid={Boolean(errors.poaName)}
{...register("poaName")}
<TextInput
label="PoA Full Name"
placeholder="Authorized Representative Name"
error={errors.poaName?.message}
{...register("poaName")}
/>
<Grid>
<Grid.Col span={6}>
<TextInput
label="PoA Email"
type="email"
placeholder="poa@company.com"
error={errors.poaEmail?.message}
{...register("poaEmail")}
/>
<FieldError errors={[errors.poaName]} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.poaEmail)}>
<FieldLabel>PoA Email</FieldLabel>
<Input
type="email"
placeholder="poa@company.com"
aria-invalid={Boolean(errors.poaEmail)}
{...register("poaEmail")}
/>
<FieldError errors={[errors.poaEmail]} />
</Field>
</Grid.Col>
<Grid.Col span={6}>
<PhoneInput
countryCode={{ ...register("poaPhoneCountryCode") }}
phone={{ ...register("poaPhone"), placeholder: "912345678" }}
@@ -140,47 +129,50 @@ export default function TabPowerOfAttorney({
countryCodeError={errors.poaPhoneCountryCode}
phoneError={errors.poaPhone}
/>
</div>
</Grid.Col>
</Grid>
<div className="grid grid-cols-2 gap-4">
<Field data-invalid={Boolean(errors.poaLocation)}>
<FieldLabel>PoA Location</FieldLabel>
<Input
placeholder="City, Country"
aria-invalid={Boolean(errors.poaLocation)}
{...register("poaLocation")}
/>
<FieldError errors={[errors.poaLocation]} />
</Field>
<Grid>
<Grid.Col span={6}>
<TextInput
label="PoA Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label="PoA Address"
placeholder="Full Address"
error={errors.poaAddress?.message}
{...register("poaAddress")}
/>
</Grid.Col>
</Grid>
</Stack>
<Field data-invalid={Boolean(errors.poaAddress)}>
<FieldLabel>PoA Address</FieldLabel>
<Input
placeholder="Full Address"
aria-invalid={Boolean(errors.poaAddress)}
{...register("poaAddress")}
/>
<FieldError errors={[errors.poaAddress]} />
</Field>
</div>
</FieldGroup>
</CardContent>
<CardFooter className="flex items-center justify-between gap-4 border-t border-border px-6 py-4">
<div className="flex items-center gap-2">
<Group
justify="space-between"
mt="xl"
pt="md"
style={{ borderTop: "1px solid var(--mantine-color-edr-border-0)" }}
>
<Group gap="xs">
{mutation.isSuccess && (
<span className="flex items-center gap-1.5 text-sm font-medium text-emerald-600">
<CheckCircle2 className="size-4" />
Saved successfully
</span>
<Group gap={6} c="green">
<CheckCircle2 size={16} />
<Text size="sm" fw={500}>Saved successfully</Text>
</Group>
)}
{mutation.isError && (
<span className="flex items-center gap-1.5 text-sm font-medium text-red-600">
<XCircle className="size-4" />
Save failed
</span>
<Group gap={6} c="red">
<XCircle size={16} />
<Text size="sm" fw={500}>Save failed</Text>
</Group>
)}
</div>
<div className="flex items-center gap-3">
</Group>
<Group gap="md">
<Button
type="button"
variant="outline"
@@ -189,19 +181,15 @@ export default function TabPowerOfAttorney({
>
Reset
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? (
<>
<Loader2 className="size-4 animate-spin" /> Saving...
</>
) : (
<>
<Save className="size-4" /> Save Changes
</>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
Save Changes
</Button>
</div>
</CardFooter>
</Group>
</Group>
</form>
</Card>
);