first commit

This commit is contained in:
CHIEFSOFT\ameye
2025-07-21 05:51:52 -04:00
commit 7e28fc8f51
114 changed files with 23749 additions and 0 deletions
+20
View File
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
+23
View File
@@ -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*
+19
View File
@@ -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"]
+70
View File
@@ -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)
+32
View File
@@ -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
+17710
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+43
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+25
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+18
View File
@@ -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;
}
+45
View File
@@ -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;
+8
View File
@@ -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();
});
+13
View File
@@ -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
+47
View File
@@ -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>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+87
View File
@@ -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>
}
</>
)
}
+40
View File
@@ -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;
+8
View File
@@ -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>
)
}
+60
View File
@@ -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
}
</>
)
}
+9
View File
@@ -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>
)
}
+7
View File
@@ -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>
)
}
+20
View File
@@ -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>
)
}
+12
View File
@@ -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>
)
}
+165
View File
@@ -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>
)
}
+37
View File
@@ -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>
</>
)
}
+188
View File
@@ -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>
)
}
+187
View File
@@ -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}
+147
View File
@@ -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>
)
}
+35
View File
@@ -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>
)
}
+52
View File
@@ -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>
)
}
+33
View File
@@ -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>
)
}
+34
View File
@@ -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>
)
}
+174
View File
@@ -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>
)
}
+132
View File
@@ -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 Unions 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>
</>
)
}
+15
View File
@@ -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"
+135
View File
@@ -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>
)
}
+152
View File
@@ -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>
)
}
+25
View File
@@ -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)
}
+113
View File
@@ -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)
}
+17
View File
@@ -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
+17
View File
@@ -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
+15
View File
@@ -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
+10
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
const localImgLoader = (location) => require(`../assets/${location}`);
export default localImgLoader
+82
View File
@@ -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;}
}
}
+23
View File
@@ -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>
);
+1
View File
@@ -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

+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import ErrorCom from '../components/error/ErrorCom'
export default function ErrorPage() {
return (
<ErrorCom />
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import HomeCom from '../components/home/HomeCom'
export default function HomePage() {
return (
<HomeCom />
)
}

Some files were not shown because too many files have changed in this diff Show More