This commit is contained in:
CHIEFSOFT\ameye
2025-04-04 13:15:42 -04:00
parent e3b8c374d3
commit 0f4844cb05
76 changed files with 20352 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
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://devcore.digifi.chiefsoft.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'
+17
View File
@@ -0,0 +1,17 @@
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://devcore.digifi.chiefsoft.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'
+17
View File
@@ -0,0 +1,17 @@
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://devcore.digifi.chiefsoft.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'
+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"]
+31
View File
@@ -0,0 +1,31 @@
version: '3'
services:
digifi-office:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
volumes:
- ./:/app
- '/app/node_modules'
ports:
- 6039: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
+49
View File
@@ -0,0 +1,49 @@
{
"name": "digifi-salaryloan",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"cra-template": "1.2.0",
"formik": "^2.4.6",
"react": "^19.0.0",
"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>digiFi - 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;
}
+44
View File
@@ -0,0 +1,44 @@
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,
// 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',
errorPage: '*',
homePage: '/',
usersPage: '/users',
approvedLoansPage: '/loans/approved',
applicationsLoanPage: '/loans/apply',
disbursementsLoanPage: '/loans/disbursements',
selectedLoanPage: '/loans/select',
loanOffersPage: '/loans/offers',
}
export default RouteLinks
+46
View File
@@ -0,0 +1,46 @@
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 UsersPage from './pages/UsersPage' // Users PAGE
import ApprovedLoansPage from './pages/ApprovedLoansPage' // APPROVED LOANS PAGE
import DisbursementsLoanPage from './pages/DisbursementsLoanPage' // DISBURSEMENTS LOANS PAGE
import ApplicationsLoanPage from './pages/ApplicationsLoanPage' // APPLICATIONS LOANS PAGE
import SelectedLoanPage from './pages/SelectedLoanPage' // SELECTED LOANS PAGE
import LoanOffersPage from './pages/LoanOffersPage' // SELECTED LOANS 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.usersPage} element={<UsersPage />} /> {`*/USERS PAGE*/`}
<Route path={RouteLinks.approvedLoansPage} element={<ApprovedLoansPage />} /> {`*/APPROVED LOANS PAGE*/`}
<Route path={RouteLinks.disbursementsLoanPage} element={<DisbursementsLoanPage />} /> {`*/DISBURSEMENTS LOANS PAGE*/`}
<Route path={RouteLinks.applicationsLoanPage} element={<ApplicationsLoanPage />} /> {`*/APPLICATIONS LOANS PAGE*/`}
<Route path={RouteLinks.selectedLoanPage} element={<SelectedLoanPage />} /> {`*/SELECTED LOANS PAGE*/`}
<Route path={RouteLinks.loanOffersPage} element={<LoanOffersPage />} /> {`*/LOANS OFFERS PAGE*/`}
</Route>
{/* ERROR PAGE */}
<Route
path={RouteLinks.errorPage} // error page
element={
<Suspense fallback={<PageLoader />}>
<p>Error Page 1</p>
</Suspense>
}
/>
</Routes>
)
}
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: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+46
View File
@@ -0,0 +1,46 @@
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"
export default function UserExist() {
const dispatch = useDispatch()
const navigate = useNavigate()
const [pageIsLoading, setPageIsLoading] = useState(true)
const {userDetails} = useSelector((state) => state.userDetails)
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>
}
</>
)
}
+8
View File
@@ -0,0 +1,8 @@
export default function DummyLogo() {
return (
<div className="w-20 rounded cursor-pointer bg-primary text-white-light p-2 flex flex-col justify-center items-center gap-0">
<h1 className="text-sm font-bold">digiFI</h1>
<p className="text-12">Logo</p>
</div>
)
}
+37
View File
@@ -0,0 +1,37 @@
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'
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}`} />
:
null
}
</>
)
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react'
export default function InputText({id, name, type='text', value, handleChange}) {
return (
<div className='w-full h-10 round overflow-hidden'>
<input id={id} name={name} type={type} value={value} onChange={handleChange} className='p-2 w-full h-full text-black outline-0 ring-0 border border-slate-400 focus:border-purple-600 rounded' />
</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,122 @@
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']} />
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
:
<TableWrapper data={appliedUsers} itemsPerPage={15}>
{({ data }) => (
<>
<table className="py-2 w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-sm md:text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-4 py-2">
Name
</th>
<th scope="col" className="px-4 py-2">
Loan
</th>
<th scope="col" className="px-4 py-2">
Amount
</th>
<th scope="col" className="px-4 py-2">
Added
</th>
<th scope="col" className="px-4 py-2">
Verified
</th>
<th scope="col" className="px-4 py-2">
Action
</th>
</tr>
</thead>
<tbody>
{(data && data.length > 0) ? data?.map((item, index) => (
<tr key={index} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" className="mr-4 flex items-center px-3 py-2 text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-10 h-10 rounded-full" src={Avatar} alt="Jese image" />
<div className="px-3">
<div className="text-base font-semibold">{item?.name || ''}</div>
<div className="font-normal text-gray-500">{item?.bvn}</div>
</div>
</th>
<td className="px-3 py-2">
{item?.loan} - {item?.description}
</td>
<td className="px-3 py-2">
{item?.amount || ''}
</td>
<td className="px-3 py-2">
<div className="flex items-center">
{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}
</div>
</td>
<td className="px-3 py-2">
<div className="flex items-center">
{!item?.verified ? 'N/A' : `${getDateFromDateString(item?.verified)} ${getTimeFromDateString(item?.verified)}`}
</div>
</td>
<td className="px-3 py-2 flex gap-3 md:gap-4">
{/* <!-- Modal toggle --> */}
{/* <Link to={RouteLinks.manageAdminPage}>
<i onClick={handleShowEditModal} className="fa-solid fa-eye text-base md:text-lg cursor-pointer p-2 text-sky-600"></i>
</Link> */}
{/* <i onClick={handleShowEditModal} className="fa-solid fa-pen-to-square text-base md:text-lg cursor-pointer p-2"></i> */}
{/* <i onClick={handleDeleteModal} className="fa-solid fa-trash text-base md:text-lg cursor-pointer p-2 text-red-500"></i> */}
<Icons name='edit' />
<Icons name='eye' />
<Icons name='trash' className={'hidden text-red-500'} />
</td>
</tr>
))
:
<tr className="w-3 p-3">
<td className="px-3 py-2" colSpan={6}>
<div className="flex justify-center items-center">
No Record Found
</div>
</td>
</tr>
}
</tbody>
</table>
</>
)}
</TableWrapper>
}
</div>
)
}
@@ -0,0 +1,107 @@
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']} />
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
:
<TableWrapper data={approvedUsers} itemsPerPage={15}>
{({ data }) => (
<>
<table className="py-2 w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-sm md:text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-4 py-2">
Name
</th>
<th scope="col" className="px-4 py-2">
Loan
</th>
<th scope="col" className="px-4 py-2">
Added
</th>
<th scope="col" className="px-4 py-2">
Action
</th>
</tr>
</thead>
<tbody>
{(data && data.length > 0) ? data?.map((item, index) => (
<tr key={index} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" className="mr-4 flex items-center px-3 py-2 text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-10 h-10 rounded-full" src={Avatar} alt="Jese image" />
<div className="px-3">
<div className="text-base font-semibold">{item?.name || ''}</div>
<div className="font-normal text-gray-500">{item?.bvn}</div>
</div>
</th>
<td className="px-3 py-2">
{item?.loan} - {item?.description}
</td>
<td className="px-3 py-2">
<div className="flex items-center">
{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}
</div>
</td>
<td className="px-3 py-2 flex gap-3 md:gap-4">
{/* <!-- Modal toggle --> */}
{/* <Link to={RouteLinks.manageAdminPage}>
<i onClick={handleShowEditModal} className="fa-solid fa-eye text-base md:text-lg cursor-pointer p-2 text-sky-600"></i>
</Link> */}
{/* <i onClick={handleShowEditModal} className="fa-solid fa-pen-to-square text-base md:text-lg cursor-pointer p-2"></i> */}
{/* <i onClick={handleDeleteModal} className="fa-solid fa-trash text-base md:text-lg cursor-pointer p-2 text-red-500"></i> */}
<Icons name='edit' />
<Icons name='eye' />
<Icons name='trash' className={'hidden text-red-500'} />
</td>
</tr>
))
:
<tr className="w-3 p-3">
<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>
)
}
+113
View File
@@ -0,0 +1,113 @@
import React, { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useLocation, useNavigate } from 'react-router-dom'
// import { useMutation } from '@tanstack/react-query'
import Label from '../Label'
import InputText from '../InputText'
import PageLoader from '../PageLoader'
import { updateUserDetails } from "../../store/UserDetails";
// import { loginUser } from '../../services/siteServices'
import GoogleDownload from '../../assets/download/andriod.jpg'
import IOSDownload from '../../assets/download/apple.jpg'
import RouteLinks from '../../RouteLinks'
export default function LoginCom() {
const dispatch = useDispatch()
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
const [fields, setFields] = useState({
username: '',
password: '',
})
const handleChange = ({target:{name, value}}) => {
setFields(prev => ({...prev, [name]:value}))
}
// 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 {token, room} = res?.data?.data
// // if(token){
// // localStorage.setItem('token', token)
// // localStorage.setItem('room', room)
// // // const data = {token}
// // // dispatch(updateUserDetails({ ...data }));
// // }
// navigate(myLinks.home, {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)
}
return (
<>
<div className={`h-screen bg-sky-300 flex flex-col items-center bg-[url('./assets/login-bg.jpg')] bg-cover bg-center bg-no-repeat`}>
<div className='flex flex-col gap-3 w-[80%] sm:w-[500px] bg-white rounded-xl mt-8 p-8 shadow'>
<div className='w-full mb-8 flex flex-col gap-1'>
<h1 className='text-2xl md:text-3xl font-bold'>Digifi BackOffice</h1>
<p className='text-sm font-medium text-slate-400'>Welcome back, please login to your account</p>
</div>
<div className='text-input flex flex-col gap-2'>
<Label name='Username' htmlfor='username' />
<InputText id='username' name='username' value={fields.username} handleChange={handleChange} />
</div>
<div className='text-input flex flex-col gap-2'>
<Label name='Password' htmlfor='password' />
<InputText id='password' name='password' type='password' value={fields.password} handleChange={handleChange} />
</div>
{/* {login.error &&
<>
<div className="w-full text-center p-2">
<p className='text-red-500 text-sm'>{login.error.message}</p>
</div>
</>
} */}
<div className='mt-5 flex justify-end items-center'>
{/* <button onClick={()=>{login.mutate(fields)}} disabled={login.isPending} className='px-3 py-2 bg-purple-800 text-white font-bold rounded'>{login.isPending ? 'loading...' : 'Login'}</button> */}
<button onClick={handleLogin} className='px-4 py-[5px] bg-purple-600 text-white font-bold rounded'>{loading ? 'Loading...' : 'SIGN IN'}</button>
</div>
<div className="hidden mt-8 gri grid-cols-2 gap-8">
<div className="w-full">
<a className="icon google"
href='#' >
<img src={IOSDownload} className='w-100 h-auto' alt='IOS Download' />
</a>
</div>
<div className="w-full">
<a className="icon apple" href='#'>
<img src={GoogleDownload} className='w-100 h-auto' alt='IOS Download' />
</a>
</div>
</div>
</div>
</div>
</>
)
}
@@ -0,0 +1,22 @@
import { MdKeyboardDoubleArrowRight } from 'react-icons/md'
import { TiHomeOutline } from 'react-icons/ti'
export default function BreadcrumbCom({title, span, paths}) {
return (
<div className='w-full py-2 flex justify-between items-center'>
<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-black-gray font-semibold'>{title}</h1>
<span className='text-red-500 text-10 sm:text-base md:text-xl'>{span && span}</span>
</div>
<div className='flex gap-2 items-center text-black-gray dark:text-black-gray text-base'>
<TiHomeOutline className='text-black dark:text-black-gray' />
{paths.map((item, index) => (
<div className='flex gap-2 items-center text-black dark:text-black-gray text-10 sm:text-sm' key={index}>
<MdKeyboardDoubleArrowRight />
<p className={`${index + 1 == paths.length ? 'text-sky-600 dark:text-black-gray' : ''}`}>{item}</p>
</div>
))}
</div>
</div>
)
}
@@ -0,0 +1,107 @@
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']} />
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
:
<TableWrapper data={approvedUsers} itemsPerPage={15}>
{({ data }) => (
<>
<table className="py-2 w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-sm md:text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-4 py-2">
Name
</th>
<th scope="col" className="px-4 py-2">
Loan
</th>
<th scope="col" className="px-4 py-2">
Added
</th>
<th scope="col" className="px-4 py-2">
Action
</th>
</tr>
</thead>
<tbody>
{(data && data.length > 0) ? data?.map((item, index) => (
<tr key={index} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" className="mr-4 flex items-center px-3 py-2 text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-10 h-10 rounded-full" src={Avatar} alt="Jese image" />
<div className="px-3">
<div className="text-base font-semibold">{item?.name || ''}</div>
<div className="font-normal text-gray-500">{item?.bvn}</div>
</div>
</th>
<td className="px-3 py-2">
{item?.loan} - {item?.description}
</td>
<td className="px-3 py-2">
<div className="flex items-center">
{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}
</div>
</td>
<td className="px-3 py-2 flex gap-3 md:gap-4">
{/* <!-- Modal toggle --> */}
{/* <Link to={RouteLinks.manageAdminPage}>
<i onClick={handleShowEditModal} className="fa-solid fa-eye text-base md:text-lg cursor-pointer p-2 text-sky-600"></i>
</Link> */}
{/* <i onClick={handleShowEditModal} className="fa-solid fa-pen-to-square text-base md:text-lg cursor-pointer p-2"></i> */}
{/* <i onClick={handleDeleteModal} className="fa-solid fa-trash text-base md:text-lg cursor-pointer p-2 text-red-500"></i> */}
<Icons name='edit' />
<Icons name='eye' />
<Icons name='trash' className={'hidden text-red-500'} />
</td>
</tr>
))
:
<tr className="w-3 p-3">
<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>
)
}
@@ -0,0 +1,98 @@
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 { useState } from "react";
import MainBtn from "../MainBtn";
import { TbLogout2 } from "react-icons/tb";
export default function DashboardHeader({showAsideDrawer, setShowAsideDrawer}) {
const {theme, handleTheme, handleDrawer, setLogoutModal} = generalLayoutContext()
const [activeMenu, setActiveMenu] = useState('')
const handleActiveMenu = (name) => {
if(activeMenu == name){
setActiveMenu('')
}else{
setActiveMenu(name)
}
}
return (
<>
{/* HEADER SECTION*/}
<header className='sticky top-0 z-[777] w-full px-3 md:px-10 bg-white shadow-sm text-brown dark:shadow-white dark:bg-black dark:text-white'>
<div className='w-full flex h-20 justify-between lg:justify-end items-center'>
<div className="lg:hidden">
<HandBurger asideDisplay={()=>setShowAsideDrawer(prev => !prev)} showAside={showAsideDrawer} barColor="bg-black-gray" />
</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>
{/* 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> */}
{/* NOTIFICATION */}
{/* <button onClick={()=>{}} className="relative px-2 flex justify-center items-center gap-2 cursor-pointer">
<i className="fa-regular fa-bell 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='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>
{/* USER AVATRA */}
{/* <div className='w-10 h-10 rounded shadow-round_black dark:shadow-round_white'>
<img src={UserAvatar} alt='user image' className='w-full h-full p-1 rounded-full' />
</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 absolute p-4 w-52 sm:w-96 bg-white dark:bg-black 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="bg-primary rounded w-full flex justify-center items-center gap-2">
<MainBtn
text='Logout'
className="w-full text-center text-white font-medium 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,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>
)
}
+36
View File
@@ -0,0 +1,36 @@
export default function HandBurger({showAside, asideDisplay, barColor}) {
return (
<div
className="relative lg:hidden w-5 h-5 flex flex-col items-center justify-between cursor-pointer"
onClick={asideDisplay}
>
{/* <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-primary'} dark:bg-white-light ${
showAside
? "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-primary'} dark:bg-white-light transition-all duration-300 ${
showAside
? "rotate-[2000deg] opacity-0"
: ""
}`}
></div>
<div
className={`absolute left-0 w-5 h-1 rounded-md ${barColor ? barColor :'bg-primary'} dark:bg-white-light ${
showAside
? "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 rounded-lg shadow-round_black dark:shadow-round_white dark:bg-gray-700 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,17 @@
import { Link, useLocation } from "react-router-dom"
import Icons from "../../Icons"
export default function AsideLink({shrinkAside, name, to, icon}) {
const {pathname} = useLocation()
return (
<Link
className={`w-full flex items-center gap-2 px-4 py-2 my-1 text-[13px] font-semibold rounded-md hover:text-white-light hover:bg-white/10 ${pathname == to ? 'bg-white/10 text-white-light' : 'text-slate-400'}`}
to={to}
>
{icon && <Icons name={icon} />}
{shrinkAside ? '' : name}
</Link>
)
}
@@ -0,0 +1,31 @@
import { useState } from "react";
import { useLocation } from "react-router-dom"
import { FaCaretDown } from "react-icons/fa";
import Icons from "../../Icons";
export default function AsideLinkWithSubLinks({shrinkAside, name, icon, to, children, isOpen}) {
// const btnName = name
const {pathname} = useLocation()
const isMatchedPath = pathname.split('/').includes('')
// isMatchedPath.splice(0,1)
const [hideSubMenu, setHideSubMenu] = useState(isOpen)
return (
<div
className={`w-full px-4 text-[13px] font-semibold rounded`}
>
<button onClick={()=>setHideSubMenu(prev => !prev)} name={name} className="py-2 w-full flex items-center justify-between gap-2 cursor-pointer text-slate-400">
<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) ? 'block' : 'hidden'}`}>
{children}
</div>
</div>
)
}
@@ -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,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},
]
},
]
+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,129 @@
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']} />
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
:
<TableWrapper data={loanOffersUsersList} itemsPerPage={15}>
{({ data }) => (
<>
<table className="py-2 w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-sm md:text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-4 py-2">
CID
</th>
<th scope="col" className="px-4 py-2">
Loan
</th>
<th scope="col" className="px-4 py-2">
Amount
</th>
<th scope="col" className="px-4 py-2">
Description
</th>
<th scope="col" className="px-4 py-2">
Days Duration
</th>
<th scope="col" className="px-4 py-2">
Ative
</th>
<th scope="col" className="px-4 py-2">
Score
</th>
<th scope="col" className="px-4 py-2">
Lorder
</th>
{/* <th scope="col" className="px-4 py-2">
Action
</th> */}
</tr>
</thead>
<tbody>
{(data && data.length > 0) ? data?.map((item, index) => (
<tr key={index} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" className="mr-4 flex items-center px-3 py-2 text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-10 h-10 rounded-full" src={Avatar} alt="Jese image" />
<div className="px-3">
<div className="text-base font-semibold">{item?.cid}</div>
</div>
</th>
<td className="px-3 py-2">
{item?.loan}
</td>
<td className="px-3 py-2">
{item?.amount || ''}
</td>
<td className="px-3 py-2">
{item?.description}
</td>
<td className="px-3 py-2">
{item?.days_duration}
</td>
<td className="px-3 py-2">
{item?.active}
</td>
<td className="px-3 py-2">
{item?.score}
</td>
<td className="px-3 py-2">
{item?.lorder}
</td>
{/* <td className="px-3 py-2 flex gap-3 md:gap-4">
<Icons name='edit' />
<Icons name='eye' />
<Icons name='trash' className={'hidden text-red-500'} />
</td> */}
</tr>
))
:
<tr className="w-3 p-3">
<td className="px-3 py-2" colSpan={8}>
<div className="flex justify-center items-center">
No Record Found
</div>
</td>
</tr>
}
</tbody>
</table>
</>
)}
</TableWrapper>
}
</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-gray-900/40 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-[999] flex flex-col justify-center items-center w-full md:inset-0 h-[calc(100%)] max-h-full">
<div className={`pop-modal relative p-4 w-full ${maxWidth ? maxWidth : 'max-w-2xl'} max-h-full`}>
{/* <!-- Modal content --> */}
<div className="pb-4">
{children}
</div>
</div>
</div>
)
}
// id="default-modal" tabIndex={-1} aria-hidden="true"
@@ -0,0 +1,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']} />
{isFetching ?
<>
<div className="w-full py-4">
<p className='text-slate-800'>Loading...</p>
</div>
</>
: isError ?
<div className="w-full py-4">
<p className='text-red-500'>{error.message}</p>
</div>
:
<TableWrapper data={selectUsers} itemsPerPage={15}>
{({ data }) => (
<>
<table className="py-2 w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-sm md:text-base text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-4 py-2">
Name
</th>
<th scope="col" className="px-4 py-2">
Loan
</th>
<th scope="col" className="px-4 py-2">
Added
</th>
<th scope="col" className="px-4 py-2">
Action
</th>
</tr>
</thead>
<tbody>
{(data && data.length > 0) ? data?.map((item, index) => (
<tr key={index} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" className="mr-4 flex items-center px-3 py-2 text-gray-900 whitespace-nowrap dark:text-white">
<img className="w-10 h-10 rounded-full" src={Avatar} alt="Jese image" />
<div className="px-3">
<div className="text-base font-semibold">{item?.name || ''}</div>
<div className="font-normal text-gray-500">{item?.bvn}</div>
</div>
</th>
<td className="px-3 py-2">
{item?.loan} - {item?.description}
</td>
<td className="px-3 py-2">
<div className="flex items-center">
{getDateFromDateString(item?.added)} {getTimeFromDateString(item?.added)}
</div>
</td>
<td className="px-3 py-2 flex gap-3 md:gap-4">
{/* <!-- Modal toggle --> */}
{/* <Link to={RouteLinks.manageAdminPage}>
<i onClick={handleShowEditModal} className="fa-solid fa-eye text-base md:text-lg cursor-pointer p-2 text-sky-600"></i>
</Link> */}
{/* <i onClick={handleShowEditModal} className="fa-solid fa-pen-to-square text-base md:text-lg cursor-pointer p-2"></i> */}
{/* <i onClick={handleDeleteModal} className="fa-solid fa-trash text-base md:text-lg cursor-pointer p-2 text-red-500"></i> */}
<Icons name='edit' />
<Icons name='eye' />
<Icons name='trash' className={'hidden text-red-500'} />
</td>
</tr>
))
:
<tr className="w-3 p-3">
<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>
)
}
const dummy = [
{name:'ok'}
]
@@ -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>
)
}
+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)
}
+77
View File
@@ -0,0 +1,77 @@
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 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)
}
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]);
const handleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
}
let value = {theme, handleTheme, drawer, handleDrawer, booking, handleBooking, alertBox, handleAlertBox, logoutModal, setLogoutModal}
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
+61
View File
@@ -0,0 +1,61 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html{
scroll-behavior: smooth;
}
*{
transition: all .3s;
}
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 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;
}
/* 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;}
}
}
+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 ApplicationsLoanCom from '../components/applicationsloancom/ApplicationsLoanCom'
export default function ApplicationsLoanPage() {
return (
<ApplicationsLoanCom />
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import ApprovedLoanCom from '../components/approvedloancom/ApprovedLoanCom'
export default function ApprovedLoansPage() {
return (
<ApprovedLoanCom />
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import DisbursementsLoanCom from '../components/disbursementsloancom/DisbursementsLoanCom'
export default function DisbursementsLoanPage() {
return (
<DisbursementsLoanCom />
)
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import BreadcrumbCom from '../components/breadcrumb/BreadcrumbCom'
export default function HomePage() {
return (
<div className='w-full flex flex-col gap-8'>
<BreadcrumbCom title='Dashboard' paths={['Home', 'Dashboard']} />
<div className='grid grid-cols-1 2xl:grid-cols-2 gap-8'>
<div className='w-full grid grid-cols-1 sm:grid-cols-2 gap-8'>
<div className='w-full bg-white dark:bg-slate-800 hover:scale-[1.02] cursor-pointer rounded h-[300px] shadow-round_black dark:shadow-round_white'></div>
<div className='w-full bg-white dark:bg-slate-800 hover:scale-[1.02] cursor-pointer rounded h-[300px] shadow-round_black dark:shadow-round_white'></div>
<div className='w-full bg-white dark:bg-slate-800 hover:scale-[1.02] cursor-pointer rounded h-[300px] shadow-round_black dark:shadow-round_white'></div>
<div className='w-full bg-white dark:bg-slate-800 hover:scale-[1.02] cursor-pointer rounded h-[300px] shadow-round_black dark:shadow-round_white'></div>
</div>
<div className='w-full'>
<div className='w-full bg-white dark:bg-slate-800 hover:scale-[1.02] cursor-pointer rounded h-full shadow-round_black dark:shadow-round_white'></div>
</div>
</div>
</div>
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import LoanOffersCom from '../components/loanofferscom/LoanOffersCom'
export default function LoanOffersPage() {
return (
<LoanOffersCom />
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import LoginCom from '../components/auth/LoginCom'
export default function LoginPage() {
return (
<LoginCom />
)
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import SelectedLoanCom from '../components/selectedloancom/SelectedLoanCom'
export default function SelectedLoanPage() {
return (
<SelectedLoanCom />
)
}
+13
View File
@@ -0,0 +1,13 @@
import React from 'react'
import BreadcrumbCom from '../components/breadcrumb/BreadcrumbCom'
export default function UsersPage() {
return (
<div className='w-full'>
<BreadcrumbCom title='Users' paths={['Dashboard', 'Users']} />
<p className=''>
coming soon ...
</p>
</div>
)
}
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+8
View File
@@ -0,0 +1,8 @@
const queryKeys = {
apply_loan: ['apply'],
select_loan: ['select-loan'],
approved_loan: ['approved-loan'],
loan_offers: ['loan-offers'],
}
export default queryKeys
+75
View File
@@ -0,0 +1,75 @@
import axios from "axios"
axios.interceptors.request.use(
config => {
config.headers = {
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
// "Access-Control-Expose-Headers": "Access-Control-Allow-Origin",
// "Access-Control-Allow-Headers": "Origin, X-API-KEY, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, Access-Control-Allow-Headers, Authorization, observe, enctype, Content-Length, X-Csrf-Token",
"Content-Type": "application/json;charset=UTF-8",
// 'Authorization': `Bearer ${localStorage.getItem('token')}`
};
// config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`;
// config.baseURL = process.env.REACT_APP_MAIN_API
return config;
},
error => {
return Promise.reject(error);
}
);
const postAuxEnd = (path, postData, media=false) => {
const basePath = media ? process.env.REACT_APP_MAIN_API : process.env.REACT_APP_MAIN_API
return axios.post(`${basePath}${path}`, postData).then(res => {
return res
}).catch(err => {
throw new Error(err.response.data.message);
})
}
const getAuxEnd = (path, reqData= null) => {
const basePath = process.env.REACT_APP_MAIN_API
return axios.get(`${basePath}${path}`,{ params: reqData }).then(res => {
return res
// localStorage.clear();
// window.location.href = `/login?sessionExpired=true`;
}).catch(err => {
throw new Error(err);
// throw new Error(err.response.data.message);
// return err
})
}
// FUNCTION TO LOGIN USER IN
export const loginUser = (reqData) => {
let postData = {
...reqData
}
return postAuxEnd('/salary/login', postData, false)
}
// FUNCTION TO GET APPLIED LOANS TABLE
export const applyLoan = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/office/loan/apply`, postData)
}
// FUNCTION TO GET APPLIED LOANS TABLE
export const loanOffers = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/office/offers`, postData)
}
// FUNCTION TO GET APPLIED LOANS TABLE
export const selectLoan = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/office/loan/select`, postData)
}
// FUNCTION TO GET APPLIED LOANS TABLE
export const approvedLoan = (reqData) => {
const postData = { ...reqData }
return getAuxEnd(`/office/loan/approved`, postData)
}
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+20
View File
@@ -0,0 +1,20 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
userDetails: {},
};
export const userSlice = createSlice({
name: "userDetails",
initialState,
reducers: {
updateUserDetails: (state, action) => {
state.userDetails = { ...action.payload };
},
},
});
// Action creators are generated for each case reducer function
export const { updateUserDetails } = userSlice.actions;
export default userSlice.reducer;
+9
View File
@@ -0,0 +1,9 @@
import { configureStore } from "@reduxjs/toolkit";
import userDetailReducer from "./UserDetails";
export default configureStore({
reducer: {
userDetails: userDetailReducer,
},
});
+46
View File
@@ -0,0 +1,46 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors:{
brown:{
DEFAULT: '#393536',
},
black:{
DEFAULT: '#222',
gray: '#a6a9b7'
},
primary: {
DEFAULT: '#0284c7',
},
orange: {
light: '#ED7747',
dark: '#9a3412'
},
white: {
DEFAULT: '#fff',
light: '#f1f5f9'
}
},
screens: {
max_width: '1700px'
},
fontSize:{
10: '10px',
12: '12px',
14: '14px'
},
backgroundImage: {
login_gradient: 'linear-gradient(to right, #8e54e9 0, #4776e6 100%)'
},
boxShadow: {
round_black: '0 0px 1px 0 rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.19)',
round_white: '0 0px 1px 0 rgba(255, 255, 255, 0.2), 0 1px 5px 0 rgba(255, 255, 255, 0.19)'
}
},
},
plugins: [],
}