first commit
@@ -0,0 +1,20 @@
|
||||
DIGIFI_PORT=5173
|
||||
|
||||
# Social Links
|
||||
FACEBOOK_URL=https://www.facebook.com
|
||||
TWITTER_URL=https://twitter.com
|
||||
INSTAGRAM_URL=https://www.instagram.com
|
||||
|
||||
# BACKEND END POINTS
|
||||
REACT_APP_MAIN_API='https://backoffice-apidev.simbrellang.net'
|
||||
|
||||
# ENQUIRIES CONTACTS
|
||||
VITE_CALL_ENDPOINT='09099000000'
|
||||
VITE_EMAIL_ENDPOINT='fcmbloan@support.com'
|
||||
|
||||
#BANK NAME
|
||||
VITE_BANK_NAME='First City Monument Bank'
|
||||
VITE_BANK_NAME_SHORT='FCMB'
|
||||
|
||||
# Inactivity timeout/logout AT 10MINS
|
||||
REACT_APP_TIMEOUT=600000
|
||||
@@ -0,0 +1,20 @@
|
||||
DIGIFI_PORT=5173
|
||||
|
||||
# Social Links
|
||||
VITE_FACEBOOK_URL=https://www.facebook.com
|
||||
VITE_TWITTER_URL=https://twitter.com
|
||||
VITE_INSTAGRAM_URL=https://www.instagram.com
|
||||
|
||||
# BACKEND END POINTS
|
||||
REACT_APP_MAIN_API='https://backoffice-apidev.simbrellang.net'
|
||||
|
||||
# ENQUIRIES CONTACTS
|
||||
VITE_CALL_ENDPOINT='09099000000'
|
||||
VITE_EMAIL_ENDPOINT='fcmbloan@support.com'
|
||||
|
||||
#BANK NAME
|
||||
VITE_BANK_NAME='First City Monument Bank'
|
||||
VITE_BANK_NAME_SHORT='FCMB'
|
||||
|
||||
# Inactivity timeout/logout AT 10MINS
|
||||
REACT_APP_TIMEOUT=600000
|
||||
@@ -0,0 +1,20 @@
|
||||
DIGIFI_PORT=5173
|
||||
|
||||
# Social Links
|
||||
FACEBOOK_URL=https://www.facebook.com
|
||||
TWITTER_URL=https://twitter.com
|
||||
INSTAGRAM_URL=https://www.instagram.com
|
||||
|
||||
# BACKEND END POINTS
|
||||
REACT_APP_MAIN_API='https://backoffice-apidev.simbrellang.net'
|
||||
|
||||
# ENQUIRIES CONTACTS
|
||||
VITE_CALL_ENDPOINT='09099000000'
|
||||
VITE_EMAIL_ENDPOINT='fcmbloan@support.com'
|
||||
|
||||
#BANK NAME
|
||||
VITE_BANK_NAME='First City Monument Bank'
|
||||
VITE_BANK_NAME_SHORT='FCMB'
|
||||
|
||||
# Inactivity timeout/logout AT 10MINS
|
||||
REACT_APP_TIMEOUT=600000
|
||||
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,19 @@
|
||||
# FROM node:erbium
|
||||
# pull the base image
|
||||
# FROM node:alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
#RUN npm run build
|
||||
|
||||
|
||||
# CMD [ "npm", "run", "preview" ]
|
||||
CMD [ "npm", "run", "start" ,"--", "--host"]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
@@ -0,0 +1,32 @@
|
||||
version: '3'
|
||||
services:
|
||||
merms-office:
|
||||
#image: registry.chiefsoft.net/digifi-firstoffice:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./:/app
|
||||
- '/app/node_modules'
|
||||
ports:
|
||||
- 8092:5173
|
||||
expose:
|
||||
- "5173"
|
||||
- "3000"
|
||||
extra_hosts:
|
||||
- digifi-apidev.chiefsoft.net:10.10.33.15
|
||||
- backend.wrenchboard.api.test:10.10.33.15
|
||||
environment:
|
||||
- PORT=${DIGIFI_PORT}
|
||||
tty: true
|
||||
stdin_open: true
|
||||
volumes:
|
||||
src:
|
||||
|
||||
#networks:
|
||||
# default:
|
||||
# driver: bridge
|
||||
# ipam:
|
||||
# config:
|
||||
# - subnet: 172.15.56.0/24
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "digifi-salaryloan",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"cra-template": "1.2.0",
|
||||
"formik": "^2.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-countup": "^6.5.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-scripts": "5.0.1",
|
||||
"redux": "^5.0.1",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="digiFi global back office systems"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>MERMS - BackOffice</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,18 @@
|
||||
/*@media (prefers-reduced-motion: no-preference) {*/
|
||||
/* .App-logo {*/
|
||||
/* animation: App-logo-spin infinite 20s linear;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
button, a {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
|
||||
|
||||
import SiteRoutes from './SiteRoutes';
|
||||
import LogoutModal from './components/layouts/LogoutModal';
|
||||
import { generalLayoutContext } from './context/GeneralLayoutContext';
|
||||
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {logoutModal, setLogoutModal} = generalLayoutContext()
|
||||
|
||||
useEffect(()=>{
|
||||
window.scrollTo(0,0)
|
||||
},[pathname])
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 3,
|
||||
staleTime: 300000 //5 mins
|
||||
// refetchOnMount: false,
|
||||
// staleTime: Infinity // can also be a number in millisecond
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SiteRoutes />
|
||||
|
||||
{/* LOGOUT MODAL */}
|
||||
{logoutModal && <LogoutModal close={()=>setLogoutModal(false)} />}
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
const RouteLinks = {
|
||||
loginPage: '/auth/login',
|
||||
homePage: '/',
|
||||
loansPage: '/loans',
|
||||
transactionsPage: '/transactions',
|
||||
repaymentsPage: '/repayments',
|
||||
loanChargesPage: '/loan-charges',
|
||||
offers: '/offers',
|
||||
transaction_details_page: '/transaction/details',
|
||||
errorPage: '*',
|
||||
}
|
||||
|
||||
export default RouteLinks
|
||||
@@ -0,0 +1,47 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import RouteLinks from './RouteLinks'
|
||||
|
||||
import UserExist from './authorization/UserExist'
|
||||
import PageLoader from './components/PageLoader'
|
||||
|
||||
import LoginPage from './pages/LoginPage' // LOGIN PAGE
|
||||
import HomePage from './pages/HomePage' // Home PAGE
|
||||
import LoansPage from './pages/LoansPage' // SELECTED LOANS PAGE
|
||||
import TransactionsPage from './pages/TransactionsPage' // TRANSACTIONS PAGE
|
||||
import RepaymentsPage from './pages/RepaymentsPage' // REPAYMENTS PAGE
|
||||
import LoanChargesPage from './pages/LoanChargesPage' // LOAN CHARGES PAGE
|
||||
import TransactionDetailsPage from './pages/TransactionDetailsPage' // TRANSACTION DETAILS PAGE
|
||||
import OffersPage from './pages/OffersPage' // LOAN OFFERS PAGE
|
||||
import ErrorPage from './pages/ErrorPage' // ERROR PAGE
|
||||
|
||||
|
||||
// const Home = lazy(() => import('./pages/Home'));
|
||||
|
||||
export default function SiteRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={RouteLinks.loginPage} element={<LoginPage />} /> {`*/LOGIN PAGE*/`}
|
||||
|
||||
<Route element={<UserExist />}>
|
||||
<Route path={RouteLinks.homePage} element={<HomePage />} /> {`*/HOME PAGE*/`}
|
||||
<Route path={RouteLinks.loansPage} element={<LoansPage />} /> {`*/LOANS PAGE*/`}
|
||||
<Route path={RouteLinks.transactionsPage} element={<TransactionsPage />} /> {`*/Transactions PAGE*/`}
|
||||
<Route path={RouteLinks.repaymentsPage} element={<RepaymentsPage />} /> {`*/REPAYMENTS PAGE*/`}
|
||||
<Route path={RouteLinks.loanChargesPage} element={<LoanChargesPage />} /> {`*/LOAN CHARGES PAGE*/`}
|
||||
<Route path={RouteLinks.transaction_details_page} element={<TransactionDetailsPage />} /> {`*/TRANSACTION PAGE*/`}
|
||||
<Route path={RouteLinks.offers} element={<OffersPage />} /> {`*/LOAN OFFERS PAGE*/`}
|
||||
</Route>
|
||||
|
||||
{/* ERROR PAGE */}
|
||||
<Route
|
||||
path={RouteLinks.errorPage} // error page
|
||||
element={
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<ErrorPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import DashboardLayout from "../components/layouts/DashboardLayout"
|
||||
import PageLoader from "../components/PageLoader"
|
||||
import { updateUserDetails } from "../store/UserDetails"
|
||||
import RouteLinks from "../RouteLinks"
|
||||
import debounceFunction from '../helpers/debounceFunction'
|
||||
|
||||
|
||||
export default function UserExist() {
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [lastActivityTime, setLastActivityTime] = useState(Date.now()); // HOLDS THE INITIAL TIME USER LOGS IN
|
||||
|
||||
const [pageIsLoading, setPageIsLoading] = useState(true)
|
||||
|
||||
const {userDetails} = useSelector((state) => state.userDetails)
|
||||
|
||||
|
||||
// Function to log the user out
|
||||
const logoutUser = () => {
|
||||
localStorage.clear()
|
||||
navigate(RouteLinks.login, {replace:true})
|
||||
window.location.reload()
|
||||
};
|
||||
|
||||
// Function to reset the activity time
|
||||
const resetTimer = () => {
|
||||
debounceFunction(setLastActivityTime(Date.now()), 1000)
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
const timer = setTimeout(()=>{
|
||||
if(Date.now() - Number(lastActivityTime) >= Number(process.env.REACT_APP_TIMEOUT)){
|
||||
logoutUser()
|
||||
}
|
||||
}, Number(process.env.REACT_APP_TIMEOUT))
|
||||
|
||||
// Listen for activity events
|
||||
const events = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'];
|
||||
|
||||
// Adding event listeners
|
||||
events.forEach(event => {
|
||||
window.addEventListener(event, resetTimer);
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
events.forEach(event => {
|
||||
window.removeEventListener(event, resetTimer);
|
||||
})
|
||||
}
|
||||
},[lastActivityTime])
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
const loadUser = (token) =>{
|
||||
const userExist = [{name:'dummy'}]
|
||||
if(userExist && userExist.length > 0){
|
||||
dispatch(updateUserDetails(userExist[0]))
|
||||
setPageIsLoading(false)
|
||||
}else{
|
||||
navigate(RouteLinks.login, {replace:true})
|
||||
}
|
||||
}
|
||||
if(userDetails.name){
|
||||
setPageIsLoading(false)
|
||||
}else if(!userDetails.name && localStorage.getItem('token')){
|
||||
loadUser(localStorage.getItem('token'))
|
||||
}else{
|
||||
navigate(RouteLinks.loginPage, {replace:true})
|
||||
}
|
||||
},[])
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageIsLoading ?
|
||||
<PageLoader />
|
||||
:
|
||||
<DashboardLayout></DashboardLayout>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
|
||||
// const CustomCounter = ({ targetNumber, timeInSeconds }) => {
|
||||
// const [count, setCount] = useState(0);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (targetNumber <= 0 || timeInSeconds <= 0) return; // Handle edge cases
|
||||
|
||||
// const interval = Math.floor(timeInSeconds * 1000 / targetNumber); // Time interval for each count in milliseconds
|
||||
// const totalTime = timeInSeconds * 1000; // Total time for the entire count in milliseconds
|
||||
|
||||
// let currentCount = 0;
|
||||
// const intervalId = setInterval(() => {
|
||||
// currentCount++;
|
||||
// setCount((prevCount) => prevCount + 1); // Update state using the previous state
|
||||
|
||||
// if (currentCount >= targetNumber) {
|
||||
// clearInterval(intervalId); // Stop the counting when the target number is reached
|
||||
// }
|
||||
// }, interval);
|
||||
|
||||
// // Cleanup the interval on component unmount
|
||||
// return () => clearInterval(intervalId);
|
||||
// }, [targetNumber, timeInSeconds]);
|
||||
|
||||
// return <>{count}</>;
|
||||
// };
|
||||
|
||||
// export default CustomCounter;
|
||||
|
||||
|
||||
// import React from 'react';
|
||||
// import CountUp from 'react-countup';
|
||||
|
||||
const CustomCounter = ({ targetNumber, timeInSeconds }) => {
|
||||
// return <CountUp end={targetNumber} duration={timeInSeconds} />;
|
||||
return targetNumber
|
||||
};
|
||||
|
||||
export default CustomCounter;
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function DummyLogo() {
|
||||
return (
|
||||
<div className="w-20 rounded cursor-pointer bg-black text-white p-2 flex flex-col justify-center items-center gap-0">
|
||||
<h1 className="text-sm font-bold">digiFI</h1>
|
||||
<p className="text-12">logo here</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react'
|
||||
import { AiFillProduct, AiOutlineDashboard } from 'react-icons/ai'
|
||||
import { FaEdit } from 'react-icons/fa'
|
||||
import { FaEye, FaRegMoneyBill1 } from 'react-icons/fa6'
|
||||
import { GoDotFill } from 'react-icons/go'
|
||||
import { IoPeople, IoTrash } from 'react-icons/io5'
|
||||
import { TbPlayerTrackNext, TbPlayerTrackPrev } from 'react-icons/tb'
|
||||
import { IoMdSettings } from "react-icons/io";
|
||||
import { LuMessageSquareText } from "react-icons/lu";
|
||||
import { LuPanelRight } from "react-icons/lu";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { IoLogoApple } from "react-icons/io5";
|
||||
import { FcSalesPerformance } from "react-icons/fc";
|
||||
import { FaLongArrowAltRight, FaFilter } from "react-icons/fa";
|
||||
|
||||
export default function Icons({name, className}) {
|
||||
return (
|
||||
<>
|
||||
{name.toLowerCase() == 'dashboard' ?
|
||||
<AiOutlineDashboard className={`text-base ${className}`} />
|
||||
: name.toLowerCase() == 'money' ?
|
||||
<FaRegMoneyBill1 className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'dot' ?
|
||||
<GoDotFill className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'people' ?
|
||||
<IoPeople className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'product' ?
|
||||
<AiFillProduct className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'trash' ?
|
||||
<IoTrash className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'eye' ?
|
||||
<FaEye className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'next' ?
|
||||
<TbPlayerTrackNext className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'prev' ?
|
||||
<TbPlayerTrackPrev className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'edit' ?
|
||||
<FaEdit className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'settings' ?
|
||||
<IoMdSettings className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'message' ?
|
||||
<LuMessageSquareText className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'right-panel' ?
|
||||
<LuPanelRight className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'google' ?
|
||||
<FcGoogle className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'apple' ?
|
||||
<IoLogoApple className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'sales' ?
|
||||
<FcSalesPerformance className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'arrow-right' ?
|
||||
<FaLongArrowAltRight className={`text-base ${className}`} />
|
||||
:name.toLowerCase() == 'filter' ?
|
||||
<FaFilter className={`text-base ${className}`} />
|
||||
:
|
||||
null
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function InputText({id, name, type='text', placeholder, value, handleChange}) {
|
||||
return (
|
||||
<div className='w-full h-12 overflow-hidden'>
|
||||
<input id={id} name={name} type={type} value={value} placeholder={placeholder} onChange={handleChange} className='p-2 w-full h-full text-black outline-0 ring-0 border border-slate-300 focus:border-purple-600 rounded-md' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Label({name, htmlfor, error}) {
|
||||
return (
|
||||
<label className='text-black font-semibold flex gap-1 items-center' htmlFor={htmlfor}>{name} {error && <span className='text-red-500 text-sm'>{error}</span>}</label>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function MainBtn({
|
||||
onClick,
|
||||
className,
|
||||
text,
|
||||
shrinkAside,
|
||||
children,
|
||||
loading,
|
||||
disabled
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
className={`py-2 px-3 rounded text-12 lg:text-lg ${className || ''} ${(disabled || loading) && 'opacity-60'}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children && children}
|
||||
{shrinkAside ? '' : loading? 'Loading...' : text}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { FaCircleNotch } from "react-icons/fa";
|
||||
|
||||
export default function PageLoader() {
|
||||
return (
|
||||
<div className="w-full h-full fixed top-0 left-0 bg-white opacity-75 z-50">
|
||||
<div className="flex justify-center items-center mt-[50vh]">
|
||||
<FaCircleNotch className='text-5xl text-violet-600 animate-spin' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { applyLoan } from '../../services/siteServices'
|
||||
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
|
||||
export default function LoanOffersCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.apply_loan,
|
||||
queryFn: () => applyLoan()
|
||||
})
|
||||
|
||||
const appliedUsers = data?.data?.result_data?.data // APPLY LOAN LIST
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Applications' paths={['Dashboard', 'Applications']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TableWrapper data={appliedUsers} itemsPerPage={15}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Loan
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Verified
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.name || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.bvn}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{item?.loan}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{item?.amount}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
{!item?.verified ? 'N/A' : `${getDateFromDateString(item?.verified)} ${getTimeFromDateString(item?.verified)}`}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='edit' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
<div className='hidden fle p-2 justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='trash' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TableWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { approvedLoan } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
|
||||
export default function ApprovedLoanCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.approved_loan,
|
||||
queryFn: () => approvedLoan()
|
||||
})
|
||||
|
||||
const approvedUsers = data?.data?.result_data?.data // APPLY LOAN LIST
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Approved' paths={['Dashboard', 'Approved']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TableWrapper data={approvedUsers} itemsPerPage={15}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Loan
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.name || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.bvn}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan}</div>
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='edit' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='trash' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TableWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { approvedLoan } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
|
||||
export default function DisbursementsLoanCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.approved_loan,
|
||||
queryFn: () => approvedLoan()
|
||||
})
|
||||
|
||||
const approvedUsers = data?.data?.result_data?.data // DISBURSED LOAN LIST
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Disbursements' paths={['Dashboard', 'Disbursements']} />
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TableWrapper data={approvedUsers} itemsPerPage={15}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Loan
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.name || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.bvn}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan}</div>
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='edit' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
<div className='hidden fle p-2 justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='trash' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TableWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { loanOffers } from '../../services/siteServices'
|
||||
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
|
||||
export default function ApplicationsLoanCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.apply_loan,
|
||||
queryFn: () => loanOffers()
|
||||
})
|
||||
|
||||
const loanOffersUsersList = data?.data // LOAN OFFERS LIST
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Loan offers' paths={['Dashboard', 'Loan offers']} />
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TableWrapper data={loanOffersUsersList} itemsPerPage={15}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
CID
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Loan
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Description
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Days Duration
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Active
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Score
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Lorder
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.cid || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{item?.loan}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{item?.amount}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.days_duration}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.active}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.score}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.lorder}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TableWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { selectLoan } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
|
||||
export default function SelectedLoanCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.select_loan,
|
||||
queryFn: () => selectLoan()
|
||||
})
|
||||
|
||||
const selectUsers = data?.data?.result_data?.data // APPLY LOAN LIST
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Selected - ' span='Loan options selected by the users before applying' paths={['Dashboard', 'Selected']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TableWrapper data={selectUsers} itemsPerPage={15}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Loan
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.name || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.bvn}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan}</div>
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='edit' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='trash' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TableWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useLocation, useNavigate, Link } from 'react-router-dom'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import {Formik, Form} from 'formik'
|
||||
import * as Yup from "yup";
|
||||
|
||||
import InputText from '../InputText'
|
||||
import { updateUserDetails } from "../../store/UserDetails";
|
||||
import { loginUser } from '../../services/siteServices'
|
||||
|
||||
import RouteLinks from '../../RouteLinks'
|
||||
import Icons from '../Icons'
|
||||
|
||||
|
||||
const initialValues = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
// To get the validation schema
|
||||
const validationSchema = Yup.object().shape({
|
||||
username: Yup.string().required("username is required"),
|
||||
password: Yup.string().required("password is required").min(6, 'must be upto 6 characters').max(12, 'must not exceed 12 characters'),
|
||||
});
|
||||
|
||||
export default function LoginCom() {
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const login = useMutation({
|
||||
mutationFn: (fields) => {
|
||||
if(!fields.username || !fields.password){
|
||||
throw new Error('Please provide all fields marked *')
|
||||
}
|
||||
return loginUser(fields)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error)
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
const {jwt_token, user} = res?.data
|
||||
if(jwt_token){
|
||||
localStorage.setItem('token', jwt_token)
|
||||
// localStorage.setItem('room', room)
|
||||
const data = {jwt_token}
|
||||
dispatch(updateUserDetails({ ...data, ...user }));
|
||||
}
|
||||
navigate(RouteLinks.homePage, {state:{proceed:'true'}}) // later add redux to dispatch state
|
||||
}
|
||||
})
|
||||
|
||||
// const handleLogin = () => {
|
||||
// setLoading(true)
|
||||
// const data = {name: 'dummy'}
|
||||
// localStorage.setItem('token', 'token')
|
||||
// dispatch(updateUserDetails({ ...data }));
|
||||
// setTimeout(()=>{
|
||||
// navigate(RouteLinks.homePage, {replace:true})
|
||||
// },500)
|
||||
// }
|
||||
|
||||
//FUNCTION TO HANDLE LOGIN
|
||||
const handleSubmit = (values, helper) => {
|
||||
login.mutate(values)
|
||||
// handleLogin()
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`h-screen bg-sky-300 flex flex-col items-center justify-center bg-[url('./assets/login-bg.jpg')] bg-cover bg-center bg-no-repeat`}>
|
||||
<div className='p-4 sm:p-8 w-full max-w-7xl mx-auto grid gap-8 grid-cols-1 md:grid-cols-3 lg:grid-cols-2'>
|
||||
<div className='col-span-1 md:col-span-2 lg:col-span-1 h-full'>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{(props)=>(
|
||||
<Form>
|
||||
<div className='flex flex-col gap-8 w-full bg-white rounded-xl p-16 sm:px-20 sm:py-16 shadow'>
|
||||
<div className='w-full flex flex-col gap-1 items-center'>
|
||||
<h1 className='text-2xl md:text-3xl font-bold text-black-body'>Sign In</h1>
|
||||
<p className='text-sm font-medium text-slate-500'>Welcome back, please login to your account</p>
|
||||
</div>
|
||||
|
||||
{/* social login */}
|
||||
<div className='grid grid-cols-2 gap-4 text-sm'>
|
||||
<div className='px-4 py-2 flex gap-2 items-center justify-center text-black-body font-medium border border-slate-300/50 rounded-md hover:text-primary hover:bg-sky-50 cursor-pointer'>
|
||||
<Icons name='google' />
|
||||
<span>Sign in with Google</span>
|
||||
</div>
|
||||
<div className='px-4 py-2 flex gap-2 items-center justify-center text-black-body font-medium border border-slate-300/50 rounded-md hover:text-primary hover:bg-sky-50 cursor-pointer'>
|
||||
<Icons name='apple' />
|
||||
<span>Sign in with Apple</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='relative h-[1px] bg-slate-300/50'>
|
||||
<p className='absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 bg-white p-4 text-12 text-slate-500'>Or with email</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='relative text-input flex flex-col gap-2'>
|
||||
<p className='absolute left-0 -top-4 text-red-500 text-10'>
|
||||
{(props.errors.username && props.touched.username) ? props.errors.username : ''}
|
||||
</p>
|
||||
<InputText
|
||||
id='username'
|
||||
placeholder='Username'
|
||||
name='username'
|
||||
value={props.values.username}
|
||||
handleChange={props.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative text-input flex flex-col gap-2 mb-10'>
|
||||
<p className='absolute left-0 -top-4 text-red-500 text-10'>
|
||||
{(props.errors.password && props.touched.password) ? props.errors.password : ''}
|
||||
</p>
|
||||
<InputText
|
||||
id='password'
|
||||
placeholder='Password'
|
||||
name='password'
|
||||
type='password'
|
||||
value={props.values.password}
|
||||
handleChange={props.handleChange}
|
||||
/>
|
||||
{/* <p className='text-sm text-end font-medium text-primary'>Forget password ?</p> */}
|
||||
</div>
|
||||
<div className='h-10 mb-10'>
|
||||
<button type='submit' disabled={login.isPending} className='w-full h-full bg-primary text-white font-bold rounded-md'>{login.isPending || loading ? 'loading...' : 'Login'}</button>
|
||||
</div>
|
||||
|
||||
{login.error &&
|
||||
<>
|
||||
<div className="w-full text-center">
|
||||
<p className='text-red-500 text-sm'>{login.error.message}</p>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* <p className='text-sm text-center font-medium text-slate-500'>Not yet a member? <span className='text-primary'>Sign Up</span></p> */}
|
||||
|
||||
<div className='flex justify-end gap-4 mt-6 text-[13px] font-medium'>
|
||||
<Link className='text-primary' to=''>Terms</Link>
|
||||
<Link className='text-primary' to=''>Plans</Link>
|
||||
<Link className='text-primary' to=''>Contact Us</Link>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MdKeyboardDoubleArrowRight } from 'react-icons/md'
|
||||
import { TiHomeOutline } from 'react-icons/ti'
|
||||
import DashboardHeader from '../layouts/DashboardHeader'
|
||||
|
||||
export default function BreadcrumbCom({title, span, paths}) {
|
||||
|
||||
const [stickNav, setStickNav] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
// var rect = navRef?.current?.getBoundingClientRect()?.bottom;
|
||||
var rect = 10;
|
||||
window.addEventListener('scroll', ()=>{
|
||||
if(window.scrollY >= rect + 20){
|
||||
setStickNav(true)
|
||||
console.log('tru')
|
||||
}else{
|
||||
setStickNav(false)
|
||||
console.log('false')
|
||||
}
|
||||
})
|
||||
},[])
|
||||
|
||||
return (
|
||||
// ${stickNav ? 'sticky top-0 transition-[top] duration-1000 shadow-md shadow-black' : '-top-[100px] static'}
|
||||
<div className={`sticky z-[970] top-[78px] lg:-top-10 bg-white-body dark:bg-black-body dark:border-b dark:border-black-box dark:shadow-sm dark:shadow-black-box`}>
|
||||
<div className= {`w-full py-2 flex justify-between items-center`}>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex flex-col md:flex-row gap-1 md:items-center'>
|
||||
<h1 className='text-12 sm:text-lg md:text-2xl text-black dark:text-white-body font-semibold'>{title}</h1>
|
||||
<span className='text-red-500 text-10 sm:text-base'>{span && span}</span>
|
||||
</div>
|
||||
<div className='flex gap-2 items-center text-black-gray dark:text-white-body text-base'>
|
||||
<TiHomeOutline className='text-black dark:text-white-body' />
|
||||
{paths.map((item, index) => (
|
||||
<div className='flex gap-2 items-center text-black dark:text-white-body text-10 sm:text-sm' key={index}>
|
||||
<MdKeyboardDoubleArrowRight />
|
||||
<p className={`${index + 1 == paths.length ? 'text-sky-600 dark:text-white-body/70' : ''}`}>{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden lg:flex'>
|
||||
<DashboardHeader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import ErrorLogo from '../../assets/404-error.png'
|
||||
|
||||
export default function ErrorCom() {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`h-screen bg-sky-300 flex flex-col items-center justify-center bg-[url('./assets/login-bg.jpg')] bg-cover bg-center bg-no-repeat`}>
|
||||
<div className='p-4 sm:p-8 w-full max-w-7xl mx-auto grid gap-8 place-content-center'>
|
||||
<div className='w-4/5 md:w-[650px] h-full'>
|
||||
<div className='flex flex-col gap-8 w-full bg-white rounded-xl p-16 sm:px-20 sm:py-16 shadow'>
|
||||
<div className='w-full flex flex-col gap-1 items-center'>
|
||||
<h1 className='text-2xl md:text-3xl font-bold text-black-body'>Oops!</h1>
|
||||
<p className='text-sm font-medium text-slate-500'>We can't find that page.</p>
|
||||
</div>
|
||||
|
||||
{/* <div className='relative h-[1px] bg-slate-300/50'>
|
||||
<p className='absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 bg-white p-4 text-12 text-slate-500'>Or with email</p>
|
||||
</div> */}
|
||||
|
||||
<div className='flex flex-col items-center gap-6'>
|
||||
<img src={ErrorLogo} className='w-4/5 md:w-80 h-auto' alt='error logo' />
|
||||
<div className='h-10 mb-10 flex justify-center'>
|
||||
<button onClick={()=>navigate('/', {replace:true})} className='px-2 h-full bg-primary text-sm text-white font-medium rounded-md'>Return Home</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import React from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import BreadcrumbCom from '../../components/breadcrumb/BreadcrumbCom'
|
||||
import CustomCounter from '../../components/CustomCounter'
|
||||
import Icons from '../../components/Icons'
|
||||
import formatNumber from '../../helpers/formatNumber'
|
||||
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getDashData } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import localImgLoader from '../../helpers/localImageLoader';
|
||||
import RouteLinks from '../../RouteLinks';
|
||||
|
||||
export default function HomeCom() {
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.dashboard,
|
||||
queryFn: () => getDashData(),
|
||||
})
|
||||
|
||||
const dashData = data?.data // DASHBOARD DATA
|
||||
|
||||
// console.log('dashData', dashData)
|
||||
// loans, payments, recent_transactions [], request_summary
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Dashboard' paths={['Home', 'Dashboard']} />
|
||||
|
||||
{(isFetching || isError) ?
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isError ? <p className='text-red-500'>{error.message}</p> : <p className='text-slate-800'>Loading...</p>}
|
||||
</div>
|
||||
:
|
||||
<div className='grid grid-cols-1 gap-8'>
|
||||
<div className='w-full grid grid-cols-1 xl:grid-cols-3 gap-8'>
|
||||
<div className='box min-h-[230] justify-between bg-[#F7D9E3] dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='text-base sm:text-lg font-bold hover:text-primary'>Loans</p>
|
||||
<div className='flex flex-wrap gap-2 items-end font-bold'>
|
||||
{/* <p className='text-3xl sm:text-[39px]'><span className='text-xl sm:text-2xl'>{dashData?.loans?.currency_text}</span><CustomCounter targetNumber={dashData?.loans?.value} timeInSeconds='1' /></p> */}
|
||||
<p className='text-xl sm:text-[30px]'><span className='text-lg sm:text-xl'>
|
||||
{dashData?.loans?.currency_text}
|
||||
</span><CustomCounter targetNumber={formatNumber(dashData?.loans?.value)} timeInSeconds='1' />
|
||||
</p>
|
||||
<p className='sm:text-[13.9px]'>{dashData?.loans?.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='box min-h-[230] justify-between bg-[#CBF0F5] dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='text-base sm:text-lg font-bold hover:text-primary'>Payments</p>
|
||||
<div className='flex flex-wrap gap-2 items-end font-bold'>
|
||||
<p className='text-xl sm:text-[30px]'><span className='text-lg sm:text-xl'>
|
||||
{dashData?.payments?.currency_text}
|
||||
</span><CustomCounter targetNumber={formatNumber(dashData?.payments?.value)} timeInSeconds='1' />
|
||||
</p>
|
||||
<p className='sm:text-[13.9px]'>{dashData?.payments?.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='box min-h-[230] justify-between bg-[#CBD4F4] dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='mb-4 text-base sm:text-lg font-bold hover:text-primary'>Request Summary</p>
|
||||
<div className='grid grid-cols-2 gap-4 font-bold'>
|
||||
{
|
||||
Object.values(dashData?.request_summary).map((item, index) => {
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div className='min-w-10 min-h-10 bg-white-body dark:bg-black-box dark:shadow-[0_0_0_1px_#f9f9f9] rounded-md flex justify-center items-center'>
|
||||
<Icons name='sales' />
|
||||
</div>
|
||||
<div>
|
||||
<p className='font-bold text-base'><CustomCounter targetNumber={Object.values(item)[0]} timeInSeconds='1' /></p>
|
||||
<p className='text-12 text-slate-500'>{Object.keys(item)[0]}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='box gap-8 bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<div className='grid grid-cols-1 xs:grid-cols-2 gap-4'>
|
||||
<div className='flex flex-col gap-1 order-2 xs:order-1'>
|
||||
<p className='font-bold text-base'>Recent Request</p>
|
||||
{/* <p className='text-12'>Over 500 members</p> */}
|
||||
</div>
|
||||
{/* <div className='order-1 xs:order-2 text-left xs:text-right'>
|
||||
<button className='font-bold bg-white-aside text-black-body text-12 px-4 py-2 hover:text-primary hover:bg-sky-50 dark:hover:text-white dark:hover:bg-primary dark:text-white-body dark:bg-black-body rounded-md'>+ New Member</button>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left font-semibold">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Request
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Activity
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(dashData?.recent_transactions && dashData?.recent_transactions.length > 0) ? dashData?.recent_transactions?.map((item, index) => {
|
||||
if(index <= 10) {
|
||||
return (
|
||||
<tr key={item?.id} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={localImgLoader(`loan_icons/${item?.type}.png`)} alt="Icon" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id}</div>
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)} {getTimeFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id}</div>
|
||||
<div className="font-normal text-gray-500">{item?.type}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">50%</div>
|
||||
<div className="relative h-[6px] w-full bg-white-body dark:bg-black-body rounded-full overflow-hidden">
|
||||
<div className={`absolute left-0 h-full w-1/2 bg-emerald-600`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
{/* <div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='edit' />
|
||||
</div> */}
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Link to={RouteLinks.transaction_details_page} state={{transactionID: item?.transaction_id}}>
|
||||
<Icons name='eye' />
|
||||
</Link>
|
||||
</div>
|
||||
{/* <div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='trash' />
|
||||
</div> */}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<>
|
||||
{dashData?.recent_transactions.length > 10 &&
|
||||
<tr className="py-2 border-t border-dashed text-right text-primary border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<Link to={RouteLinks.transactionsPage} className="flex justify-end items-center">
|
||||
More ...
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// import {useEffect, useRef} from 'react'
|
||||
// import ApexCharts from 'apexcharts'
|
||||
|
||||
// const Widget1 = ({chartHeight='50px'}) => {
|
||||
// const chartRef = useRef(null)
|
||||
|
||||
// const {mode} = '' // to be replaced by theme mode value later
|
||||
|
||||
// useEffect(() => {
|
||||
// const chart = refreshChart()
|
||||
|
||||
// return () => {
|
||||
// if (chart) {
|
||||
// chart.destroy()
|
||||
// }
|
||||
// }
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [chartRef, mode])
|
||||
|
||||
// const refreshChart = () => {
|
||||
// if (!chartRef.current) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const chart = new ApexCharts(chartRef.current, chartOptions(chartHeight))
|
||||
// if (chart) {
|
||||
// chart.render()
|
||||
// }
|
||||
|
||||
// return chart
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div className='w-full'>
|
||||
// {/* end::Title */}
|
||||
// <div
|
||||
// ref={chartRef}
|
||||
// className='mixed-widget-13-chart'
|
||||
// style={{height: chartHeight, minHeight: chartHeight}}
|
||||
// ></div>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// const chartOptions = (chartHeight) => {
|
||||
// // const labelColor = getCSSVariableValue('--bs-gray-800')
|
||||
// // const strokeColor = getCSSVariableValue('--bs-gray-300')
|
||||
// const labelColor = '#e9e9e9'
|
||||
// const strokeColor = '#e3e3e3'
|
||||
// // const strokeColor = getCSSVariableValue('--bs-gray-300') as string
|
||||
|
||||
// return {
|
||||
// series: [
|
||||
// {
|
||||
// name: 'Loans',
|
||||
// data: [15, 25, 15, 40, 20, 50],
|
||||
// },
|
||||
// ],
|
||||
// grid: {
|
||||
// show: false,
|
||||
// padding: {
|
||||
// top: 0,
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// },
|
||||
// },
|
||||
// chart: {
|
||||
// fontFamily: 'inherit',
|
||||
// type: 'area',
|
||||
// height: chartHeight,
|
||||
// toolbar: {
|
||||
// show: false,
|
||||
// },
|
||||
// zoom: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// sparkline: {
|
||||
// enabled: true,
|
||||
// },
|
||||
// },
|
||||
// plotOptions: {},
|
||||
// legend: {
|
||||
// show: false,
|
||||
// },
|
||||
// dataLabels: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// fill: {
|
||||
// type: 'gradient',
|
||||
// gradient: {
|
||||
// opacityFrom: 0.4,
|
||||
// opacityTo: 0,
|
||||
// stops: [20, 120, 120, 120],
|
||||
// },
|
||||
// },
|
||||
// stroke: {
|
||||
// curve: 'smooth',
|
||||
// show: true,
|
||||
// width: 3,
|
||||
// colors: ['#FFFFFF'],
|
||||
// },
|
||||
// xaxis: {
|
||||
// categories: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],
|
||||
// axisBorder: {
|
||||
// show: false,
|
||||
// },
|
||||
// axisTicks: {
|
||||
// show: false,
|
||||
// },
|
||||
// labels: {
|
||||
// show: false,
|
||||
// style: {
|
||||
// colors: labelColor,
|
||||
// fontSize: '12px',
|
||||
// },
|
||||
// },
|
||||
// crosshairs: {
|
||||
// show: false,
|
||||
// position: 'front',
|
||||
// stroke: {
|
||||
// color: strokeColor,
|
||||
// width: 1,
|
||||
// dashArray: 3,
|
||||
// },
|
||||
// },
|
||||
// tooltip: {
|
||||
// enabled: true,
|
||||
// formatter: undefined,
|
||||
// offsetY: 0,
|
||||
// style: {
|
||||
// fontSize: '12px',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// yaxis: {
|
||||
// min: 0,
|
||||
// max: 60,
|
||||
// labels: {
|
||||
// show: false,
|
||||
// style: {
|
||||
// colors: labelColor,
|
||||
// fontSize: '12px',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// states: {
|
||||
// normal: {
|
||||
// filter: {
|
||||
// type: 'none',
|
||||
// value: 0,
|
||||
// },
|
||||
// },
|
||||
// hover: {
|
||||
// filter: {
|
||||
// type: 'none',
|
||||
// value: 0,
|
||||
// },
|
||||
// },
|
||||
// active: {
|
||||
// allowMultipleDataPointsSelection: false,
|
||||
// filter: {
|
||||
// type: 'none',
|
||||
// value: 0,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// tooltip: {
|
||||
// style: {
|
||||
// fontSize: '12px',
|
||||
// },
|
||||
// y: {
|
||||
// formatter: function (val) {
|
||||
// return '$' + val + ' thousand'
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// colors: ['#ffffff'],
|
||||
// markers: {
|
||||
// colors: [labelColor],
|
||||
// strokeColors: [strokeColor],
|
||||
// strokeWidth: 3,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// export {Widget1}
|
||||
@@ -0,0 +1,147 @@
|
||||
// import {useEffect, useRef} from 'react'
|
||||
// import ApexCharts from 'apexcharts'
|
||||
|
||||
// const Widget2 = ({chartHeight='100px'}) => {
|
||||
// const chartRef = useRef(null)
|
||||
|
||||
// const {mode} = '' // to be replaced by theme mode value later
|
||||
|
||||
// useEffect(() => {
|
||||
// const chart = refreshChart()
|
||||
|
||||
// return () => {
|
||||
// if (chart) {
|
||||
// chart.destroy()
|
||||
// }
|
||||
// }
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [chartRef, mode])
|
||||
|
||||
// const refreshChart = () => {
|
||||
// if (!chartRef.current) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const chart = new ApexCharts(chartRef.current, chartOptions(chartHeight))
|
||||
// if (chart) {
|
||||
// chart.render()
|
||||
// }
|
||||
|
||||
// return chart
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div className='w-full'>
|
||||
// {/* end::Title */}
|
||||
// <div
|
||||
// ref={chartRef}
|
||||
// className='mixed-widget-13-chart'
|
||||
// style={{height: chartHeight, minHeight: chartHeight}}
|
||||
// ></div>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// const chartOptions = (chartHeight) => {
|
||||
// // const labelColor = getCSSVariableValue('--bs-gray-800')
|
||||
|
||||
// return {
|
||||
// series: [
|
||||
// {
|
||||
// name: 'Payments',
|
||||
// data: [1, 2.1, 1, 2.1, 4.1, 6.1, 4.1, 4.1, 2.1, 4.1, 2.1, 3.1, 1, 1, 2.1],
|
||||
// },
|
||||
// ],
|
||||
// chart: {
|
||||
// fontFamily: 'inherit',
|
||||
// height: chartHeight,
|
||||
// type: 'bar',
|
||||
// toolbar: {
|
||||
// show: false,
|
||||
// },
|
||||
// },
|
||||
// grid: {
|
||||
// show: false,
|
||||
// padding: {
|
||||
// top: 0,
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// },
|
||||
// },
|
||||
// colors: ['#ffffff'],
|
||||
// plotOptions: {
|
||||
// bar: {
|
||||
// borderRadius: 2.5,
|
||||
// dataLabels: {
|
||||
// position: 'top', // top, center, bottom
|
||||
// },
|
||||
// columnWidth: '20%',
|
||||
// },
|
||||
// },
|
||||
// dataLabels: {
|
||||
// enabled: false,
|
||||
// formatter: function (val) {
|
||||
// return val + '%'
|
||||
// },
|
||||
// offsetY: -20,
|
||||
// style: {
|
||||
// fontSize: '12px',
|
||||
// colors: ['#304758'],
|
||||
// },
|
||||
// },
|
||||
// xaxis: {
|
||||
// labels: {
|
||||
// show: false,
|
||||
// },
|
||||
// categories: [
|
||||
// 'Jan',
|
||||
// 'Feb',
|
||||
// 'Mar',
|
||||
// 'Apr',
|
||||
// 'May',
|
||||
// 'Jun',
|
||||
// 'Jul',
|
||||
// 'Aug',
|
||||
// 'Sep',
|
||||
// 'Oct',
|
||||
// 'Nov',
|
||||
// 'Dec',
|
||||
// 'Jan',
|
||||
// 'Feb',
|
||||
// 'Mar',
|
||||
// ],
|
||||
// position: 'top',
|
||||
// axisBorder: {
|
||||
// show: false,
|
||||
// },
|
||||
// axisTicks: {
|
||||
// show: false,
|
||||
// },
|
||||
// crosshairs: {
|
||||
// show: false,
|
||||
// },
|
||||
// tooltip: {
|
||||
// enabled: false,
|
||||
// },
|
||||
// },
|
||||
// yaxis: {
|
||||
// show: false,
|
||||
// axisBorder: {
|
||||
// show: false,
|
||||
// },
|
||||
// axisTicks: {
|
||||
// show: false,
|
||||
// // background: labelColor,
|
||||
// },
|
||||
// labels: {
|
||||
// show: false,
|
||||
// formatter: function (val) {
|
||||
// return val + '%'
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// export {Widget2}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { LuSunDim } from "react-icons/lu";
|
||||
import { IoMdSunny } from "react-icons/io";
|
||||
|
||||
import { generalLayoutContext } from "../../context/GeneralLayoutContext"
|
||||
|
||||
import UserAvatar from '../../assets/user_avatar.jpg'
|
||||
import HandBurger from "./HandBurger"
|
||||
import { Link } from "react-router-dom"
|
||||
import RouteLinks from "../../RouteLinks"
|
||||
import MainBtn from "../MainBtn";
|
||||
import { TbLogout2 } from "react-icons/tb";
|
||||
import Icons from "../Icons";
|
||||
|
||||
export default function DashboardHeader() {
|
||||
|
||||
// let {pathname} = useLocation()
|
||||
|
||||
const {theme, handleTheme, setLogoutModal, activeMenu, handleActiveMenu, showAsideDrawer, setShowAsideDrawer} = generalLayoutContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* HEADER SECTION*/}
|
||||
<header className='text-brown dark:text-white'>
|
||||
<div className='w-full flex h-20 justify-between lg:justify-end items-center'>
|
||||
<div className="lg:hidden cursor-pointer" onClick={()=>setShowAsideDrawer('aside')}>
|
||||
<HandBurger showAside={showAsideDrawer} />
|
||||
</div>
|
||||
|
||||
{/* USER AVATAR */}
|
||||
<div className="flex gap-4 justify-end">
|
||||
|
||||
{/* GO TO WALLET */}
|
||||
<Link to={RouteLinks.walletPage} className="relative px-2 flex justify-center items-center gap-2 cursor-pointer">
|
||||
<i className="fa-solid fa-wallet text-xl"></i>
|
||||
</Link>
|
||||
|
||||
{/* RIGHT DRAWER BUTTON */}
|
||||
<div onClick={()=>setShowAsideDrawer('right-aside')} className='large:hidden w-10 h-10 border border-slate-300 text-slate-500 dark:text-white-body rounded-md px-2 flex justify-center items-center gap-2 cursor-pointer' title='Switch Color Mode'>
|
||||
<Icons name='right-panel' className="text-sm md:text-xl font-bold" />
|
||||
</div>
|
||||
|
||||
{/* MESSAGE */}
|
||||
{/* <button onClick={()=>handleDrawer(drawerName.chat)} className="relative px-2 flex justify-center items-center gap-2 cursor-pointer">
|
||||
<i className="fa-regular fa-envelope text-xl"></i>
|
||||
<div className="absolute w-4 h-4 right-0 top-4 text-[8px] flex justify-center items-center rounded-full bg-emerald-500 animate-pulse">1</div>
|
||||
</button> */}
|
||||
|
||||
{/* THEME SELECTION */}
|
||||
<div onClick={handleTheme} className='w-10 h-10 border border-slate-300 text-slate-500 dark:text-white-body rounded-md px-2 flex justify-center items-center gap-2 cursor-pointer' title='Switch Color Mode'>
|
||||
{theme == 'dark' ?
|
||||
<IoMdSunny className="text-sm md:text-xl font-bold" />
|
||||
:
|
||||
<LuSunDim className="text-sm md:text-xl font-bold" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div onClick={()=>handleActiveMenu('avatar')} className='relative cursor-pointer w-10 h-10 rounded shadow-round_black dark:shadow-round_white'>
|
||||
<img src={UserAvatar} alt='user avatar' className='w-full h-full p-1 rounded-full' />
|
||||
{activeMenu == 'avatar' &&
|
||||
<div className="pop-modal z-[777] absolute p-4 w-52 sm:w-96 bg-white dark:bg-black-box right-0 top-16 rounded shadow-round_black dark:shadow-round_white">
|
||||
<div className="w-full h-full flex flex-col gap-4">
|
||||
<div className="flex flex-col text-black dark:text-white text-base sm:text-lg">
|
||||
<h1 className="font-semibold">Username</h1>
|
||||
<p className="-mt-2">username@gmail.com</p>
|
||||
</div>
|
||||
<div className="rounded w-full flex justify-center items-center gap-2">
|
||||
<MainBtn
|
||||
text='Logout'
|
||||
className="w-full text-center text-black-body hover:text-red-500 dark:text-white-body font-bold text-lg flex justify-center gap-2 items-center"
|
||||
onClick={()=>setLogoutModal(true)}
|
||||
>
|
||||
<TbLogout2 className="text-base" />
|
||||
</MainBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
import DashboardHeader from './DashboardHeader'
|
||||
import { generalLayoutContext } from '../../context/GeneralLayoutContext'
|
||||
import DashboardAside from './aside/DashboardAside'
|
||||
import RightAsideBar from './rightaside/RightAsideBar'
|
||||
|
||||
export default function DashboardLayout() {
|
||||
|
||||
// let {pathname} = useLocation()
|
||||
|
||||
const {showAsideDrawer, setShowAsideDrawer} = generalLayoutContext()
|
||||
|
||||
return (
|
||||
<div className='w-full flex gap-10 relative m-auto h-screen overflow-x-hidden overflow-y-auto bg-white-body dark:bg-black-body p-8 pt-0 lg:p-10'>
|
||||
<div className='hidden lg:block min-w-[300px] px-8 py-4 rounded-3xl sticky top-0 h-full bg-white-aside dark:bg-black-aside shadow-round_black dark:shadow-round_white'>
|
||||
<DashboardAside />
|
||||
</div>
|
||||
|
||||
<div className={`${showAsideDrawer =='aside' ? 'left-0' : '-left-full'} lg:hidden w-full fixed inset-0 z-[999]`}>
|
||||
<div className={`${showAsideDrawer =='aside' ? 'fixed left-0 top-0 inset-0' : '-left-full'} w-full bg-[rgba(0,_0,_0,_0.2)] dark:bg-[rgba(0,_0,_0,_0.4)] transition-all cursor-pointer`} onClick={()=>setShowAsideDrawer('')} ></div>
|
||||
<div className={`fixed px-8 py-4 h-full w-4/5 sm:w-[400px] bg-white-aside dark:bg-black-aside dark:text-white-light`}>
|
||||
<DashboardAside />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`main w-full bg-inherit large:mr-[400px]`}>
|
||||
<div className='fixed top-0 left-0 z-[980] w-full px-8 bg-inherit lg:hidden'>
|
||||
<DashboardHeader />
|
||||
</div>
|
||||
|
||||
{/* main body section */}
|
||||
<div className='min-h-[calc(99vh-111px)] pt-20 lg:pt-0'>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* FOOTER SECTION */}
|
||||
<footer className="py-5 text-center lg:text-end w-full text-brown dark:text-white-light">
|
||||
<p className="text-10">Copyright @ {new Date().getFullYear()} - Developed by digiFi. All Rights Reserved</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{/* Right Aisde */}
|
||||
<>
|
||||
<div className={`px-8 py-4 hidden large:flex fixed right-5 top-0 bottom-0 sm:w-[400px] bg-[#192440] dark:bg-[#1E1E2D] text-white-body`}>
|
||||
<RightAsideBar />
|
||||
</div>
|
||||
<div className={`${showAsideDrawer =='right-aside' ? 'right-0 w-full' : '-right-full w-0'} fixed inset-0 z-[999] large:hidden bg-[rgba(0,_0,_0,_0.2)] dark:bg-[rgba(0,_0,_0,_0.4)] transition-all cursor-pointer`} onClick={()=>setShowAsideDrawer('')}>
|
||||
{/* <div className={`${showAsideDrawer =='right-aside' ? 'right-0' : '-right-full'} fixed z-[999] right-0 top-0 inset-0 w-full bg-white/20 bg-red-400 transition-all cursor-pointer`} onClick={()=>setShowAsideDrawer('')} ></div> */}
|
||||
<div onClick={(e)=>e.stopPropagation()} className={`${showAsideDrawer =='right-aside' ? 'right-0' : '-right-full'} fixed z-[999] top-0 botom-0 px-8 py-4 h-full w-4/5 sm:w-[400px] bg-[#192440] dark:bg-[#1E1E2D] text-white-body`}>
|
||||
<RightAsideBar />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
export default function HandBurger({showAside, barColor}) {
|
||||
return (
|
||||
<div
|
||||
className="relative lg:hidden w-5 h-5 flex flex-col items-center justify-between"
|
||||
>
|
||||
{/* <div
|
||||
className={`absolute left-0 w-5 h-1 rounded-md ${barColor ? barColor :'bg-primary'} dark:bg-dark-light${
|
||||
showAside ? "top-1/2 -translate-y-1/2 rotate-45" : "top-0"
|
||||
}`}
|
||||
></div> */}
|
||||
<div
|
||||
className={`absolute left-0 w-5 h-1 rounded-md ${barColor ? barColor :'bg-black-box'} dark:bg-white-light ${
|
||||
showAside =='aside'
|
||||
? "bottom-1/2 translate-y-1/2 rotate-45"
|
||||
: ""
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-0 top-1/2 -translate-y-1/2 w-5 h-1 rounded-md ${barColor ? barColor :'bg-black-box/50'} dark:bg-white-light transition-all duration-300 ${
|
||||
showAside =='aside'
|
||||
? "rotate-[2000deg] opacity-0"
|
||||
: ""
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-0 w-5 h-1 rounded-md ${barColor ? barColor :'bg-black-box/50'} dark:bg-white-light ${
|
||||
showAside =='aside'
|
||||
? "top-1/2 -translate-y-1/2 -rotate-45"
|
||||
: "bottom-0"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import MainBtn from "../MainBtn"
|
||||
import ModalWrapper from "../modals/ModalWrapper"
|
||||
import RouteLinks from "../../RouteLinks"
|
||||
import { updateUserDetails } from "../../store/UserDetails"
|
||||
import { useDispatch } from "react-redux"
|
||||
|
||||
|
||||
export default function LogoutModal({close}) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(updateUserDetails({}))
|
||||
localStorage.clear()
|
||||
navigate(RouteLinks.loginPage, {replace:true})
|
||||
close()
|
||||
// location.reload()
|
||||
}
|
||||
return (
|
||||
<ModalWrapper
|
||||
>
|
||||
<div className="relative bg-white-body rounded-lg shadow-round_black dark:border-[1px] dark:border-[#1E2027] dark:bg-black-box dark:text-white">
|
||||
{/* <!-- Modal header --> */}
|
||||
<div className="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
LOGOUT
|
||||
</h3>
|
||||
<button onClick={close} type="button" className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="default-modal">
|
||||
<svg className="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* <!-- Modal body --> */}
|
||||
<div className="p-4 md:p-5 text-center">
|
||||
<svg className="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||
</svg>
|
||||
<h3 className="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to logout?</h3>
|
||||
<div className="flex justify-center items-center gap-6">
|
||||
<MainBtn onClick={handleLogout} text='Logout' className="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800" />
|
||||
<MainBtn onClick={close} text='Cancel' className="border text-black dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
{/* <!-- Modal footer --> */}
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { FaArrowRight, FaArrowLeft } from "react-icons/fa6";
|
||||
|
||||
import DashboardAside from './aside/DashboardAside'
|
||||
import DashboardHeader from './DashboardHeader'
|
||||
import { generalLayoutContext } from '../../context/GeneralLayoutContext'
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const [shrinkAside, setShrinkAside] = useState(false)
|
||||
|
||||
const [showAsideDrawer, setShowAsideDrawer] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
window.addEventListener('resize', ()=>{
|
||||
setShrinkAside(false)
|
||||
setShowAsideDrawer(false)
|
||||
})
|
||||
},[])
|
||||
return (
|
||||
<div className='w-full flex relative m-auto h-screen overflow-hidden'>
|
||||
<div
|
||||
className={`${shrinkAside ? 'w-28' : 'w-72'} hidden lg:block relative z-[999] bg-black text-white-light`}
|
||||
>
|
||||
<div className='sticky top-0 h-full'>
|
||||
<DashboardAside shrinkAside={shrinkAside} />
|
||||
<button
|
||||
className='absolute top-[72px] -translate-y-[50px] -right-5 block p-2 rounded shadow-round_black dark:shadow-round_white bg-white dark:bg-black text-black dark:text-white'
|
||||
onClick={()=>setShrinkAside(prev => !prev)}
|
||||
>
|
||||
{shrinkAside ? <FaArrowRight />: <FaArrowLeft />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
// onClick={()=>setShowAsideDrawer(prev => !prev)}
|
||||
className={`${showAsideDrawer ? 'left-0' : '-left-96'} w-4/5 sm:w-72 lg:hidden fixed inset-0 z-[999] bg-black text-white-light`}>
|
||||
<DashboardAside />
|
||||
<button
|
||||
className='absolute top-[72px] -translate-y-[50px] -right-5 block p-2 rounded shadow-round_black dark:shadow-round_white bg-white dark:bg-black text-black dark:text-white'
|
||||
onClick={(e)=>setShowAsideDrawer(prev => !prev)}
|
||||
>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='relative w-full h-full overflow-y-auto'>
|
||||
{/* HEADER SECTION generalLayoutContext*/}
|
||||
<DashboardHeader showAsideDrawer={showAsideDrawer} setShowAsideDrawer={setShowAsideDrawer} />
|
||||
|
||||
{/* BODY SECTION */}
|
||||
{/* main takes the full width minus that of the header and footer 72 for header, 39 for footer total 111 */}
|
||||
<div className='main p-3 md:p-10 bg-white-light dark:bg-slate-800 text-brown dark:text-white-light min-h-[calc(99vh-111px)]'>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* FOOTER SECTION */}
|
||||
<footer className="sticky bottom-0 text-center lg:text-end w-full bg-white dark:bg-slate-800 dark:text-white-light p-3 md:px-10 shadow-[0px_0px_2px_black]">
|
||||
<p className="text-10">Copyright @ {new Date().getFullYear()} - Developed by digiFi. All Rights Reserved</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
import Icons from "../../Icons"
|
||||
import { generalLayoutContext } from "../../../context/GeneralLayoutContext"
|
||||
|
||||
export default function AsideLink({name, to, icon}) {
|
||||
|
||||
const {shrinkAside, setShowAsideDrawer} = generalLayoutContext()
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={`w-full flex items-center gap-2 px-4 py-2 my-1 text-[13px] sm:text-sm font-semibold rounded-md hover:bg-white dark:hover:bg-black-box text-slate-500 dark:text-white-body/80 ${pathname == to ? 'bg-white-body dark:bg-black-box' : ''}`}
|
||||
to={to}
|
||||
onClick={()=>setShowAsideDrawer(false)}
|
||||
>
|
||||
{icon && <Icons name={icon} />}
|
||||
{shrinkAside ? '' : name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { FaCaretDown } from "react-icons/fa";
|
||||
import Icons from "../../Icons";
|
||||
import { generalLayoutContext } from "../../../context/GeneralLayoutContext";
|
||||
|
||||
|
||||
export default function AsideLinkWithSubLinks({name, icon, to, children, isOpen}) {
|
||||
|
||||
const {shrinkAside} = generalLayoutContext()
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const isMatchedPath = pathname.split('/').includes('')
|
||||
// isMatchedPath.splice(0,1)
|
||||
|
||||
const [hideSubMenu, setHideSubMenu] = useState(isOpen)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full px-4 py-2 my-1 text-[13px] sm:text-sm font-semibold rounded overflow-hidden`}
|
||||
>
|
||||
<button onClick={()=>setHideSubMenu(prev => !prev)} name={name} className="py-2 w-full flex items-center justify-between gap-2 cursor-pointer text-slate-500 dark:text-white-body/90">
|
||||
<span className="flex gap-2 items-center">{icon && <Icons name={icon} />}{shrinkAside ? '' : name}</span>
|
||||
<FaCaretDown className={`text-base ${(hideSubMenu) ? 'rotate-180' : 'rotate-0'}`} />
|
||||
</button>
|
||||
<div className={`w-full ${(hideSubMenu) ? 'opacity-100 min-h-72' : 'opacity-0 h-0'} transition-all duration-500`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import {useLocation} from 'react-router-dom'
|
||||
import RouteLinks from "../../../RouteLinks";
|
||||
import DummyLogo from "../../DummyLogo";
|
||||
import MainBtn from "../../MainBtn";
|
||||
import AsideLink from "./AsideLink";
|
||||
import AsideLinkWithSubLinks from "./AsideLinkWithSubLinks";
|
||||
import { useSelector } from "react-redux";
|
||||
import { generalLayoutContext } from "../../../context/GeneralLayoutContext";
|
||||
import { TbLogout2 } from "react-icons/tb";
|
||||
import UserAvatar from '../../../assets/user_avatar.jpg'
|
||||
import Icons from "../../Icons";
|
||||
|
||||
export default function DashboardAside() {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {setLogoutModal, activeMenu, handleActiveMenu} = generalLayoutContext()
|
||||
|
||||
const {userDetails} = useSelector((state) => state.userDetails) // GETS LOGGED IN USER ROLE DETAILS
|
||||
const {role}= userDetails
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col'>
|
||||
<div className="mb-3 w-full h-24 logo flex items-center">
|
||||
<DummyLogo />
|
||||
</div>
|
||||
{/* <hr className="border-slate-400" /> */}
|
||||
|
||||
<div className="aside-scroll-design w-full flex flex-col gap-2 h-full overflow-y-auto">
|
||||
{asideNavLinks.map((link, index) => {
|
||||
let active = link.status == 1 ? true : false
|
||||
let hasSubLinks = (link.subLinks && link.subLinks.length > 0) ? true : false
|
||||
if(active && !hasSubLinks){
|
||||
return (
|
||||
<div key={link.name}>
|
||||
<AsideLink to={link.to} name={link.name} icon={link.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if(active && hasSubLinks){
|
||||
let subLinkList = []
|
||||
link.subLinks.forEach(item =>{
|
||||
if(item.to){
|
||||
subLinkList.push(item.to)
|
||||
}else if(item.subLinks?.length > 0){
|
||||
item.subLinks.forEach(item => {
|
||||
subLinkList.push(item.to)
|
||||
})
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div key={link.name} className="w-full">
|
||||
{link.title &&
|
||||
<h1 className="px-4 py-2 text-sm sm:text-sm text-slate-500 dark:text-white font-semibold uppercase mt-3 mb-1 border-b border-slate-500 dark:border-white">{link.title}</h1>
|
||||
}
|
||||
<AsideLinkWithSubLinks name={link.name} icon={link.icon} isOpen={subLinkList.includes(pathname) || index==1} >
|
||||
<>
|
||||
{link.subLinks.map((subItem, index)=>{
|
||||
let active = subItem.status == 1 ? true : false
|
||||
let hasSubLinks = (subItem.subLinks && subItem.subLinks.length > 0) ? true : false
|
||||
if(active && !hasSubLinks){
|
||||
return (
|
||||
<div key={subItem.name}>
|
||||
<AsideLink to={subItem.to} name={subItem.name} icon={subItem.icon} />
|
||||
</div>
|
||||
)
|
||||
}else if(active && hasSubLinks){
|
||||
let subLinkList = subItem.subLinks.filter(value => value.to).map(item => { // specific open
|
||||
if(item.to){
|
||||
return item.to
|
||||
}
|
||||
})
|
||||
return(
|
||||
<AsideLinkWithSubLinks key={subItem.name} name={subItem.name} icon={subItem.icon} isOpen={subLinkList.includes(pathname)}>
|
||||
<>
|
||||
{subItem.subLinks.map((item, index)=>{
|
||||
let active = item.status == 1 ? true : false
|
||||
if(active){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink key={index} to={item.to} name={item.name} icon={item.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</AsideLinkWithSubLinks>
|
||||
)
|
||||
}else{
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</AsideLinkWithSubLinks>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='py-2 mt-4 relative'>
|
||||
<div className="group w-full flex items-center gap-2">
|
||||
<div className="w-full flex items-center gap-2">
|
||||
<img src={UserAvatar} alt='user avatar' className='w-12 h-12 p-1 rounded-full' />
|
||||
<div>
|
||||
<p className="text-sm font-bold text-black-body dark:text-white-body">Username</p>
|
||||
<p className="text-12 text-black-box/90 dark:text-white-body/80">username@gmail.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={()=>handleActiveMenu('settings')} className="peer text-slate-500 dark:text-white-body">
|
||||
<Icons name='settings' className='text-3xl' />
|
||||
</button>
|
||||
<div className="hidden group-hover:block pop-modal-down absolute p-4 w-full bg-white dark:bg-black-box left-0 bottom-[60%] rounded shadow-round_black dark:shadow-round_white">
|
||||
<div className="w-full min-h-48 flex flex-col justify-between gap-4">
|
||||
<div className="w-full h-full">
|
||||
<div className="flex flex-col text-black dark:text-white text-base sm:text-lg">
|
||||
<h1 className="font-semibold">Username</h1>
|
||||
<p className="-mt-2">username@gmail.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded w-full flex items-center gap-2">
|
||||
<MainBtn
|
||||
text='Logout'
|
||||
className="w-full text-center text-black-body hover:text-red-500 dark:text-white-body font-bold text-lg flex justify-center gap-2 items-center"
|
||||
onClick={()=>setLogoutModal(true)}
|
||||
>
|
||||
<TbLogout2 className="text-base" />
|
||||
</MainBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const asideNavLinks = [
|
||||
{name:'Dashboard', status:1, icon: 'dashboard', to: RouteLinks.homePage},
|
||||
{name:'First Advance', title:'Loan', status:1, icon: 'arrow-right', subLinks: [
|
||||
{name: 'Transactions', status:1, icon: 'dot', to: RouteLinks.transactionsPage},
|
||||
{name: 'Loans', status:1, icon: 'dot', to: RouteLinks.loansPage},
|
||||
{name: 'Repayments', status:1, icon: 'dot', to: RouteLinks.repaymentsPage},
|
||||
{name: 'Loan Charges', status:1, icon: 'dot', to: RouteLinks.loanChargesPage},
|
||||
{name: 'Configurations', status:1, icon: 'arrow-right', subLinks: [
|
||||
{name: 'Loan Offers', status:1, icon: 'dot', to: RouteLinks.offers },
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
// {name:'Product 2', title:'Product 2', status:1, icon: 'product', subLinks: [
|
||||
// {name: 'Applications', status:1, icon: 'dot', to: ''},
|
||||
// ]
|
||||
// },
|
||||
// {name:'Product 3', title:'Product 3', status:1, icon: 'product', subLinks: [
|
||||
// {name: 'Applications', status:1, icon: 'dot', to: ''},
|
||||
// {name: 'Configuration', status:1, icon: 'dot', to: ''},
|
||||
// ]
|
||||
// },
|
||||
// {name:'Administration', title:'Admin', status:1, icon: 'people', subLinks: [
|
||||
// {name: 'Users', status:1, icon: 'dot', to: RouteLinks.usersPage},
|
||||
// ]
|
||||
// },
|
||||
]
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {Link, useLocation} from 'react-router-dom'
|
||||
import RouteLinks from "../../../RouteLinks";
|
||||
import DummyLogo from "../../DummyLogo";
|
||||
import MainBtn from "../../MainBtn";
|
||||
import AsideLink from "./AsideLink";
|
||||
import AsideLinkWithSubLinks from "./AsideLinkWithSubLinks";
|
||||
import { useSelector } from "react-redux";
|
||||
import { generalLayoutContext } from "../../../context/GeneralLayoutContext";
|
||||
import { TbLogout2 } from "react-icons/tb";
|
||||
|
||||
|
||||
export default function DashboardAside({shrinkAside=false}) {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {setLogoutModal} = generalLayoutContext()
|
||||
|
||||
const {userDetails} = useSelector((state) => state.userDetails) // GETS LOGGED IN USER ROLE DETAILS
|
||||
const {role}= userDetails
|
||||
|
||||
const [hideSubMenu, setHideSubMenu] = useState('')
|
||||
|
||||
const handleHideSubMenu = (name) => {
|
||||
// e.stopPropagation()
|
||||
setHideSubMenu((prev) => {
|
||||
if(prev == name){
|
||||
return ''
|
||||
}else{
|
||||
return name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col'>
|
||||
<div className="px-4 w-full h-24 logo flex items-center">
|
||||
<DummyLogo />
|
||||
</div>
|
||||
<hr className="border-slate-400" />
|
||||
|
||||
<div className="p-4 w-full flex flex-col gap-2 h-full overflow-y-auto">
|
||||
{asideNavLinks.map((link, index) => {
|
||||
let active = link.status == 1 ? true : false
|
||||
let hasSubLinks = (link.subLinks && link.subLinks.length > 0) ? true : false
|
||||
if(active && !hasSubLinks){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink key={index} to={link.to} shrinkAside={shrinkAside} name={link.name} icon={link.icon} />
|
||||
</div>
|
||||
)
|
||||
}else if(active && hasSubLinks){
|
||||
return (
|
||||
<div key={index} className="w-full">
|
||||
{link.title &&
|
||||
<h1 className="px-4 py-2 text-12 text-slate-400 font-semibold uppercase mt-3 mb-1 border-b border-slate-800">{link.title}</h1>
|
||||
}
|
||||
<AsideLinkWithSubLinks hideSubMenu={hideSubMenu} setHideSubMenu={handleHideSubMenu} shrinkAside={shrinkAside} name={link.name} icon={link.icon}>
|
||||
<>
|
||||
{link.subLinks.map((subItem, index)=>{
|
||||
let active = subItem.status == 1 ? true : false
|
||||
if(active){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink key={index} to={subItem.to} shrinkAside={shrinkAside} name={subItem.name} icon={subItem.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</AsideLinkWithSubLinks>
|
||||
</div>
|
||||
)
|
||||
}else{
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className="bg-primary rounded w-full flex justify-center items-center gap-2">
|
||||
<MainBtn
|
||||
shrinkAside={shrinkAside}
|
||||
text='Logout'
|
||||
className="w-full text-center flex justify-center gap-2 items-center"
|
||||
onClick={()=>setLogoutModal(true)}
|
||||
>
|
||||
<TbLogout2 className="text-base" />
|
||||
</MainBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const asideNavLinks = [
|
||||
{name:'Dashboard', status:1, icon: 'dashboard', to: RouteLinks.homePage},
|
||||
{name:'Salary Loan', title:'Loan', status:1, icon: 'money', subLinks: [
|
||||
{name: 'Selected Loans', status:1, icon: 'dot', to: RouteLinks.selectedLoanPage},
|
||||
{name: 'Applications', status:1, icon: 'dot', to: RouteLinks.applicationsLoanPage},
|
||||
{name: 'Approved Loans', status:1, icon: 'dot', to: RouteLinks.approvedLoansPage},
|
||||
{name: 'Disbursements', status:1, icon: 'dot', to: RouteLinks.disbursementsLoanPage},
|
||||
{name: 'Payments', status:1, icon: 'dot', to: '#'},
|
||||
{name: 'Configurations', status:1, icon: 'dot', to: '#'},
|
||||
],
|
||||
},
|
||||
{name:'Product 2', title:'Product 2', status:1, icon: 'product', subLinks: [
|
||||
{name: 'Applications', status:1, icon: 'dot', to: '#'},
|
||||
{name: 'Configuration', status:1, icon: 'dot', to: '#'},
|
||||
]
|
||||
},
|
||||
{name:'Product 3', title:'Product 3', status:1, icon: 'product', subLinks: [
|
||||
{name: 'Applications', status:1, icon: 'dot', to: '#'},
|
||||
{name: 'Configuration', status:1, icon: 'dot', to: '#'},
|
||||
]
|
||||
},
|
||||
{name:'Administration', title:'Admin', status:1, icon: 'people', subLinks: [
|
||||
{name: 'Users', status:1, icon: 'dot', to: RouteLinks.usersPage},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {Link, useLocation} from 'react-router-dom'
|
||||
import RouteLinks from "../../../RouteLinks";
|
||||
import DummyLogo from "../../DummyLogo";
|
||||
import MainBtn from "../../MainBtn";
|
||||
import AsideLink from "./AsideLink";
|
||||
import AsideLinkWithSubLinks from "./AsideLinkWithSubLinks";
|
||||
import { useSelector } from "react-redux";
|
||||
import { generalLayoutContext } from "../../../context/GeneralLayoutContext";
|
||||
import { TbLogout2 } from "react-icons/tb";
|
||||
|
||||
|
||||
export default function DashboardAside({shrinkAside=false}) {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {setLogoutModal} = generalLayoutContext()
|
||||
|
||||
const {userDetails} = useSelector((state) => state.userDetails) // GETS LOGGED IN USER ROLE DETAILS
|
||||
const {role}= userDetails
|
||||
|
||||
// const [hideSubMenu, setHideSubMenu] = useState('')
|
||||
|
||||
// const handleHideSubMenu = (name) => {
|
||||
// // e.stopPropagation()
|
||||
// setHideSubMenu((prev) => {
|
||||
// if(prev == name){
|
||||
// return ''
|
||||
// }else{
|
||||
// return name
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col'>
|
||||
<div className="px-4 w-full h-24 logo flex items-center">
|
||||
<DummyLogo />
|
||||
</div>
|
||||
<hr className="border-slate-400" />
|
||||
|
||||
<div className="p-4 w-full flex flex-col gap-2 h-full overflow-y-auto">
|
||||
{asideNavLinks.map((link, index) => {
|
||||
let active = link.status == 1 ? true : false
|
||||
let hasSubLinks = (link.subLinks && link.subLinks.length > 0) ? true : false
|
||||
if(active && !hasSubLinks){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink to={link.to} shrinkAside={shrinkAside} name={link.name} icon={link.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if(active && hasSubLinks){
|
||||
// let subLinkList = link.subLinks.filter(value => value.to).map(item => { //any of all open
|
||||
// if(item.to){
|
||||
// return item.to
|
||||
// }
|
||||
// })
|
||||
let subLinkList = []
|
||||
link.subLinks.forEach(item =>{
|
||||
if(item.to){
|
||||
subLinkList.push(item.to)
|
||||
}else if(item.subLinks?.length > 0){
|
||||
item.subLinks.forEach(item => {
|
||||
subLinkList.push(item.to)
|
||||
})
|
||||
}
|
||||
})
|
||||
// console.log('subLinkList', subLinkList)
|
||||
return (
|
||||
<div key={index} className="w-full">
|
||||
{link.title &&
|
||||
<h1 className="px-4 py-2 text-12 text-slate-400 font-semibold uppercase mt-3 mb-1 border-b border-slate-800">{link.title}</h1>
|
||||
}
|
||||
<AsideLinkWithSubLinks shrinkAside={shrinkAside} name={link.name} icon={link.icon} isOpen={subLinkList.includes(pathname)} >
|
||||
<>
|
||||
{link.subLinks.map((subItem, index)=>{
|
||||
let active = subItem.status == 1 ? true : false
|
||||
let hasSubLinks = (subItem.subLinks && subItem.subLinks.length > 0) ? true : false
|
||||
if(active && !hasSubLinks){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink key={index} to={subItem.to} shrinkAside={shrinkAside} name={subItem.name} icon={subItem.icon} />
|
||||
</div>
|
||||
)
|
||||
}else if(active && hasSubLinks){
|
||||
let subLinkList = subItem.subLinks.filter(value => value.to).map(item => { // specific open
|
||||
if(item.to){
|
||||
return item.to
|
||||
}
|
||||
})
|
||||
return(
|
||||
<AsideLinkWithSubLinks shrinkAside={shrinkAside} name={subItem.name} icon={subItem.icon} isOpen={subLinkList.includes(pathname)}>
|
||||
<>
|
||||
{subItem.subLinks.map((item, index)=>{
|
||||
let active = item.status == 1 ? true : false
|
||||
if(active){
|
||||
return (
|
||||
<div key={index}>
|
||||
<AsideLink key={index} to={item.to} shrinkAside={shrinkAside} name={item.name} icon={item.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</AsideLinkWithSubLinks>
|
||||
)
|
||||
}else{
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</>
|
||||
</AsideLinkWithSubLinks>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='px-4 py-2'>
|
||||
<div className="bg-primary rounded w-full flex justify-center items-center gap-2">
|
||||
<MainBtn
|
||||
shrinkAside={shrinkAside}
|
||||
text='Logout'
|
||||
className="w-full text-center flex justify-center gap-2 items-center"
|
||||
onClick={()=>setLogoutModal(true)}
|
||||
>
|
||||
<TbLogout2 className="text-base" />
|
||||
</MainBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const asideNavLinks = [
|
||||
{name:'Dashboard', status:1, icon: 'dashboard', to: RouteLinks.homePage},
|
||||
{name:'Salary Loan', title:'Loan', status:1, icon: 'money', subLinks: [
|
||||
{name: 'Selected Loans', status:1, icon: 'dot', to: RouteLinks.selectedLoanPage},
|
||||
{name: 'Applications', status:1, icon: 'dot', to: RouteLinks.applicationsLoanPage},
|
||||
{name: 'Approved Loans', status:1, icon: 'dot', to: RouteLinks.approvedLoansPage},
|
||||
{name: 'Disbursements', status:1, icon: 'dot', to: RouteLinks.disbursementsLoanPage},
|
||||
{name: 'Payments', status:1, icon: 'dot', to: ''},
|
||||
{name: 'Configurations', status:1, icon: 'dot', subLinks: [
|
||||
{name: 'Loan Offers', status:1, icon: 'dot', to: RouteLinks.loanOffersPage },
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
// {name:'Product 2', title:'Product 2', status:1, icon: 'product', subLinks: [
|
||||
// {name: 'Applications', status:1, icon: 'dot', to: ''},
|
||||
// ]
|
||||
// },
|
||||
// {name:'Product 3', title:'Product 3', status:1, icon: 'product', subLinks: [
|
||||
// {name: 'Applications', status:1, icon: 'dot', to: ''},
|
||||
// {name: 'Configuration', status:1, icon: 'dot', to: ''},
|
||||
// ]
|
||||
// },
|
||||
// {name:'Administration', title:'Admin', status:1, icon: 'people', subLinks: [
|
||||
// {name: 'Users', status:1, icon: 'dot', to: RouteLinks.usersPage},
|
||||
// ]
|
||||
// },
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import Img from '../../../assets/user_avatar.jpg'
|
||||
import CustomCounter from '../../CustomCounter'
|
||||
|
||||
export default function Orders() {
|
||||
return (
|
||||
<div className='h-full p-2 sm:p-4 large:p-8 flex flex-col gap-16 overflow-y-auto aside-scroll-design'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Recent Eligibility</p>
|
||||
<div className='grid grid-cols-2 gap-4 sm:gap-6 large:gap-8'>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={18} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Pending</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={5} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Approved</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={1} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Rejected</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={1} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Created</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Recent Loans</p>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { useState } from 'react'
|
||||
import Icons from '../../Icons'
|
||||
import Orders from './Orders'
|
||||
import Tickets from './Tickets'
|
||||
import Tasks from './Tasks'
|
||||
|
||||
export default function RightAsideBar() {
|
||||
|
||||
let [active, setActive] = useState('orders')
|
||||
|
||||
const handleActiveMenu = (name) => {
|
||||
let lowerStr = name.toLowerCase()
|
||||
setActive(lowerStr)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col gap-8'>
|
||||
{/* Menu */}
|
||||
<div className='grid grid-cols-3 gap-8'>
|
||||
<button name='orders' onClick={() => handleActiveMenu('orders')} className={`flex justify-center items-center px-2 py-3 large:px-4 large:py-5 rounded-md shadow-round_white bg-[#0E172E] text-white-body hover:scale-[1.1] ${active == 'orders' && 'scale-[1.2]'}`}>
|
||||
<Icons name='dashboard' className='text-3xl' />
|
||||
</button>
|
||||
<button name='tickets' onClick={() => handleActiveMenu('tickets')} className={`flex justify-center items-center px-2 py-3 large:px-4 large:py-5 rounded-md shadow-round_white bg-[#0E172E] text-white-body hover:scale-[1.1] ${active == 'tickets' && 'scale-[1.2]'}`}>
|
||||
<Icons name='settings' className='text-3xl' />
|
||||
</button>
|
||||
<button name='tasks' onClick={() => handleActiveMenu('tasks')} className={`flex justify-center items-center px-2 py-3 large:px-4 large:py-5 rounded-md shadow-round_white bg-[#0E172E] text-white-body hover:scale-[1.1] ${active == 'tasks' && 'scale-[1.2]'}`}>
|
||||
<Icons name='dashboard' className='text-3xl' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
{active == 'orders' && <Orders />}
|
||||
{active == 'tickets' && <Tickets />}
|
||||
{active == 'tasks' && <Tasks />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import Img from '../../../assets/user_avatar.jpg'
|
||||
import CustomCounter from '../../CustomCounter'
|
||||
|
||||
export default function Tasks() {
|
||||
return (
|
||||
<div className='h-full p-2 sm:p-4 large:p-8 flex flex-col gap-16 overflow-y-auto aside-scroll-design'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Support Tasks</p>
|
||||
<div className='grid grid-cols-2 gap-4 sm:gap-6 large:gap-8'>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={2} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Pending</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={20} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Completed</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={3} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Rejected</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={8} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Created</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Best Tasks</p>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import Img from '../../../assets/user_avatar.jpg'
|
||||
import CustomCounter from '../../CustomCounter'
|
||||
|
||||
export default function Tickets() {
|
||||
return (
|
||||
<div className='h-full p-2 sm:p-4 large:p-8 flex flex-col gap-16 overflow-y-auto aside-scroll-design'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Recent Repayment</p>
|
||||
<div className='grid grid-cols-2 gap-4 sm:gap-6 large:gap-8'>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={18} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Pending</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={2} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Offers</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={3} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Created</p>
|
||||
</div>
|
||||
<div className='p-2 sm:p-3 large:p-4 flex flex-col border border-slate-500 border-dashed'>
|
||||
<p className='text-base font-bold text-white-body'>
|
||||
<CustomCounter targetNumber={1} timeInSeconds={1} />
|
||||
</p>
|
||||
<p className='text-sm text-slate-500'>Rejected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<p className='text-base text-white-body font-bold'>Tracked Errors</p>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='px-4 py-2 bg-[#0E172E] rounded-md'>
|
||||
<img src={Img} className='w-8' alt="Order Image" />
|
||||
</div>
|
||||
<div className='flex-col'>
|
||||
<p className='text-base font-bold text-white-body'>Project Briefing</p>
|
||||
<p className='text-sm text-slate-500'>Project Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
import { layoutDefaultContext } from "../../context/DefaultLayoutContext"
|
||||
|
||||
|
||||
export default function FooterLinks({
|
||||
linkName,
|
||||
href,
|
||||
}) {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {subLinkIsActive, setSubLinkIsActive} = layoutDefaultContext() // CONTEXT TO GET WHEN A SUBLINK IS CLICKED
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`${pathname == href && 'text-white-light/50'} block relative p-1 font-semibold text-sm lg:text-sm text-white-light before:absolute before:left-0 before:top-full before:w-0 before:content-[''] before:h-[2px] before:bg-white-light hover:before:w-full hover:text-white-light/50`}
|
||||
onClick={()=>{
|
||||
if(subLinkIsActive){
|
||||
setSubLinkIsActive((prev)=>{
|
||||
if(prev == linkName){
|
||||
return null
|
||||
}else{
|
||||
return linkName
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{linkName}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
import { layoutDefaultContext } from "../../context/DefaultLayoutContext"
|
||||
|
||||
export default function NavLinks({
|
||||
linkName,
|
||||
href,
|
||||
}) {
|
||||
|
||||
const {pathname} = useLocation()
|
||||
|
||||
const {subLinkIsActive, setSubLinkIsActive} = layoutDefaultContext() // CONTEXT TO GET WHEN A SUBLINK IS CLICKED
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`${pathname == href && 'text-brown/50'} block uppercase relative p-1 font-semibold text-sm lg:text-sm text-brown before:absolute before:left-0 before:top-full before:w-0 before:content-[''] before:h-[2px] before:bg-brown hover:before:w-full hover:text-brown/50`}
|
||||
onClick={()=>{
|
||||
if(subLinkIsActive){
|
||||
setSubLinkIsActive((prev)=>{
|
||||
// e.stopPropagation()
|
||||
// console.log('bubble')
|
||||
if(prev == linkName){
|
||||
return null
|
||||
}else{
|
||||
return linkName
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{linkName}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// import { useLocation } from "react-router-dom"
|
||||
import NavLinks from "./NavLinks"
|
||||
import { layoutDefaultContext } from "../../context/DefaultLayoutContext"
|
||||
|
||||
|
||||
export default function NavLinksWithSubLinks({
|
||||
linkName,
|
||||
subLink=[],
|
||||
asLink=true,
|
||||
}) {
|
||||
|
||||
// const {pathname} = useLocation()
|
||||
const {subLinkIsActive, setSubLinkIsActive} = layoutDefaultContext() // CONTEXT TO GET WHEN A SUBLINK IS CLICKED
|
||||
|
||||
return (
|
||||
// <Link
|
||||
// to={''}
|
||||
// className="z-[999] relative p-1 font-semibold text-sm lg:text-lg text-brown"
|
||||
// >
|
||||
// <span className="flex gap-1 items-center">
|
||||
// {linkName}
|
||||
// <i className="fa-solid fa-caret-up rotate-180"></i>
|
||||
// </span>
|
||||
// {/* sub links section */}
|
||||
// <div className="py-3 absolute top-[60px] w-48 rounded-md bg-white-light">
|
||||
// {subLink.map(item => (
|
||||
// <div className="w-full px-2 py-1">
|
||||
// <NavLinks
|
||||
// linkName={item.linkName}
|
||||
// href={item.link}
|
||||
// />
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </Link>
|
||||
|
||||
<>
|
||||
{asLink ?
|
||||
<button
|
||||
className={`${(subLinkIsActive && subLinkIsActive == linkName) && 'text-brown/50'} z-[999] uppercase relative text-left p-1 font-semibold text-sm lg:text-sm text-brown hover:text-brown/50`}
|
||||
onClick={()=>setSubLinkIsActive((prev)=>{
|
||||
// e.stopPropagation()
|
||||
// console.log('bubble')
|
||||
if(prev == linkName){
|
||||
return null
|
||||
}else{
|
||||
return linkName
|
||||
}
|
||||
})}
|
||||
>
|
||||
<span className="flex gap-1 items-center">
|
||||
{linkName}
|
||||
<i className={`fa-solid fa-caret-up ${(subLinkIsActive && subLinkIsActive == linkName) ? 'rotate-0' : 'rotate-180'}`}></i>
|
||||
</span>
|
||||
{/* sub links section */}
|
||||
<div
|
||||
className={`p-0 overflow-hidden absolute top-[55px] w-48 rounded-md bg-white shadow-md ${(subLinkIsActive && subLinkIsActive == linkName) && 'p-3'}`}
|
||||
onClick={()=>setSubLinkIsActive((prev)=>{
|
||||
// e.stopPropagation()
|
||||
// console.log('bubble')
|
||||
if(prev == linkName){
|
||||
return null
|
||||
}else{
|
||||
return linkName
|
||||
}
|
||||
})}
|
||||
>
|
||||
{subLink.map(item => (
|
||||
<div key={item.linkName} className={`w-full h-0 overflow-hidden px-2 ${(subLinkIsActive && subLinkIsActive == linkName) && 'h-10'}`}>
|
||||
<NavLinks
|
||||
linkName={item.linkName}
|
||||
href={item.link}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
:
|
||||
<button
|
||||
className={`${(subLinkIsActive && subLinkIsActive == linkName) && 'text-brown/50'} z-[999] uppercase relative text-left p-1 font-semibold text-sm lg:text-sm text-brown hover:text-brown/50`}
|
||||
onClick={()=>setSubLinkIsActive((prev)=>{
|
||||
if(prev == linkName){
|
||||
return null
|
||||
}else{
|
||||
return linkName
|
||||
}
|
||||
})}
|
||||
>
|
||||
<span className="flex gap-1 items-center">
|
||||
{linkName}
|
||||
<i className={`fa-solid fa-caret-up ${(subLinkIsActive && subLinkIsActive == linkName) ? 'rotate-0' : 'rotate-180'}`}></i>
|
||||
</span>
|
||||
{/* sub links section */}
|
||||
<div className={`p-0 overflow-hidden absolute top-[55px] w-48 rounded-md bg-white-light shadow-md ${(subLinkIsActive && subLinkIsActive == linkName) && 'p-3'}`}>
|
||||
{subLink.map(item => (
|
||||
<div key={item.linkName} className={`w-full h-0 overflow-hidden px-2 ${(subLinkIsActive && subLinkIsActive == linkName) && 'h-10'}`}>
|
||||
{/* <NavLinks
|
||||
linkName={item.linkName}
|
||||
href={item.link}
|
||||
/> */}
|
||||
coming soon
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
import { getLoanCharges } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber'
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
|
||||
|
||||
export default function LoansChargesCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [allLoanCharges, setAllLoanCharges] = useState({loading:true, error:'', data:{}})
|
||||
|
||||
const [willFilter, setWillFilter] = useState(false)
|
||||
const [filter, setFilter] = useState({type: '', id: ''})
|
||||
const handleFilter = ({target:{name, value}}) => {
|
||||
setFilter(prev => ({...prev, [name]:value}))
|
||||
}
|
||||
|
||||
const handleFilterByParams = () => {
|
||||
if(filter.type && !filter.id){
|
||||
return
|
||||
}else if(!filter.type){
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
setFilter({type: '', id: ''})
|
||||
}else{
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const loanCharges = allLoanCharges?.data?.loan_charges // LOAN CHARGES LIST
|
||||
const pagination = allLoanCharges?.data?.pagination
|
||||
const isFetching = allLoanCharges?.loading
|
||||
const isError = allLoanCharges?.error
|
||||
|
||||
useEffect(()=>{
|
||||
setAllLoanCharges(prev => ({...prev, loading:true}))
|
||||
const payload = filter?.type ? {[filter?.type]: filter.id} : {}
|
||||
getLoanCharges({...payload, page}).then(res => {
|
||||
if(res?.status != 200){
|
||||
setAllLoanCharges(prev => ({...prev, loading:false}))
|
||||
return
|
||||
}
|
||||
setAllLoanCharges({loading:false, error:'', data:res?.data})
|
||||
}).catch(err => {
|
||||
setAllLoanCharges({loading:false, error:'error occurred', data:{}})
|
||||
console.log('ERR', err)
|
||||
})
|
||||
},[page, willFilter])
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transactions' paths={['Dashboard', 'Transactions']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{ isError ?
|
||||
<p className='text-red-500'>{allLoanCharges?.error}</p>
|
||||
:
|
||||
<>
|
||||
{/* filter section */}
|
||||
<div className='px-2 py-2 mb-4 flex flex-col sm:flex-row flex-wrap sm:items-center gap-2'>
|
||||
<Icons name='filter' className='text-3xl' />
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<select name='type' value={filter?.type} className='h-10 w-full p-2 rounded-md' onChange={handleFilter}>
|
||||
<option value=''>All</option>
|
||||
<option value='transaction_id'>Transaction ID</option>
|
||||
<option value='account_id'>Account ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<input name='id' value={filter?.id} disabled={!filter.type} className={`h-10 w-full p-2 rounded-md outline-none border border-black-aside ${!filter.type && 'opacity-30'}`} onChange={handleFilter} />
|
||||
</div>
|
||||
<button onClick={handleFilterByParams} disabled={filter.type && !filter.id} className={`h-10 bg-primary px-2 py-1 rounded-md text-white font-medium sm:self-end ${(filter.type && !filter.id) && 'opacity-50'}`}>Submit</button>
|
||||
</div>
|
||||
{/* end of filter section */}
|
||||
|
||||
<TablePaginatedWrapper data={loanCharges} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id || ''}</div>
|
||||
<div className="font-normal text-gray-500 line-clamp-1">{item?.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getLoanCharges } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber';
|
||||
|
||||
export default function LoanChargesCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.loan_charges, page],
|
||||
queryFn: () => getLoanCharges({page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const loanCharges = data?.data?.loan_charges // LOAN CHARGES LIST
|
||||
const pagination = data?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Loan Charges' paths={['Dashboard', 'Loan Charges']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TablePaginatedWrapper data={loanCharges} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id || ''}</div>
|
||||
<div className="font-normal text-gray-500 line-clamp-1">{item?.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
import { getLoans } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import RouteLinks from '../../RouteLinks';
|
||||
import formatNumber from '../../helpers/formatNumber'
|
||||
|
||||
export default function LoansCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [allLoans, setAllLoans] = useState({loading:true, error:'', data:{}})
|
||||
|
||||
const [willFilter, setWillFilter] = useState(false)
|
||||
const [filter, setFilter] = useState({type: '', id: ''})
|
||||
const handleFilter = ({target:{name, value}}) => {
|
||||
setFilter(prev => ({...prev, [name]:value}))
|
||||
}
|
||||
|
||||
const handleFilterByParams = () => {
|
||||
if(filter.type && !filter.id){
|
||||
return
|
||||
}else if(!filter.type){
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
setFilter({type: '', id: ''})
|
||||
}else{
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const loans = allLoans?.data?.loans // LOANS LIST
|
||||
const pagination = allLoans?.data?.pagination
|
||||
const isFetching = allLoans?.loading
|
||||
const isError = allLoans?.error
|
||||
|
||||
useEffect(()=>{
|
||||
setAllLoans(prev => ({...prev, loading:true}))
|
||||
const payload = filter?.type ? {[filter?.type]: filter.id} : {}
|
||||
getLoans({...payload, page}).then(res => {
|
||||
if(res?.status != 200){
|
||||
setAllLoans(prev => ({...prev, loading:false}))
|
||||
return
|
||||
}
|
||||
setAllLoans({loading:false, error:'', data:res?.data})
|
||||
}).catch(err => {
|
||||
setAllLoans({loading:false, error:'error occurred', data:{}})
|
||||
console.log('ERR', err)
|
||||
})
|
||||
},[page, willFilter])
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transactions' paths={['Dashboard', 'Transactions']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{ isError ?
|
||||
<p className='text-red-500'>{allLoans?.error}</p>
|
||||
:
|
||||
<>
|
||||
{/* filter section */}
|
||||
<div className='px-2 py-2 mb-4 flex flex-col sm:flex-row flex-wrap sm:items-center gap-2'>
|
||||
<Icons name='filter' className='text-3xl' />
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<select name='type' value={filter?.type} className='h-10 w-full p-2 rounded-md' onChange={handleFilter}>
|
||||
<option value=''>All</option>
|
||||
<option value='transaction_id'>Transaction ID</option>
|
||||
<option value='account_id'>Account ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<input name='id' value={filter?.id} disabled={!filter.type} className={`h-10 w-full p-2 rounded-md outline-none border border-black-aside ${!filter.type && 'opacity-30'}`} onChange={handleFilter} />
|
||||
</div>
|
||||
<button onClick={handleFilterByParams} disabled={filter.type && !filter.id} className={`h-10 bg-primary px-2 py-1 rounded-md text-white font-medium sm:self-end ${(filter.type && !filter.id) && 'opacity-50'}`}>Submit</button>
|
||||
</div>
|
||||
{/* end of filter section */}
|
||||
|
||||
<TablePaginatedWrapper data={loans} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="table-auto py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Loan Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Product/Tenor
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Repay/Install Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.id} : {item?.transaction_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.initial_loan_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.product_id)}</div>
|
||||
<div className="font-normal text-gray-500">{item?.tenor} days</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.repayment_amount)}</div>
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.installment_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Link to={RouteLinks.transaction_details_page} state={{transactionID: item?.transaction_id}}>
|
||||
<Icons name='eye' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={6}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
// import TableWrapper from '../tableWrapper/TableWrapper'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper';
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getLoans } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber'
|
||||
import RouteLinks from '../../RouteLinks';
|
||||
|
||||
export default function LoansCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data:allLoans, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.loans, page],
|
||||
queryFn: () => getLoans({page}),
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
const loans = allLoans?.data?.loans // LOANS LIST
|
||||
const pagination = allLoans?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Loans' paths={['Dashboard', 'Loans']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TablePaginatedWrapper data={loans} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="table-auto py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Loan Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Product/Tenor
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Repay/Install Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.id} : {item?.transaction_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.initial_loan_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.product_id)}</div>
|
||||
<div className="font-normal text-gray-500">{item?.tenor} days</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.repayment_amount)}</div>
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.installment_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Link to={RouteLinks.transaction_details_page} state={{transactionID: item?.transaction_id}}>
|
||||
<Icons name='eye' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={6}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export default function DummyModalChildren() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
{/* <!-- Modal header --> */}
|
||||
<div className="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Terms of Service
|
||||
</h3>
|
||||
<button type="button" className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="default-modal">
|
||||
<svg className="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* <!-- Modal body --> */}
|
||||
<div className="p-4 md:p-5 space-y-4">
|
||||
<p className="text-base leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, companies around the world are updating their terms of service agreements to comply.
|
||||
</p>
|
||||
<p className="text-base leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
The European Union’s General Data Protection Regulation (G.D.P.R.) goes into effect on May 25 and is meant to ensure a common set of data rights in the European Union. It requires organizations to notify users as soon as possible of high-risk data breaches that could personally affect them.
|
||||
</p>
|
||||
</div>
|
||||
{/* <!-- Modal footer --> */}
|
||||
<div className="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<button data-modal-hide="default-modal" type="button" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">I accept</button>
|
||||
<button data-modal-hide="default-modal" type="button" className="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">Decline</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
export default function ModalWrapper({children, maxWidth}) {
|
||||
return (
|
||||
<div className="bg-[rgba(0,_0,_0,_0.2)] dark:bg-[rgba(0,_0,_0,_0.4)] overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-[999] flex flex-col justify-center items-center w-full md:inset-0 h-[calc(100%)] max-h-full">
|
||||
<div className={`pop-modal relative p-4 w-full ${maxWidth ? maxWidth : 'max-w-2xl'} max-h-full`}>
|
||||
{/* <!-- Modal content --> */}
|
||||
<div className="pb-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// id="default-modal" tabIndex={-1} aria-hidden="true"
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getOffers } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber';
|
||||
|
||||
export default function OffersCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.offers, page],
|
||||
queryFn: () => getOffers({page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const offers = data?.data?.offers // LOAN CHARGES LIST
|
||||
const pagination = data?.data?.pagination
|
||||
console.log('offers', offers)
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Offers' paths={['Dashboard', 'Offers']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TablePaginatedWrapper data={offers} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Interest Rate
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Insurance Rate
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Mgt. Rate
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Max/Min Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Tenor
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.product_id || ''}</div>
|
||||
{/* <div className="font-normal text-gray-500 line-clamp-1">{item?.description}</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.interest_rate)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.insurance_rate)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.management_rate)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.maximum_amount)}</div>
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.minimum_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.tenor}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={7}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
import { getRepayments } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
|
||||
|
||||
export default function RepaymentsCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [allRepayments, setAllRepayments] = useState({loading:true, error:'', data:{}})
|
||||
|
||||
const [willFilter, setWillFilter] = useState(false)
|
||||
const [filter, setFilter] = useState({type: '', id: ''})
|
||||
const handleFilter = ({target:{name, value}}) => {
|
||||
setFilter(prev => ({...prev, [name]:value}))
|
||||
}
|
||||
|
||||
const handleFilterByParams = () => {
|
||||
if(filter.type && !filter.id){
|
||||
return
|
||||
}else if(!filter.type){
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
setFilter({type: '', id: ''})
|
||||
}else{
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const repayments = allRepayments?.data?.repayments // LOAN REPAYMENTS LIST
|
||||
const pagination = allRepayments?.data?.pagination
|
||||
const isFetching = allRepayments?.loading
|
||||
const isError = allRepayments?.error
|
||||
|
||||
useEffect(()=>{
|
||||
setAllRepayments(prev => ({...prev, loading:true}))
|
||||
const payload = filter?.type ? {[filter?.type]: filter.id} : {}
|
||||
getRepayments({...payload, page}).then(res => {
|
||||
if(res?.status != 200){
|
||||
setAllRepayments(prev => ({...prev, loading:false}))
|
||||
return
|
||||
}
|
||||
setAllRepayments({loading:false, error:'', data:res?.data})
|
||||
}).catch(err => {
|
||||
setAllRepayments({loading:false, error:'error occurred', data:{}})
|
||||
console.log('ERR', err)
|
||||
})
|
||||
},[page, willFilter])
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transactions' paths={['Dashboard', 'Transactions']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{ isError ?
|
||||
<p className='text-red-500'>{allRepayments?.error}</p>
|
||||
:
|
||||
<>
|
||||
{/* filter section */}
|
||||
<div className='px-2 py-2 mb-4 flex flex-col sm:flex-row flex-wrap sm:items-center gap-2'>
|
||||
<Icons name='filter' className='text-3xl' />
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<select name='type' value={filter?.type} className='h-10 w-full p-2 rounded-md' onChange={handleFilter}>
|
||||
<option value=''>All</option>
|
||||
<option value='transaction_id'>Transaction ID</option>
|
||||
<option value='account_id'>Account ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<input name='id' value={filter?.id} disabled={!filter.type} className={`h-10 w-full p-2 rounded-md outline-none border border-black-aside ${!filter.type && 'opacity-30'}`} onChange={handleFilter} />
|
||||
</div>
|
||||
<button onClick={handleFilterByParams} disabled={filter.type && !filter.id} className={`h-10 bg-primary px-2 py-1 rounded-md text-white font-medium sm:self-end ${(filter.type && !filter.id) && 'opacity-50'}`}>Submit</button>
|
||||
</div>
|
||||
{/* end of filter section */}
|
||||
|
||||
<TablePaginatedWrapper data={repayments} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.customer_id || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.loan_id} : {item?.transaction_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* <td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan}</div>
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td> */}
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={3}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getRepayments } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
|
||||
export default function RepaymentsCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.transactions, page],
|
||||
queryFn: () => getRepayments({page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const repayments = data?.data?.repayments // REPAYMENTS LIST
|
||||
const pagination = data?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Repayments' paths={['Dashboard', 'Repayments']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<TablePaginatedWrapper data={repayments} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2">
|
||||
Added
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.customer_id || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.loan_id} : {item?.transaction_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* <td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan}</div>
|
||||
<div className="font-normal text-gray-500">{item?.description}</div>
|
||||
</div>
|
||||
</td> */}
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={3}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react";
|
||||
import MainBtn from "../MainBtn";
|
||||
import Icons from "../Icons";
|
||||
|
||||
|
||||
export default function TablePaginatedWrapper({
|
||||
data = [],
|
||||
isFetching,
|
||||
pagination,
|
||||
setPage,
|
||||
children,
|
||||
}) {
|
||||
|
||||
const handlePrev = () => {
|
||||
setPage(prev => prev - 1)
|
||||
};
|
||||
const handleNext = () => {
|
||||
setPage(prev => prev + 1)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full overflow-x-auto">
|
||||
{children({ data })}
|
||||
</div>
|
||||
|
||||
<div className='w-full flex flex-col lg:flex-row justify-center items-center gap-3 md:gap-6'>
|
||||
<div className="text-sm text-center lg:text-left font-normal text-gray-500 dark:text-gray-400 block w-full">Showing <span className="font-semibold text-gray-900 dark:text-white">
|
||||
{isFetching ? '----' : `page ${pagination?.current_page}`}</span> of <span className="font-semibold text-gray-900 dark:text-white">{pagination?.total_pages}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-3 md:gap-6'>
|
||||
<MainBtn
|
||||
onClick={handlePrev}
|
||||
// text='Prev'
|
||||
className={`${!pagination?.has_prev ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isFetching || !pagination?.has_prev}
|
||||
>
|
||||
<Icons name='prev' />
|
||||
</MainBtn>
|
||||
<MainBtn
|
||||
onClick={handleNext}
|
||||
// text='Next'
|
||||
className={`${!pagination?.has_next ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isFetching || !pagination?.has_next}
|
||||
>
|
||||
<Icons name='next' />
|
||||
</MainBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFetching && <TableIsLoading />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableIsLoading = () => {
|
||||
return (
|
||||
<div className="w-full fixed z-[991] inset-0 flex justify-center items-center bg-black/10">
|
||||
<p className="rounded-md shadow-md p-4 bg-white/90 dark:bg-gray-900 text-brown dark:text-white">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import MainBtn from "../MainBtn";
|
||||
import Icons from "../Icons";
|
||||
|
||||
|
||||
export default function TableWrapper({
|
||||
data = [],
|
||||
itemsPerPage = 5,
|
||||
filterItem,
|
||||
children,
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredData, setFilteredData] = useState(data);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [newData, setNewData] = useState([]);
|
||||
|
||||
const numberOfSelection = itemsPerPage;
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentPage !== 0) {
|
||||
setCurrentPage((prev) => prev - numberOfSelection);
|
||||
}
|
||||
};
|
||||
const handleNext = () => {
|
||||
if (currentPage < data.length) {
|
||||
setCurrentPage((prev) => prev + numberOfSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = ({ target: { value } }, name) => {
|
||||
setSearchTerm(value);
|
||||
let newFilteredData = data.filter((item) =>
|
||||
item[name].toLowerCase().startsWith(value.toLowerCase())
|
||||
);
|
||||
setFilteredData(newFilteredData);
|
||||
setCurrentPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
setTimeout(()=>{
|
||||
setNewData(
|
||||
filteredData?.slice(currentPage, numberOfSelection + currentPage)
|
||||
);
|
||||
setIsLoading(false)
|
||||
},1000)
|
||||
}, [currentPage, filteredData, numberOfSelection]);
|
||||
|
||||
useEffect(()=>{
|
||||
setCurrentPage(0)
|
||||
},[itemsPerPage])
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
{data.length > 0 && filterItem && (
|
||||
<div className="mb-10 flex justify-end items-center gap-2">
|
||||
{filterItem.map((item, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className="flex flex-col sm:flex-row items-center gap-2 text-slate-600 dark:text-slate-100 transition-all duration-500"
|
||||
>
|
||||
Search by {item[0].toUpperCase() + item.slice(1)}
|
||||
<input
|
||||
name={item}
|
||||
type="text"
|
||||
className="py-1 px-2 text-sm min-w-[100px] text-black dark:text-white bg-white dark:bg-slate-800 rounded-full border-0 outline-none ring-1 ring-slate-300 dark:ring-white transition-all duration-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
handleSearch(e, item);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full overflow-x-auto">
|
||||
{children({ data: newData })}
|
||||
</div>
|
||||
|
||||
{/* PAGINATION BUTTON */}
|
||||
{newData.length > 0 &&
|
||||
<div className='w-full flex flex-col lg:flex-row justify-center items-center gap-3 md:gap-6'>
|
||||
<div className="text-sm text-center lg:text-left font-normal text-gray-500 dark:text-gray-400 block w-full">Showing <span className="font-semibold text-gray-900 dark:text-white">
|
||||
{isLoading ? '----' : `${currentPage + 1}-${currentPage + numberOfSelection >= data.length ? data.length : (currentPage + numberOfSelection)}`}</span> of <span className="font-semibold text-gray-900 dark:text-white">{data.length}</span>
|
||||
</div>
|
||||
{(newData.length >= 0) &&
|
||||
<div className='flex items-center gap-3 md:gap-6'>
|
||||
<MainBtn
|
||||
onClick={handlePrev}
|
||||
// text='Prev'
|
||||
className={`${currentPage === 0 ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Icons name='prev' />
|
||||
</MainBtn>
|
||||
<MainBtn
|
||||
onClick={handleNext}
|
||||
// text='Next'
|
||||
className={`${currentPage + numberOfSelection >= data.length ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Icons name='next' />
|
||||
</MainBtn>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{isLoading && <TableIsLoading />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TableIsLoading = () => {
|
||||
return (
|
||||
<div className="w-full absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[991] inset-0 flex justify-center items-center">
|
||||
<p className="rounded-md shadow-md p-4 bg-white/90 dark:bg-gray-900 text-brown dark:text-white">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import MainBtn from "../MainBtn";
|
||||
import Icons from "../Icons";
|
||||
|
||||
|
||||
export default function TableWrapper({
|
||||
data = [],
|
||||
itemsPerPage = 5,
|
||||
filterItem,
|
||||
children,
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredData, setFilteredData] = useState(data);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [newData, setNewData] = useState([]);
|
||||
|
||||
const numberOfSelection = itemsPerPage;
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentPage != 0) {
|
||||
setCurrentPage((prev) => prev - numberOfSelection);
|
||||
}
|
||||
};
|
||||
const handleNext = () => {
|
||||
if (currentPage < data.length) {
|
||||
setCurrentPage((prev) => prev + numberOfSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = ({ target: { value } }, name) => {
|
||||
setSearchTerm(value);
|
||||
let newFilteredData = data.filter((item) =>
|
||||
item[name].toLowerCase().startsWith(value.toLowerCase())
|
||||
);
|
||||
setFilteredData(newFilteredData);
|
||||
setCurrentPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
setTimeout(()=>{
|
||||
setNewData(
|
||||
filteredData?.slice(currentPage, numberOfSelection + currentPage)
|
||||
);
|
||||
setIsLoading(false)
|
||||
},1000)
|
||||
}, [currentPage, filteredData]);
|
||||
|
||||
useEffect(()=>{
|
||||
setCurrentPage(0)
|
||||
},[itemsPerPage])
|
||||
|
||||
return (
|
||||
<div className="p-2 w-full bg-white shadow-round_black dark:shadow-round_white dark:bg-gray-800 rounded-md">
|
||||
{data.length > 0 && filterItem && (
|
||||
<div className="mb-10 flex justify-end items-center gap-2">
|
||||
{filterItem.map((item, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className="flex flex-col sm:flex-row items-center gap-2 text-slate-600 dark:text-slate-100 transition-all duration-500"
|
||||
>
|
||||
Search by {item[0].toUpperCase() + item.slice(1)}
|
||||
<input
|
||||
name={item}
|
||||
type="text"
|
||||
className="py-1 px-2 text-sm min-w-[100px] text-black dark:text-white bg-white dark:bg-slate-800 rounded-full border-0 outline-none ring-1 ring-slate-300 dark:ring-white transition-all duration-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
handleSearch(e, item);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full overflow-x-auto">
|
||||
{children({ data: newData })}
|
||||
</div>
|
||||
|
||||
{/* PAGINATION BUTTON */}
|
||||
{newData.length > 0 &&
|
||||
<div className='p-2 w-full flex flex-col lg:flex-row justify-center items-center gap-3 md:gap-6'>
|
||||
<div className="text-sm text-center lg:text-left font-normal text-gray-500 dark:text-gray-400 block w-full">Showing <span className="font-semibold text-gray-900 dark:text-white">
|
||||
{isLoading ? '----' : `${currentPage + 1}-${currentPage + numberOfSelection >= data.length ? data.length : (currentPage + numberOfSelection)}`}</span> of <span className="font-semibold text-gray-900 dark:text-white">{data.length}</span>
|
||||
</div>
|
||||
{(newData.length >= itemsPerPage) &&
|
||||
<div className='flex items-center gap-3 md:gap-6'>
|
||||
<MainBtn
|
||||
onClick={handlePrev}
|
||||
// text='Prev'
|
||||
className={`${currentPage == 0 ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Icons name='prev' />
|
||||
</MainBtn>
|
||||
<MainBtn
|
||||
onClick={handleNext}
|
||||
// text='Next'
|
||||
className={`${currentPage + numberOfSelection >= data.length ? 'bg-primary/50 pointer-events-none' : 'bg-primary'} text-white-light text-center flex justify-center gap-2 items-center`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Icons name='next' />
|
||||
</MainBtn>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{isLoading && <TableIsLoading />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TableIsLoading = () => {
|
||||
return (
|
||||
<div className="w-full fixed z-[991] inset-0 flex justify-center items-center">
|
||||
<p className="rounded-md shadow-md p-4 bg-white/90 dark:bg-gray-900 text-brown dark:text-white">Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getLoanCharges } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber';
|
||||
|
||||
export default function LoanChargeDetails({transactionID}) {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.loan_charges, page],
|
||||
queryFn: () => getLoanCharges({transaction_id: transactionID, page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const loanCharges = data?.data?.loan_charges // LOAN CHARGES LIST
|
||||
// const pagination = data?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Loan Charges</p>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(loanCharges && loanCharges.length > 0) ? loanCharges?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id || ''}</div>
|
||||
{/* <div className="font-normal text-gray-500 line-clamp-1">{item?.description}</div> */}
|
||||
<div className="font-normal text-gray-500">{item?.loan_id} : {item?.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.created_at ? getDateFromDateString(item?.created_at) : 'Not available'}</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* <td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td> */}
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={3}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getLoans } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber'
|
||||
|
||||
export default function LoanDetails({transactionID}) {
|
||||
|
||||
const {data:allLoans, isFetching, isError, error} = useQuery({
|
||||
queryKey: queryKeys.loans,
|
||||
queryFn: () => getLoans({transaction_id: transactionID})
|
||||
})
|
||||
|
||||
const loans = allLoans?.data?.loans // LOANS LIST
|
||||
// const loansCount = allLoans?.data?.count // LOANS LIST COUNT
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetching ?
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Loans</p>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</div>
|
||||
: isError ?
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Loans</p>
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
</div>
|
||||
: (loans && loans.length > 0) ?
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Loans</p>
|
||||
<table className="table-auto py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Loan/Eligible Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Product/Tenor
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Repay/Install Amount.
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Added/Due
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loans?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id || ''}</div>
|
||||
<div className="font-normal text-gray-500">{item?.id} : {item?.transaction_id}</div>
|
||||
<div className="font-semibold text-red-500">ORIGIN : {item?.original_transaction}</div>
|
||||
<div className="font-bold text-blue-500">OFFER : {item?.offer_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
{/* <div className="text-base font-semibold">{formatNumber(item?.initial_loan_amount)}</div> */}
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.initial_loan_amount)}</div>
|
||||
<div className="font-semibold text-red-500">{formatNumber(item?.eligible_amount)}</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.product_id)}</div>
|
||||
<div className="font-normal text-gray-500">{item?.tenor} days</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.repayment_amount)}</div>
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.installment_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)}</div>
|
||||
<div className="font-semibold text-red-500">{getDateFromDateString(item?.due_date)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getRepaymentSchedule } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import formatNumber from '../../helpers/formatNumber';
|
||||
|
||||
export default function RepaymentScheduleDetails({transactionID}) {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.repayment_schedule, page],
|
||||
queryFn: () => getRepaymentSchedule({transaction_id: transactionID, page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const repaymentSchedule = data?.data?.repayment_schedules // LOAN CHARGES LIST
|
||||
// const pagination = data?.data?.pagination
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
},[])
|
||||
|
||||
return (
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Repayment Schedule</p>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
ID
|
||||
</th>
|
||||
{/* <th scope="col" className="px-2">
|
||||
Loan
|
||||
</th> */}
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Repay Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Install Amount
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Due Date
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Paid Date
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(repaymentSchedule && repaymentSchedule.length > 0) ? repaymentSchedule?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={Avatar} alt="Jese image" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.loan_id || ''}</div>
|
||||
{/* <div className="font-normal text-gray-500 line-clamp-1">{item?.description}</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.total_repayment_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{formatNumber(item?.installment_amount)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.due_date)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-right">
|
||||
<div className="font-normal text-gray-500">{item?.paid_at ? getDateFromDateString(item?.paid_at) : 'Not available'}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={6}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import Icons from '../Icons'
|
||||
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getTransactions } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import localImgLoader from '../../helpers/localImageLoader';
|
||||
|
||||
export default function TransactionDetails({transactionID}) {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const {data, isFetching, isError, error} = useQuery({
|
||||
queryKey: [...queryKeys.transactions, page],
|
||||
queryFn: () => getTransactions({transaction_id: transactionID, page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const transactions = data?.data?.transactions // TRANSACTIONS LIST
|
||||
// const pagination = data?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
<p className='pb-4 font-bold text-base'>Transactions</p>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Request
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Activity
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(transactions && transactions.length > 0) ? transactions?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={localImgLoader(`loan_icons/${item?.type}.png`)} alt="Icon" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id}</div>
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)} {getTimeFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id}</div>
|
||||
<div className="font-normal text-gray-500">{item?.type}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">50%</div>
|
||||
<div className="relative h-[6px] w-full bg-white-body dark:bg-black-body rounded-full overflow-hidden">
|
||||
<div className={`absolute left-0 h-full w-1/2 bg-emerald-600`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Icons name='eye' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TransactionDetails from './TransactionDetails'
|
||||
import LoanDetails from './LoanDetails'
|
||||
import LoanChargeDetails from './LoanChargeDetails'
|
||||
import RepaymentScheduleDetails from './RepaymentScheduleDetails'
|
||||
|
||||
export default function TransactionDetailsCom({id}) {
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transaction Details' span={`[${id}]`} paths={['Dashboard', 'Transactions Details']} />
|
||||
<TransactionDetails transactionID={id} />
|
||||
<LoanDetails transactionID={id} />
|
||||
<RepaymentScheduleDetails transactionID={id} />
|
||||
<LoanChargeDetails transactionID={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
import { getTransactions } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import localImgLoader from '../../helpers/localImageLoader';
|
||||
import RouteLinks from '../../RouteLinks';
|
||||
|
||||
export default function TransactionsCom() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [allTransactions, setAllTransaction] = useState({loading:true, error:'', data:{}})
|
||||
|
||||
const [willFilter, setWillFilter] = useState(false)
|
||||
const [filter, setFilter] = useState({type: '', id: ''})
|
||||
const handleFilter = ({target:{name, value}}) => {
|
||||
setFilter(prev => ({...prev, [name]:value}))
|
||||
}
|
||||
|
||||
const handleFilterByParams = () => {
|
||||
if(filter.type && !filter.id){
|
||||
return
|
||||
}else if(!filter.type){
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
setFilter({type: '', id: ''})
|
||||
}else{
|
||||
setPage(1)
|
||||
setWillFilter(prev => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const transactions = allTransactions?.data?.transactions // TRANSACTIONS LIST
|
||||
const pagination = allTransactions?.data?.pagination
|
||||
const isFetching = allTransactions?.loading
|
||||
const isError = allTransactions?.error
|
||||
|
||||
useEffect(()=>{
|
||||
setAllTransaction(prev => ({...prev, loading:true}))
|
||||
const payload = filter?.type ? {[filter?.type]: filter.id} : {}
|
||||
getTransactions({...payload, page}).then(res => {
|
||||
if(res?.status != 200){
|
||||
setAllTransaction(prev => ({...prev, loading:false}))
|
||||
return
|
||||
}
|
||||
setAllTransaction({loading:false, error:'', data:res?.data})
|
||||
}).catch(err => {
|
||||
setAllTransaction({loading:false, error:'error occurred', data:{}})
|
||||
console.log('ERR', err)
|
||||
})
|
||||
},[page, willFilter])
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transactions' paths={['Dashboard', 'Transactions']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{ isError ?
|
||||
<p className='text-red-500'>{allTransactions?.error}</p>
|
||||
:
|
||||
<>
|
||||
{/* filter section */}
|
||||
<div className='px-2 py-2 mb-4 flex flex-col sm:flex-row flex-wrap sm:items-center gap-2'>
|
||||
<Icons name='filter' className='text-3xl' />
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<select name='type' value={filter?.type} className='h-10 w-full p-2 rounded-md' onChange={handleFilter}>
|
||||
<option value=''>All</option>
|
||||
<option value='transaction_id'>Transaction ID</option>
|
||||
<option value='account_id'>Account ID</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='w-full sm:max-w-48'>
|
||||
<input name='id' value={filter?.id} disabled={!filter.type} className={`h-10 w-full p-2 rounded-md outline-none border border-black-aside ${!filter.type && 'opacity-30'}`} onChange={handleFilter} />
|
||||
</div>
|
||||
<button onClick={handleFilterByParams} disabled={filter.type && !filter.id} className={`h-10 bg-primary px-2 py-1 rounded-md text-white font-medium sm:self-end ${(filter.type && !filter.id) && 'opacity-50'}`}>Submit</button>
|
||||
</div>
|
||||
{/* end of filter section */}
|
||||
|
||||
<TablePaginatedWrapper data={transactions} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Request
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Activity
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={localImgLoader(`loan_icons/${item?.type}.png`)} alt="Icon" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id}</div>
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)} {getTimeFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id}</div>
|
||||
<div className="font-normal text-gray-500">{item?.type}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">50%</div>
|
||||
<div className="relative h-[6px] w-full bg-white-body dark:bg-black-body rounded-full overflow-hidden">
|
||||
<div className={`absolute left-0 h-full w-1/2 bg-emerald-600`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Link to={RouteLinks.transaction_details_page} state={{transactionID: item?.transaction_id}}>
|
||||
<Icons name='eye' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
import BreadcrumbCom from '../breadcrumb/BreadcrumbCom'
|
||||
import TablePaginatedWrapper from '../tableWrapper/TablePaginatedWrapper'
|
||||
import Icons from '../Icons'
|
||||
|
||||
import Avatar from '../../assets/user_avatar.jpg'
|
||||
import queryKeys from '../../services/queryKeys'
|
||||
import { getTransactions } from '../../services/siteServices'
|
||||
import getDateFromDateString from '../../helpers/GetDateFromDateString';
|
||||
import getTimeFromDateString from '../../helpers/GetTimeFromDateString';
|
||||
import localImgLoader from '../../helpers/localImageLoader';
|
||||
import RouteLinks from '../../RouteLinks';
|
||||
|
||||
export default function TransactionsCom() {
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const [filter, setFilter] = useState({transaction_id: '', account_id: ''})
|
||||
const handleFilter = ({target:{name, value}}) => {
|
||||
setFilter(prev => ({...prev, [name]:value}))
|
||||
}
|
||||
|
||||
const handleFilterByParams = () => {
|
||||
// setPage(1)
|
||||
queryClient.invalidateQueries({ queryKey: [...queryKeys.transactions] })
|
||||
}
|
||||
|
||||
const {data, isFetching, isError, error, isPlaceholderData, isPending} = useQuery({
|
||||
queryKey: [...queryKeys.transactions, page],
|
||||
queryFn: () => getTransactions({...filter, page}),
|
||||
staleTime: 0,
|
||||
// placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const transactions = data?.data?.transactions // TRANSACTIONS LIST
|
||||
const pagination = data?.data?.pagination
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-col gap-8'>
|
||||
<BreadcrumbCom title='Transactions' paths={['Dashboard', 'Transactions']} />
|
||||
|
||||
<div className='box bg-white dark:bg-black-box text-black-body dark:text-white-body'>
|
||||
{isFetching ?
|
||||
<>
|
||||
<p className='text-slate-800'>Loading...</p>
|
||||
</>
|
||||
: isError ?
|
||||
<p className='text-red-500'>{error.message}</p>
|
||||
:
|
||||
<>
|
||||
{/* filter section */}
|
||||
<div className='px-2 py-2 mb-4 flex items-end gap-2'>
|
||||
<Icons name='filter' className='text-3xl' />
|
||||
<div className='flex gap-4 items-center'>
|
||||
<div className=''>
|
||||
<p>Transaction ID</p>
|
||||
<select name='transaction_id' value={filter?.transaction_id} className='p-2' onChange={handleFilter}>
|
||||
<option value=''>select</option>
|
||||
<>
|
||||
{transactions.map((item, index) => (
|
||||
<option key={index} value={item?.transaction_id}>{item?.transaction_id}</option>
|
||||
))}
|
||||
</>
|
||||
</select>
|
||||
</div>
|
||||
<div className=''>
|
||||
<p>Account ID</p>
|
||||
<select name='account_id' value={filter?.account_id} className='p-2' onChange={handleFilter}>
|
||||
<option value=''>select</option>
|
||||
<>
|
||||
{transactions.map((item, index) => (
|
||||
<option key={index} value={item?.account_id}>{item?.account_id}</option>
|
||||
))}
|
||||
</>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleFilterByParams} className={`bg-primary px-2 py-1 rounded-md text-white font-medium`}>Submit</button>
|
||||
</div>
|
||||
{/* end of filter section */}
|
||||
|
||||
<TablePaginatedWrapper data={transactions} isFetching={isFetching} setPage={setPage} itemsPerPage={pagination?.limit} pagination={pagination}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<table className="py-2 w-full text-sm">
|
||||
<thead className="py-2 text-sm text-slate-500 text-left">
|
||||
<tr>
|
||||
<th scope="col" className="px-2 py-2">
|
||||
Request
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col" className="px-2">
|
||||
Activity
|
||||
</th>
|
||||
<th scope="col" className="px-2 text-right">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(data && data.length > 0) ? data?.map((item, index) => (
|
||||
<tr key={index} className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-2 py-2">
|
||||
<div className='w-full min-w-48 flex items-center gap-2 whitespace-nowrap'>
|
||||
<img className="w-10 h-10 rounded-md" src={localImgLoader(`loan_icons/${item?.type}.png`)} alt="Icon" />
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.transaction_id}</div>
|
||||
<div className="font-normal text-gray-500">{getDateFromDateString(item?.created_at)} {getTimeFromDateString(item?.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="text-base font-semibold">{item?.account_id}</div>
|
||||
<div className="font-normal text-gray-500">{item?.type}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<div className="text-left">
|
||||
<div className="font-normal text-gray-500">50%</div>
|
||||
<div className="relative h-[6px] w-full bg-white-body dark:bg-black-body rounded-full overflow-hidden">
|
||||
<div className={`absolute left-0 h-full w-1/2 bg-emerald-600`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 text-right">
|
||||
<div className='flex items-center justify-end gap-3 md:gap-4'>
|
||||
<div className='p-2 flex justify-center items-center text-slate-500 bg-white-body dark:text-white-body dark:bg-black-body rounded-md'>
|
||||
<Link to={RouteLinks.transaction_details_page} state={{transactionID: item?.transaction_id}}>
|
||||
<Icons name='eye' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
:
|
||||
<tr className="py-2 border-t border-dashed border-slate-300">
|
||||
<td className="px-3 py-2" colSpan={4}>
|
||||
<div className="flex justify-center items-center">
|
||||
No Record Found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</TablePaginatedWrapper>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useState } from 'react'
|
||||
|
||||
const DefaultLayoutProvider = createContext<any>({})
|
||||
|
||||
export default function DefaultLayoutContext({children}) {
|
||||
|
||||
const [subLinkIsActive, setSubLinkIsActive] = useState(null)
|
||||
|
||||
|
||||
const values = {
|
||||
subLinkIsActive,
|
||||
setSubLinkIsActive
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaultLayoutProvider.Provider value={values}>
|
||||
{children}
|
||||
</DefaultLayoutProvider.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const layoutDefaultContext = () => {
|
||||
return useContext(DefaultLayoutProvider)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
const GeneralContextProvider = createContext({})
|
||||
|
||||
export default function GeneralLayoutContext({children}) {
|
||||
|
||||
const [theme, setTheme] = useState(null)
|
||||
|
||||
const [drawer, setDrawer] = useState('')
|
||||
|
||||
const [booking, setBooking] = useState('')
|
||||
|
||||
const [alertBox, setAlertBox] = useState({status:false, msg:''}) // USE TO SHOW SUcCESS OR FAILED ALERT MESSAGE
|
||||
|
||||
const [logoutModal, setLogoutModal] = useState(false) // USE TO SHOW LOGOUT MODAL BOX
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState('')
|
||||
|
||||
const [shrinkAside, setShrinkAside] = useState(false)
|
||||
|
||||
const [showAsideDrawer, setShowAsideDrawer] = useState('')
|
||||
|
||||
const handleActiveMenu = (name) => {
|
||||
if(activeMenu == name){
|
||||
setActiveMenu('')
|
||||
}else{
|
||||
setActiveMenu(name)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrawer = (drawerToOpen) => { // FUNCTION TO DETERMINE WHICH ASIDE DRAWER TO SHOW
|
||||
setDrawer((prev)=>{
|
||||
if(!prev){
|
||||
return drawerToOpen
|
||||
}else if(drawerToOpen == prev){
|
||||
return ''
|
||||
}else{
|
||||
return drawerToOpen
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBooking = (bookingToOpen) => { // FUNCTION TO DETERMINE WHICH ASIDE DRAWER TO SHOW
|
||||
setBooking((prev)=>{
|
||||
if(!prev){
|
||||
return bookingToOpen
|
||||
}else if(bookingToOpen == prev){
|
||||
return ''
|
||||
}else{
|
||||
return bookingToOpen
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAlertBox = (valObj) => {
|
||||
setAlertBox(valObj)
|
||||
}
|
||||
|
||||
const handleTheme = () => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
setTheme("dark");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
useEffect(()=>{
|
||||
window.addEventListener('resize', ()=>{
|
||||
setShrinkAside(false)
|
||||
setShowAsideDrawer('')
|
||||
setActiveMenu('')
|
||||
})
|
||||
return () => window.removeEventListener('resize', window.addEventListener('resize', ()=>{
|
||||
setShrinkAside(false)
|
||||
setShowAsideDrawer('')
|
||||
setActiveMenu('')
|
||||
}))
|
||||
},[])
|
||||
|
||||
let value = {
|
||||
theme, handleTheme,
|
||||
activeMenu, handleActiveMenu,
|
||||
drawer, handleDrawer,
|
||||
booking, handleBooking,
|
||||
alertBox, handleAlertBox,
|
||||
logoutModal, setLogoutModal,
|
||||
shrinkAside, setShrinkAside,
|
||||
showAsideDrawer, setShowAsideDrawer
|
||||
}
|
||||
|
||||
return (
|
||||
<GeneralContextProvider.Provider value={value}>
|
||||
{children}
|
||||
</GeneralContextProvider.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const generalLayoutContext = () => {
|
||||
return useContext(GeneralContextProvider)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
function getDateFromDateString(dateString) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Ensure the date is valid
|
||||
if (isNaN(date)) {
|
||||
return "Invalid date string";
|
||||
}
|
||||
|
||||
// Get the year, month, and day
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed, so we add 1
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export default getDateFromDateString
|
||||
@@ -0,0 +1,17 @@
|
||||
function getTimeFromDateString(dateString) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Ensure the date is valid
|
||||
if (isNaN(date)) {
|
||||
return "Invalid date string";
|
||||
}
|
||||
|
||||
// Get hours, minutes, and seconds
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
// const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${hours <= 12 ? hours : hours%12}:${minutes} ${hours >= 12 ? 'PM' : 'AM'}`;
|
||||
}
|
||||
|
||||
export default getTimeFromDateString
|
||||
@@ -0,0 +1,15 @@
|
||||
function debounceFunction(func, delay) {
|
||||
let timer;
|
||||
|
||||
return function(...args) {
|
||||
// Clear the previous timer if the function is called before the delay
|
||||
clearTimeout(timer);
|
||||
|
||||
// Set a new timer to execute the function after the specified delay
|
||||
timer = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
export default debounceFunction
|
||||
@@ -0,0 +1,10 @@
|
||||
const formatNumber = (number = 0) => {
|
||||
// return new Intl.NumberFormat().format(number);
|
||||
// return number.toFixed(2);
|
||||
return number.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
export default formatNumber
|
||||
@@ -0,0 +1,2 @@
|
||||
const localImgLoader = (location) => require(`../assets/${location}`);
|
||||
export default localImgLoader
|
||||
@@ -0,0 +1,82 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html{
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
*{
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.aside-scroll-design {
|
||||
@apply [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 [&::-webkit-scrollbar-track]:rounded-full [&::-webkit-scrollbar-thumb]:rounded-full
|
||||
}
|
||||
.box {
|
||||
@apply flex flex-col w-full p-8 rounded-lg h-full border-[1px] border-[#F1F1F4] dark:border-[#1E2027] shadow-[0px_3px_4px_0px_rgba(0,_0,_0,_0.03)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer components {
|
||||
.change-text::after{
|
||||
content: '';
|
||||
animation: text-change 5s linear 0s infinite;
|
||||
}
|
||||
.text-slide-in{
|
||||
position: relative;
|
||||
animation: slide-text-in .5s linear 0s forwards;
|
||||
}
|
||||
|
||||
.pop-modal{
|
||||
animation: pop-modal .1s linear 0s forwards;
|
||||
}
|
||||
|
||||
.pop-modal-down{
|
||||
animation: pop-modal-down .1s linear 0s forwards;
|
||||
}
|
||||
|
||||
|
||||
/* ANIMATIONS */
|
||||
@keyframes text-change {
|
||||
0%{content: 'Boundaries';}
|
||||
100%{content: 'Limitations';}
|
||||
}
|
||||
|
||||
@keyframes slide-text-in {
|
||||
/* 0%{left: -200%;}
|
||||
100%{left: 0%;} */
|
||||
0%{transform: scale(0); opacity: 0;}
|
||||
100%{transform: scale(1); opacity: 1;}
|
||||
}
|
||||
|
||||
@keyframes pop-modal {
|
||||
/* 0%{left: -200%;}
|
||||
100%{left: 0%;} */
|
||||
0%{margin-top: 20px; opacity: 0;}
|
||||
100%{margin-top: 0; opacity: 1;}
|
||||
}
|
||||
|
||||
@keyframes pop-modal-down {
|
||||
/* 0%{left: -200%;}
|
||||
100%{left: 0%;} */
|
||||
0%{margin-bottom: 20px; opacity: 0;}
|
||||
100%{margin-bottom: 0; opacity: 1;}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import store from './store/store.js'
|
||||
import GeneralLayoutContext from './context/GeneralLayoutContext.jsx';
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<GeneralLayoutContext>
|
||||
<App />
|
||||
</GeneralLayoutContext>
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import ErrorCom from '../components/error/ErrorCom'
|
||||
|
||||
export default function ErrorPage() {
|
||||
return (
|
||||
<ErrorCom />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import HomeCom from '../components/home/HomeCom'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<HomeCom />
|
||||
)
|
||||
}
|
||||