Added more to table filtering

This commit is contained in:
Mitchell Magro 2025-07-14 19:25:10 +02:00
parent 60af9a6e28
commit 3d4f0e86da
26 changed files with 4118 additions and 2314 deletions

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

@ -0,0 +1,103 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import InboxIcon from "@mui/icons-material/MoveToInbox";
import MailIcon from "@mui/icons-material/Mail";
import SearchIcon from "@mui/icons-material/Search";
export default function RightTemporaryDrawer() {
const [open, setOpen] = React.useState(false);
const toggleDrawer =
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
event.type === "keydown" &&
((event as React.KeyboardEvent).key === "Tab" ||
(event as React.KeyboardEvent).key === "Shift")
) {
return;
}
setOpen(open);
};
const list = () => (
<Box
sx={{ width: 400 }}
role="presentation"
onClick={toggleDrawer(false)}
onKeyDown={toggleDrawer(false)}
>
<List>
{["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{["All mail", "Trash", "Spam"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
return (
<div>
<Button
sx={{
borderRadius: "8px",
textTransform: "none",
backgroundColor: "#f5f5f5",
color: "#555",
padding: "6px 12px",
boxShadow: "inset 0 0 0 1px #ddd",
fontWeight: 400,
fontSize: "16px",
justifyContent: "flex-start",
"& .MuiButton-startIcon": {
marginRight: "12px",
backgroundColor: "#eee",
padding: "8px",
borderRadius: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
"&:hover": {
backgroundColor: "#e0e0e0",
},
}}
startIcon={<SearchIcon />}
onClick={toggleDrawer(true)}
>
Advanced Search
</Button>
{/* <Button onClick={toggleDrawer(true)}>Open Right Drawer</Button> */}
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
{list()}
</Drawer>
</div>
);
}

View File

@ -0,0 +1,189 @@
// app/transactions/page.tsx
'use client';
import { useState } from 'react';
export default function TransactionsPage() {
const [userId, setUserId] = useState('');
const [state, setState] = useState('');
const [statusCode, setStatusCode] = useState('');
const [transactions, setTransactions] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const fetchTransactions = async () => {
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 response = await fetch(url.toString());
const data = await response.json();
setTransactions(data.transactions);
} catch (error) {
console.error('Error fetching transactions:', error);
} finally {
setLoading(false);
}
};
return (
<div className="p-4">
<h1 className="text-xl font-bold mb-4">Transaction Search</h1>
<div className="flex gap-4 mb-4">
<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>
<div>
<label className="block text-sm mb-1">State</label>
<input
type="text"
value={state}
onChange={(e) => setState(e.target.value)}
className="border p-2 rounded text-sm"
placeholder="Filter by state"
/>
</div>
<div>
<label className="block text-sm mb-1">Status Code</label>
<input
type="text"
value={statusCode}
onChange={(e) => setStatusCode(e.target.value)}
className="border p-2 rounded text-sm"
placeholder="Filter by status code"
/>
</div>
<div className="flex items-end">
<button
onClick={fetchTransactions}
disabled={loading}
className="bg-blue-500 text-white px-4 py-2 rounded text-sm"
>
{loading ? 'Loading...' : 'Search'}
</button>
</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
"use client";
import TransactionTable from "@/app/features/Pages/Transactions/Transactions";
export default function TransactionPage() {
return (
<div style={{ width: "100%" }}>
<div style={{ width: "70%" }}>
{/* This page will now be rendered on the client-side */}
<TransactionTable />
</div>

View File

@ -0,0 +1,208 @@
import { useState } from "react";
import {
Box,
TextField,
MenuItem,
Button,
Drawer,
FormControl,
Select,
Typography,
Stack,
} 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"];
const states = ["Pending", "Completed", "Failed"];
const transactionTypes = ["Credit", "Debit"];
const paymentMethods = ["Card", "Bank Transfer"];
export default function AdvancedSearch({ setForm, form, resetForm }) {
const [open, setOpen] = useState(false);
const handleChange = (field: string, value: any) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
const toggleDrawer =
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
event.type === "keydown" &&
((event as React.KeyboardEvent).key === "Tab" ||
(event as React.KeyboardEvent).key === "Shift")
) {
return;
}
setOpen(open);
};
const list = () => (
<Box sx={{ width: 400 }} role="presentation">
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Box p={2}>
<Box sx={{ display: "flex", gap: "60px" }}>
<Typography variant="h6" gutterBottom>
Search
</Typography>
{/* Buttons */}
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
startIcon={<SearchIcon />}
onClick={() => console.log("Apply Filter", form)}
sx={{ "& .span": { margin: "0px", padding: "0px" } }}
>
Apply Filter
</Button>
<Button
variant="outlined"
startIcon={<RefreshIcon sx={{ margin: "0px" }} />}
onClick={resetForm}
sx={{ "& span": { margin: "0px", padding: "0px" } }}
></Button>
</Box>
</Box>
<Stack spacing={2}>
{[
{ label: "Keyword", field: "keyword", type: "text" },
{ label: "Transaction ID", field: "transactionID", type: "text" },
{
label: "Transaction Reference ID",
field: "transactionReferenceId",
type: "text",
},
{ label: "User", field: "user", type: "text" },
{
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 }) => (
<Box key={field}>
<Typography variant="body2" fontWeight={600} mb={0.5}>
{label}
</Typography>
{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} />
)}
/>
)}
</Box>
))}
</Stack>
</Box>
</LocalizationProvider>
</Box>
);
return (
<Box sx={{ width: '185px' }}>
<Button
sx={{
borderRadius: "8px",
textTransform: "none",
backgroundColor: "#f5f5f5",
color: "#555",
padding: "6px 12px",
boxShadow: "inset 0 0 0 1px #ddd",
fontWeight: 400,
fontSize: "16px",
justifyContent: "flex-start",
"& .MuiButton-startIcon": {
borderRadius: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
"&:hover": {
backgroundColor: "#e0e0e0",
},
}}
startIcon={<SearchIcon />}
onClick={toggleDrawer(true)}
>
Advanced Search
</Button>
{/* <Button onClick={toggleDrawer(true)}>Open Right Drawer</Button> */}
<Drawer anchor="right" open={open} onClose={toggleDrawer(false)}>
{list()}
</Drawer>
</Box>
);

View File

@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Button,
Dialog,
@ -21,20 +21,68 @@ import * as XLSX from "xlsx";
import { saveAs } from "file-saver";
import { DataGrid } from "@mui/x-data-grid";
import { columns } from "./constants";
import { rows } from "./mockData";
// import { rows } from "./mockData";
import AdvancedSearch from "../../AdvancedSearch/AdvancedSearch";
import SearchFilters from "@/app/components/searchFilter/SearchFilters";
const paginationModel = { page: 0, pageSize: 50 };
export default function TransactionTable() {
const [form, setForm] = useState({
keyword: "",
transactionID: "",
transactionReferenceId: "",
user: "",
currency: "",
state: "",
statusDescription: "",
transactionType: "",
paymentMethod: "",
psps: "",
initialPsps: "",
merchants: "",
startDate: null,
endDate: null,
lastUpdatedFrom: null,
lastUpdatedTo: null,
minAmount: "",
maxAmount: "",
channel: "",
});
const resetForm = () => {
setForm({
keyword: "",
transactionID: "",
transactionReferenceId: "",
user: "",
currency: "",
state: "",
statusDescription: "",
transactionType: "",
paymentMethod: "",
psps: "",
initialPsps: "",
merchants: "",
startDate: null,
endDate: null,
lastUpdatedFrom: null,
lastUpdatedTo: null,
minAmount: "",
maxAmount: "",
channel: "",
});
};
const [open, setOpen] = useState(false);
const [fileType, setFileType] = useState<"csv" | "xls" | "xlsx">("csv");
const [onlyCurrentTable, setOnlyCurrentTable] = useState(false);
const handleExport = () => {
const exportRows = onlyCurrentTable ? rows.slice(0, 5) : rows;
const exportRows = onlyCurrentTable ? transactions.slice(0, 5) : transactions;
const exportData = [
columns.map((col) => col.headerName),
// @ts-expect-error - Dynamic field access from DataGrid columns
...exportRows.map((row) => columns.map((col) => row[col.field] ?? "")),
];
@ -55,6 +103,43 @@ export default function TransactionTable() {
setOpen(false);
};
const [transactions, setTransactions] = useState<any[]>([]);
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);
}
};
useEffect(() => {
fetchTransactions();
}, [form]);
const handleDeleteFilter = (key) => {
setForm((prev) => ({ ...prev, [key]: '' }));
};
const handleClearAll = () => {
resetForm()
fetchTransactions()
};
const handleClickField = (field: string, value: any) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
return (
<StyledPaper>
<Stack
@ -71,6 +156,10 @@ export default function TransactionTable() {
onChange={(e) => console.log(`setSearchQuery(${e.target.value})`)}
sx={{ width: 300 }}
/>
<AdvancedSearch form={form} resetForm={resetForm} setForm={setForm} />
<SearchFilters filters={form} onDeleteFilter={handleDeleteFilter} onClearAll={handleClearAll} />
{/* <RightTemporaryDrawer /> */}
{/* <SearchFilterForm /> */}
<Button
variant="outlined"
startIcon={<FileUploadIcon />}
@ -81,11 +170,20 @@ export default function TransactionTable() {
</Stack>
<DataGrid
rows={rows}
rows={transactions}
columns={columns}
initialState={{ pagination: { paginationModel } }}
pageSizeOptions={[50, 100]}
sx={{ border: 0 }}
sx={{ border: 0, cursor: 'pointer' }}
onCellClick={(params, event) => {
// Check if the click is on a specific column
// Do something when this specific column is clicked
handleClickField(params.field, params.value)
console.log('Clicked cell value:', params.value); // The cell's value
console.log('Column field:', params.field); // The column's field name (from your columns definition)
console.log('Column header:', params.colDef.headerName); // The column's display name // Your custom logic here
}}
/>
{/* Export Dialog */}

View File

@ -1,80 +1,121 @@
import { GridColDef } from "@mui/x-data-grid";
export const columns: GridColDef[] = [
{ field: 'merchandId', headerName: 'Merchant ID', width: 130 },
{ field: 'transactionID', headerName: 'Transaction ID', width: 130 },
{ field: 'user', headerName: 'User', width: 75 },
{ field: 'created', headerName: 'Created', type: 'number', width: 130 },
{ field: 'state', headerName: 'State', type: 'number', width: 130 },
{ field: 'statusDescription', headerName: 'Status Description', type: 'number', width: 130 },
{ field: 'pspStatusCode', headerName: 'PSP Status Code', type: 'number', width: 130 },
{ field: 'pspStatusMessage', headerName: 'PSP Status Message', type: 'number', width: 90 },
{ field: 'psp', headerName: 'PSP', type: 'number', width: 90 },
{ field: 'pspAccount', headerName: 'PSP Account', type: 'number', width: 90 },
{ field: 'initPSP', headerName: 'Init PSP', type: 'number', width: 90 },
{ field: 'initPSPAccout', headerName: 'Init PSP Account', type: 'number', width: 90 },
{ field: 'pspService', headerName: 'PSP Service', type: 'number', width: 90 },
{ field: 'transactionType', headerName: 'Transaction Type', type: 'number', width: 90 },
{ field: 'paymentMethod', headerName: 'Payment Method', type: 'number', width: 90 },
{ field: 'rules', headerName: 'Rules', type: 'number', width: 90 },
{ field: 'amount', headerName: 'Amount', type: 'number', width: 90 },
{ field: 'fee', headerName: 'Fee', type: 'number', width: 90 },
{ field: 'transactionAmount', headerName: 'Transaction Amount', type: 'number', width: 90 },
{ field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 },
{ field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 },
{ field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 },
{ field: 'pspFee', headerName: 'PSP Fee', type: 'number', width: 90 },
{ field: 'basePspFee', headerName: 'Base PSP Fee', type: 'number', width: 90 },
{ field: 'authAmount', headerName: 'Auth Amount', type: 'number', width: 90 },
{ field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 },
{ field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 },
{ field: 'updated', headerName: 'Updated', type: 'number', width: 90 },
{ field: 'userIp', headerName: 'User IP', type: 'number', width: 90 },
{ field: 'channel', headerName: 'Channel', type: 'number', width: 90 },
{ field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 },
{ field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 },
{ field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 },
{ field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 },
{ field: 'userAccount', headerName: 'User Account', type: 'number', width: 90 },
{ field: 'bankName', headerName: 'Bank Name', type: 'number', width: 90 },
{ field: 'pspUserReference', headerName: 'PSP User Reference', type: 'number', width: 90 },
{ field: 'pspFraudScore', headerName: 'PSP Fraud Score', type: 'number', width: 90 },
{ field: 'fraudStatus', headerName: 'FraudStatus', type: 'number', width: 90 },
{ field: 'blocked', headerName: 'Blocked', type: 'number', width: 90 },
{ field: 'abuse', headerName: 'Abuse', type: 'number', width: 90 },
{ field: 'kycStatus', headerName: 'KYC Status', type: 'number', width: 90 },
{ field: 'kycPSPName', headerName: 'KYC PSP Name', type: 'number', width: 90 },
{ field: 'kycPSPStatus', headerName: 'KYC PSP Status', type: 'number', width: 90 },
{ field: 'kycIdStatus', headerName: 'KYC ID Status', type: 'number', width: 90 },
{ field: 'kycAddressStatus', headerName: 'KYC Address Status', type: 'number', width: 90 },
{ field: 'kycAgeStatus', headerName: 'KYC Age Status', type: 'number', width: 90 },
{ field: 'kycPEPAndSanction', headerName: 'KYC PEP And Sanction', type: 'number', width: 90 },
{ field: 'pspReferenceId', headerName: 'PSPReferenceID', type: 'number', width: 90 },
{ field: 'siteReferenceId', headerName: 'Site Reference ID', type: 'number', width: 90 },
{ field: 'info', headerName: 'Info', type: 'number', width: 90 },
{ field: 'accountHolder', headerName: 'Account Holder', type: 'number', width: 90 },
{ field: 'firstName', headerName: 'First Name', type: 'number', width: 90 },
{ field: 'lastName', headerName: 'Last Name', type: 'number', width: 90 },
{ field: 'street', headerName: 'Street', type: 'number', width: 90 },
{ field: 'city', headerName: 'City', type: 'number', width: 90 },
{ field: 'zip', headerName: 'ZIP', type: 'number', width: 90 },
{ field: 'dob', headerName: 'DOB', type: 'number', width: 90 },
{ field: 'mobile', headerName: 'Mobile', type: 'number', width: 90 },
{ field: 'lastUpdatedBy', headerName: 'Last Updated By', type: 'number', width: 90 },
{ field: 'ipCity', headerName: 'IP City', type: 'number', width: 90 },
{ field: 'ipRegion', headerName: 'IP Region', type: 'number', width: 90 },
{ field: 'ipCountry', headerName: 'IP Country', type: 'number', width: 90 },
{ field: 'cardIssuerCountry', headerName: 'Card Issuer Country', type: 'number', width: 90 },
{ field: 'cardBand', headerName: 'Card Band', type: 'number', width: 90 },
{ field: 'cardCategory', headerName: 'Card Category', type: 'number', width: 90 },
{ field: 'cardIssuerName', headerName: 'Card Issuer Name', type: 'number', width: 90 },
{ field: 'inn', headerName: 'INN', type: 'number', width: 90 },
{ field: 'cardType', headerName: 'Card Type', type: 'number', width: 90 },
{ field: 'firstAttempt', headerName: 'First Attempt', type: 'number', width: 90 },
{ field: 'firstSuccessful', headerName: 'First Successful', type: 'number', width: 90 },
{ field: 'firstTransaction', headerName: 'First Transaction', type: 'number', width: 90 },
{ field: 'firstPspAcountAttempt', headerName: 'First PSP Acount Attempt', type: 'number', width: 90 },
{ field: 'firstPspAcountSuccessful', headerName: 'First PSP Acount Successful', type: 'number', width: 90 },
{ field: 'originTransactionId', headerName: 'Origin Transaction ID', type: 'number', width: 90 },
{ field: 'transactionReferenceId', headerName: 'Transaction Reference ID', type: 'number', width: 90 },
];
{ field: "merchandId", headerName: "Merchant ID", width: 130 },
{ field: "transactionID", headerName: "Transaction ID", width: 130 },
{ field: "user", headerName: "User", width: 75 },
{ field: "created", headerName: "Created", type: "number", width: 130 },
{ field: "state", headerName: "State", type: "number", width: 130 },
{
field: "statusDescription",
headerName: "Status Description",
type: "number",
width: 130,
},
{
field: "statusCode",
headerName: "Status Code",
type: "number",
width: 130,
},
{
field: "pspStatusCode",
headerName: "PSP Status Code",
type: "number",
width: 130,
},
{
field: "pspStatusMessage",
headerName: "PSP Status Message",
type: "number",
width: 90,
},
{ field: "psp", headerName: "PSP", type: "number", width: 90 },
{ field: "pspAccount", headerName: "PSP Account", type: "number", width: 90 },
{ field: "initPSP", headerName: "Init PSP", type: "number", width: 90 },
{
field: "initPSPAccout",
headerName: "Init PSP Account",
type: "number",
width: 90,
},
{ field: "pspService", headerName: "PSP Service", type: "number", width: 90 },
{
field: "transactionType",
headerName: "Transaction Type",
type: "number",
width: 90,
},
{
field: "paymentMethod",
headerName: "Payment Method",
type: "number",
width: 90,
},
{ field: "rules", headerName: "Rules", type: "number", width: 90 },
{ field: "amount", headerName: "Amount", type: "number", width: 90 },
{ field: "fee", headerName: "Fee", type: "number", width: 90 },
{
field: "transactionAmount",
headerName: "Transaction Amount",
type: "number",
width: 90,
},
// { field: 'baseAmount', headerName: 'Base Amount', type: 'number', width: 90 },
// { field: 'baseFee', headerName: 'Base Fee', type: 'number', width: 90 },
// { field: 'baseTransaction', headerName: 'Base Transaction', type: 'number', width: 90 },
// { field: 'pspFee', headerName: 'PSP Fee', type: 'number', width: 90 },
// { field: 'basePspFee', headerName: 'Base PSP Fee', type: 'number', width: 90 },
// { field: 'authAmount', headerName: 'Auth Amount', type: 'number', width: 90 },
// { field: 'baseAuthAmount', headerName: 'Base Auth Amount', type: 'number', width: 90 },
// { field: 'userBalance', headerName: 'User Balance', type: 'number', width: 90 },
// { field: 'updated', headerName: 'Updated', type: 'number', width: 90 },
// { field: 'userIp', headerName: 'User IP', type: 'number', width: 90 },
// { field: 'channel', headerName: 'Channel', type: 'number', width: 90 },
// { field: 'depositType', headerName: 'Deposit Type', type: 'number', width: 90 },
// { field: 'userEmal', headerName: 'User Emal', type: 'number', width: 90 },
// { field: 'userCategory', headerName: 'User Category', type: 'number', width: 90 },
// { field: 'userCountry', headerName: 'User Country', type: 'number', width: 90 },
// { field: 'userAccount', headerName: 'User Account', type: 'number', width: 90 },
// { field: 'bankName', headerName: 'Bank Name', type: 'number', width: 90 },
// { field: 'pspUserReference', headerName: 'PSP User Reference', type: 'number', width: 90 },
// { field: 'pspFraudScore', headerName: 'PSP Fraud Score', type: 'number', width: 90 },
// { field: 'fraudStatus', headerName: 'FraudStatus', type: 'number', width: 90 },
// { field: 'blocked', headerName: 'Blocked', type: 'number', width: 90 },
// { field: 'abuse', headerName: 'Abuse', type: 'number', width: 90 },
// { field: 'kycStatus', headerName: 'KYC Status', type: 'number', width: 90 },
// { field: 'kycPSPName', headerName: 'KYC PSP Name', type: 'number', width: 90 },
// { field: 'kycPSPStatus', headerName: 'KYC PSP Status', type: 'number', width: 90 },
// { field: 'kycIdStatus', headerName: 'KYC ID Status', type: 'number', width: 90 },
// { field: 'kycAddressStatus', headerName: 'KYC Address Status', type: 'number', width: 90 },
// { field: 'kycAgeStatus', headerName: 'KYC Age Status', type: 'number', width: 90 },
// { field: 'kycPEPAndSanction', headerName: 'KYC PEP And Sanction', type: 'number', width: 90 },
// { field: 'pspReferenceId', headerName: 'PSPReferenceID', type: 'number', width: 90 },
// { field: 'siteReferenceId', headerName: 'Site Reference ID', type: 'number', width: 90 },
// { field: 'info', headerName: 'Info', type: 'number', width: 90 },
// { field: 'accountHolder', headerName: 'Account Holder', type: 'number', width: 90 },
// { field: 'firstName', headerName: 'First Name', type: 'number', width: 90 },
// { field: 'lastName', headerName: 'Last Name', type: 'number', width: 90 },
// { field: 'street', headerName: 'Street', type: 'number', width: 90 },
// { field: 'city', headerName: 'City', type: 'number', width: 90 },
// { field: 'zip', headerName: 'ZIP', type: 'number', width: 90 },
// { field: 'dob', headerName: 'DOB', type: 'number', width: 90 },
// { field: 'mobile', headerName: 'Mobile', type: 'number', width: 90 },
// { field: 'lastUpdatedBy', headerName: 'Last Updated By', type: 'number', width: 90 },
// { field: 'ipCity', headerName: 'IP City', type: 'number', width: 90 },
// { field: 'ipRegion', headerName: 'IP Region', type: 'number', width: 90 },
// { field: 'ipCountry', headerName: 'IP Country', type: 'number', width: 90 },
// { field: 'cardIssuerCountry', headerName: 'Card Issuer Country', type: 'number', width: 90 },
// { field: 'cardBand', headerName: 'Card Band', type: 'number', width: 90 },
// { field: 'cardCategory', headerName: 'Card Category', type: 'number', width: 90 },
// { field: 'cardIssuerName', headerName: 'Card Issuer Name', type: 'number', width: 90 },
// { field: 'inn', headerName: 'INN', type: 'number', width: 90 },
// { field: 'cardType', headerName: 'Card Type', type: 'number', width: 90 },
// { field: 'firstAttempt', headerName: 'First Attempt', type: 'number', width: 90 },
// { field: 'firstSuccessful', headerName: 'First Successful', type: 'number', width: 90 },
// { field: 'firstTransaction', headerName: 'First Transaction', type: 'number', width: 90 },
// { field: 'firstPspAcountAttempt', headerName: 'First PSP Acount Attempt', type: 'number', width: 90 },
// { field: 'firstPspAcountSuccessful', headerName: 'First PSP Acount Successful', type: 'number', width: 90 },
// { field: 'originTransactionId', headerName: 'Origin Transaction ID', type: 'number', width: 90 },
// { field: 'transactionReferenceId', headerName: 'Transaction Reference ID', type: 'number', width: 90 },
];

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
import ThemeRegistry from "@/config/ThemeRegistry";
import type { Metadata } from "next";
import "../styles/globals.scss";
import { Providers } from "./providers/providers";
import { MSWProvider } from './providers/msw-provider';
export const metadata: Metadata = {
title: "Your App",
@ -15,7 +17,11 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<ThemeRegistry>{children}</ThemeRegistry>
<Providers>
<MSWProvider>
<ThemeRegistry>{children}</ThemeRegistry>
</MSWProvider>
</Providers>
</body>
</html>
);

View File

@ -0,0 +1,13 @@
'use client';
import { useEffect } from 'react';
export function MSWProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
const { worker } = require('../../mock/browser.ts');
worker.start();
}
}, []);
return <>{children}</>;
}

