diff --git a/app/api/transactions/route.ts b/app/api/transactions/route.ts new file mode 100644 index 0000000..0d8f238 --- /dev/null +++ b/app/api/transactions/route.ts @@ -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); +} \ No newline at end of file diff --git a/app/components/searchFilter/SearchFilters.tsx b/app/components/searchFilter/SearchFilters.tsx new file mode 100644 index 0000000..82f5692 --- /dev/null +++ b/app/components/searchFilter/SearchFilters.tsx @@ -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) => ( + + {label} {value} + + } + onDelete={() => onDeleteFilter(key)} + sx={{ mr: 1, mb: 1 }} + /> + ); + + return ( + + {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) && ( + + )} + + ); +}; + +export default SearchFilters; diff --git a/app/components/test/test1.tsx b/app/components/test/test1.tsx new file mode 100644 index 0000000..5a9accf --- /dev/null +++ b/app/components/test/test1.tsx @@ -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 = () => ( + + + {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + + + {["All mail", "Trash", "Spam"].map((text, index) => ( + + + + {index % 2 === 0 ? : } + + + + + ))} + + + ); + + return ( +
+ + {/* */} + + {list()} + +
+ ); +} diff --git a/app/components/test/test2.tsx b/app/components/test/test2.tsx new file mode 100644 index 0000000..93792b4 --- /dev/null +++ b/app/components/test/test2.tsx @@ -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([]); + 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 ( +
+

Transaction Search

+ +
+
+ + setUserId(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by user ID" + /> +
+ +
+ + setState(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by state" + /> +
+ +
+ + setStatusCode(e.target.value)} + className="border p-2 rounded text-sm" + placeholder="Filter by status code" + /> +
+ +
+ +
+
+ + {transactions.length > 0 ? ( +
+ + + + + + + + + + + + {transactions.map((tx) => ( + + + + + + + + ))} + +
IDUserStateStatus CodeCreated
{tx.id}{tx.user}{tx.state}{tx.pspStatusCode}{tx.created}
+
+ ) : ( +
+ {loading ? 'Loading transactions...' : 'No transactions found'} +
+ )} +
+ ); +} + + +// 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, + } +]; diff --git a/app/dashboard/transactions/page.tsx b/app/dashboard/transactions/page.tsx index f2059f1..64e0f47 100644 --- a/app/dashboard/transactions/page.tsx +++ b/app/dashboard/transactions/page.tsx @@ -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 ( -
+
{/* This page will now be rendered on the client-side */}
diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx new file mode 100644 index 0000000..9c03417 --- /dev/null +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -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 = () => ( + + + + + + Search + + {/* Buttons */} + + + + + + + {[ + { 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 }) => ( + + + {label} + + {type === "text" && ( + handleChange(field, e.target.value)} + /> + )} + {type === "select" && ( + + + + )} + {type === "date" && ( + handleChange(field, newValue)} + renderInput={(params) => ( + + )} + /> + )} + + ))} + + + + + ); + return ( + + + {/* */} + + {list()} + + + ); + diff --git a/app/features/Pages/transactions/Transactions.tsx b/app/features/Pages/transactions/Transactions.tsx index 58f7deb..473908d 100644 --- a/app/features/Pages/transactions/Transactions.tsx +++ b/app/features/Pages/transactions/Transactions.tsx @@ -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([]); + + 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 ( console.log(`setSearchQuery(${e.target.value})`)} sx={{ width: 300 }} /> + + + {/* */} + {/* */} +

{loginStatus}

+ +
+ ); +} diff --git a/app/test1/page.tsx b/app/test1/page.tsx new file mode 100644 index 0000000..67a44e9 --- /dev/null +++ b/app/test1/page.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/app/testWithParams/page.tsx b/app/testWithParams/page.tsx new file mode 100644 index 0000000..71f1def --- /dev/null +++ b/app/testWithParams/page.tsx @@ -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([]); + 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 ( +
+

Product Search

+ +
+
+ + setCategory(e.target.value)} + className="border p-2 rounded" + placeholder="electronics, clothing, etc." + /> +
+ +
+ + +
+ +
+ + setLimit(e.target.value)} + className="border p-2 rounded" + min="1" + max="100" + /> +
+ + +
+ + {products.length > 0 && ( +
+

Results

+
+ {products.map((product) => ( +
+

{product.name}

+

Category: {product.category}

+

Price: ${product.price}

+
+ ))} +
+
+ )} +
+ ); +} diff --git a/mock/browser.ts b/mock/browser.ts index 975f2cd..2845183 100644 --- a/mock/browser.ts +++ b/mock/browser.ts @@ -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); diff --git a/mock/handlers.ts b/mock/handlers.ts index 6db727a..d80ae73 100644 --- a/mock/handlers.ts +++ b/mock/handlers.ts @@ -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 + }); + }), ]; diff --git a/mock/server.ts b/mock/server.ts new file mode 100644 index 0000000..719bd8c --- /dev/null +++ b/mock/server.ts @@ -0,0 +1,5 @@ +// mocks/server.ts +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/next.config.ts b/next.config.ts index e9ffa30..b9172f5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package.json b/package.json index d3f094a..c37bf54 100644 --- a/package.json +++ b/package.json @@ -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" ] } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index cc739b9..2400af5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==