mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: ui fixes
This commit is contained in:
@@ -1,15 +1,23 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { BadgeCheck, Clock, ShieldCheck, XCircle } from "lucide-react";
|
import {
|
||||||
|
BadgeCheck,
|
||||||
|
Clock,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Phone,
|
||||||
|
ShieldCheck,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifaydaService,
|
verifaydaService,
|
||||||
@@ -42,10 +50,12 @@ interface FaydaVerifyPanelProps {
|
|||||||
pendingReview?: boolean;
|
pendingReview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(iso: string | null): string {
|
function getInitials(name: string | null): string {
|
||||||
if (!iso) return "";
|
if (!name) return "?";
|
||||||
const d = new Date(iso);
|
const parts = name.trim().split(/\s+/);
|
||||||
return Number.isNaN(d.getTime()) ? "" : d.toLocaleDateString();
|
const first = parts[0]?.[0] ?? "";
|
||||||
|
const last = parts.length > 1 ? (parts[parts.length - 1]?.[0] ?? "") : "";
|
||||||
|
return (first + last).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +182,7 @@ export default function FaydaVerifyPanel({
|
|||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<ShieldCheck size={18} />
|
<ShieldCheck size={18} />
|
||||||
<Text fw={600} c="edr-text">
|
<Text fw={600} c="edr-text">
|
||||||
{title} identity
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{verified ? (
|
{verified ? (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -222,13 +232,21 @@ export default function FaydaVerifyPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{verified && state && (
|
{verified && state && (
|
||||||
<SimpleGrid cols={2} spacing="xs">
|
<Group align="flex-start" gap="sm" wrap="nowrap">
|
||||||
<VerifiedField label="Name" value={state.name} />
|
<Avatar radius="xl" size={44} color="edr-green" variant="light">
|
||||||
<VerifiedField label="Phone" value={state.phone} />
|
{getInitials(state.name)}
|
||||||
<VerifiedField label="Email" value={state.email} />
|
</Avatar>
|
||||||
<VerifiedField label="Address" value={state.address} />
|
<Stack gap={4} style={{ minWidth: 0, flex: 1 }}>
|
||||||
<VerifiedField label="Verified" value={formatDate(state.verifiedAt)} />
|
<Text fw={600} size="sm" c="edr-text" truncate>
|
||||||
</SimpleGrid>
|
{state.name}
|
||||||
|
</Text>
|
||||||
|
<Group gap="md" wrap="wrap">
|
||||||
|
<DataRow icon={<Phone size={13} />} value={state.phone} />
|
||||||
|
<DataRow icon={<Mail size={13} />} value={state.email} />
|
||||||
|
</Group>
|
||||||
|
<DataRow icon={<MapPin size={13} />} value={state.address} />
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -240,22 +258,16 @@ export default function FaydaVerifyPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VerifiedField({
|
function DataRow({ icon, value }: { icon: ReactNode; value: string | null }) {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string | null;
|
|
||||||
}) {
|
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return (
|
return (
|
||||||
<Stack gap={0}>
|
<Group gap={6} wrap="nowrap">
|
||||||
<Text size="xs" c="edr-muted">
|
<span style={{ color: "var(--mantine-color-edr-muted-6)", display: "flex", flexShrink: 0 }}>
|
||||||
{label}
|
{icon}
|
||||||
</Text>
|
</span>
|
||||||
<Text size="sm" fw={500} c="edr-text">
|
<Text size="xs" c="edr-muted" truncate>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,31 +28,6 @@ interface ETradeInfoProps {
|
|||||||
|
|
||||||
const isValidTin = (tin: string) => tin.length === 10;
|
const isValidTin = (tin: string) => tin.length === 10;
|
||||||
|
|
||||||
/** Plain-text summary of the fetched eTrade record, downloaded client-side (eTrade returns data, not a document). */
|
|
||||||
function downloadTinRecord(tin: string, data: CompanyRegistrationData) {
|
|
||||||
const lines = [
|
|
||||||
`TIN: ${tin}`,
|
|
||||||
`Company name: ${data.companyName}`,
|
|
||||||
`Licence number: ${data.licenceNumber}`,
|
|
||||||
`Status: ${data.statusDescription}`,
|
|
||||||
`Date registered: ${data.dateRegistered}`,
|
|
||||||
`Renewed from: ${data.renewedFrom}`,
|
|
||||||
`Renewal date: ${data.renewalDate}`,
|
|
||||||
`Renewed to: ${data.renewedTo}`,
|
|
||||||
`Address: ${[data.region, data.zone, data.woreda, data.kebele, data.houseNo].filter(Boolean).join(", ")}`,
|
|
||||||
`Manager: ${data.managerName}`,
|
|
||||||
];
|
|
||||||
const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = `tin-${tin}.txt`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ETradeInfo({
|
export default function ETradeInfo({
|
||||||
tin,
|
tin,
|
||||||
register,
|
register,
|
||||||
@@ -86,9 +61,7 @@ export default function ETradeInfo({
|
|||||||
}, [tin]);
|
}, [tin]);
|
||||||
|
|
||||||
const apiError =
|
const apiError =
|
||||||
mutation.isError && mutation.error
|
mutation.isError && mutation.error ? extractApiError(mutation.error) : null;
|
||||||
? extractApiError(mutation.error)
|
|
||||||
: null;
|
|
||||||
// A 400 here means eTrade simply has no record for this TIN.
|
// A 400 here means eTrade simply has no record for this TIN.
|
||||||
const notFound = apiError?.statusCode === 400;
|
const notFound = apiError?.statusCode === 400;
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@@ -117,18 +90,14 @@ export default function ETradeInfo({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
const showRetry = isValidTin(tin) && status !== "verified" && status !== "loading";
|
const showRetry =
|
||||||
|
isValidTin(tin) && status !== "verified" && status !== "loading";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group align="flex-start" grow>
|
<div className="max-sm:flex-col! max-sm: grow flex items-start gap-4">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={
|
aria-label="TIN Number (10 digits), required"
|
||||||
<>
|
|
||||||
TIN Number (10 digits){" "}
|
|
||||||
<span style={{ color: "var(--mantine-color-red-6)" }}>*</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placeholder="0012345678"
|
placeholder="0012345678"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
error={error}
|
error={error}
|
||||||
@@ -136,6 +105,7 @@ export default function ETradeInfo({
|
|||||||
/>
|
/>
|
||||||
{showRetry && (
|
{showRetry && (
|
||||||
<Button
|
<Button
|
||||||
|
className="max-w-none"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="edr-green"
|
color="edr-green"
|
||||||
onClick={handleFetch}
|
onClick={handleFetch}
|
||||||
@@ -143,23 +113,11 @@ export default function ETradeInfo({
|
|||||||
leftSection={
|
leftSection={
|
||||||
isLoading ? <Loader size={16} /> : <Download size={16} />
|
isLoading ? <Loader size={16} /> : <Download size={16} />
|
||||||
}
|
}
|
||||||
mt="24px"
|
|
||||||
>
|
>
|
||||||
{isLoading ? "Getting..." : "Get Data"}
|
{isLoading ? "Getting..." : "Get Data"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{status === "verified" && mutation.data && !mutation.data.tinTaken && (
|
</div>
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
color="edr-green"
|
|
||||||
onClick={() => downloadTinRecord(tin, mutation.data!)}
|
|
||||||
leftSection={<Download size={16} />}
|
|
||||||
mt="24px"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{notFound && (
|
{notFound && (
|
||||||
<Alert
|
<Alert
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ export default function CompanyProfileForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="VAT Number"
|
aria-label="VAT Number"
|
||||||
placeholder="0012345678"
|
placeholder="0012345678"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
error={errors.vatNumber?.message}
|
error={errors.vatNumber?.message}
|
||||||
@@ -661,9 +661,9 @@ export default function CompanyProfileForm({
|
|||||||
index={2}
|
index={2}
|
||||||
title="Owner identity"
|
title="Owner identity"
|
||||||
subtitle={
|
subtitle={
|
||||||
verifiedIdentity
|
!identity?.owner.verified && !verifiedIdentity
|
||||||
? "Verify the company owner with Fayda — their name, phone, email and address come from the verification."
|
? "Provide the company owner's passport number."
|
||||||
: "Provide the company owner's passport number."
|
: undefined
|
||||||
}
|
}
|
||||||
status={
|
status={
|
||||||
verifiedIdentity
|
verifiedIdentity
|
||||||
@@ -683,7 +683,7 @@ export default function CompanyProfileForm({
|
|||||||
<>
|
<>
|
||||||
<FaydaVerifyPanel
|
<FaydaVerifyPanel
|
||||||
subject="owner"
|
subject="owner"
|
||||||
title="Company owner"
|
title="Owner"
|
||||||
state={identity.owner}
|
state={identity.owner}
|
||||||
required={identity.faydaRequired}
|
required={identity.faydaRequired}
|
||||||
onVerified={() => onIdentityChange?.()}
|
onVerified={() => onIdentityChange?.()}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function ReadOnlyField({
|
|||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="edr-text" fw={500}>
|
<Text className="wrap-break-word" size="sm" c="edr-text" fw={500}>
|
||||||
{value && value.trim() ? value : "—"}
|
{value && value.trim() ? value : "—"}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Badge, Group, Stack, Text } from "@mantine/core";
|
import { Badge, Group, Stack, Text } from "@mantine/core";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export default function StepSection({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const badge = STATUS_BADGE[status];
|
const badge = STATUS_BADGE[status];
|
||||||
|
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
@@ -75,7 +77,7 @@ export default function StepSection({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<div style={{ paddingLeft: 34 }}>
|
<div style={{ paddingLeft: isMobile ? 0 : 34 }}>
|
||||||
<Stack gap="sm">{children}</Stack>
|
<Stack gap="sm">{children}</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user