From 209d65e44612073a76a7810e5348a3bb6b5d183f Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 12:51:30 +0200
Subject: [PATCH 01/13] feat: bank transaction details screen in compliance
(#1064)
* feat: add bank transaction details screen in compliance
Adds a dedicated details page for unassigned bank transactions in the
compliance search results. Replaces the inline return icon with a chevron
that opens the new details view, which lists all fields returned by the
search API and exposes a Return button linking to the existing refund flow.
The search row data is cached in sessionStorage on navigation to survive
the URL-param stripping in app-handling.context that wipes router state.
* style: use blue color for Return button instead of default red
* refactor: align bank-tx details with existing compliance screen style
- Switch to card-based layout matching compliance-support-issue
- Replace alarmist ErrorHint with neutral no-data message on the
missing-state fallback
- Explain the sessionStorage cache with a comment
- Drop leading slash in navigate() call to match nearby compliance
routes
- Use static title Bank Transaction Details to differentiate from the
refund screen
* refactor: extract bank-tx cache helper and remove redundant header
- Move sessionStorage get/set into src/util/bank-tx-cache.ts so list
and details screens share one cache-key definition
- Drop the in-card h2 Bank Transaction in the details screen since
the layout title already reads Bank Transaction Details
* refactor: render bank-tx fields with shared DetailRow helper
Use the DetailRow component from compliance-helpers so the details
screen matches how bank-tx fields are rendered elsewhere in the
compliance UI (see transactions-tab.tsx). Also aligns labels (Name
instead of User name) and switches IBAN to mono formatting to match
that pattern.
* refactor: prefix bank-tx cache keys with dfx. to match session-store convention
---
src/App.tsx | 5 ++
src/screens/compliance-bank-tx.screen.tsx | 68 +++++++++++++++++++++++
src/screens/compliance.screen.tsx | 18 +++---
src/util/bank-tx-cache.ts | 20 +++++++
4 files changed, 103 insertions(+), 8 deletions(-)
create mode 100644 src/screens/compliance-bank-tx.screen.tsx
create mode 100644 src/util/bank-tx-cache.ts
diff --git a/src/App.tsx b/src/App.tsx
index 0410e662..0381a05d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -55,6 +55,7 @@ const EditMailScreen = lazy(() => import('./screens/edit-mail.screen'));
const SafeScreen = lazy(() => import('./screens/safe.screen'));
const TelegramSupportScreen = lazy(() => import('./screens/telegram-support.screen'));
const ComplianceScreen = lazy(() => import('./screens/compliance.screen'));
+const ComplianceBankTxScreen = lazy(() => import('./screens/compliance-bank-tx.screen'));
const ComplianceBankTxReturnScreen = lazy(() => import('./screens/compliance-bank-tx-return.screen'));
const ComplianceKycFilesScreen = lazy(() => import('./screens/compliance-kyc-files.screen'));
const ComplianceKycFilesDetailsScreen = lazy(() => import('./screens/compliance-kyc-files-details.screen'));
@@ -367,6 +368,10 @@ export const Routes = [
path: 'compliance/recommendations/:id',
element: withSuspense(),
},
+ {
+ path: 'compliance/bank-tx/:id',
+ element: withSuspense(),
+ },
{
path: 'compliance/bank-tx/:id/return',
element: withSuspense(),
diff --git a/src/screens/compliance-bank-tx.screen.tsx b/src/screens/compliance-bank-tx.screen.tsx
new file mode 100644
index 00000000..bedfaae9
--- /dev/null
+++ b/src/screens/compliance-bank-tx.screen.tsx
@@ -0,0 +1,68 @@
+import { StyledButton, StyledButtonColor, StyledButtonWidth, StyledVerticalStack } from '@dfx.swiss/react-components';
+import { useLocation, useParams } from 'react-router-dom';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { BankTxSearchResult } from 'src/hooks/compliance.hook';
+import { useComplianceGuard } from 'src/hooks/guard.hook';
+import { useLayoutOptions } from 'src/hooks/layout-config.hook';
+import { useNavigation } from 'src/hooks/navigation.hook';
+import { readCachedBankTx } from 'src/util/bank-tx-cache';
+import { DetailRow } from 'src/util/compliance-helpers';
+
+function loadBankTx(id?: string, state?: unknown): BankTxSearchResult | undefined {
+ const fromState = (state as { bankTx?: BankTxSearchResult } | null)?.bankTx;
+ if (fromState) return fromState;
+ return id ? readCachedBankTx(id) : undefined;
+}
+
+export default function ComplianceBankTxScreen(): JSX.Element {
+ useComplianceGuard();
+
+ const { id } = useParams<{ id: string }>();
+ const { translate } = useSettingsContext();
+ const { navigate, goBack } = useNavigation();
+ const location = useLocation();
+ const bankTx = loadBankTx(id, location.state);
+
+ useLayoutOptions({ title: 'Bank Transaction Details', backButton: true });
+
+ if (!bankTx) {
+ return (
+
+ No data available. Please open from search.
+
+
+ );
+ }
+
+ return (
+
+
+
+ {bankTx.transactionId && (
+
navigate(`compliance/bank-tx/${bankTx.transactionId}/return`)}
+ width={StyledButtonWidth.FULL}
+ color={StyledButtonColor.BLUE}
+ />
+ )}
+
+ );
+}
diff --git a/src/screens/compliance.screen.tsx b/src/screens/compliance.screen.tsx
index 0e89891e..7c21a6ec 100644
--- a/src/screens/compliance.screen.tsx
+++ b/src/screens/compliance.screen.tsx
@@ -26,6 +26,7 @@ import {
import { useComplianceGuard } from 'src/hooks/guard.hook';
import { useLayoutOptions } from 'src/hooks/layout-config.hook';
import { useNavigation } from 'src/hooks/navigation.hook';
+import { cacheBankTx } from 'src/util/bank-tx-cache';
interface FormData {
key: string;
@@ -186,14 +187,15 @@ export default function ComplianceScreen(): JSX.Element {
label: '',
render: (b: BankTxSearchResult) => (
- {b.transactionId && (
- navigate(`compliance/bank-tx/${b.transactionId}/return`)}
- />
- )}
+ {
+ cacheBankTx(b);
+ navigate(`compliance/bank-tx/${b.id}`);
+ }}
+ />
),
},
diff --git a/src/util/bank-tx-cache.ts b/src/util/bank-tx-cache.ts
new file mode 100644
index 00000000..3eb27288
--- /dev/null
+++ b/src/util/bank-tx-cache.ts
@@ -0,0 +1,20 @@
+import { BankTxSearchResult } from 'src/hooks/compliance.hook';
+
+// Used to carry a bank-tx row from the compliance search list into the
+// details screen. sessionStorage is needed because app-handling.context
+// calls history.replaceState(undefined, ...) on mount, which wipes router
+// state, and there is no backend endpoint to refetch a single bank-tx.
+const BANK_TX_CACHE_PREFIX = 'dfx.bankTx.';
+
+export function cacheBankTx(bankTx: BankTxSearchResult): void {
+ sessionStorage.setItem(`${BANK_TX_CACHE_PREFIX}${bankTx.id}`, JSON.stringify(bankTx));
+}
+
+export function readCachedBankTx(id: string): BankTxSearchResult | undefined {
+ try {
+ const cached = sessionStorage.getItem(`${BANK_TX_CACHE_PREFIX}${id}`);
+ return cached ? (JSON.parse(cached) as BankTxSearchResult) : undefined;
+ } catch {
+ return undefined;
+ }
+}
From 192f933f71f80ee745952cc9b6f33bda27bc2207 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 13:16:11 +0200
Subject: [PATCH 02/13] feat: recall creation for bank transactions in
compliance (#1066)
* feat: add recall creation for bank transactions in compliance
Adds a Recall erfassen button to the bank transaction details screen
that opens a form to create a recall (POST /recall). Form captures
reason, fee and comment; sequence is fixed to 1 and bankTxId is taken
from the URL.
* fix: cancel/back buttons return to details, pre-fill fee and comment
- Cancel and success Back now use navigate(-1) so they return to the
bank-tx details screen instead of falling through to /account
- Pre-fill fee with 500 and comment with n.a. as sensible defaults
* style: translate submit label and success title for consistency
Match the return-screen pattern where button labels and success
messages use translate().
* style: translate form title and field labels
Match the refund-creditor-fields pattern where field labels and the
layout title go through translate() so future translations can
override the English defaults.
* style: translate details-page action button labels
---
src/App.tsx | 5 +
src/dto/recall.dto.ts | 10 ++
src/hooks/compliance.hook.ts | 11 +-
.../compliance-bank-tx-recall.screen.tsx | 143 ++++++++++++++++++
src/screens/compliance-bank-tx.screen.tsx | 9 +-
5 files changed, 176 insertions(+), 2 deletions(-)
create mode 100644 src/screens/compliance-bank-tx-recall.screen.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 0381a05d..e522637c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -56,6 +56,7 @@ const SafeScreen = lazy(() => import('./screens/safe.screen'));
const TelegramSupportScreen = lazy(() => import('./screens/telegram-support.screen'));
const ComplianceScreen = lazy(() => import('./screens/compliance.screen'));
const ComplianceBankTxScreen = lazy(() => import('./screens/compliance-bank-tx.screen'));
+const ComplianceBankTxRecallScreen = lazy(() => import('./screens/compliance-bank-tx-recall.screen'));
const ComplianceBankTxReturnScreen = lazy(() => import('./screens/compliance-bank-tx-return.screen'));
const ComplianceKycFilesScreen = lazy(() => import('./screens/compliance-kyc-files.screen'));
const ComplianceKycFilesDetailsScreen = lazy(() => import('./screens/compliance-kyc-files-details.screen'));
@@ -372,6 +373,10 @@ export const Routes = [
path: 'compliance/bank-tx/:id',
element: withSuspense(),
},
+ {
+ path: 'compliance/bank-tx/:id/recall',
+ element: withSuspense(),
+ },
{
path: 'compliance/bank-tx/:id/return',
element: withSuspense(),
diff --git a/src/dto/recall.dto.ts b/src/dto/recall.dto.ts
index bedc8601..4280ea0b 100644
--- a/src/dto/recall.dto.ts
+++ b/src/dto/recall.dto.ts
@@ -20,3 +20,13 @@ export interface RecallListEntry {
checkoutTx?: { id: number };
user?: { id: number };
}
+
+export interface CreateRecallDto {
+ bankTxId?: number;
+ checkoutTxId?: number;
+ sequence: number;
+ comment: string;
+ fee: number;
+ reason: RecallReason;
+ userId?: number;
+}
diff --git a/src/hooks/compliance.hook.ts b/src/hooks/compliance.hook.ts
index 374e352c..e61dec4b 100644
--- a/src/hooks/compliance.hook.ts
+++ b/src/hooks/compliance.hook.ts
@@ -10,7 +10,7 @@ import {
} from '@dfx.swiss/react';
import { MrosListEntry } from 'src/dto/mros.dto';
import { CustodyOrderListEntry } from 'src/dto/order.dto';
-import { RecallListEntry } from 'src/dto/recall.dto';
+import { CreateRecallDto, RecallListEntry } from 'src/dto/recall.dto';
import { electronicFormatIBAN, isValidIBAN } from 'ibantools';
import { useMemo } from 'react';
import { downloadFile, downloadPdfFromString, filenameDateFormat } from 'src/util/utils';
@@ -525,6 +525,14 @@ export function useCompliance() {
});
}
+ async function createRecall(dto: CreateRecallDto): Promise {
+ return call({
+ url: 'recall',
+ method: 'POST',
+ data: dto,
+ });
+ }
+
async function getRecommendationGraph(userDataId: number): Promise {
return call({
url: `support/recommendation-graph/${userDataId}`,
@@ -664,6 +672,7 @@ export function useCompliance() {
approveCustodyOrder,
getMrosList,
getRecalls,
+ createRecall,
updateKycStep,
updateUserData,
createLimitRequest,
diff --git a/src/screens/compliance-bank-tx-recall.screen.tsx b/src/screens/compliance-bank-tx-recall.screen.tsx
new file mode 100644
index 00000000..b4a73f61
--- /dev/null
+++ b/src/screens/compliance-bank-tx-recall.screen.tsx
@@ -0,0 +1,143 @@
+import { Utils, Validations } from '@dfx.swiss/react';
+import {
+ Form,
+ StyledButton,
+ StyledButtonColor,
+ StyledButtonWidth,
+ StyledDropdown,
+ StyledInput,
+ StyledVerticalStack,
+} from '@dfx.swiss/react-components';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useParams } from 'react-router-dom';
+import { ErrorHint } from 'src/components/error-hint';
+import { useLayoutContext } from 'src/contexts/layout.context';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { RecallReason } from 'src/dto/recall.dto';
+import { useCompliance } from 'src/hooks/compliance.hook';
+import { useComplianceGuard } from 'src/hooks/guard.hook';
+import { useLayoutOptions } from 'src/hooks/layout-config.hook';
+import { useNavigation } from 'src/hooks/navigation.hook';
+
+interface FormData {
+ reason: RecallReason;
+ comment: string;
+ fee: string;
+}
+
+export default function ComplianceBankTxRecallScreen(): JSX.Element {
+ useComplianceGuard();
+
+ const { id } = useParams<{ id: string }>();
+ const { translate, translateError } = useSettingsContext();
+ const { createRecall } = useCompliance();
+ const { navigate } = useNavigation();
+ const { rootRef } = useLayoutContext();
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState();
+ const [success, setSuccess] = useState(false);
+
+ const {
+ control,
+ handleSubmit,
+ formState: { isValid, errors },
+ } = useForm({
+ mode: 'onTouched',
+ defaultValues: { fee: '500', comment: 'n.a.' },
+ });
+
+ useLayoutOptions({ title: translate('screens/compliance', 'Recall erfassen'), backButton: true });
+
+ async function onSubmit(formData: FormData) {
+ if (!id) return;
+
+ setIsSubmitting(true);
+ setError(undefined);
+
+ try {
+ await createRecall({
+ bankTxId: +id,
+ sequence: 1,
+ reason: formData.reason,
+ comment: formData.comment,
+ fee: Number(formData.fee),
+ });
+ setSuccess(true);
+ } catch (e: unknown) {
+ setError(e instanceof Error ? e.message : 'Unknown error');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ const rules = Utils.createRules({
+ reason: Validations.Required,
+ comment: Validations.Required,
+ fee: [Validations.Required, Validations.Custom((v) => (isNaN(Number(v)) ? 'pattern' : true))],
+ });
+
+ if (success) {
+ return (
+
+
+
+ {translate('screens/compliance', 'Recall created successfully')}
+
+ navigate(-1)} width={StyledButtonWidth.MD} />
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/screens/compliance-bank-tx.screen.tsx b/src/screens/compliance-bank-tx.screen.tsx
index bedfaae9..851e0e55 100644
--- a/src/screens/compliance-bank-tx.screen.tsx
+++ b/src/screens/compliance-bank-tx.screen.tsx
@@ -55,9 +55,16 @@ export default function ComplianceBankTxScreen(): JSX.Element {
+ navigate(`compliance/bank-tx/${bankTx.id}/recall`)}
+ width={StyledButtonWidth.FULL}
+ color={StyledButtonColor.BLUE}
+ />
+
{bankTx.transactionId && (
navigate(`compliance/bank-tx/${bankTx.transactionId}/return`)}
width={StyledButtonWidth.FULL}
color={StyledButtonColor.BLUE}
From 87f8acdfb0fdd31c6c31478f5dadd2d8807afc35 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 13:46:23 +0200
Subject: [PATCH 03/13] feat: add recall button to user-data transactions tab
(#1068)
* feat: add recall button to user-data transactions tab
Adds a Recall action button to the expanded transaction detail, shown
for rows with a linked bank tx. Opens a modal that captures reason, fee
and comment and submits POST /recall. userId is derived server-side from
the bank tx (see api PR feat/recall-auto-derive-user).
* refactor: use reset() without args to rely on useForm defaultValues
Matches the ChargebackModal pattern and avoids repeating the default
values in three places.
---
src/components/compliance/recall-modal.tsx | 155 ++++++++++++++++++
.../compliance/transactions-tab.tsx | 62 ++++---
2 files changed, 193 insertions(+), 24 deletions(-)
create mode 100644 src/components/compliance/recall-modal.tsx
diff --git a/src/components/compliance/recall-modal.tsx b/src/components/compliance/recall-modal.tsx
new file mode 100644
index 00000000..d0d15eef
--- /dev/null
+++ b/src/components/compliance/recall-modal.tsx
@@ -0,0 +1,155 @@
+import { Utils, Validations } from '@dfx.swiss/react';
+import {
+ Form,
+ StyledButton,
+ StyledButtonColor,
+ StyledButtonWidth,
+ StyledDropdown,
+ StyledInput,
+ StyledVerticalStack,
+} from '@dfx.swiss/react-components';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { ErrorHint } from 'src/components/error-hint';
+import { Modal } from 'src/components/modal';
+import { useLayoutContext } from 'src/contexts/layout.context';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { RecallReason } from 'src/dto/recall.dto';
+import { useCompliance } from 'src/hooks/compliance.hook';
+
+interface RecallModalProps {
+ readonly isOpen: boolean;
+ readonly bankTxId: number | undefined;
+ readonly onClose: () => void;
+ readonly onSuccess: () => void;
+}
+
+interface RecallFormData {
+ reason: RecallReason;
+ comment: string;
+ fee: string;
+}
+
+export function RecallModal({ isOpen, bankTxId, onClose, onSuccess }: RecallModalProps): JSX.Element {
+ const { translate, translateError } = useSettingsContext();
+ const { createRecall } = useCompliance();
+ const { rootRef } = useLayoutContext();
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState();
+
+ const {
+ control,
+ handleSubmit,
+ formState: { isValid, errors },
+ reset,
+ } = useForm({
+ mode: 'onTouched',
+ defaultValues: { fee: '500', comment: 'n.a.' },
+ });
+
+ useEffect(() => {
+ if (!isOpen) {
+ setError(undefined);
+ reset();
+ }
+ }, [isOpen]);
+
+ async function onSubmit(formData: RecallFormData): Promise {
+ if (!bankTxId) return;
+
+ setIsSubmitting(true);
+ setError(undefined);
+
+ try {
+ await createRecall({
+ bankTxId,
+ sequence: 1,
+ reason: formData.reason,
+ comment: formData.comment,
+ fee: Number(formData.fee),
+ });
+ onSuccess();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Unknown error');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ function handleClose(): void {
+ reset();
+ setError(undefined);
+ onClose();
+ }
+
+ const rules = Utils.createRules({
+ reason: Validations.Required,
+ comment: Validations.Required,
+ fee: [Validations.Required, Validations.Custom((v) => (isNaN(Number(v)) ? 'pattern' : true))],
+ });
+
+ if (!isOpen) return <>>;
+
+ return (
+
+
+
+ {translate('screens/compliance', 'Recall erfassen')}
+
+
+
+
+
+ );
+}
diff --git a/src/components/compliance/transactions-tab.tsx b/src/components/compliance/transactions-tab.tsx
index 96bff968..b9d930b8 100644
--- a/src/components/compliance/transactions-tab.tsx
+++ b/src/components/compliance/transactions-tab.tsx
@@ -2,6 +2,7 @@ import { Transaction, TransactionState, useTransaction } from '@dfx.swiss/react'
import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components';
import { Fragment, useState } from 'react';
import { ChargebackModal } from 'src/components/compliance/chargeback-modal';
+import { RecallModal } from 'src/components/compliance/recall-modal';
import { ConfirmDialog } from 'src/components/confirm-dialog';
import { BankTxInfo, CryptoInputInfo, TransactionInfo, useCompliance } from 'src/hooks/compliance.hook';
import { DetailRow, TransactionDetailRows, formatDate, statusBadge } from 'src/util/compliance-helpers';
@@ -44,6 +45,7 @@ export function TransactionsTable({
const [stopConfirmTxId, setStopConfirmTxId] = useState();
const [stopError, setStopError] = useState();
const [chargebackTxId, setChargebackTxId] = useState();
+ const [recallBankTxId, setRecallBankTxId] = useState();
async function confirmStop(): Promise {
const txId = stopConfirmTxId;
@@ -294,33 +296,30 @@ export function TransactionsTable({
return (
<>
- {((tx.type === 'BuyCrypto' && !tx.isCompleted) ||
- ([
- TransactionState.FAILED,
- TransactionState.CHECK_PENDING,
- TransactionState.KYC_REQUIRED,
- TransactionState.LIMIT_EXCEEDED,
- TransactionState.UNASSIGNED,
- ].includes(detail.state) &&
- !detail.chargebackAmount)) && (
-
- {tx.type === 'BuyCrypto' && !tx.isCompleted && (
-
- )}
- {[
+ {(() => {
+ const canStop = tx.type === 'BuyCrypto' && !tx.isCompleted;
+ const canChargeback =
+ [
TransactionState.FAILED,
TransactionState.CHECK_PENDING,
TransactionState.KYC_REQUIRED,
TransactionState.LIMIT_EXCEEDED,
TransactionState.UNASSIGNED,
- ].includes(detail.state) &&
- !detail.chargebackAmount && (
+ ].includes(detail.state) && !detail.chargebackAmount;
+ const canRecall = bankTx != null;
+ if (!canStop && !canChargeback && !canRecall) return null;
+ return (
+
+ {canStop && (
+
+ )}
+ {canChargeback && (
)}
-
- )}
+ {canRecall && (
+
+ )}
+
+ );
+ })()}
>
);
})()
@@ -378,6 +386,12 @@ export function TransactionsTable({
onStopped?.();
}}
/>
+ setRecallBankTxId(undefined)}
+ onSuccess={() => setRecallBankTxId(undefined)}
+ />
);
}
From 696cd772a25a25b1f2d42a3260a742c504de1a42 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 14:01:42 +0200
Subject: [PATCH 04/13] feat: polish compliance bank tx list after API search
extension (#1067)
* feat: rename compliance bank tx section and hide return button for non-unassigned types
* i18n: update de/fr/it translation keys for matching bank transactions section
* refactor: move BankTxUnassignedTypes constant to compliance-helpers
* i18n: shorten section title to Bank Transactions for consistency with sibling sections
---
src/screens/compliance-bank-tx.screen.tsx | 4 ++--
src/screens/compliance.screen.tsx | 4 +---
src/translations/languages/de.json | 2 +-
src/translations/languages/fr.json | 2 +-
src/translations/languages/it.json | 2 +-
src/util/compliance-helpers.tsx | 4 ++++
6 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/screens/compliance-bank-tx.screen.tsx b/src/screens/compliance-bank-tx.screen.tsx
index 851e0e55..6c0a0e72 100644
--- a/src/screens/compliance-bank-tx.screen.tsx
+++ b/src/screens/compliance-bank-tx.screen.tsx
@@ -6,7 +6,7 @@ import { useComplianceGuard } from 'src/hooks/guard.hook';
import { useLayoutOptions } from 'src/hooks/layout-config.hook';
import { useNavigation } from 'src/hooks/navigation.hook';
import { readCachedBankTx } from 'src/util/bank-tx-cache';
-import { DetailRow } from 'src/util/compliance-helpers';
+import { BankTxUnassignedTypes, DetailRow } from 'src/util/compliance-helpers';
function loadBankTx(id?: string, state?: unknown): BankTxSearchResult | undefined {
const fromState = (state as { bankTx?: BankTxSearchResult } | null)?.bankTx;
@@ -62,7 +62,7 @@ export default function ComplianceBankTxScreen(): JSX.Element {
color={StyledButtonColor.BLUE}
/>
- {bankTx.transactionId && (
+ {bankTx.transactionId && BankTxUnassignedTypes.includes(bankTx.type) && (
navigate(`compliance/bank-tx/${bankTx.transactionId}/return`)}
diff --git a/src/screens/compliance.screen.tsx b/src/screens/compliance.screen.tsx
index 7c21a6ec..e8f20f4f 100644
--- a/src/screens/compliance.screen.tsx
+++ b/src/screens/compliance.screen.tsx
@@ -312,9 +312,7 @@ export default function ComplianceScreen(): JSX.Element {
{searchResult.bankTx.length > 0 && (
-
- {translate('screens/compliance', 'Unassigned Bank Transactions')}
-
+
{translate('screens/compliance', 'Bank Transactions')}
diff --git a/src/translations/languages/de.json b/src/translations/languages/de.json
index b3c30c71..31bf1eb7 100644
--- a/src/translations/languages/de.json
+++ b/src/translations/languages/de.json
@@ -396,7 +396,7 @@
"No entries found": "Keine Einträge gefunden",
"found by {{type}}": "gefunden per {{type}}",
"Customers": "Kunden",
- "Unassigned Bank Transactions": "Nicht zugeordnete Banktransaktionen"
+ "Bank Transactions": "Banktransaktionen"
},
"screens/2fa": {
"2FA": "2FA",
diff --git a/src/translations/languages/fr.json b/src/translations/languages/fr.json
index c3509c05..daa97dd6 100644
--- a/src/translations/languages/fr.json
+++ b/src/translations/languages/fr.json
@@ -396,7 +396,7 @@
"No entries found": "Aucune entrée trouvée",
"found by {{type}}": "trouvés par {{type}}",
"Customers": "Clients",
- "Unassigned Bank Transactions": "Transactions bancaires non attribuées"
+ "Bank Transactions": "Transactions bancaires"
},
"screens/2fa": {
"2FA": "2FA",
diff --git a/src/translations/languages/it.json b/src/translations/languages/it.json
index 171838bc..85201294 100644
--- a/src/translations/languages/it.json
+++ b/src/translations/languages/it.json
@@ -396,7 +396,7 @@
"No entries found": "Nessuna voce trovata",
"found by {{type}}": "trovati tramite {{type}}",
"Customers": "Clienti",
- "Unassigned Bank Transactions": "Transazioni bancarie non assegnate"
+ "Bank Transactions": "Transazioni bancarie"
},
"screens/2fa": {
"2FA": "2FA",
diff --git a/src/util/compliance-helpers.tsx b/src/util/compliance-helpers.tsx
index 114da023..12ef845e 100644
--- a/src/util/compliance-helpers.tsx
+++ b/src/util/compliance-helpers.tsx
@@ -122,6 +122,10 @@ export function todayAsString(): string {
return new Date().toISOString().split('T')[0];
}
+// Mirrors `BankTxUnassignedTypes` in DFXswiss/api (bank-tx.entity.ts).
+// Only these types still allow a manual Return via compliance.
+export const BankTxUnassignedTypes = ['GSheet', 'Unknown', 'Pending'];
+
export function formatDate(value: string): string {
return new Date(value).toLocaleDateString();
}
From e6e4dcde3b36117fb44ee33f82fd7c60ff177607 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 14:29:21 +0200
Subject: [PATCH 05/13] feat: show existing recall state on bank-tx views
(#1069)
* feat: show existing recall state on bank-tx views
When a bank tx already has a recall, hide the Recall erfassen action
and show the recall info instead:
- Bank Transaction Details page: inline Recall card below the details
- Transactions tab (user data): Recall #ID button opens a details modal
Shared RecallDetails component renders the fields (id, created,
sequence, reason, fee, comment) via the DetailRow helper. The api
now includes recall on BankTxInfo and BankTxSearchResult so no extra
roundtrip is needed.
* style: add h2 header to bank transaction card for multi-card consistency
---
.../compliance/recall-details-modal.tsx | 37 +++++++++++++++++++
src/components/compliance/recall-details.tsx | 17 +++++++++
.../compliance/transactions-tab.tsx | 22 +++++++++--
src/hooks/compliance.hook.ts | 11 ++++++
src/screens/compliance-bank-tx.screen.tsx | 21 ++++++++---
5 files changed, 99 insertions(+), 9 deletions(-)
create mode 100644 src/components/compliance/recall-details-modal.tsx
create mode 100644 src/components/compliance/recall-details.tsx
diff --git a/src/components/compliance/recall-details-modal.tsx b/src/components/compliance/recall-details-modal.tsx
new file mode 100644
index 00000000..2db5e839
--- /dev/null
+++ b/src/components/compliance/recall-details-modal.tsx
@@ -0,0 +1,37 @@
+import { StyledButton, StyledButtonColor, StyledButtonWidth, StyledVerticalStack } from '@dfx.swiss/react-components';
+import { Modal } from 'src/components/modal';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { RecallInfo } from 'src/hooks/compliance.hook';
+import { RecallDetails } from './recall-details';
+
+interface RecallDetailsModalProps {
+ readonly isOpen: boolean;
+ readonly recall: RecallInfo | undefined;
+ readonly onClose: () => void;
+}
+
+export function RecallDetailsModal({ isOpen, recall, onClose }: RecallDetailsModalProps): JSX.Element {
+ const { translate } = useSettingsContext();
+
+ if (!isOpen || !recall) return <>>;
+
+ return (
+
+
+
+ {translate('screens/compliance', 'Recall')}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/compliance/recall-details.tsx b/src/components/compliance/recall-details.tsx
new file mode 100644
index 00000000..73765e14
--- /dev/null
+++ b/src/components/compliance/recall-details.tsx
@@ -0,0 +1,17 @@
+import { RecallInfo } from 'src/hooks/compliance.hook';
+import { DetailRow, formatDateTime } from 'src/util/compliance-helpers';
+
+export function RecallDetails({ recall }: { recall: RecallInfo }): JSX.Element {
+ return (
+
+ );
+}
diff --git a/src/components/compliance/transactions-tab.tsx b/src/components/compliance/transactions-tab.tsx
index b9d930b8..a069116f 100644
--- a/src/components/compliance/transactions-tab.tsx
+++ b/src/components/compliance/transactions-tab.tsx
@@ -2,9 +2,10 @@ import { Transaction, TransactionState, useTransaction } from '@dfx.swiss/react'
import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components';
import { Fragment, useState } from 'react';
import { ChargebackModal } from 'src/components/compliance/chargeback-modal';
+import { RecallDetailsModal } from 'src/components/compliance/recall-details-modal';
import { RecallModal } from 'src/components/compliance/recall-modal';
import { ConfirmDialog } from 'src/components/confirm-dialog';
-import { BankTxInfo, CryptoInputInfo, TransactionInfo, useCompliance } from 'src/hooks/compliance.hook';
+import { BankTxInfo, CryptoInputInfo, RecallInfo, TransactionInfo, useCompliance } from 'src/hooks/compliance.hook';
import { DetailRow, TransactionDetailRows, formatDate, statusBadge } from 'src/util/compliance-helpers';
interface TransactionsTableProps {
@@ -46,6 +47,7 @@ export function TransactionsTable({
const [stopError, setStopError] = useState();
const [chargebackTxId, setChargebackTxId] = useState();
const [recallBankTxId, setRecallBankTxId] = useState();
+ const [viewingRecall, setViewingRecall] = useState();
async function confirmStop(): Promise {
const txId = stopConfirmTxId;
@@ -306,8 +308,9 @@ export function TransactionsTable({
TransactionState.LIMIT_EXCEEDED,
TransactionState.UNASSIGNED,
].includes(detail.state) && !detail.chargebackAmount;
- const canRecall = bankTx != null;
- if (!canStop && !canChargeback && !canRecall) return null;
+ const existingRecall = bankTx?.recall;
+ const canRecall = bankTx != null && !existingRecall;
+ if (!canStop && !canChargeback && !canRecall && !existingRecall) return null;
return (
{canStop && (
@@ -335,6 +338,14 @@ export function TransactionsTable({
Recall
)}
+ {existingRecall && (
+
+ )}
);
})()}
@@ -392,6 +403,11 @@ export function TransactionsTable({
onClose={() => setRecallBankTxId(undefined)}
onSuccess={() => setRecallBankTxId(undefined)}
/>
+ setViewingRecall(undefined)}
+ />
);
}
diff --git a/src/hooks/compliance.hook.ts b/src/hooks/compliance.hook.ts
index e61dec4b..dd90f654 100644
--- a/src/hooks/compliance.hook.ts
+++ b/src/hooks/compliance.hook.ts
@@ -101,6 +101,7 @@ export interface BankTxSearchResult {
type: string;
name?: string;
iban?: string;
+ recall?: RecallInfo;
}
export interface BankTxInfo {
@@ -113,6 +114,16 @@ export interface BankTxInfo {
name?: string;
iban?: string;
remittanceInfo?: string;
+ recall?: RecallInfo;
+}
+
+export interface RecallInfo {
+ id: number;
+ created: string;
+ sequence: number;
+ reason?: string;
+ comment: string;
+ fee: number;
}
export interface IpLogInfo {
diff --git a/src/screens/compliance-bank-tx.screen.tsx b/src/screens/compliance-bank-tx.screen.tsx
index 6c0a0e72..dc599a1d 100644
--- a/src/screens/compliance-bank-tx.screen.tsx
+++ b/src/screens/compliance-bank-tx.screen.tsx
@@ -1,5 +1,6 @@
import { StyledButton, StyledButtonColor, StyledButtonWidth, StyledVerticalStack } from '@dfx.swiss/react-components';
import { useLocation, useParams } from 'react-router-dom';
+import { RecallDetails } from 'src/components/compliance/recall-details';
import { useSettingsContext } from 'src/contexts/settings.context';
import { BankTxSearchResult } from 'src/hooks/compliance.hook';
import { useComplianceGuard } from 'src/hooks/guard.hook';
@@ -42,6 +43,7 @@ export default function ComplianceBankTxScreen(): JSX.Element {
return (
+
{translate('screens/compliance', 'Bank Transaction')}
@@ -55,12 +57,19 @@ export default function ComplianceBankTxScreen(): JSX.Element {
-
navigate(`compliance/bank-tx/${bankTx.id}/recall`)}
- width={StyledButtonWidth.FULL}
- color={StyledButtonColor.BLUE}
- />
+ {bankTx.recall ? (
+
+
{translate('screens/compliance', 'Recall')}
+
+
+ ) : (
+ navigate(`compliance/bank-tx/${bankTx.id}/recall`)}
+ width={StyledButtonWidth.FULL}
+ color={StyledButtonColor.BLUE}
+ />
+ )}
{bankTx.transactionId && BankTxUnassignedTypes.includes(bankTx.type) && (
Date: Thu, 23 Apr 2026 14:51:22 +0200
Subject: [PATCH 06/13] feat: add mros creation page in compliance (#1070)
* feat: add mros creation page in compliance
Standalone page at compliance/mros/create. Form captures the full
CreateMrosDto: userDataId, status (default Draft), submissionDate
(optional), authorityReference (optional), caseManager.
* feat: simplify mros create form
- Rename Authority Reference label to MROS ID
- Remove Case Manager input; auto-populate from the logged-in user's
firstName + lastName (via useUser().getProfile)
- Default Submission Date to today
* style: move useEffect after useForm to match return-screen ordering
---
src/App.tsx | 5 +
src/dto/mros.dto.ts | 8 +
src/hooks/compliance.hook.ts | 11 +-
src/screens/compliance-mros-create.screen.tsx | 166 ++++++++++++++++++
4 files changed, 189 insertions(+), 1 deletion(-)
create mode 100644 src/screens/compliance-mros-create.screen.tsx
diff --git a/src/App.tsx b/src/App.tsx
index e522637c..b2c85468 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -67,6 +67,7 @@ const ComplianceSupportIssueScreen = lazy(() => import('./screens/compliance-sup
const ComplianceRecommendationGraphScreen = lazy(() => import('./screens/compliance-recommendation-graph.screen'));
const ComplianceCustodyOrdersScreen = lazy(() => import('./screens/compliance-custody-orders.screen'));
const ComplianceMrosListScreen = lazy(() => import('./screens/compliance-mros-list.screen'));
+const ComplianceMrosCreateScreen = lazy(() => import('./screens/compliance-mros-create.screen'));
const ComplianceRecallListScreen = lazy(() => import('./screens/compliance-recall-list.screen'));
const ComplianceReviewScreen = lazy(() => import('./screens/compliance-review.screen'));
const SupportDashboardScreen = lazy(() => import('./screens/support-dashboard.screen'));
@@ -405,6 +406,10 @@ export const Routes = [
path: 'compliance/mros',
element: withSuspense(),
},
+ {
+ path: 'compliance/mros/create',
+ element: withSuspense(),
+ },
{
path: 'compliance/recalls',
element: withSuspense(),
diff --git a/src/dto/mros.dto.ts b/src/dto/mros.dto.ts
index 3f20ea8e..596c6c83 100644
--- a/src/dto/mros.dto.ts
+++ b/src/dto/mros.dto.ts
@@ -15,3 +15,11 @@ export interface MrosListEntry {
caseManager: string;
userData: { id: number };
}
+
+export interface CreateMrosDto {
+ userDataId: number;
+ status: MrosStatus;
+ submissionDate?: string;
+ authorityReference?: string;
+ caseManager: string;
+}
diff --git a/src/hooks/compliance.hook.ts b/src/hooks/compliance.hook.ts
index dd90f654..db5f914f 100644
--- a/src/hooks/compliance.hook.ts
+++ b/src/hooks/compliance.hook.ts
@@ -8,7 +8,7 @@ import {
ResponseType,
useApi,
} from '@dfx.swiss/react';
-import { MrosListEntry } from 'src/dto/mros.dto';
+import { CreateMrosDto, MrosListEntry } from 'src/dto/mros.dto';
import { CustodyOrderListEntry } from 'src/dto/order.dto';
import { CreateRecallDto, RecallListEntry } from 'src/dto/recall.dto';
import { electronicFormatIBAN, isValidIBAN } from 'ibantools';
@@ -529,6 +529,14 @@ export function useCompliance() {
});
}
+ async function createMros(dto: CreateMrosDto): Promise {
+ return call({
+ url: 'mros',
+ method: 'POST',
+ data: dto,
+ });
+ }
+
async function getRecalls(): Promise {
return call({
url: 'recall',
@@ -682,6 +690,7 @@ export function useCompliance() {
getCustodyOrders,
approveCustodyOrder,
getMrosList,
+ createMros,
getRecalls,
createRecall,
updateKycStep,
diff --git a/src/screens/compliance-mros-create.screen.tsx b/src/screens/compliance-mros-create.screen.tsx
new file mode 100644
index 00000000..d8620a22
--- /dev/null
+++ b/src/screens/compliance-mros-create.screen.tsx
@@ -0,0 +1,166 @@
+import { Utils, Validations, useUser } from '@dfx.swiss/react';
+import {
+ Form,
+ StyledButton,
+ StyledButtonColor,
+ StyledButtonWidth,
+ StyledDropdown,
+ StyledInput,
+ StyledVerticalStack,
+} from '@dfx.swiss/react-components';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { ErrorHint } from 'src/components/error-hint';
+import { useLayoutContext } from 'src/contexts/layout.context';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { MrosStatus } from 'src/dto/mros.dto';
+import { useCompliance } from 'src/hooks/compliance.hook';
+import { useComplianceGuard } from 'src/hooks/guard.hook';
+import { useLayoutOptions } from 'src/hooks/layout-config.hook';
+import { useNavigation } from 'src/hooks/navigation.hook';
+import { todayAsString } from 'src/util/compliance-helpers';
+
+interface FormData {
+ userDataId: string;
+ status: MrosStatus;
+ submissionDate: string;
+ authorityReference: string;
+}
+
+export default function ComplianceMrosCreateScreen(): JSX.Element {
+ useComplianceGuard();
+
+ const { translate, translateError } = useSettingsContext();
+ const { createMros } = useCompliance();
+ const { getProfile } = useUser();
+ const { navigate } = useNavigation();
+ const { rootRef } = useLayoutContext();
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState();
+ const [success, setSuccess] = useState(false);
+ const [caseManager, setCaseManager] = useState();
+
+ const {
+ control,
+ handleSubmit,
+ formState: { isValid, errors },
+ } = useForm({
+ mode: 'onTouched',
+ defaultValues: { status: MrosStatus.DRAFT, submissionDate: todayAsString() },
+ });
+
+ useEffect(() => {
+ getProfile()
+ .then((p) => setCaseManager([p?.firstName, p?.lastName].filter(Boolean).join(' ')))
+ .catch(() => setCaseManager(''));
+ }, []);
+
+ useLayoutOptions({ title: translate('screens/compliance', 'MROS erfassen'), backButton: true });
+
+ async function onSubmit(formData: FormData): Promise {
+ if (!caseManager) return;
+ setIsSubmitting(true);
+ setError(undefined);
+
+ try {
+ await createMros({
+ userDataId: Number(formData.userDataId),
+ status: formData.status,
+ submissionDate: formData.submissionDate || undefined,
+ authorityReference: formData.authorityReference || undefined,
+ caseManager,
+ });
+ setSuccess(true);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Unknown error');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ const rules = Utils.createRules({
+ userDataId: [Validations.Required, Validations.Custom((v) => (isNaN(Number(v)) ? 'pattern' : true))],
+ status: Validations.Required,
+ });
+
+ if (success) {
+ return (
+
+
+
+ {translate('screens/compliance', 'MROS created successfully')}
+
+ navigate(-1)}
+ width={StyledButtonWidth.MD}
+ />
+
+
+ );
+ }
+
+ return (
+
+ );
+}
From 5a07208ec7b1c80271eefd767599a3c9d27d3be7 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 14:58:26 +0200
Subject: [PATCH 07/13] feat: add mros detail page (#1071)
Clicking a row on the MROS list navigates to compliance/mros/:id,
which fetches GET /mros/:id and renders all fields via DetailRow.
- getMrosById hook method
- ComplianceMrosDetailScreen with loading/error/not-found states
- mrosStatusBadge moved to compliance-helpers and reused in both
list and detail
---
src/App.tsx | 5 ++
src/hooks/compliance.hook.ts | 8 +++
src/screens/compliance-mros-detail.screen.tsx | 63 +++++++++++++++++++
src/screens/compliance-mros-list.screen.tsx | 26 +++-----
src/util/compliance-helpers.tsx | 13 ++++
5 files changed, 99 insertions(+), 16 deletions(-)
create mode 100644 src/screens/compliance-mros-detail.screen.tsx
diff --git a/src/App.tsx b/src/App.tsx
index b2c85468..45c2fab6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -68,6 +68,7 @@ const ComplianceRecommendationGraphScreen = lazy(() => import('./screens/complia
const ComplianceCustodyOrdersScreen = lazy(() => import('./screens/compliance-custody-orders.screen'));
const ComplianceMrosListScreen = lazy(() => import('./screens/compliance-mros-list.screen'));
const ComplianceMrosCreateScreen = lazy(() => import('./screens/compliance-mros-create.screen'));
+const ComplianceMrosDetailScreen = lazy(() => import('./screens/compliance-mros-detail.screen'));
const ComplianceRecallListScreen = lazy(() => import('./screens/compliance-recall-list.screen'));
const ComplianceReviewScreen = lazy(() => import('./screens/compliance-review.screen'));
const SupportDashboardScreen = lazy(() => import('./screens/support-dashboard.screen'));
@@ -410,6 +411,10 @@ export const Routes = [
path: 'compliance/mros/create',
element: withSuspense(),
},
+ {
+ path: 'compliance/mros/:id',
+ element: withSuspense(),
+ },
{
path: 'compliance/recalls',
element: withSuspense(),
diff --git a/src/hooks/compliance.hook.ts b/src/hooks/compliance.hook.ts
index db5f914f..f6deb9f9 100644
--- a/src/hooks/compliance.hook.ts
+++ b/src/hooks/compliance.hook.ts
@@ -529,6 +529,13 @@ export function useCompliance() {
});
}
+ async function getMrosById(id: number): Promise {
+ return call({
+ url: `mros/${id}`,
+ method: 'GET',
+ });
+ }
+
async function createMros(dto: CreateMrosDto): Promise {
return call({
url: 'mros',
@@ -690,6 +697,7 @@ export function useCompliance() {
getCustodyOrders,
approveCustodyOrder,
getMrosList,
+ getMrosById,
createMros,
getRecalls,
createRecall,
diff --git a/src/screens/compliance-mros-detail.screen.tsx b/src/screens/compliance-mros-detail.screen.tsx
new file mode 100644
index 00000000..5db69cdd
--- /dev/null
+++ b/src/screens/compliance-mros-detail.screen.tsx
@@ -0,0 +1,63 @@
+import { SpinnerSize, StyledLoadingSpinner, StyledVerticalStack } from '@dfx.swiss/react-components';
+import { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { ErrorHint } from 'src/components/error-hint';
+import { useSettingsContext } from 'src/contexts/settings.context';
+import { MrosListEntry } from 'src/dto/mros.dto';
+import { useCompliance } from 'src/hooks/compliance.hook';
+import { useComplianceGuard } from 'src/hooks/guard.hook';
+import { useLayoutOptions } from 'src/hooks/layout-config.hook';
+import { DetailRow, formatDateTime, mrosStatusBadge } from 'src/util/compliance-helpers';
+
+export default function ComplianceMrosDetailScreen(): JSX.Element {
+ useComplianceGuard();
+
+ const { id } = useParams<{ id: string }>();
+ const { translate } = useSettingsContext();
+ const { getMrosById } = useCompliance();
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState();
+ const [mros, setMros] = useState();
+
+ useLayoutOptions({ title: translate('screens/compliance', 'MROS Report'), backButton: true });
+
+ useEffect(() => {
+ if (!id) return;
+ setIsLoading(true);
+ setError(undefined);
+ getMrosById(+id)
+ .then(setMros)
+ .catch((e: Error) => setError(e.message))
+ .finally(() => setIsLoading(false));
+ }, [id]);
+
+ if (isLoading) return ;
+ if (error) return ;
+ if (!mros) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+ | Status: |
+ {mrosStatusBadge(mros.status)} |
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/screens/compliance-mros-list.screen.tsx b/src/screens/compliance-mros-list.screen.tsx
index 160c1dfb..8d1d52d2 100644
--- a/src/screens/compliance-mros-list.screen.tsx
+++ b/src/screens/compliance-mros-list.screen.tsx
@@ -2,23 +2,12 @@ import { useSessionContext } from '@dfx.swiss/react';
import { SpinnerSize, StyledLoadingSpinner, StyledVerticalStack } from '@dfx.swiss/react-components';
import { useCallback, useEffect, useState } from 'react';
import { ErrorHint } from 'src/components/error-hint';
-import { MrosListEntry, MrosStatus } from 'src/dto/mros.dto';
+import { MrosListEntry } from 'src/dto/mros.dto';
import { useCompliance } from 'src/hooks/compliance.hook';
import { useComplianceGuard } from 'src/hooks/guard.hook';
import { useLayoutOptions } from 'src/hooks/layout-config.hook';
-
-const statusClasses: Record = {
- [MrosStatus.DRAFT]: 'bg-dfxGray-400 text-dfxBlue-800',
- [MrosStatus.SUBMITTED]: 'bg-dfxBlue-300/20 text-dfxBlue-400',
- [MrosStatus.CONFIRMED]: 'bg-dfxGreen-100/20 text-dfxGreen-300',
- [MrosStatus.CLOSED]: 'bg-dfxGray-700/20 text-dfxGray-800',
-};
-
-function statusBadge(status: MrosStatus): JSX.Element {
- const classes = statusClasses[status] ?? 'bg-dfxGray-400 text-dfxBlue-800';
-
- return {status};
-}
+import { useNavigation } from 'src/hooks/navigation.hook';
+import { mrosStatusBadge } from 'src/util/compliance-helpers';
export default function ComplianceMrosListScreen(): JSX.Element {
useComplianceGuard();
@@ -26,6 +15,7 @@ export default function ComplianceMrosListScreen(): JSX.Element {
const { isLoggedIn } = useSessionContext();
const { getMrosList } = useCompliance();
+ const { navigate } = useNavigation();
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState();
@@ -82,12 +72,16 @@ export default function ComplianceMrosListScreen(): JSX.Element {
{data.length > 0 ? (
data.map((entry) => (
-
+
navigate(`compliance/mros/${entry.id}`)}
+ >
| {entry.id} |
{formatDate(entry.created)} |
{formatDate(entry.updated)} |
{entry.userData.id} |
- {statusBadge(entry.status)} |
+ {mrosStatusBadge(entry.status)} |
{formatDate(entry.submissionDate)} |
{entry.authorityReference ?? '-'} |
{entry.caseManager} |
diff --git a/src/util/compliance-helpers.tsx b/src/util/compliance-helpers.tsx
index 12ef845e..43c24516 100644
--- a/src/util/compliance-helpers.tsx
+++ b/src/util/compliance-helpers.tsx
@@ -1,4 +1,5 @@
import { Transaction } from '@dfx.swiss/react';
+import { MrosStatus } from 'src/dto/mros.dto';
export function DetailRow({
label,
@@ -118,6 +119,18 @@ export function boolBadge(value: boolean, trueLabel = 'Yes', falseLabel = 'No'):
return statusBadge(value ? trueLabel : falseLabel);
}
+const mrosStatusClasses: Record = {
+ [MrosStatus.DRAFT]: 'bg-dfxGray-400 text-dfxBlue-800',
+ [MrosStatus.SUBMITTED]: 'bg-dfxBlue-300/20 text-dfxBlue-400',
+ [MrosStatus.CONFIRMED]: 'bg-dfxGreen-100/20 text-dfxGreen-300',
+ [MrosStatus.CLOSED]: 'bg-dfxGray-700/20 text-dfxGray-800',
+};
+
+export function mrosStatusBadge(status: MrosStatus): JSX.Element {
+ const classes = mrosStatusClasses[status] ?? 'bg-dfxGray-400 text-dfxBlue-800';
+ return {status};
+}
+
export function todayAsString(): string {
return new Date().toISOString().split('T')[0];
}
From ca64cb118b5409f7465894745d69bcfc8ca513a9 Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:01:45 +0200
Subject: [PATCH 08/13] feat: add sitemap link to navigation for admins (#1072)
Only visible when session.role === UserRole.ADMIN, placed after the
other role-gated entries (Compliance, Support Dashboard, RealUnit).
---
src/components/navigation.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
index d208c0ab..079e4468 100644
--- a/src/components/navigation.tsx
+++ b/src/components/navigation.tsx
@@ -225,6 +225,15 @@ function NavigationMenu({ setIsNavigationOpen, small = false }: NavigationMenuCo
onClose={() => setIsNavigationOpen(false)}
/>
)}
+ {session?.role === UserRole.ADMIN && (
+ setIsNavigationOpen(false)}
+ />
+ )}
>
)}
From 88afbc38cd2e451bbe4de1a2d9c20266e42ad8ea Mon Sep 17 00:00:00 2001
From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:03:40 +0200
Subject: [PATCH 09/13] fix: default booking and value date to today on
sepa-manual form (#1073)
---
src/screens/sepa-manual.screen.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/screens/sepa-manual.screen.tsx b/src/screens/sepa-manual.screen.tsx
index 1aa1cd77..4bfd276e 100644
--- a/src/screens/sepa-manual.screen.tsx
+++ b/src/screens/sepa-manual.screen.tsx
@@ -18,6 +18,7 @@ import { ErrorHint } from 'src/components/error-hint';
import { useLayoutContext } from 'src/contexts/layout.context';
import { useLayoutOptions } from 'src/hooks/layout-config.hook';
import { buildCamt053Xml } from 'src/util/camt053-builder';
+import { todayAsString } from 'src/util/compliance-helpers';
import { useSettingsContext } from '../contexts/settings.context';
import { useAdminGuard } from '../hooks/guard.hook';
@@ -68,6 +69,8 @@ export default function SepaManualScreen(): JSX.Element {
defaultValues: {
currency: 'EUR',
direction: CreditDebitIndicator.CRDT,
+ bookingDate: todayAsString(),
+ valueDate: todayAsString(),
},
});
From fe224d02c50334567972d0e6c3062be522b70107 Mon Sep 17 00:00:00 2001
From: bernd2022 <104787072+bernd2022@users.noreply.github.com>
Date: Thu, 23 Apr 2026 18:21:41 +0200
Subject: [PATCH 10/13] feat: support dashboard state refactor, configurable
clerks, layout fixes (#1058)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: support dashboard state refactor, configurable clerks, layout fixes
- load clerks dynamically from GET /support/issue/clerks (no more hardcoded names)
- validate assigned clerk against loaded list, fall back to first entry
- refactor tab state into Record to kill 12 useStates + 5 ternary chains
- replace 3× count requests with single getIssueCounts() call
- migrate all state filters to comma-separated states param
- split loadMessages(boolean) into loadMessages() + pollForNewMessages()
- add support-helpers with typeLabel()/reasonLabel() reused in 3 screens
- drop dead client-side open-state filter and unreachable messageAuthor fallback
- replace 100vh calc with flex-1 min-h-0 + layout noPadding so table fills bottom
- add top scroll shadow on message list, fix import order
* feat: dashboard reload banner for new support messages
---
src/hooks/support-dashboard.hook.ts | 27 +-
src/index.css | 8 +-
.../support-dashboard-create.screen.tsx | 42 ++-
.../support-dashboard-issue.screen.tsx | 124 ++++++--
src/screens/support-dashboard.screen.tsx | 284 ++++++++++--------
src/util/support-helpers.ts | 10 +
6 files changed, 335 insertions(+), 160 deletions(-)
create mode 100644 src/util/support-helpers.ts
diff --git a/src/hooks/support-dashboard.hook.ts b/src/hooks/support-dashboard.hook.ts
index de37f88c..0942e035 100644
--- a/src/hooks/support-dashboard.hook.ts
+++ b/src/hooks/support-dashboard.hook.ts
@@ -102,7 +102,7 @@ export function useSupportDashboard() {
async function getIssueList(params?: {
department?: string;
- state?: string;
+ states?: string;
type?: string;
take?: number;
skip?: number;
@@ -119,6 +119,28 @@ export function useSupportDashboard() {
});
}
+ async function getIssueCounts(): Promise> {
+ return call>({
+ url: 'support/issue/counts',
+ method: 'GET',
+ });
+ }
+
+ async function getIssueActivity(since?: Date): Promise<{ count: number; latestAt?: string }> {
+ const query = since ? `?since=${encodeURIComponent(since.toISOString())}` : '';
+ return call<{ count: number; latestAt?: string }>({
+ url: `support/issue/activity${query}`,
+ method: 'GET',
+ });
+ }
+
+ async function getClerks(): Promise {
+ return call({
+ url: 'support/issue/clerks',
+ method: 'GET',
+ });
+ }
+
async function getIssueData(issueId: number): Promise {
return call({
url: `support/issue/${issueId}/data`,
@@ -198,6 +220,9 @@ export function useSupportDashboard() {
return useMemo(
() => ({
getIssueList,
+ getIssueCounts,
+ getIssueActivity,
+ getClerks,
getIssueData,
updateIssue,
sendMessage,
diff --git a/src/index.css b/src/index.css
index d5e2aa1c..e7c0096e 100644
--- a/src/index.css
+++ b/src/index.css
@@ -72,16 +72,20 @@ input[type='number'] {
@tailwind components;
@tailwind utilities;
-/* Scroll shadow — bottom only, visible when content overflows */
+/* Scroll shadow — top + bottom, each visible only when content overflows in that direction */
.scroll-shadow {
background:
+ linear-gradient(white 30%, transparent) center top,
linear-gradient(transparent, white 70%) center bottom,
+ radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.12), transparent) center top,
radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.12), transparent) center bottom;
background-repeat: no-repeat;
background-size:
100% 40px,
+ 100% 40px,
+ 100% 10px,
100% 10px;
- background-attachment: local, scroll;
+ background-attachment: local, local, scroll, scroll;
}
/* Shadow DOM Animation */
diff --git a/src/screens/support-dashboard-create.screen.tsx b/src/screens/support-dashboard-create.screen.tsx
index 3e4fa92f..96282a8f 100644
--- a/src/screens/support-dashboard-create.screen.tsx
+++ b/src/screens/support-dashboard-create.screen.tsx
@@ -1,13 +1,14 @@
-import { SupportIssueReason, SupportIssueType, useAuthContext, useUserContext } from '@dfx.swiss/react';
+import { SupportIssueReason, SupportIssueType } from '@dfx.swiss/react';
import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { toBase64 } from 'src/util/utils';
import { ErrorHint } from 'src/components/error-hint';
import { useSettingsContext } from 'src/contexts/settings.context';
import { useSupportDashboardGuard } from 'src/hooks/guard.hook';
import { useLayoutOptions } from 'src/hooks/layout-config.hook';
import { useNavigation } from 'src/hooks/navigation.hook';
import { ASSIGNABLE_DEPARTMENTS, UserSearchResult, useSupportDashboard } from 'src/hooks/support-dashboard.hook';
+import { reasonLabel, typeLabel } from 'src/util/support-helpers';
+import { toBase64 } from 'src/util/utils';
const ISSUE_TYPES = Object.values(SupportIssueType);
const ISSUE_REASONS = Object.values(SupportIssueReason);
@@ -16,11 +17,11 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
useSupportDashboardGuard();
const { translate } = useSettingsContext();
- const { session } = useAuthContext();
- const { user } = useUserContext();
- const { createIssue, searchUsers } = useSupportDashboard();
+ const { createIssue, searchUsers, getClerks } = useSupportDashboard();
const { navigate } = useNavigation();
+ const [clerks, setClerks] = useState([]);
+
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState();
@@ -38,6 +39,16 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
const [reason, setReason] = useState(ISSUE_REASONS[0]);
const [name, setName] = useState('');
const [department, setDepartment] = useState(ASSIGNABLE_DEPARTMENTS[0]);
+ const [clerk, setClerk] = useState('');
+
+ useEffect(() => {
+ getClerks()
+ .then((list) => {
+ setClerks(list);
+ setClerk((prev) => prev || list[0] || '');
+ })
+ .catch(() => undefined);
+ }, [getClerks]);
const [message, setMessage] = useState('');
const [selectedFile, setSelectedFile] = useState();
const fileInputRef = useRef(null);
@@ -106,14 +117,13 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
setError(undefined);
try {
- const author = user?.mail ?? session?.address ?? 'Support';
const fileData = selectedFile ? await toBase64(selectedFile) : undefined;
await createIssue(selectedUser.id, {
type,
reason,
name: name.trim(),
department,
- author,
+ author: clerk,
message: message.trim(),
file: fileData ?? undefined,
fileName: selectedFile?.name,
@@ -209,7 +219,7 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
>
{ISSUE_TYPES.map((t) => (
))}
@@ -223,7 +233,7 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
>
{ISSUE_REASONS.map((r) => (
))}
@@ -254,6 +264,20 @@ export default function SupportDashboardCreateScreen(): JSX.Element {
+
+
+
+