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
1 change: 1 addition & 0 deletions src/hooks/compliance.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export interface TransactionListEntry {
id: number;
type?: string;
accountId?: number;
kycFileId?: number;
name?: string;
domicile?: string;
created?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/screens/compliance-kyc-files-details.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function ComplianceKycFilesDetailsScreen(): JSX.Element {

function formatVolume(volume?: number): string {
if (volume == null) return '-';
return Math.round(volume).toLocaleString('de-CH');
return volume.toFixed(2);
}

function getStatus(entry: KycFileListEntry): string {
Expand Down
224 changes: 129 additions & 95 deletions src/screens/compliance-transaction-list.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
StyledLoadingSpinner,
StyledVerticalStack,
} from '@dfx.swiss/react-components';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ErrorHint } from 'src/components/error-hint';
import { useSettingsContext } from 'src/contexts/settings.context';
import { TransactionListEntry, useCompliance } from 'src/hooks/compliance.hook';
Expand All @@ -35,6 +35,12 @@ export default function ComplianceTransactionListScreen(): JSX.Element {
const [createdTo, setCreatedTo] = useState<string>(today);
const [outputFrom, setOutputFrom] = useState<string>(threeDaysAgo);
const [outputTo, setOutputTo] = useState<string>(today);
const [onlyWithKycFile, setOnlyWithKycFile] = useState<boolean>(false);

const filteredData = useMemo(
() => (onlyWithKycFile ? data.filter((e) => e.kycFileId && e.kycFileId > 0) : data),
[data, onlyWithKycFile],
);

function formatDate(dateString?: string): string {
if (!dateString) return '-';
Expand All @@ -47,14 +53,15 @@ export default function ComplianceTransactionListScreen(): JSX.Element {

function formatChf(value?: number): string {
if (value == null) return '-';
return Math.round(value).toLocaleString('de-CH');
return value.toFixed(2);
}

function exportCsv() {
const headers = [
'Id',
'Type',
'AccountId',
'KycFileId',
'Name',
'Domizil',
'Created',
Expand All @@ -64,10 +71,11 @@ export default function ComplianceTransactionListScreen(): JSX.Element {
'CHF Value',
'TMER',
];
const rows = data.map((entry) => [
const rows = filteredData.map((entry) => [
entry.id,
entry.type ?? '',
entry.accountId ?? '',
entry.kycFileId ?? '',
entry.name ?? '',
entry.domicile ?? '',
formatDate(entry.created),
Expand All @@ -91,7 +99,7 @@ export default function ComplianceTransactionListScreen(): JSX.Element {
URL.revokeObjectURL(url);
}

useEffect(() => {
const loadData = useCallback(() => {
if (!isLoggedIn) return;

setIsLoading(true);
Expand All @@ -110,15 +118,11 @@ export default function ComplianceTransactionListScreen(): JSX.Element {
.finally(() => setIsLoading(false));
}, [isLoggedIn, getTransactionList, createdFrom, createdTo, outputFrom, outputTo]);

useLayoutOptions({ title: translate('screens/compliance', 'Transaction List'), noMaxWidth: true });

if (isLoading) {
return <StyledLoadingSpinner size={SpinnerSize.LG} />;
}
useEffect(() => {
loadData();
}, [isLoggedIn]);

if (error) {
return <ErrorHint message={error} />;
}
useLayoutOptions({ title: translate('screens/compliance', 'Transaction List'), noMaxWidth: true });

return (
<StyledVerticalStack gap={6} full>
Expand Down Expand Up @@ -173,106 +177,136 @@ export default function ComplianceTransactionListScreen(): JSX.Element {

<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-dfxBlue-800">&nbsp;</span>
<button
className="px-3 py-2 text-sm text-dfxBlue-800 hover:bg-dfxGray-300 rounded-lg transition-colors"
onClick={() => {
setCreatedFrom('');
setCreatedTo('');
setOutputFrom('');
setOutputTo('');
}}
>
{translate('screens/compliance', 'Reset')}
</button>
<div className="flex gap-2">
<button
className="px-4 py-2 text-sm text-white bg-dfxBlue-800 hover:bg-dfxBlue-800/80 rounded-lg transition-colors disabled:opacity-50"
onClick={loadData}
disabled={isLoading}
>
{isLoading ? translate('screens/compliance', 'Loading...') : translate('general/actions', 'Search')}
</button>
<button
className="px-3 py-2 text-sm text-dfxBlue-800 hover:bg-dfxGray-300 rounded-lg transition-colors"
onClick={() => {
setCreatedFrom('');
setCreatedTo('');
setOutputFrom('');
setOutputTo('');
}}
>
{translate('screens/compliance', 'Reset')}
</button>
</div>
</div>

<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-dfxBlue-800">&nbsp;</span>
<label className="flex items-center gap-2 px-3 py-2 cursor-pointer">
<input
type="checkbox"
checked={onlyWithKycFile}
onChange={(e) => setOnlyWithKycFile(e.target.checked)}
className="w-4 h-4"
/>
<span className="text-sm text-dfxBlue-800">{translate('screens/compliance', 'Nur mit KYC-File')}</span>
</label>
</div>

<div className="ml-auto flex items-center gap-4">
<span className="text-sm text-dfxGray-700">
{data.length} {translate('screens/compliance', 'entries')}
{filteredData.length} {translate('screens/compliance', 'entries')}
</span>
<button
className="p-2 rounded-lg hover:bg-dfxBlue-800/10 transition-colors cursor-pointer"
onClick={exportCsv}
title={translate('screens/compliance', 'Export CSV')}
disabled={data.length === 0}
disabled={filteredData.length === 0}
>
<DfxIcon icon={IconVariant.ARROW_DOWN} color={IconColor.BLUE} size={IconSize.MD} />
</button>
</div>
</div>

<div className="w-full overflow-x-auto">
<table className="w-full border-collapse bg-white rounded-lg shadow-sm">
<thead>
<tr className="bg-dfxGray-300">
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Id')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Type')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'AccountId')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Name')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Domizil')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Created')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Transaktionsdatum')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Output Datum')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Assets')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'CHF Value')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'TMER')}
</th>
</tr>
</thead>
<tbody>
{data.length > 0 ? (
data.map((entry) => (
<tr
key={entry.id}
className={`border-b border-dfxGray-300 transition-colors hover:bg-dfxGray-300 ${entry.accountId ? 'cursor-pointer' : ''}`}
onClick={() => entry.accountId && navigate(`compliance/user/${entry.accountId}`)}
>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{entry.id}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.type ?? '-'}</td>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{entry.accountId ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.name ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.domicile ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.created)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.eventDate)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.outputDate)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.assets ?? '-'}</td>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{formatChf(entry.amountInChf)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">
{entry.highRisk ? 'Ja' : 'Nein'}
{error && <ErrorHint message={error} />}

{isLoading && data.length === 0 ? (
<StyledLoadingSpinner size={SpinnerSize.LG} />
) : (
<div className="w-full overflow-x-auto">
<table className="w-full border-collapse bg-white rounded-lg shadow-sm">
<thead>
<tr className="bg-dfxGray-300">
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Id')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Type')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'AccountId')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'KycFileId')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Name')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Domizil')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Created')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Transaktionsdatum')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Output Datum')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'Assets')}
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'CHF Value')}
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-dfxBlue-800">
{translate('screens/compliance', 'TMER')}
</th>
</tr>
</thead>
<tbody>
{filteredData.length > 0 ? (
filteredData.map((entry) => (
<tr
key={entry.id}
className={`border-b border-dfxGray-300 transition-colors hover:bg-dfxGray-300 ${entry.accountId ? 'cursor-pointer' : ''}`}
onClick={() => entry.accountId && navigate(`compliance/user/${entry.accountId}`)}
>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{entry.id}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.type ?? '-'}</td>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{entry.accountId ?? '-'}</td>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{entry.kycFileId ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.name ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.domicile ?? '-'}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.created)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.eventDate)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{formatDate(entry.outputDate)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.assets ?? '-'}</td>
<td className="px-4 py-3 text-right text-sm text-dfxBlue-800">{formatChf(entry.amountInChf)}</td>
<td className="px-4 py-3 text-left text-sm text-dfxBlue-800">{entry.highRisk ? 'Ja' : 'Nein'}</td>
</tr>
))
) : (
<tr>
<td colSpan={12} className="px-4 py-3 text-center text-dfxGray-700">
{translate('screens/compliance', 'No entries found')}
</td>
</tr>
))
) : (
<tr>
<td colSpan={11} className="px-4 py-3 text-center text-dfxGray-700">
{translate('screens/compliance', 'No entries found')}
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</tbody>
</table>
</div>
)}
</StyledVerticalStack>
);
}
Loading