Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const ComplianceKycFilesDetailsScreen = lazy(() => import('./screens/compliance-
const ComplianceKycStatsScreen = lazy(() => import('./screens/compliance-kyc-stats.screen'));
const ComplianceTransactionListScreen = lazy(() => import('./screens/compliance-transaction-list.screen'));
const ComplianceKycStepScreen = lazy(() => import('./screens/compliance-kyc-step.screen'));
const ComplianceSupportIssueScreen = lazy(() => import('./screens/compliance-support-issue.screen'));
const ComplianceRecommendationGraphScreen = lazy(() => import('./screens/compliance-recommendation-graph.screen'));
const ComplianceCustodyOrdersScreen = lazy(() => import('./screens/compliance-custody-orders.screen'));
const RealunitScreen = lazy(() => import('./screens/realunit.screen'));
Expand Down Expand Up @@ -346,6 +347,10 @@ export const Routes = [
path: 'compliance/user/:id/kyc-step/:stepId',
element: withSuspense(<ComplianceKycStepScreen />),
},
{
path: 'compliance/user/:id/support-issue/:issueId',
element: withSuspense(<ComplianceSupportIssueScreen />),
},
{
path: 'compliance/recommendations/:id',
element: withSuspense(<ComplianceRecommendationGraphScreen />),
Expand Down
157 changes: 157 additions & 0 deletions src/components/compliance/detail-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { ReactNode } from 'react';
import {
BankDataInfo,
BuyRouteInfo,
KycLogInfo,
KycStepInfo,
SellRouteInfo,
UserInfo,
} from 'src/hooks/compliance.hook';
import { boolBadge, formatDate, statusBadge } from 'src/util/compliance-helpers';

interface ColumnDef<T> {
header: string;
align?: 'left' | 'center' | 'right';
render: (item: T) => ReactNode;
className?: string;
}

function DataTable<T extends { id: number }>({
data,
columns,
emptyLabel,
}: {
data: T[];
columns: ColumnDef<T>[];
emptyLabel: string;
}): JSX.Element {
return (
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-dfxGray-300">
<tr>
{columns.map((col) => (
<th
key={col.header}
className={`px-3 py-2 text-sm font-semibold text-dfxBlue-800 text-${col.align ?? 'center'}`}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map((item) => (
<tr key={item.id} className="border-b border-dfxGray-300 transition-colors hover:bg-dfxGray-300">
{columns.map((col) => (
<td
key={col.header}
className={`px-3 py-2 text-sm text-dfxBlue-800 text-${col.align ?? 'center'} ${col.className ?? ''}`}
>
{col.render(item)}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="px-3 py-4 text-center text-dfxGray-700">
{emptyLabel}
</td>
</tr>
)}
</tbody>
</table>
);
}

const usersColumns: ColumnDef<UserInfo>[] = [
{ header: 'ID', render: (u) => u.id },
{ header: 'Address', align: 'left', render: (u) => u.address, className: 'font-mono' },
{ header: 'Role', render: (u) => u.role },
{ header: 'Status', render: (u) => u.status },
{ header: 'Created', render: (u) => formatDate(u.created) },
];

export function UsersTable({ users }: { users: UserInfo[] }): JSX.Element {
return <DataTable data={users} columns={usersColumns} emptyLabel="No users" />;
}

const kycStepsColumns: ColumnDef<KycStepInfo>[] = [
{ header: 'ID', render: (s) => s.id },
{ header: 'Name', align: 'left', render: (s) => s.name },
{ header: 'Type', align: 'left', render: (s) => s.type || '-' },
{ header: 'Status', render: (s) => statusBadge(s.status) },
{ header: 'Sequence', render: (s) => s.sequenceNumber },
{
header: 'Comment',
align: 'left',
render: (s) => <span title={s.comment}>{s.comment || '-'}</span>,
className: 'max-w-xs truncate',
},
{ header: 'Created', render: (s) => formatDate(s.created) },
];

export function KycStepsTable({ kycSteps }: { kycSteps: KycStepInfo[] }): JSX.Element {
return <DataTable data={kycSteps} columns={kycStepsColumns} emptyLabel="No KYC steps" />;
}

const kycLogsColumns: ColumnDef<KycLogInfo>[] = [
{ header: 'Date', render: (l) => formatDate(l.created) },
{ header: 'Type', align: 'left', render: (l) => l.type },
{ header: 'Comment', align: 'left', render: (l) => l.comment || '-' },
];

export function KycLogsTable({ kycLogs }: { kycLogs: KycLogInfo[] }): JSX.Element {
return <DataTable data={kycLogs} columns={kycLogsColumns} emptyLabel="No KYC logs" />;
}

const bankDatasColumns: ColumnDef<BankDataInfo>[] = [
{ header: 'ID', render: (b) => b.id },
{ header: 'IBAN', align: 'left', render: (b) => b.iban, className: 'font-mono' },
{ header: 'Name', align: 'left', render: (b) => b.name },
{ header: 'Type', render: (b) => b.type || '-' },
{ header: 'Status', render: (b) => (b.status ? statusBadge(b.status) : '-') },
{ header: 'Approved', render: (b) => boolBadge(b.approved) },
{ header: 'Manual', render: (b) => (b.manualApproved == null ? '-' : boolBadge(b.manualApproved)) },
{ header: 'Active', render: (b) => boolBadge(b.active) },
{
header: 'Comment',
align: 'left',
render: (b) => <span title={b.comment}>{b.comment || '-'}</span>,
className: 'max-w-xs truncate',
},
{ header: 'Created', render: (b) => formatDate(b.created) },
];

export function BankDatasTable({ bankDatas }: { bankDatas: BankDataInfo[] }): JSX.Element {
return <DataTable data={bankDatas} columns={bankDatasColumns} emptyLabel="No bank data" />;
}

const buyRoutesColumns: ColumnDef<BuyRouteInfo>[] = [
{ header: 'ID', render: (b) => b.id },
{ header: 'IBAN', align: 'left', render: (b) => b.iban || '-', className: 'font-mono' },
{ header: 'Bank Usage', render: (b) => b.bankUsage, className: 'font-mono' },
{ header: 'Asset', render: (b) => b.assetName },
{ header: 'Blockchain', align: 'left', render: (b) => b.blockchain },
{ header: 'Volume', align: 'right', render: (b) => b.volume?.toFixed(2) },
{ header: 'Active', render: (b) => boolBadge(b.active) },
{ header: 'Created', render: (b) => formatDate(b.created) },
];

export function BuyRoutesTable({ buyRoutes }: { buyRoutes: BuyRouteInfo[] }): JSX.Element {
return <DataTable data={buyRoutes} columns={buyRoutesColumns} emptyLabel="No buy routes" />;
}

const sellRoutesColumns: ColumnDef<SellRouteInfo>[] = [
{ header: 'ID', render: (s) => s.id },
{ header: 'IBAN', align: 'left', render: (s) => s.iban, className: 'font-mono' },
{ header: 'Fiat', render: (s) => s.fiatName || '-' },
{ header: 'Volume', align: 'right', render: (s) => s.volume?.toFixed(2) },
{ header: 'Active', render: (s) => boolBadge(s.active) },
{ header: 'Created', render: (s) => formatDate(s.created) },
];

export function SellRoutesTable({ sellRoutes }: { sellRoutes: SellRouteInfo[] }): JSX.Element {
return <DataTable data={sellRoutes} columns={sellRoutesColumns} emptyLabel="No sell routes" />;
}
31 changes: 31 additions & 0 deletions src/components/compliance/file-preview-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
interface FilePreviewPanelProps {
preview?: { url: string; contentType: string; name: string };
label: string;
onClose: () => void;
}

export function FilePreviewPanel({ preview, label, onClose }: FilePreviewPanelProps): JSX.Element {
return (
<div className="flex-1 min-w-[400px]">
<div className="flex justify-between items-center mb-2">
<h2 className="text-dfxGray-700">{preview ? preview.name : label}</h2>
{preview && (
<button onClick={onClose} className="text-dfxGray-700 hover:text-dfxBlue-800 text-2xl font-bold px-2">
×
</button>
)}
</div>
<div className="bg-white rounded-lg shadow-sm h-[70vh] flex items-center justify-center">
{preview ? (
preview.contentType.includes('pdf') ? (
<embed src={`${preview.url}#navpanes=0`} type="application/pdf" className="w-full h-full" />
) : (
<img src={preview.url} alt={preview.name} className="max-w-full max-h-full object-contain" />
)
) : (
<div className="text-dfxGray-700 text-sm">Click a file to preview</div>
)}
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions src/components/compliance/ip-logs-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IpLogInfo } from 'src/hooks/compliance.hook';
import { boolBadge, formatDateTimeShort } from 'src/util/compliance-helpers';

interface IpLogsPanelProps {
ipLogs: IpLogInfo[];
}

export function IpLogsPanel({ ipLogs }: IpLogsPanelProps): JSX.Element {
return (
<div className="border-t border-dfxGray-500 pt-4">
<h2 className="text-dfxGray-700 mb-2">IP Logs ({ipLogs?.length || 0})</h2>
<div className="bg-white rounded-lg shadow-sm max-h-[35vh] overflow-auto scroll-shadow">
{ipLogs?.length > 0 ? (
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-dfxGray-300">
<tr>
<th className="px-2 py-1.5 text-left text-xs font-semibold text-dfxBlue-800">IP / Country</th>
<th className="px-2 py-1.5 text-left text-xs font-semibold text-dfxBlue-800">Endpoint</th>
<th className="px-2 py-1.5 text-center text-xs font-semibold text-dfxBlue-800">Status</th>
<th className="px-2 py-1.5 text-center text-xs font-semibold text-dfxBlue-800">Date</th>
</tr>
</thead>
<tbody>
{ipLogs.map((log) => (
<tr
key={log.id}
className={`border-b border-dfxGray-300 transition-colors hover:bg-dfxGray-300 ${!log.result ? 'bg-dfxRed-100/15' : ''}`}
>
<td className="px-2 py-1.5 text-xs text-dfxBlue-800 text-left">
<span className="font-mono">{log.ip}</span>
{log.country && <span className="ml-1 text-dfxGray-700">({log.country})</span>}
</td>
<td className="px-2 py-1.5 text-xs text-dfxBlue-800 text-left">{log.url.replace('/v1/', '')}</td>
<td className="px-2 py-1.5 text-xs text-center">{boolBadge(log.result, 'Pass', 'Fail')}</td>
<td className="px-2 py-1.5 text-xs text-dfxBlue-800 text-center whitespace-nowrap">
{formatDateTimeShort(log.created)}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="p-4 text-dfxGray-700 text-sm">No IP logs</div>
)}
</div>
</div>
);
}
47 changes: 47 additions & 0 deletions src/components/compliance/kyc-files-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { KycFile } from 'src/hooks/compliance.hook';

interface KycFilesPanelProps {
kycFiles: KycFile[];
label: string;
onOpenFile: (file: KycFile) => void;
}

export function KycFilesPanel({ kycFiles, label, onOpenFile }: KycFilesPanelProps): JSX.Element {
return (
<div className="border-t border-dfxGray-500 pt-4">
<h2 className="text-dfxGray-700 mb-2">
{label} ({kycFiles?.length || 0})
</h2>
<div className="bg-white rounded-lg shadow-sm max-h-[35vh] overflow-auto scroll-shadow">
{kycFiles?.length > 0 ? (
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-dfxGray-300">
<tr>
<th className="px-3 py-2 text-left text-sm font-semibold text-dfxBlue-800">ID</th>
<th className="px-3 py-2 text-center text-sm font-semibold text-dfxBlue-800">Name</th>
<th className="px-3 py-2 text-center text-sm font-semibold text-dfxBlue-800">Type</th>
</tr>
</thead>
<tbody>
{kycFiles.map((file) => (
<tr
key={file.id}
className="group border-b border-dfxGray-300 transition-colors hover:bg-dfxBlue-400 cursor-pointer"
onClick={() => onOpenFile(file)}
>
<td className="px-3 py-2 text-sm text-dfxBlue-800 text-left group-hover:text-white">{file.id}</td>
<td className="px-3 py-2 text-sm text-dfxBlue-800 text-center group-hover:text-white underline">
{file.name}
</td>
<td className="px-3 py-2 text-sm text-dfxBlue-800 text-center group-hover:text-white">{file.type}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="p-4 text-dfxGray-700 text-sm">No KYC files</div>
)}
</div>
</div>
);
}
47 changes: 47 additions & 0 deletions src/components/compliance/recommendation-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NavigateFunction } from 'react-router-dom';
import { KycStepInfo } from 'src/hooks/compliance.hook';
import { formatDate, statusBadge } from 'src/util/compliance-helpers';

interface RecommendationPanelProps {
kycSteps: KycStepInfo[];
userDataId: string;
navigate: NavigateFunction;
}

export function RecommendationPanel({ kycSteps, userDataId, navigate }: RecommendationPanelProps): JSX.Element {
const recommendations = kycSteps?.filter((s) => s.name === 'Recommendation') || [];

return (
<div>
<h2 className="text-dfxGray-700 mb-2">Recommendation ({recommendations.length})</h2>
<div className="bg-white rounded-lg shadow-sm max-h-[35vh] overflow-auto scroll-shadow">
{recommendations.length > 0 ? (
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-dfxGray-300">
<tr>
<th className="px-3 py-2 text-center text-sm font-semibold text-dfxBlue-800">Status</th>
<th className="px-3 py-2 text-center text-sm font-semibold text-dfxBlue-800">Created</th>
</tr>
</thead>
<tbody>
{recommendations.map((step) => (
<tr
key={step.id}
className="border-b border-dfxGray-300 transition-colors hover:bg-dfxBlue-400 cursor-pointer group"
onClick={() => navigate(`/compliance/user/${userDataId}/kyc-step/${step.id}`, { state: { step } })}
>
<td className="px-3 py-2 text-sm text-center">{statusBadge(step.status)}</td>
<td className="px-3 py-2 text-sm text-dfxBlue-800 text-center group-hover:text-white">
{formatDate(step.created)}
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="p-4 text-dfxGray-700 text-sm">No recommendation</div>
)}
</div>
</div>
);
}
Loading
Loading