View File

@ -0,0 +1,10 @@
'use client';
import { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { store } from '../redux/store';
export function Providers({ children }: { children: ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}

View File

@ -0,0 +1,56 @@
import { createSlice } from '@reduxjs/toolkit';
interface AdvancedSearchState {
keyword: string,
transactionID: string,
transactionReferenceId: string,
user: string,
currency: string,
state: string,
statusDescription: string,
transactionType: string,
paymentMethod: string,
psps: string,
initialPsps: string,
merchants: string,
startDate: null | string,
endDate: null | string,
lastUpdatedFrom: null | string,
lastUpdatedTo: null | string,
minAmount: string,
maxAmount: string,
channel: string,
}
const initialState: AdvancedSearchState = {
keyword: "",
transactionID: "",
transactionReferenceId: "",
user: "",
currency: "",
state: "",
statusDescription: "",
transactionType: "",
paymentMethod: "",
psps: "",
initialPsps: "",
merchants: "",
startDate: null,
endDate: null,
lastUpdatedFrom: null,
lastUpdatedTo: null,
minAmount: "",
maxAmount: "",
channel: "",
};
const advancedSearchSlice = createSlice({
name: 'advancedSearch',
initialState,
reducers: {
},
},
);
export default advancedSearchSlice.reducer;

11
app/redux/store.ts Normal file
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
}
);

