mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 05:02:22 +00:00
feat(web): Add modal to view raw transaction.
This commit is contained in:
116
frontend/src/components/RawTransactionModal.tsx
Normal file
116
frontend/src/components/RawTransactionModal.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { X, Copy, Check } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { RawTransactionData } from "../types/api";
|
||||||
|
|
||||||
|
interface RawTransactionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
rawTransaction: RawTransactionData | undefined;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RawTransactionModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
rawTransaction,
|
||||||
|
transactionId,
|
||||||
|
}: RawTransactionModalProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!rawTransaction) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
JSON.stringify(rawTransaction, null, 2)
|
||||||
|
);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy to clipboard:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
{/* Background overlay */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal panel */}
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
|
||||||
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
Raw Transaction Data
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={!rawTransaction}
|
||||||
|
className="inline-flex items-center px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4 mr-1 text-green-600" />
|
||||||
|
Copied!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy JSON
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="inline-flex items-center p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Transaction ID: <code className="bg-gray-100 px-2 py-1 rounded text-xs">{transactionId}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rawTransaction ? (
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4 overflow-auto max-h-96">
|
||||||
|
<pre className="text-sm text-gray-800 whitespace-pre-wrap">
|
||||||
|
{JSON.stringify(rawTransaction, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-gray-50 rounded-lg p-8 text-center">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Raw transaction data is not available for this transaction.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
Try refreshing the page or check if the transaction was fetched with summary_only=false.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
X,
|
X,
|
||||||
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { apiClient } from "../lib/api";
|
import { apiClient } from "../lib/api";
|
||||||
import { formatCurrency, formatDate } from "../lib/utils";
|
import { formatCurrency, formatDate } from "../lib/utils";
|
||||||
import LoadingSpinner from "./LoadingSpinner";
|
import LoadingSpinner from "./LoadingSpinner";
|
||||||
|
import RawTransactionModal from "./RawTransactionModal";
|
||||||
import type { Account, Transaction } from "../types/api";
|
import type { Account, Transaction } from "../types/api";
|
||||||
|
|
||||||
export default function TransactionsList() {
|
export default function TransactionsList() {
|
||||||
@@ -21,6 +23,8 @@ export default function TransactionsList() {
|
|||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [showRawModal, setShowRawModal] = useState(false);
|
||||||
|
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
|
||||||
|
|
||||||
const { data: accounts } = useQuery<Account[]>({
|
const { data: accounts } = useQuery<Account[]>({
|
||||||
queryKey: ["accounts"],
|
queryKey: ["accounts"],
|
||||||
@@ -39,6 +43,7 @@ export default function TransactionsList() {
|
|||||||
accountId: selectedAccount || undefined,
|
accountId: selectedAccount || undefined,
|
||||||
startDate: startDate || undefined,
|
startDate: startDate || undefined,
|
||||||
endDate: endDate || undefined,
|
endDate: endDate || undefined,
|
||||||
|
summaryOnly: false, // Always fetch raw transaction data
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,6 +79,16 @@ export default function TransactionsList() {
|
|||||||
setEndDate("");
|
setEndDate("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewRaw = (transaction: Transaction) => {
|
||||||
|
setSelectedTransaction(transaction);
|
||||||
|
setShowRawModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowRawModal(false);
|
||||||
|
setSelectedTransaction(null);
|
||||||
|
};
|
||||||
|
|
||||||
const hasActiveFilters =
|
const hasActiveFilters =
|
||||||
searchTerm || selectedAccount || startDate || endDate;
|
searchTerm || selectedAccount || startDate || endDate;
|
||||||
|
|
||||||
@@ -248,13 +263,13 @@ export default function TransactionsList() {
|
|||||||
const account = accounts?.find(
|
const account = accounts?.find(
|
||||||
(acc) => acc.id === transaction.account_id,
|
(acc) => acc.id === transaction.account_id,
|
||||||
);
|
);
|
||||||
const isPositive = transaction.amount > 0;
|
const isPositive = transaction.transaction_value > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={
|
key={
|
||||||
transaction.internal_transaction_id ||
|
transaction.internal_transaction_id ||
|
||||||
`${transaction.account_id}-${transaction.date}-${transaction.amount}`
|
`${transaction.account_id}-${transaction.transaction_date}-${transaction.transaction_value}`
|
||||||
}
|
}
|
||||||
className="p-6 hover:bg-gray-50 transition-colors"
|
className="p-6 hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -307,33 +322,51 @@ export default function TransactionsList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right ml-4">
|
<div className="text-right ml-4">
|
||||||
<p
|
<div className="flex items-center justify-end space-x-2 mb-2">
|
||||||
className={`text-lg font-semibold ${
|
<button
|
||||||
isPositive ? "text-green-600" : "text-red-600"
|
onClick={() => handleViewRaw(transaction)}
|
||||||
}`}
|
className="inline-flex items-center px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
|
||||||
>
|
title="View raw transaction data"
|
||||||
{isPositive ? "+" : ""}
|
>
|
||||||
{formatCurrency(transaction.amount, transaction.currency)}
|
<Eye className="h-3 w-3 mr-1" />
|
||||||
</p>
|
Raw
|
||||||
<p className="text-sm text-gray-500">
|
</button>
|
||||||
{transaction.date
|
</div>
|
||||||
? formatDate(transaction.date)
|
<p
|
||||||
: "No date"}
|
className={`text-lg font-semibold ${
|
||||||
</p>
|
isPositive ? "text-green-600" : "text-red-600"
|
||||||
{transaction.booking_date &&
|
}`}
|
||||||
transaction.booking_date !== transaction.date && (
|
>
|
||||||
<p className="text-xs text-gray-400">
|
{isPositive ? "+" : ""}
|
||||||
Booked: {formatDate(transaction.booking_date)}
|
{formatCurrency(transaction.transaction_value, transaction.transaction_currency)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
<p className="text-sm text-gray-500">
|
||||||
</div>
|
{transaction.transaction_date
|
||||||
|
? formatDate(transaction.transaction_date)
|
||||||
|
: "No date"}
|
||||||
|
</p>
|
||||||
|
{transaction.booking_date &&
|
||||||
|
transaction.booking_date !== transaction.transaction_date && (
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Booked: {formatDate(transaction.booking_date)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Raw Transaction Modal */}
|
||||||
|
<RawTransactionModal
|
||||||
|
isOpen={showRawModal}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
rawTransaction={selectedTransaction?.raw_transaction}
|
||||||
|
transactionId={selectedTransaction?.internal_transaction_id || "unknown"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const apiClient = {
|
|||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
summaryOnly?: boolean;
|
||||||
}): Promise<Transaction[]> => {
|
}): Promise<Transaction[]> => {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
@@ -66,6 +67,9 @@ export const apiClient = {
|
|||||||
if (params?.perPage)
|
if (params?.perPage)
|
||||||
queryParams.append("per_page", params.perPage.toString());
|
queryParams.append("per_page", params.perPage.toString());
|
||||||
if (params?.search) queryParams.append("search", params.search);
|
if (params?.search) queryParams.append("search", params.search);
|
||||||
|
if (params?.summaryOnly !== undefined) {
|
||||||
|
queryParams.append("summary_only", params.summaryOnly.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const response = await api.get<ApiResponse<Transaction[]>>(
|
const response = await api.get<ApiResponse<Transaction[]>>(
|
||||||
`/transactions?${queryParams.toString()}`,
|
`/transactions?${queryParams.toString()}`,
|
||||||
|
|||||||
@@ -17,15 +17,55 @@ export interface Account {
|
|||||||
balances: AccountBalance[];
|
balances: AccountBalance[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawTransactionData {
|
||||||
|
transactionId?: string;
|
||||||
|
bookingDate?: string;
|
||||||
|
valueDate?: string;
|
||||||
|
bookingDateTime?: string;
|
||||||
|
valueDateTime?: string;
|
||||||
|
transactionAmount?: {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
currencyExchange?: {
|
||||||
|
instructedAmount?: {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
sourceCurrency?: string;
|
||||||
|
exchangeRate?: string;
|
||||||
|
unitCurrency?: string;
|
||||||
|
targetCurrency?: string;
|
||||||
|
};
|
||||||
|
creditorName?: string;
|
||||||
|
debtorName?: string;
|
||||||
|
debtorAccount?: {
|
||||||
|
iban?: string;
|
||||||
|
};
|
||||||
|
remittanceInformationUnstructuredArray?: string[];
|
||||||
|
proprietaryBankTransactionCode?: string;
|
||||||
|
balanceAfterTransaction?: {
|
||||||
|
balanceAmount: {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
balanceType: string;
|
||||||
|
};
|
||||||
|
internalTransactionId?: string;
|
||||||
|
[key: string]: unknown; // Allow additional fields
|
||||||
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
internal_transaction_id: string | null;
|
internal_transaction_id: string | null;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
amount: number;
|
transaction_value: number;
|
||||||
currency: string;
|
transaction_currency: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
transaction_date: string;
|
||||||
status: string;
|
transaction_status: string;
|
||||||
// Optional fields that may be present in some transactions
|
// Optional fields that may be present in some transactions
|
||||||
|
institution_id?: string;
|
||||||
|
iban?: string;
|
||||||
booking_date?: string;
|
booking_date?: string;
|
||||||
value_date?: string;
|
value_date?: string;
|
||||||
creditor_name?: string;
|
creditor_name?: string;
|
||||||
@@ -34,6 +74,8 @@ export interface Transaction {
|
|||||||
category?: string;
|
category?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
// Raw transaction data (only present when summary_only=false)
|
||||||
|
raw_transaction?: RawTransactionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for raw transaction data from API (before sanitization)
|
// Type for raw transaction data from API (before sanitization)
|
||||||
|
|||||||
Reference in New Issue
Block a user