This commit is contained in:
Petropoulos Evangelos 2025-07-06 18:39:59 +03:00
parent 4202f6e62e
commit d354ac1760
17 changed files with 3197 additions and 2396 deletions

10
node_modules/.yarn-integrity generated vendored Normal file
View File

@ -0,0 +1,10 @@
{
"systemParams": "darwin-arm64-131",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}

View File

@ -0,0 +1,25 @@
import { transactionDummyData } from '@/app/features/Pages/transactions/mockData';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const state = searchParams.get('state');
const user = searchParams.get('user');
let filteredTransactions = [...transactionDummyData];
if (user) {
filteredTransactions = filteredTransactions.filter(
tx => tx.user.toString() === user
);
}
if (state) {
filteredTransactions = filteredTransactions.filter(
tx => tx.state.toLowerCase() === state.toLowerCase()
);
}
return NextResponse.json(filteredTransactions);
}

View File

@ -0,0 +1,37 @@
// components/SearchFilters.js
import React from 'react';
import { Box, Chip, Typography, Button } from '@mui/material';
const SearchFilters = ({ filters, onDeleteFilter, onClearAll }) => {
const renderChip = (label, value, key) => (
<Chip
key={key}
label={
<Typography variant="body2" sx={{ fontWeight: key === 'state' ? 'bold' : 'normal' }}>
{label} {value}
</Typography>
}
onDelete={() => onDeleteFilter(key)}
sx={{ mr: 1, mb: 1 }}
/>
);
return (
<Box display="flex" alignItems="center" flexWrap="wrap" sx={{ p: 2 }}>
{filters.user && renderChip('User', filters.user, 'user')}
{filters.state && renderChip('State', filters.state, 'state')}
{filters.startDate && renderChip('Start Date', filters.startDate, 'startDate')}
{Object.values(filters).some(Boolean) && (
<Button
onClick={onClearAll}
sx={{ ml: 1, textDecoration: 'underline', color: 'black' }}
>
Clear All
</Button>
)}
</Box>
);
};
export default SearchFilters;

View File