5
app/redux/types.ts Normal file
View File

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

50
app/test/page.tsx Normal file
View File

@ -0,0 +1,50 @@
// app/test/page.tsx
'use client';
import { useEffect, useState } from 'react';
export default function TestPage() {
const [user, setUser] = useState(null);
const [loginStatus, setLoginStatus] = useState('');
useEffect(() => {
// Test GET request
fetch('https://api.example.com/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
const handleLogin = async () => {
// Test POST request
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'admin',
password: 'password123'
})
});
const result = await response.json();
setLoginStatus(response.ok ? 'Login successful' : `Error: ${result.error}`);
};
return (
<div>
<h1>MSW Test Page</h1>
<section>
<h2>User Data (GET)</h2>
<pre>{JSON.stringify(user, null, 2)}</pre>
</section>
<section>
<h2>Login Test (POST)</h2>
<button onClick={handleLogin}>Login as Admin</button>
<p>{loginStatus}</p>
</section>
</div>
);
}

29
app/test1/page.tsx Normal file
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

@ -0,0 +1,97 @@
// app/products/page.tsx
'use client';
import { useState } from 'react';
export default function ProductsPage() {
const [category, setCategory] = useState('');
const [sort, setSort] = useState('price');
const [limit, setLimit] = useState('10');
const [products, setProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const fetchProducts = async () => {
setLoading(true);
try {
// Construct URL with query parameters
const url = new URL('https://api.example.com/products');
if (category) url.searchParams.append('category', category);
if (sort) url.searchParams.append('sort', sort);
if (limit) url.searchParams.append('limit', limit);
const response = await fetch(url.toString());
const data = await response.json();
setProducts(data.products);
} catch (error) {
console.error('Error fetching products:', error);
} finally {
setLoading(false);
}
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Product Search</h1>
<div className="flex gap-4 mb-6">
<div>
<label className="block mb-1">Category</label>
<input
type="text"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="border p-2 rounded"
placeholder="electronics, clothing, etc."
/>
</div>
<div>
<label className="block mb-1">Sort By</label>
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="border p-2 rounded"
>
<option value="price">Price</option>
<option value="name">Name</option>
</select>
</div>
<div>
<label className="block mb-1">Items Per Page</label>
<input
type="number"
value={limit}
onChange={(e) => setLimit(e.target.value)}
className="border p-2 rounded"
min="1"
max="100"
/>
</div>
<button
onClick={fetchProducts}
disabled={loading}
className="bg-blue-500 text-white px-4 py-2 rounded self-end"
>
{loading ? 'Loading...' : 'Search'}
</button>
</div>
{products.length > 0 && (
<div>
<h2 className="text-xl font-semibold mb-2">Results</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<div key={product.id} className="border p-4 rounded">
<h3 className="font-medium">{product.name}</h3>
<p>Category: {product.category}</p>
<p>Price: ${product.price}</p>
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@ -1,5 +1,9 @@
// mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
// // mocks/browser.ts import { setupWorker } from "msw/browser";
// import { handlers } from "./handlers";
//
// export const worker = setupWorker(...handlers);
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);

View File

@ -1,96 +1,131 @@
import { http, HttpResponse } from "msw";
// import { http, HttpResponse } from "msw";
//
// export const handlers = [
// http.get(
// "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary",
// (req, _res, _ctx) => {
// const merchantId = req.url.searchParams.get("merchantId");
// const fromDate = req.url.searchParams.get("fromDate");
// const toDate = req.url.searchParams.get("toDate");
//
// console.log(merchantId, fromDate, toDate);
//
// return HttpResponse.json({
// result: {
// txCount: { total: 0, successful: 0 },
// amount: { value: "0", currency: "EUR" },
// },
// });
// }
// ),
// ];
import { transactionDummyData } from '@/app/features/Pages/transactions/mockData';
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get(
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary",
(req, _res, _ctx) => {
const merchantId = req.url.searchParams.get("merchantId");
const fromDate = req.url.searchParams.get("fromDate");
const toDate = req.url.searchParams.get("toDate");
// Simple GET endpoint
http.get('https://api.example.com/user', () => {
return HttpResponse.json({
id: 'usr_123',
name: 'John Doe',
email: 'john@example.com'
});
}),
console.log(merchantId, fromDate, toDate);
// POST endpoint with request validation
http.post('https://api.example.com/login', async ({ request }) => {
const { username, password } = await request.json() as { username: string; password: string };
if (username === 'admin' && password === 'password123') {
return HttpResponse.json({
result: {
txCount: { total: 0, successful: 0 },
amount: { value: "0", currency: "EUR" },
},
token: 'mock-jwt-token',
user: { id: 'usr_123', name: 'Admin User' }
});
}
),
http.get(
"https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/",
(req, _res, _ctx) => {
// Mock data for merchantId = 100987998
if (true) {
return HttpResponse.json({
result: [
{
merchantId: 100987998,
id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
username: "lspaddy",
firstName: "Paddy",
lastName: "Man",
email: "patrick@omegasys.eu",
phone: "",
jobTitle: "",
enabled: true,
authorities: [
"ROLE_IIN",
"ROLE_FIRST_APPROVER",
"ROLE_RULES_ADMIN",
"ROLE_TRANSACTION_VIEWER",
"ROLE_IIN_ADMIN",
"ROLE_USER_PSP_ACCOUNT",
],
allowedMerchantIds: [100987998],
created: "2025-05-04T15:32:48.432Z",
disabledBy: null,
disabledDate: null,
disabledReason: null,
incidentNotes: false,
lastLogin: "",
lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
marketingNewsletter: false,
releaseNotes: false,
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
twoFactorCondition: "required",
twoFactorCredentials: [],
},
{
merchantId: 100987998,
id: "382eed15-1e21-41fa-b1f3-0c1adb3af714",
username: "lsterence",
firstName: "Terence",
lastName: "User",
email: "terence@omegasys.eu",
phone: "",
jobTitle: "",
enabled: true,
authorities: [
"ROLE_IIN",
"ROLE_FIRST_APPROVER",
"ROLE_RULES_ADMIN",
],
allowedMerchantIds: [100987998],
created: "2025-05-04T15:32:48.432Z",
disabledBy: null,
disabledDate: null,
disabledReason: null,
incidentNotes: false,
lastLogin: "",
lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
marketingNewsletter: false,
releaseNotes: false,
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
twoFactorCondition: "required",
twoFactorCredentials: [],
},
// Add more users if needed
],
total: 4,
});
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
// Example with query parameters
http.get('https://api.example.com/products', ({ request }) => {
// Parse the URL to access query parameters
const url = new URL(request.url);
// Get query parameters
const category = url.searchParams.get('category');
const sort = url.searchParams.get('sort') || 'price';
const page = url.searchParams.get('page') || '1';
const limit = url.searchParams.get('limit') || '10';
// Validate parameters
if (limit && parseInt(limit) > 100) {
return HttpResponse.json(
{ error: 'Limit cannot exceed 100' },
{ status: 400 }
);
}
),
// Generate mock response based on parameters
const mockProducts = Array.from({ length: parseInt(limit) }, (_, i) => ({
id: i + 1,
name: `Product ${i + 1}${category ? ` in ${category}` : ''}`,
price: Math.floor(Math.random() * 100),
category: category || 'general',
}));
// Sort products if sort parameter provided
if (sort === 'price') {
mockProducts.sort((a, b) => a.price - b.price);
} else if (sort === 'name') {
mockProducts.sort((a, b) => a.name.localeCompare(b.name));
}
return HttpResponse.json({
products: mockProducts,
page: parseInt(page),
totalPages: 5,
itemsPerPage: parseInt(limit),
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
});
}),
];

5
mock/server.ts Normal file
View File

@ -0,0 +1,5 @@
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

View File

@ -2,6 +2,12 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
webpack: (config) => {
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
config.resolve.alias['@mswjs/interceptors'] = false;
}
return config;
},
};
export default nextConfig;

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"msw-init": "msw init public/ --save",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
@ -15,6 +16,7 @@
"@mui/material": "^7.1.2",
"@mui/x-data-grid": "^8.5.2",
"@mui/x-date-pickers": "^8.5.3",
"@reduxjs/toolkit": "^2.8.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
@ -23,6 +25,7 @@
"react": "^19.0.0",
"react-date-range": "^2.0.1",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"recharts": "^2.15.3",
"sass": "^1.89.2",
"xlsx": "^0.18.5"
@ -34,6 +37,7 @@
"@types/react": "^19",
"@types/react-date-range": "^1.4.10",
"@types/react-dom": "^19",
"@types/react-redux": "^7.1.34",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"msw": "^2.10.2",
@ -44,4 +48,4 @@
"public"
]
}
}
}

View File

@ -47,7 +47,7 @@
dependencies:
"@babel/types" "^7.27.3"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.27.1", "@babel/runtime@^7.27.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.27.1", "@babel/runtime@^7.27.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.27.6"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz"
integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
@ -850,6 +850,18 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@reduxjs/toolkit@^2.8.2":
version "2.8.2"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.8.2.tgz#f4e9f973c6fc930c1e0f3bf462cc95210c28f5f9"
integrity sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==
dependencies:
"@standard-schema/spec" "^1.0.0"
"@standard-schema/utils" "^0.3.0"
immer "^10.0.3"
redux "^5.0.1"
redux-thunk "^3.1.0"
reselect "^5.1.0"
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz"
@ -860,6 +872,16 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz"
integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==
"@standard-schema/spec@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
"@standard-schema/utils@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@swc/counter@0.1.3":
version "0.1.3"
resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz"
@ -945,6 +967,14 @@
resolved "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz"
integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.6"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010"
integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
@ -985,6 +1015,16 @@
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz"
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
"@types/react-redux@^7.1.34":
version "7.1.34"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e"
integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-transition-group@^4.4.12":
version "4.4.12"
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
@ -1007,6 +1047,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
"@types/use-sync-external-store@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.34.1"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz"
@ -2420,7 +2465,7 @@ headers-polyfill@^4.0.2:
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07"
integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==
hoist-non-react-statics@^3.3.1:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -2437,6 +2482,11 @@ ignore@^7.0.0:
resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz"
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
immer@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
immutable@^5.0.2:
version "5.1.3"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4"
@ -3174,6 +3224,14 @@ react-list@^0.8.13:
resolved "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz"
integrity sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw==
react-redux@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5"
integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
use-sync-external-store "^1.4.0"
react-smooth@^4.0.4:
version "4.0.4"
resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz"
@ -3224,6 +3282,23 @@ recharts@^2.15.3:
tiny-invariant "^1.3.1"
victory-vendor "^36.6.8"
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
redux@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
redux@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
version "1.0.10"
resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz"
@ -3260,7 +3335,7 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
reselect@^5.1.1:
reselect@^5.1.0, reselect@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
@ -3832,7 +3907,7 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
use-sync-external-store@^1.5.0:
use-sync-external-store@^1.4.0, use-sync-external-store@^1.5.0:
version "1.5.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==