@ -1,207 +1,189 @@
import React, { useState } from "react"; // app/transactions/page.tsx
import { 'use client';
Box,
Grid,
TextField,
MenuItem,
Button,
InputLabel,
FormControl,
Select,
Typography,
} from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import SearchIcon from "@mui/icons-material/Search";
import RefreshIcon from "@mui/icons-material/Refresh";
const currencies = ["USD", "EUR", "GBP"]; import { useState } from 'react';
const states = ["Pending", "Completed", "Failed"];
const transactionTypes = ["Credit", "Debit"];
const paymentMethods = ["Card", "Bank Transfer"];
const psps = ["Stripe", "PayPal"];
const merchants = ["Amazon", "eBay"];
export default function SearchFilterForm() { export default function TransactionsPage() {
const [form, setForm] = useState({ const [userId, setUserId] = useState('');
keyword: "", const [state, setState] = useState('');
transactionId: "", const [statusCode, setStatusCode] = useState('');
transactionReferenceId: "", const [transactions, setTransactions] = useState<any[]>([]);
user: "", const [loading, setLoading] = useState(false);
currency: "",
state: "",
statusDescription: "",
transactionType: "",
paymentMethod: "",
psps: "",
initialPsps: "",
merchants: "",
startDate: null,
endDate: null,
lastUpdatedFrom: null,
lastUpdatedTo: null,
minAmount: "",
maxAmount: "",
channel: "",
});
const handleChange = (field: string, value: any) => { const fetchTransactions = async () => {
setForm((prev) => ({ ...prev, [field]: value })); setLoading(true);
}; try {
const url = new URL('https://api.example.com/transactions');
if (userId) url.searchParams.append('userId', userId);
if (state) url.searchParams.append('state', state);
if (statusCode) url.searchParams.append('statusCode', statusCode);
const resetForm = () => { const response = await fetch(url.toString());
setForm({ const data = await response.json();
keyword: "", setTransactions(data.transactions);
transactionId: "", } catch (error) {
transactionReferenceId: "", console.error('Error fetching transactions:', error);
user: "", } finally {
currency: "", setLoading(false);
state: "", }
statusDescription: "",
transactionType: "",
paymentMethod: "",
psps: "",
initialPsps: "",
merchants: "",
startDate: null,
endDate: null,
lastUpdatedFrom: null,
lastUpdatedTo: null,
minAmount: "",
maxAmount: "",
channel: "",
});
}; };
return ( return (
<LocalizationProvider dateAdapter={AdapterDateFns}> <div className="p-4">
<Box p={2}> <h1 className="text-xl font-bold mb-4">Transaction Search</h1>
<Typography variant="h6" gutterBottom>
Search <div className="flex gap-4 mb-4">
</Typography> <div>
<label className="block text-sm mb-1">User ID</label>
<input
type="text"
value={userId}
onChange={(e) => setUserId(e.target.value)}
className="border p-2 rounded text-sm"
placeholder="Filter by user ID"
/>
</div>
<Grid container spacing={2}> <div>
{[ <label className="block text-sm mb-1">State</label>
{ label: "Keyword", field: "keyword", type: "text" }, <input
{ label: "Transaction ID", field: "transactionId", type: "text" }, type="text"
{ value={state}
label: "Transaction Reference ID", onChange={(e) => setState(e.target.value)}
field: "transactionReferenceId", className="border p-2 rounded text-sm"
type: "text", placeholder="Filter by state"
}, />
{ label: "User", field: "user", type: "text" }, </div>
{
label: "Currency",
field: "currency",
type: "select",
options: currencies,
},
{ label: "State", field: "state", type: "select", options: states },
{
label: "Status Description",
field: "statusDescription",
type: "text",
},
{
label: "Transaction Type",
field: "transactionType",
type: "select",
options: transactionTypes,
},
{
label: "Payment Method",
field: "paymentMethod",
type: "select",
options: paymentMethods,
},
{ label: "PSPs", field: "psps", type: "text" },
{ label: "Initial PSPs", field: "initialPsps", type: "text" },
{ label: "Merchants", field: "merchants", type: "text" },
{ label: "Start Date", field: "startDate", type: "date" },
{ label: "End Date", field: "endDate", type: "date" },
{
label: "Last Updated From",
field: "lastUpdatedFrom",
type: "date",
},
{ label: "Last Updated To", field: "lastUpdatedTo", type: "date" },
{ label: "Min Amount", field: "minAmount", type: "text" },
{ label: "Max Amount", field: "maxAmount", type: "text" },
{ label: "Channel", field: "channel", type: "text" },
].map(({ label, field, type, options }) => (
<Grid
container
item
xs={12}
alignItems="center"
spacing={1}
key={field}
>
<Grid item xs={4}>
<Typography variant="body2" fontWeight={600}>
{label}
</Typography>
</Grid>
<Grid item xs={8}>
{type === "text" && (
<TextField
fullWidth
size="small"
value={form[field]}
onChange={(e) => handleChange(field, e.target.value)}
/>
)}
{type === "select" && (
<FormControl fullWidth size="small">
<Select
value={form[field]}
onChange={(e) => handleChange(field, e.target.value)}
displayEmpty
>
<MenuItem value="">
<em>{label}</em>
</MenuItem>
{options.map((option) => (
<MenuItem value={option} key={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
)}
{type === "date" && (
<DatePicker
value={form[field]}
onChange={(newValue) => handleChange(field, newValue)}
renderInput={(params) => (
<TextField fullWidth size="small" {...params} />
)}
/>
)}
</Grid>
</Grid>
))}
{/* Buttons */} <div>
<Grid item xs={12} display="flex" justifyContent="flex-end" gap={2}> <label className="block text-sm mb-1">Status Code</label>
<Button <input
variant="outlined" type="text"
startIcon={<RefreshIcon />} value={statusCode}
onClick={resetForm} onChange={(e) => setStatusCode(e.target.value)}
> className="border p-2 rounded text-sm"
Reset placeholder="Filter by status code"
</Button> />
<Button </div>
variant="contained"
startIcon={<SearchIcon />} <div className="flex items-end">
onClick={() => console.log("Apply Filter", form)} <button
> onClick={fetchTransactions}
Apply Filter disabled={loading}
</Button> className="bg-blue-500 text-white px-4 py-2 rounded text-sm"
</Grid> >
</Grid> {loading ? 'Loading...' : 'Search'}
</Box> </button>
</LocalizationProvider> </div>
</div>
{transactions.length > 0 ? (
<div className="border rounded overflow-hidden">
<table className="min-w-full">
<thead className="bg-gray-100">
<tr>
<th className="py-2 px-4 text-left text-sm">ID</th>
<th className="py-2 px-4 text-left text-sm">User</th>
<th className="py-2 px-4 text-left text-sm">State</th>
<th className="py-2 px-4 text-left text-sm">Status Code</th>
<th className="py-2 px-4 text-left text-sm">Created</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.id} className="border-t">
<td className="py-2 px-4 text-sm">{tx.id}</td>
<td className="py-2 px-4 text-sm">{tx.user}</td>
<td className="py-2 px-4 text-sm">{tx.state}</td>
<td className="py-2 px-4 text-sm">{tx.pspStatusCode}</td>
<td className="py-2 px-4 text-sm">{tx.created}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-4 text-sm">
{loading ? 'Loading transactions...' : 'No transactions found'}
</div>
)}
</div>
); );
} }
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
import { transactionDummyData } from './transactionData';
export const handlers = [
http.get('https://api.example.com/transactions', ({ request }) => {
const url = new URL(request.url);
// Get query parameters
const userId = url.searchParams.get('userId');
const state = url.searchParams.get('state');
const statusCode = url.searchParams.get('statusCode');
// Filter transactions based on query parameters
let filteredTransactions = [...transactionDummyData];
if (userId) {
filteredTransactions = filteredTransactions.filter(
tx => tx.user.toString() === userId
);
}
if (state) {
filteredTransactions = filteredTransactions.filter(
tx => tx.state.toLowerCase() === state.toLowerCase()
);
}
if (statusCode) {
filteredTransactions = filteredTransactions.filter(
tx => tx.pspStatusCode.toString() === statusCode
);
}
return HttpResponse.json({
transactions: filteredTransactions,
count: filteredTransactions.length
});
}),
];
// mocks/transactionData.ts
export const transactionDummyData = [
{
id: 1,
merchandId: 100987998,
transactionID: 1049131973,
user: 1,
created: "2025-06-18 10:10:30",
state: "FAILED",
statusDescription: "ERR_ABOVE_LIMIT",
pspStatusCode: 100501,
},
{
id: 2,
merchandId: 100987998,
transactionID: 1049131973,
user: 2,
created: "2025-06-18 10:10:30",
state: "FAILED",
statusDescription: "ERR_ABOVE_LIMIT",
pspStatusCode: 100501,
},
{
id: 3,
merchandId: 100987998,
transactionID: 1049131973,
user: 3,
created: "2025-06-18 10:10:30",
state: "FAILED",
statusDescription: "ERR_ABOVE_LIMIT",
pspStatusCode: 100501,
}
];

View File

@ -1,11 +1,11 @@
// This ensures this component is rendered only on the client side // This ensures this component is rendered only on the client side
"use client";
import TransactionTable from "@/app/features/Pages/Transactions/Transactions"; import TransactionTable from "@/app/features/Pages/Transactions/Transactions";
export default function TransactionPage() { export default function TransactionPage() {
return ( return (
<div style={{ width: "100%" }}> <div style={{ width: "70%" }}>
{/* This page will now be rendered on the client-side */} {/* This page will now be rendered on the client-side */}
<TransactionTable /> <TransactionTable />
</div> </div>

View File

@ -21,16 +21,12 @@ import * as XLSX from "xlsx";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
import { columns } from "./constants"; import { columns } from "./constants";
import { rows } from "./mockData"; // import { rows } from "./mockData";
import AdvancedSearch from "../../AdvancedSearch/AdvancedSearch"; import AdvancedSearch from "../../AdvancedSearch/AdvancedSearch";
const paginationModel = { page: 0, pageSize: 50 }; const paginationModel = { page: 0, pageSize: 50 };
export default function TransactionTable() { export default function TransactionTable() {
const [open, setOpen] = useState(false);
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
const [filteredRows, setFilteredRows] = useState<typeof rows>([])
const [form, setForm] = useState({ const [form, setForm] = useState({
keyword: "", keyword: "",
@ -78,54 +74,14 @@ export default function TransactionTable() {
}); });
}; };
const [open, setOpen] = useState(false);
const filterRows = (rows1, filters) => { const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
// debugger const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
return rows1.filter(row => {
const hasTransactionIdFilter = filters.transactionID !== "";
const hasStateFilter = filters.state !== "";
if (hasTransactionIdFilter && hasStateFilter) {
console.log(1234)
// Return rows that match BOTH filters
return row.transactionID == filters.transactionID && row.state.toLowerCase() === filters.state.toLowerCase();
} else if (hasTransactionIdFilter) {
console.log(12345)
// Return rows that match merchandId only
return row.transactionID == filters.transactionID;
} else if (hasStateFilter) {
// Return rows that match state only
console.log(123456)
return row.state.toLowerCase() === filters.state.toLowerCase();
} else {
console.log(1234567)
// No filters applied, return all rows
return rows;
}
});
};
useEffect(() => {
console.log(form)
console.log(filterRows(rows, { transactionID: form.transactionID, state: form.state }))
setFilteredRows(filterRows(rows, { transactionID: form.transactionID, state: form.state }));
}, [form])
// useEffect(()=>{
// if(form?.transactionId){
// setFilteredRows(rows.filter(row => (row.merchandId.toString() === form.transactionId) && (row.state !== "" && (row.state === form.state))));
// } else{
// setFilteredRows(rows)
// }
// },[form])
const handleExport = () => { const handleExport = () => {
const exportRows = onlyCurrentTable ? rows.slice(0, 5) : rows; const exportRows = onlyCurrentTable ? transactions.slice(0, 5) : transactions;
const exportData = [ const exportData = [
columns.map((col) => col.headerName), columns.map((col) => col.headerName),
// @ts-expect-error - Dynamic field access from DataGrid columns
...exportRows.map((row) => columns.map((col) => row[col.field] ?? "")), ...exportRows.map((row) => columns.map((col) => row[col.field] ?? "")),
]; ];
@ -146,6 +102,30 @@ export default function TransactionTable() {
setOpen(false); setOpen(false);
}; };
const [transactions, setTransactions] = useState<any[]>([]);
// const [filters, setFilters] = useState({
// state: '',
// user: '',
// })
useEffect(() => {
const fetchTransactions = async () => {
try {
const query = new URLSearchParams(form as any).toString();
const res = await fetch(`/api/transactions?${query}`);
const data = await res.json();
setTransactions(data);
} catch (error) {
console.error('Error fetching transactions:', error);
}
};
fetchTransactions();
}, [form]);
return ( return (
<StyledPaper> <StyledPaper>
<Stack <Stack
@ -175,7 +155,7 @@ export default function TransactionTable() {
</Stack> </Stack>
<DataGrid <DataGrid
rows={filteredRows} rows={transactions}
columns={columns} columns={columns}
initialState={{ pagination: { paginationModel } }} initialState={{ pagination: { paginationModel } }}
pageSizeOptions={[50, 100]} pageSizeOptions={[50, 100]}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from '../../lib/store'; import { store } from '../redux/store';
export function Providers({ children }: { children: ReactNode }) { export function Providers({ children }: { children: ReactNode }) {
return <Provider store={store}>{children}</Provider>; return <Provider store={store}>{children}</Provider>;

View File

@ -0,0 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import advancedSearchReducer from './advanedSearch/advancedSearchSlice';
import transactionsReducer from './transactions/transactionsSlice';
export const store = configureStore({
reducer: {
advancedSearch: advancedSearchReducer,
transactions: transactionsReducer,
},
});

View File

@ -0,0 +1,3 @@
import { RootState } from "../types";
export const selectTransactions = (state: RootState) => state.transactions.data;

View File

@ -0,0 +1,83 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
interface ITransactionsData {
id: number;
merchandId: number;
transactionID: number;
user: number;
created: string;
state: string;
statusDescription: string;
pspStatusCode: number;
}
interface ITransactionsState {
data: ITransactionsData[];
loading: boolean;
error: null | string;
totalTransactions: number
}
const initialState: ITransactionsState = {
data: [],
loading: false,
error: null,
totalTransactions: 0,
}
const transactionsSlice = createSlice({
name: 'transactions',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(getTransactions.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getTransactions.fulfilled, (state, action) => {
state.data = action.payload.transactions;
state.totalTransactions = action.payload.count;
state.loading = false;
})
.addCase(getTransactions.rejected, (state, action) => {
state.error = action.error.message || "Failed to fetch categories";
state.loading = false;
state.data = [];
})
}
},
);
export default transactionsSlice.reducer;
export const getTransactions = createAsyncThunk(
'transactions/getTransactions',
async (
{
userId = '',
state = '',
statusCode = '',
}: { userId?: string; state?: string; statusCode?: string } = {}
) => {
const url = new URL('https://api.example.com/transactions');
if (userId) url.searchParams.append('userId', userId);
if (state) url.searchParams.append('state', state);
if (statusCode) url.searchParams.append('statusCode', statusCode);
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error('Failed to fetch transactions');
}
const data = await response.json();
return data; // Let the reducer store this
}
);

View File

@ -0,0 +1,5 @@
import { store } from "./store";
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -0,0 +1,29 @@
'use client'
import React, { useState } from 'react';
import SearchFilters from '../components/searchFilter/SearchFilters';
export default function Home() {
const [filters, setFilters] = useState({
user: '42',
state: 'FAILED',
startDate: '2025-06-28 23:25',
});
const handleDeleteFilter = (key) => {
setFilters((prev) => ({ ...prev, [key]: null }));
};
const handleClearAll = () => {
setFilters({ user: null, state: null, startDate: null });
};
return (
<div>
<SearchFilters
filters={filters}
onDeleteFilter={handleDeleteFilter}
onClearAll={handleClearAll}
/>
</div>
);
}

View File

@ -1,12 +0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import advancedSearchReducer from '@/app/features/AdvancedSearch/store/advancedSearchSlice';
export const store = configureStore({
reducer: {
advancedSearch: advancedSearchReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -21,6 +21,7 @@
// ]; // ];
import { transactionDummyData } from '@/app/features/Pages/transactions/mockData';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
export const handlers = [ export const handlers = [
@ -78,8 +79,6 @@ export const handlers = [
category: category || 'general', category: category || 'general',
})); }));
console.log(1234, mockProducts)
// Sort products if sort parameter provided // Sort products if sort parameter provided
if (sort === 'price') { if (sort === 'price') {
mockProducts.sort((a, b) => a.price - b.price); mockProducts.sort((a, b) => a.price - b.price);
@ -95,4 +94,38 @@ export const handlers = [
sortBy: sort, sortBy: sort,
}); });
}), }),
http.get('https://api.example.com/transactions', ({ request }) => {
const url = new URL(request.url);
// Get query parameters
const userId = url.searchParams.get('userId');
const state = url.searchParams.get('state');
const statusCode = url.searchParams.get('statusCode');
// Filter transactions based on query parameters
let filteredTransactions = [...transactionDummyData];
if (userId) {
filteredTransactions = filteredTransactions.filter(
tx => tx.user.toString() === userId
);
}
if (state) {
filteredTransactions = filteredTransactions.filter(
tx => tx.state.toLowerCase() === state.toLowerCase()
);
}
if (statusCode) {
filteredTransactions = filteredTransactions.filter(
tx => tx.pspStatusCode.toString() === statusCode
);
}
return HttpResponse.json({
transactions: filteredTransactions,
count: filteredTransactions.length
});
}),
]; ];

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1