initial commit
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
REACT_APP_BASE_URL=
|
||||||
|
REACT_APP_MANIFOLD_API_URL=
|
20
.eslintrc.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": "off"
|
||||||
|
}
|
||||||
|
}
|
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
70
README.md
Normal 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)
|
5
babel-plugin-macros.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
"fontawesome-svg-core": {
|
||||||
|
license: "free",
|
||||||
|
},
|
||||||
|
};
|
6
babel.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache.forever();
|
||||||
|
return {
|
||||||
|
plugins: ["macros"],
|
||||||
|
};
|
||||||
|
};
|
22569
package-lock.json
generated
Normal file
78
package.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "financial-forecast",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@mui/material": "^5.15.14",
|
||||||
|
"@mui/x-data-grid": "^7.0.0",
|
||||||
|
"@mui/x-data-grid-generator": "^7.0.0",
|
||||||
|
"@popperjs/core": "^2.11.5",
|
||||||
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
|
"@tanstack/react-query": "^4.0.10",
|
||||||
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
|
"@testing-library/react": "^13.3.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"babel-plugin-macros": "^3.1.0",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
|
"dayjs": "^1.11.4",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"js-file-download": "^0.4.12",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"lbs-react-table": "^0.1.3",
|
||||||
|
"npm": "^10.5.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-currency-format": "^1.1.0",
|
||||||
|
"react-currency-input-field": "^3.6.9",
|
||||||
|
"react-data-table-component": "^7.6.2",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.34.0",
|
||||||
|
"react-loader-spinner": "^5.1.7-beta.1",
|
||||||
|
"react-moment": "^1.1.2",
|
||||||
|
"react-query": "^4.0.0",
|
||||||
|
"react-redux": "^8.0.2",
|
||||||
|
"react-router-dom": "^6.3.0",
|
||||||
|
"react-scripts": "^5.0.1",
|
||||||
|
"react-toastify": "^9.0.8",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"validator": "^13.7.0",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"start-dev": "env-cmd -f .env.development react-scripts start",
|
||||||
|
"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.7",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
|
"tailwindcss": "^3.1.6"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
23
public/index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<meta name="description" content="Manifold Financial Forecast Application" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Mono&family=Open+Sans&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<title>Financial Forecast Frontend</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
25
public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "con.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"
|
||||||
|
}
|
38
src/App.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
27
src/App.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { appRoutes } from "./routes/appRoutes";
|
||||||
|
import { setNavigate } from './utils/utils'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToastContainer />
|
||||||
|
<Routes>
|
||||||
|
|
||||||
|
{appRoutes.map((route) => {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Route key={route.key} {...route} element={<route.component />} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
99
src/api/instances.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React from "react";
|
||||||
|
import axios from 'axios';
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
let accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
let refreshToken = localStorage.getItem('refreshToken') ? JSON.parse(localStorage.getItem('refreshToken')) : null
|
||||||
|
let zohoAccessToken = localStorage.getItem('zohoAccessToken') ? JSON.parse(localStorage.getItem('zohoAccessToken')) : null
|
||||||
|
let zohoTokenExpiry = localStorage.getItem('zohoTokenExpiry') ? JSON.parse(localStorage.getItem('zohoTokenExpiry')) : null
|
||||||
|
|
||||||
|
let deleteToken = async () => {
|
||||||
|
localStorage.removeItem('accessToken')
|
||||||
|
accessToken = null
|
||||||
|
};
|
||||||
|
|
||||||
|
let deleteZohoToken = async () => {
|
||||||
|
localStorage.removeItem('zohoAccessToken')
|
||||||
|
localStorage.removeItem('zohoTokenExpiry')
|
||||||
|
zohoAccessToken = null
|
||||||
|
zohoTokenExpiry = null
|
||||||
|
};
|
||||||
|
|
||||||
|
let setToken = async (accessToken) => {
|
||||||
|
localStorage.setItem('accessToken', JSON.stringify(accessToken))
|
||||||
|
accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
let setZohoToken = async (data) => {
|
||||||
|
localStorage.setItem('zohoAccessToken', JSON.stringify(data.zohoAccessToken))
|
||||||
|
localStorage.setItem('zohoTokenExpiry', JSON.stringify(data.zohoTokenExpiry))
|
||||||
|
zohoAccessToken = localStorage.getItem('zohoAccessToken') ? JSON.parse(localStorage.getItem('zohoAccessToken')) : null
|
||||||
|
zohoTokenExpiry = localStorage.getItem('zohoTokenExpiry') ? JSON.parse(localStorage.getItem('zohoTokenExpiry')) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: `${process.env.REACT_APP_MANIFOLD_API_URL}`,
|
||||||
|
//withCredentials: true ,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Automatically refresh token if it is about to expire
|
||||||
|
instance.interceptors.request.use(async req => {
|
||||||
|
|
||||||
|
accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
refreshToken = localStorage.getItem('refreshToken') ? JSON.parse(localStorage.getItem('refreshToken')) : null
|
||||||
|
let zohoTokenExpiry = localStorage.getItem('zohoTokenExpiry') ? JSON.parse(localStorage.getItem('zohoTokenExpiry')) : null
|
||||||
|
|
||||||
|
// check if it exists
|
||||||
|
|
||||||
|
// returns true if user does not have acess token and zoho token
|
||||||
|
if (accessToken && zohoTokenExpiry) {
|
||||||
|
|
||||||
|
// check if token hasn't expired
|
||||||
|
const user = accessToken ? jwt_decode(accessToken) : null
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await deleteZohoToken();
|
||||||
|
}
|
||||||
|
let isUserExpired = (user.exp - dayjs().unix()) < 1 ? true : false;
|
||||||
|
let isZohoTokenExpired = dayjs(zohoTokenExpiry).unix() - dayjs().unix() < 1 ? true : false;
|
||||||
|
|
||||||
|
if ((!isUserExpired && isUserExpired !== null) && (!isZohoTokenExpired && isZohoTokenExpired !== null)) {
|
||||||
|
req.headers['Authorization'] = `Bearer ${accessToken}`
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUserExpired) {
|
||||||
|
localStorage.clear();
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isZohoTokenExpired) {
|
||||||
|
localStorage.clear();
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
req.headers['Authorization'] = `Bearer ${accessToken}`
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken && !zohoTokenExpiry) {
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zohoTokenExpiry) {
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
req.headers['Authorization'] = `Bearer ${accessToken}`
|
||||||
|
return req
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { instance as Axios };
|
||||||
|
|
BIN
src/assets/default.png
Normal file
After Width: | Height: | Size: 201 B |
BIN
src/assets/down_arrow.png
Normal file
After Width: | Height: | Size: 158 B |
BIN
src/assets/first-image.jpeg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/pexels-pablo-penades-10187904.jpg
Normal file
After Width: | Height: | Size: 3.5 MiB |
BIN
src/assets/second-image.jpeg
Normal file
After Width: | Height: | Size: 610 KiB |
BIN
src/assets/up_arrow.png
Normal file
After Width: | Height: | Size: 160 B |
141
src/components/BarChart.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Chart from "chart.js";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { getCharts } from "../redux/slices/zoho";
|
||||||
|
|
||||||
|
const BarChart = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const charts = useSelector((state) => state.zoho.charts);
|
||||||
|
console.log(charts,'charts')
|
||||||
|
let isChartLoading = useSelector((state) => state.zoho.isChartLoading);
|
||||||
|
console.log(isChartLoading,'ischartloading')
|
||||||
|
let { forecastNumber, forecastPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect( () => {
|
||||||
|
|
||||||
|
dispatch(getCharts({forecastNumber, forecastPeriod}));
|
||||||
|
if(isChartLoading == undefined) {
|
||||||
|
isChartLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: isChartLoading ? [] : charts.months,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Inflow',
|
||||||
|
backgroundColor: "green",
|
||||||
|
borderColor: "green",
|
||||||
|
data: isChartLoading ? [] : charts.forecastDollarInflow,
|
||||||
|
fill: false,
|
||||||
|
barThickness: 15,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Outflow',
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: "red",
|
||||||
|
borderColor: "red",
|
||||||
|
data: isChartLoading ? [] : charts.forecastDollarOutflow,
|
||||||
|
barThickness: 15,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: "Orders Chart",
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
fontColor: "rgba(0,0,0,.4)",
|
||||||
|
},
|
||||||
|
align: "end",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Month",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [2],
|
||||||
|
borderDashOffset: [2],
|
||||||
|
color: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
position: 'top',
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Value",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [2],
|
||||||
|
drawBorder: false,
|
||||||
|
borderDashOffset: [2],
|
||||||
|
color: "rgba(33, 37, 41, 0.2)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.15)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(255, 159, 64)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
'rgb(75, 192, 192)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(153, 102, 255)',
|
||||||
|
'rgb(201, 203, 207)'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let ctx = document.getElementById("bar-chart").getContext("2d");
|
||||||
|
window.myBar = new Chart(ctx, config);
|
||||||
|
}, [dispatch]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-10/12 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 border rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 bg-transparent">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 flex-auto">
|
||||||
|
{/* Chart */}
|
||||||
|
<div className="relative" style={{ height: "350px" }}>
|
||||||
|
<canvas id="bar-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default BarChart;
|
28
src/components/BarCharts.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
//import { BarChart } from '@mui/';
|
||||||
|
import { Bar } from 'react-chartjs-2';
|
||||||
|
const BarCharts = () => {
|
||||||
|
const data = {
|
||||||
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Sales',
|
||||||
|
data: [65, 59, 80, 81, 56, 55, 40],
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Sales Bar Chart</h2>
|
||||||
|
<Bar data={data} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BarCharts
|
237
src/components/BillChart.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Chart from "chart.js";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { getNonbillableExpense } from "../redux/slices/nonbill";
|
||||||
|
import { getInventory } from "../redux/slices/inventory";
|
||||||
|
const BillChart = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExpensesLoading = useSelector(
|
||||||
|
(state) => state.nonbill.isExpensesLoading
|
||||||
|
);
|
||||||
|
const expenses = useSelector(
|
||||||
|
(state) => state.nonbill.expenses
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect( () => {
|
||||||
|
|
||||||
|
dispatch(getNonbillableExpense());
|
||||||
|
console.clear()
|
||||||
|
|
||||||
|
if(isExpensesLoading == undefined) {
|
||||||
|
isExpensesLoading= false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* let config = {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: ['JAN','FEB', 'MAR', 'APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Non-Bill Expenses',
|
||||||
|
backgroundColor: "red",
|
||||||
|
borderColor: "red",
|
||||||
|
data: [4, 3, 5,9],
|
||||||
|
fill: false,
|
||||||
|
barThickness: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: "Orders Chart",
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
fontColor: "rgba(0,0,0,.4)",
|
||||||
|
},
|
||||||
|
align: "end",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Month",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [1],
|
||||||
|
borderDashOffset: [1],
|
||||||
|
color: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineBorderDash: [1],
|
||||||
|
zeroLineBorderDashOffset: [1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
position: 'top',
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Value",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [2],
|
||||||
|
drawBorder: false,
|
||||||
|
borderDashOffset: [2],
|
||||||
|
color: "rgba(33, 37, 41, 0.2)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.15)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(255, 159, 64)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
'rgb(75, 192, 192)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(153, 102, 255)',
|
||||||
|
'rgb(201, 203, 207)'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};*/
|
||||||
|
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: ['JAN','FEB', 'MAR', 'APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Non-Bill Expenses',
|
||||||
|
backgroundColor: "red",
|
||||||
|
borderColor: "red",
|
||||||
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Initial data for each month
|
||||||
|
fill: false,
|
||||||
|
barThickness: 15,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: "Orders Chart",
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
fontColor: "rgba(0,0,0,.4)",
|
||||||
|
},
|
||||||
|
align: "end",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Month",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [1],
|
||||||
|
borderDashOffset: [1],
|
||||||
|
color: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineBorderDash: [1],
|
||||||
|
zeroLineBorderDashOffset: [1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
position: 'top',
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Value",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [2],
|
||||||
|
drawBorder: false,
|
||||||
|
borderDashOffset: [2],
|
||||||
|
color: "rgba(33, 37, 41, 0.2)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.15)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(255, 159, 64)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
'rgb(75, 192, 192)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(153, 102, 255)',
|
||||||
|
'rgb(201, 203, 207)'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Initialize an object to store accumulated values for each month
|
||||||
|
let monthlyTotal = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // Index 0 represents JAN, index 1 represents FEB, and so on
|
||||||
|
expenses.forEach(item => {
|
||||||
|
let month = parseInt(item.date.split('-')[1], 10);
|
||||||
|
|
||||||
|
monthlyTotal[month - 1] += item.total_without_tax;
|
||||||
|
|
||||||
|
});
|
||||||
|
config.data.datasets[0].data = monthlyTotal;
|
||||||
|
// Now config object is updated with the accumulated values for each month
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let ctx = document.getElementById("bar-chart").getContext("2d");
|
||||||
|
window.myBar = new Chart(ctx, config);
|
||||||
|
}, [dispatch]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-11/12 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 border rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 bg-transparent">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 flex-auto">
|
||||||
|
{/* Chart */}
|
||||||
|
<div className="relative" style={{ height: "350px" }}>
|
||||||
|
<canvas id="bar-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default BillChart;
|
48
src/components/BillTable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
import BillTableBody from "./BillTableBody";
|
||||||
|
|
||||||
|
const BillTable = ({
|
||||||
|
setShowBillDetailModal,
|
||||||
|
setBillDetail,
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
Outflow details
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Bills</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<BillTableBody
|
||||||
|
{...{
|
||||||
|
setShowBillDetailModal,
|
||||||
|
setBillDetail,
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default BillTable;
|
89
src/components/BillTableBody.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const BillTableBody = ({
|
||||||
|
setShowBillDetailModal,
|
||||||
|
setBillDetail,
|
||||||
|
tableData,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const updateBillDetail = (bill) => {
|
||||||
|
setShowBillDetailModal(true);
|
||||||
|
setBillDetail(bill);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id} onClick={() => updateBillDetail(data)}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BillTableBody;
|
75
src/components/Delete.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { getOverdrafts, deleteOverdraft } from '../redux/slices/zoho';
|
||||||
|
import { resynApplication } from '../redux/slices/forecast';
|
||||||
|
|
||||||
|
const Delete = (props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
|
const onDeleteOverdraft = async () => {
|
||||||
|
await dispatch(deleteOverdraft({ accountId: props.overdraft.accountId }));
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
|
||||||
|
await dispatch(getOverdrafts());
|
||||||
|
await dispatch(resynApplication());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showDeleteModal ? (
|
||||||
|
<>
|
||||||
|
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-xl">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
{/*body*/}
|
||||||
|
{/* <form className='space-y-6 py-6' onSubmit={handleSubmit(onSubmit)}> */}
|
||||||
|
<div className="relative px-8 flex-auto">
|
||||||
|
<div className="relative w-12/12">
|
||||||
|
<p className="my-4 text-slate-500 text-lg leading-relaxed">
|
||||||
|
Are you sure you want to delete Overdraft?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteModal(false)}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 mb-1 mr-2"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
type="submit"
|
||||||
|
onClick={onDeleteOverdraft}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* </form> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="has-tooltip" onClick={() => setShowDeleteModal(true)}>
|
||||||
|
<span className="tooltip rounded shadow-lg p-2 bg-gray-100 text-red-500 -mt-8">
|
||||||
|
delete overdraft{' '}
|
||||||
|
</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrashAlt}
|
||||||
|
style={{ fontSize: 17, color: 'red' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Delete;
|
37
src/components/DurationDropdown.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { generateReport } from "../redux/slices/forecast";
|
||||||
|
|
||||||
|
|
||||||
|
const DurationDropdown = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
let { durations, selectedPeriod } = useSelector(state => state.forecast.forecastDropdown)
|
||||||
|
|
||||||
|
|
||||||
|
const updateSelectedPeriod = (e) => {
|
||||||
|
|
||||||
|
// call opening balance
|
||||||
|
dispatch(generateReport(
|
||||||
|
{
|
||||||
|
id: e.target.value,
|
||||||
|
forecastNumber: durations[e.target.value].forecastNumber,
|
||||||
|
forecastPeriod: durations[e.target.value].forecastPeriod
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
id="duration"
|
||||||
|
className=" mr-2 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
|
style={{backgroundColor:'#eaeffc'}}
|
||||||
|
value={selectedPeriod}
|
||||||
|
onChange={updateSelectedPeriod}
|
||||||
|
>
|
||||||
|
{durations.map(({ forecastNumber, label }, index) => <option key={index} value={index}>{label}</option>)}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DurationDropdown;
|
224
src/components/Edit.js
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faPencil } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { withAuth } from '../hoc/withAuth';
|
||||||
|
import CurrencyInput from 'react-currency-input-field';
|
||||||
|
import { updateOverdraft, getOverdrafts } from '../redux/slices/zoho';
|
||||||
|
import { getUsers } from '../redux/slices/user';
|
||||||
|
import { resynApplication } from '../redux/slices/forecast';
|
||||||
|
|
||||||
|
const Edit = (props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [editedAmount, setEditedAmount] = useState(false);
|
||||||
|
const [amount, setAmount] = useState('');
|
||||||
|
const [enableButton, setEnableButton] = useState('');
|
||||||
|
|
||||||
|
let isOverdraftLoading = useSelector(
|
||||||
|
(state) => state.zoho.isOverdraftLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
setShowEditModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitEditOverdraft = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let accountId = props.overdraft.accountId;
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
accountId,
|
||||||
|
amount,
|
||||||
|
};
|
||||||
|
dispatch(updateOverdraft({ data })).then(async (res) => {
|
||||||
|
if (res.error) return;
|
||||||
|
|
||||||
|
setAmount('');
|
||||||
|
setShowEditModal(false);
|
||||||
|
navigate('/overdraft');
|
||||||
|
await dispatch(getOverdrafts());
|
||||||
|
await dispatch(getUsers());
|
||||||
|
await dispatch(resynApplication());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const amountHandler = (e) => {
|
||||||
|
setEditedAmount(true);
|
||||||
|
setEnableButton(true);
|
||||||
|
setAmount(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="has-tooltip" onClick={() => setShowEditModal(true)}>
|
||||||
|
<span className="tooltip rounded shadow-lg p-2 bg-gray-100 text-green-700 -mt-8">
|
||||||
|
edit overdraft
|
||||||
|
</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPencil}
|
||||||
|
style={{ fontSize: 17, color: 'green' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showEditModal ? (
|
||||||
|
<>
|
||||||
|
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-xl">
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
<div className="flex items-start justify-around mt-5 rounded-t">
|
||||||
|
<div className="flex-initial w-64 justify-center">
|
||||||
|
<h3 className="text-red-700 font-medium text-lg mb-1">
|
||||||
|
Overdraft
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
type="button"
|
||||||
|
className=" rounded-md p-1 inline-flex items-center justify-center text-red-400 hover:text-red-500 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-red-500"
|
||||||
|
>
|
||||||
|
<span className="sr-only">close menu</span>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className="space-y-6 py-6 auto"
|
||||||
|
onSubmit={submitEditOverdraft}
|
||||||
|
>
|
||||||
|
<div className="relative px-4 flex-auto ">
|
||||||
|
<div className="relative w-14/14 grid gap-4 grid-cols-2 ">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Account Name:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
name="account-type"
|
||||||
|
placeholder="Account Type"
|
||||||
|
value={props.overdraft.accountName}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Account ID:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
name="account-type"
|
||||||
|
placeholder="Account Type"
|
||||||
|
value={props.overdraft.accountId}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Account Type:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
name="account-type"
|
||||||
|
placeholder="Account Type"
|
||||||
|
value={props.overdraft.accountType}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 ">
|
||||||
|
Bank Name:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none "
|
||||||
|
type="text"
|
||||||
|
name="bank-name"
|
||||||
|
value={props.overdraft.bankName}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 ">
|
||||||
|
Currency:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none "
|
||||||
|
type="text"
|
||||||
|
name="currency"
|
||||||
|
value={props.overdraft.currency}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Loan Amount:
|
||||||
|
</label>
|
||||||
|
<CurrencyInput
|
||||||
|
intlConfig={{
|
||||||
|
locale:
|
||||||
|
props.overdraft.currency != 'USD'
|
||||||
|
? 'en-NG'
|
||||||
|
: 'en-US',
|
||||||
|
currency:
|
||||||
|
props.overdraft.currency != 'USD' ? 'NGN' : 'USD',
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
id="loan"
|
||||||
|
name="loan"
|
||||||
|
placeholder="Please enter overdraft amount"
|
||||||
|
value={editedAmount ? amount : props.overdraft.amount}
|
||||||
|
decimalsLimit={2}
|
||||||
|
onValueChange={amountHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center p-3 mt-4 rounded-b ">
|
||||||
|
<button
|
||||||
|
className={`inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 mb-1 ${
|
||||||
|
enableButton
|
||||||
|
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||||
|
: 'bg-red-200 rounded focus:outline-none disabled:opacity-75'
|
||||||
|
} `}
|
||||||
|
type="save"
|
||||||
|
disabled={!enableButton || isOverdraftLoading}
|
||||||
|
>
|
||||||
|
{isOverdraftLoading ? 'loading...' : 'save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default withAuth(true)(Edit);
|
49
src/components/ExpensesTable.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
import ExpensesTableBody from "./ExpensesTableBody";
|
||||||
|
|
||||||
|
const ExpensesTable = ({
|
||||||
|
setShowExpensesDetailModal,
|
||||||
|
setExpensesDetail,
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
console.log(data,columns,"tsts")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Expenses</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<ExpensesTableBody
|
||||||
|
{...{
|
||||||
|
setShowExpensesDetailModal,
|
||||||
|
setExpensesDetail,
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ExpensesTable;
|
99
src/components/ExpensesTableBody.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const ExpensesTableBody = ({ tableData, columns }) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "overdraftBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.overdraftBalance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesTableBody;
|
300
src/components/Form.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { getBankAccounts } from '../redux/slices/forecast';
|
||||||
|
import CurrencyInput from 'react-currency-input-field';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { withAuth } from '../hoc/withAuth';
|
||||||
|
import { createOverdraft, getOverdrafts } from '../redux/slices/zoho';
|
||||||
|
import { getUsers } from '../redux/slices/user';
|
||||||
|
import { resynApplication } from '../redux/slices/forecast';
|
||||||
|
|
||||||
|
const Form = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [selectedValue, setSelectedValue] = useState('');
|
||||||
|
const [accountId, setAccountId] = useState('');
|
||||||
|
const [accountName, setAccountName] = useState('');
|
||||||
|
const [accountType, setAccountType] = useState('');
|
||||||
|
const [bankName, setBankName] = useState('');
|
||||||
|
const [currency, setCurrency] = useState('');
|
||||||
|
const [balance, setBalance] = useState('');
|
||||||
|
const [amount, setAmount] = useState('');
|
||||||
|
const [enableButton, setEnableButton] = useState('');
|
||||||
|
|
||||||
|
let isOverdraftLoading = useSelector(
|
||||||
|
(state) => state.zoho.isOverdraftLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
const isBankAccountsLoading = useSelector(
|
||||||
|
(state) => state.forecast.isBankAccountsLoading
|
||||||
|
);
|
||||||
|
const bankAccounts = useSelector((state) => state.forecast.bankAccounts);
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
setAccountId('');
|
||||||
|
setAccountName('');
|
||||||
|
setAccountType('');
|
||||||
|
setBalance('');
|
||||||
|
setSelectedValue('');
|
||||||
|
setBankName('');
|
||||||
|
setCurrency('');
|
||||||
|
setAmount('');
|
||||||
|
setShowModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedValueHandler = (e) => {
|
||||||
|
setEnableButton(false);
|
||||||
|
setAmount('');
|
||||||
|
|
||||||
|
if (e.target.value === 'null') {
|
||||||
|
setAccountId('');
|
||||||
|
setAccountName('');
|
||||||
|
setAccountType('');
|
||||||
|
setBalance('');
|
||||||
|
setSelectedValue('');
|
||||||
|
setBankName('');
|
||||||
|
setCurrency('');
|
||||||
|
setAmount('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bankAccount = bankAccounts.filter(function (bankAccount) {
|
||||||
|
return bankAccount.accountId == e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
setAccountId(e.target.value);
|
||||||
|
setAccountName(bankAccount[0].accountName);
|
||||||
|
setBalance(bankAccount[0].balance);
|
||||||
|
setSelectedValue(e.target.value);
|
||||||
|
setAccountType(bankAccount[0].accountType);
|
||||||
|
setBankName(bankAccount[0].bankName);
|
||||||
|
setCurrency(bankAccount[0].currency);
|
||||||
|
};
|
||||||
|
|
||||||
|
const amountHandler = (e) => {
|
||||||
|
setEnableButton(false);
|
||||||
|
if (e != undefined && accountId) {
|
||||||
|
setEnableButton(true);
|
||||||
|
}
|
||||||
|
setAmount(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitOverdraft = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let data = {
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
accountType,
|
||||||
|
bankName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
};
|
||||||
|
dispatch(createOverdraft({ data })).then(async (res) => {
|
||||||
|
if (res.error) return;
|
||||||
|
setAccountId('');
|
||||||
|
setAccountName('');
|
||||||
|
setAccountType('');
|
||||||
|
setBalance('');
|
||||||
|
setSelectedValue('');
|
||||||
|
setBankName('');
|
||||||
|
setCurrency('');
|
||||||
|
setAmount('');
|
||||||
|
setShowModal(false);
|
||||||
|
navigate('/overdraft');
|
||||||
|
await dispatch(getOverdrafts());
|
||||||
|
await dispatch(getUsers());
|
||||||
|
await dispatch(resynApplication());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getBankAccounts());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="flex-col md:flex-row list-none items-center hidden md:flex">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
className="bg-red-500 text-white active:bg-red-600 text-xs font-bold px-8 py-1 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
style={{ transition: 'all .15s ease' }}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showModal ? (
|
||||||
|
<>
|
||||||
|
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-xl">
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
<div className="flex items-start justify-around mt-5 rounded-t">
|
||||||
|
<div className="flex-initial w-64 justify-center">
|
||||||
|
<h3 className="text-red-700 font-medium text-lg mb-1">
|
||||||
|
Overdraft
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
type="button"
|
||||||
|
className=" rounded-md p-1 inline-flex items-center justify-center text-red-400 hover:text-red-500 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-red-500"
|
||||||
|
>
|
||||||
|
<span className="sr-only">close menu</span>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className="space-y-6 py-6 auto"
|
||||||
|
onSubmit={submitOverdraft}
|
||||||
|
>
|
||||||
|
<div className="relative px-4 flex-auto ">
|
||||||
|
<div className="relative w-14/14 grid gap-4 grid-cols-2 ">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 ">Name:</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
className="w-full px-6 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={selectedValueHandler}
|
||||||
|
>
|
||||||
|
<option value="null">Select an option</option>
|
||||||
|
|
||||||
|
{!isBankAccountsLoading
|
||||||
|
? bankAccounts.map((bankAccount) => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={bankAccount.accountId}
|
||||||
|
value={bankAccount.accountId}
|
||||||
|
>
|
||||||
|
{bankAccount.accountName}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Account Type:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
name="account-type"
|
||||||
|
placeholder="Account Type"
|
||||||
|
value={accountType}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 ">
|
||||||
|
Bank Name:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none "
|
||||||
|
type="text"
|
||||||
|
name="bank-name"
|
||||||
|
value={bankName}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 ">
|
||||||
|
Currency:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none "
|
||||||
|
type="text"
|
||||||
|
name="currency"
|
||||||
|
value={currency}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Balance:
|
||||||
|
</label>
|
||||||
|
<CurrencyInput
|
||||||
|
intlConfig={{
|
||||||
|
locale: currency != 'USD' ? 'en-NG' : 'en-US',
|
||||||
|
currency: currency != 'USD' ? 'NGN' : 'USD',
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
id="balance"
|
||||||
|
name="balance"
|
||||||
|
value={balance}
|
||||||
|
decimalsLimit={2}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">
|
||||||
|
Loan Amount:
|
||||||
|
</label>
|
||||||
|
<CurrencyInput
|
||||||
|
intlConfig={{
|
||||||
|
locale: currency != 'USD' ? 'en-NG' : 'en-US',
|
||||||
|
currency: currency != 'USD' ? 'NGN' : 'USD',
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
id="loan"
|
||||||
|
name="loan"
|
||||||
|
placeholder="Please enter overdraft amount"
|
||||||
|
value={amount}
|
||||||
|
decimalsLimit={2}
|
||||||
|
onValueChange={amountHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center p-3 mt-4 rounded-b ">
|
||||||
|
<button
|
||||||
|
className={`inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 mb-1 ${
|
||||||
|
enableButton
|
||||||
|
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||||
|
: 'bg-red-200 rounded focus:outline-none disabled:opacity-75'
|
||||||
|
} `}
|
||||||
|
type="save"
|
||||||
|
disabled={!enableButton || isOverdraftLoading}
|
||||||
|
>
|
||||||
|
{isOverdraftLoading ? 'loading...' : 'save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default withAuth(true)(Form);
|
47
src/components/InventoriesTable.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import InventoriesTableBody from "./InventoriesTableBody";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
|
||||||
|
const InventoriesTable = ({
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
console.log(data,"yyyyyyyyyy")
|
||||||
|
console.log(InventoriesTable,"mmmmmmmm")
|
||||||
|
console.log(tableData,"tttttttt")
|
||||||
|
console.log(handleSorting,"ttttttttm")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
Inflow details
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Inventory</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<InventoriesTableBody
|
||||||
|
{...{
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InventoriesTable;
|
99
src/components/InventoriesTableBody.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const InventoriesTableBody = ({ tableData, columns }) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency_code != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency_code != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "overdraftBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.overdraftBalance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InventoriesTableBody;
|
12
src/components/InventoryTable.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const InventoryTable = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InventoryTable
|
48
src/components/InvoiceTable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import InvoiceTableBody from "./InvoiceTableBody";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
|
||||||
|
const InvoiceTable = ({
|
||||||
|
setShowInvoiceDetailModal,
|
||||||
|
setInvoiceDetail,
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
Inflow details
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Invoices</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<InvoiceTableBody
|
||||||
|
{...{
|
||||||
|
setShowInvoiceDetailModal,
|
||||||
|
setInvoiceDetail,
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoiceTable;
|
92
src/components/InvoiceTableBody.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const InvoiceTableBody = ({
|
||||||
|
setShowInvoiceDetailModal,
|
||||||
|
setInvoiceDetail,
|
||||||
|
tableData,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const updateInvoiceDetail = (invoice) => {
|
||||||
|
setShowInvoiceDetailModal(true);
|
||||||
|
setInvoiceDetail(invoice);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id} onClick={() => updateInvoiceDetail(data)}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[field] ? data[field] : "——";
|
||||||
|
if (field == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">
|
||||||
|
₦
|
||||||
|
</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoiceTableBody;
|
0
src/components/Invoices.js
Normal file
159
src/components/LineChart.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Chart from "chart.js";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { getCharts } from "../redux/slices/zoho";
|
||||||
|
import { createPopper } from "@popperjs/core";
|
||||||
|
|
||||||
|
const LineChart = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const charts = useSelector((state) => state.zoho.charts);
|
||||||
|
let isChartLoading = useSelector((state) => state.zoho.isChartLoading);
|
||||||
|
let { forecastNumber, forecastPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
const [dropdownPopoverShow, setDropdownPopoverShow] = React.useState(false);
|
||||||
|
const btnDropdownRef = React.createRef();
|
||||||
|
const popoverDropdownRef = React.createRef();
|
||||||
|
|
||||||
|
const openDropdownPopover = () => {
|
||||||
|
createPopper(btnDropdownRef.current, popoverDropdownRef.current, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
});
|
||||||
|
setDropdownPopoverShow(true);
|
||||||
|
};
|
||||||
|
const closeDropdownPopover = () => {
|
||||||
|
setDropdownPopoverShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
dispatch(getCharts({forecastNumber, forecastPeriod}));
|
||||||
|
if(isChartLoading == undefined) {
|
||||||
|
isChartLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: isChartLoading ? [] : charts.months,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Inflow',
|
||||||
|
backgroundColor: "red",
|
||||||
|
borderColor: "red",
|
||||||
|
data: isChartLoading ? [] : charts.forecastNairaInflow,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Outflow',
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: "#437ef7",
|
||||||
|
borderColor: "#437ef7",
|
||||||
|
data: isChartLoading ? [] : charts.forecastNairaOutflow,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
text: "Sales Charts",
|
||||||
|
fontColor: "black",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
fontColor: "black",
|
||||||
|
},
|
||||||
|
align: "end",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: true,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontColor: "",
|
||||||
|
},
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
scaleType: 'band',
|
||||||
|
labelString: "Month",
|
||||||
|
fontColor: "black",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
display: false,
|
||||||
|
borderDash: [2],
|
||||||
|
borderDashOffset: [2],
|
||||||
|
color: "rgba(33, 37, 41, 0.3)",
|
||||||
|
zeroLineColor: "rgba(0, 0, 0, 0)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontColor: "black",
|
||||||
|
},
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: false,
|
||||||
|
labelString: "Value",
|
||||||
|
fontColor: "black",
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
borderDash: [3],
|
||||||
|
borderDashOffset: [3],
|
||||||
|
drawBorder: false,
|
||||||
|
color: "rgba(33, 37, 41, 0.2)",
|
||||||
|
zeroLineColor: "rgba(33, 37, 41, 0.15)",
|
||||||
|
zeroLineBorderDash: [2],
|
||||||
|
zeroLineBorderDashOffset: [2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var ctx = document.getElementById("line-chart").getContext("2d");
|
||||||
|
window.myLine = new Chart(ctx, config);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-8/12 mb-12 xl:mb-0 px-5">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words w-full mb-6 border rounded ">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 bg-transparent">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h2 className="text-black text-xl font-semibold">
|
||||||
|
Forecast Overview
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-1 w-full px-2">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 flex-auto">
|
||||||
|
{/* Chart */}
|
||||||
|
<div className="relative" style={{ height: "350px" }}>
|
||||||
|
<canvas id="line-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default LineChart;
|
32
src/components/Modal.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Modal = ({ isOpen, onClose, children }) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
||||||
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
79
src/components/Navbar.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import {
|
||||||
|
downloadReport,
|
||||||
|
generateReport,
|
||||||
|
resynApplication,
|
||||||
|
} from '../redux/slices/forecast';
|
||||||
|
import DurationDropdown from './DurationDropdown.js';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const Navbar = (props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
let { selectedPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastDropdown
|
||||||
|
);
|
||||||
|
let { forecastNumber, forecastPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastInfo
|
||||||
|
);
|
||||||
|
let user = props.user;
|
||||||
|
|
||||||
|
const downloadReportHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let filename =
|
||||||
|
`Forecast-${forecastNumber}-${forecastPeriod}` + moment().toISOString();
|
||||||
|
dispatch(downloadReport({ forecastNumber, forecastPeriod, filename }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncHandler = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await dispatch(resynApplication());
|
||||||
|
await dispatch(
|
||||||
|
generateReport({ id: selectedPeriod, forecastNumber, forecastPeriod })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className="text-white text-sm uppercase hidden lg:inline-block font-semibold"
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Duration */}
|
||||||
|
<ul className="flex-col md:flex-row list-none hidden md:flex">
|
||||||
|
<button
|
||||||
|
onClick={syncHandler}
|
||||||
|
className="mr-2 justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-black hover:text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-900"
|
||||||
|
style={{backgroundColor:'#ff0066'}}
|
||||||
|
>
|
||||||
|
Sync
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={downloadReportHandler}
|
||||||
|
className="mr-2 justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-900 hover:bg-green-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-900"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
{/* TODO:: enhance or delete this */}
|
||||||
|
|
||||||
|
<DurationDropdown />
|
||||||
|
<p className="bg-gray-50 border border-gray-300 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 mr-2" style={{backgroundColor:'#671c2d'}}>
|
||||||
|
Welcome, {user.firstName}
|
||||||
|
</p>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{/* End Navbar */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
19
src/components/OpeningBalanceTable.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import TableHead from "./TableHead";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import OpeningBalanceTableBody from "./OpeningBalanceTableBody";
|
||||||
|
|
||||||
|
const OpeningBalanceTable = ({ caption, data, columns }) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<OpeningBalanceTableBody {...{ columns, tableData }} />
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpeningBalanceTable;
|
99
src/components/OpeningBalanceTableBody.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const OpeningBalanceTableBody = ({ tableData, columns }) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "overdraftBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.overdraftBalance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpeningBalanceTableBody;
|
0
src/components/Purchase.js
Normal file
48
src/components/PurchaseTable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
import PurchaseTableBody from "./PurchaseTableBody";
|
||||||
|
|
||||||
|
const PurchaseTable = ({
|
||||||
|
setShowPurchaseDetailModal,
|
||||||
|
setPurchaseDetail,
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Purchases</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<PurchaseTableBody
|
||||||
|
{...{
|
||||||
|
setShowPurchaseDetailModal,
|
||||||
|
setPurchaseDetail,
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default PurchaseTable;
|
89
src/components/PurchaseTableBody.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const PurchaseTableBody = ({
|
||||||
|
setShowPurchaseDetailModal,
|
||||||
|
setPurchaseDetail,
|
||||||
|
tableData,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const updatePurchaseDetail = (purchase) => {
|
||||||
|
setShowPurchaseDetailModal(true);
|
||||||
|
setPurchaseDetail(purchase);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id} onClick={() => updatePurchaseDetail(data)}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchaseTableBody;
|
60
src/components/ReportTable.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React ,{useState}from "react";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
import ReportTableBody from "./ReportTableBody";
|
||||||
|
const ReportTable = ({
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
pageSize = 10, // Specify default page size
|
||||||
|
}) => {
|
||||||
|
const [tableData,handleSorting] = useSortableTable(data, columns);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
// Calculate total pages based on data length and page size
|
||||||
|
const totalPages = Math.ceil(data.length / pageSize);
|
||||||
|
// Slice data based on current page and page size
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
|
||||||
|
const tablData = data.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Handle page change
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words w-full mb-6 border border-gray-300 rounded" style={{backgroundColor:'#f9f9fa'}}>
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting,handlePageChange,
|
||||||
|
currentPage,
|
||||||
|
totalPages, }} />
|
||||||
|
<ReportTableBody
|
||||||
|
{...{
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
tablData
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ReportTable;
|
99
src/components/ReportTableBody.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const ReportTableBody = ({ tableData, columns }) => {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">₦</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "overdraftBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currency != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.overdraftBalance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportTableBody;
|
48
src/components/SaleTable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortableTable } from "../useSortableTable";
|
||||||
|
import SaleTableBody from "./SaleTableBody";
|
||||||
|
import TableHead from "./TableHead";
|
||||||
|
|
||||||
|
const SaleTable = ({
|
||||||
|
setShowSaleDetailModal,
|
||||||
|
setSaleDetail,
|
||||||
|
caption,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const [tableData, handleSorting] = useSortableTable(data, columns);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full xl:w-12/12 mb-12 xl:mb-0 px-4">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-red-700">
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-black text-sm font-semibold px-7">Sales order</h2>
|
||||||
|
<div className="table-wrp block max-h-96 overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
<TableHead {...{ columns, handleSorting }} />
|
||||||
|
<SaleTableBody
|
||||||
|
{...{
|
||||||
|
setShowSaleDetailModal,
|
||||||
|
setSaleDetail,
|
||||||
|
columns,
|
||||||
|
tableData,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SaleTable;
|
91
src/components/SaleTableBody.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
|
||||||
|
const SaleTableBody = ({
|
||||||
|
setShowSaleDetailModal,
|
||||||
|
setSaleDetail,
|
||||||
|
tableData,
|
||||||
|
columns,
|
||||||
|
}) => {
|
||||||
|
const updateSaleDetail = (sale) => {
|
||||||
|
setShowSaleDetailModal(true);
|
||||||
|
setSaleDetail(sale);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tbody>
|
||||||
|
{tableData.map((data) => {
|
||||||
|
return (
|
||||||
|
<tr key={data.id} onClick={() => updateSaleDetail(data)}>
|
||||||
|
{columns.map(({ accessor }) => {
|
||||||
|
const tData = data[accessor] ? data[accessor] : "——";
|
||||||
|
if (accessor == "nairaBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">
|
||||||
|
₦
|
||||||
|
</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "dollarBalance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "NGN" ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold text-sm">$</span>{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessor == "balance") {
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{data.currencyCode != "USD" ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}{" "}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(data.balance)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td key={accessor}>{tData}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaleTableBody;
|
50
src/components/Sales.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
import React from "react";
|
||||||
|
export default currencyCell = ({ rows }) => {
|
||||||
|
console.log("Rows:", rows); // Debugging statement
|
||||||
|
|
||||||
|
const { balance, currencyCode } = rows;
|
||||||
|
|
||||||
|
console.log("Balance:", balance); // Debugging statement
|
||||||
|
console.log("Currency Code:", currencyCode); // Debugging statement
|
||||||
|
|
||||||
|
// Check if balance is a valid number
|
||||||
|
const parsedBalance = parseFloat(balance);
|
||||||
|
|
||||||
|
console.log("Parsed Balance:", parsedBalance); // Debugging statement
|
||||||
|
|
||||||
|
if (isNaN(parsedBalance)) {
|
||||||
|
// If balance is not a valid number, display a fallback value
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
Invalid Balance
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter balance based on currencyCode
|
||||||
|
const currencyBalance =
|
||||||
|
currencyCode === "NGN"
|
||||||
|
? rows.nairaBalance
|
||||||
|
: currencyCode === "USD"
|
||||||
|
? rows.dollarBalance
|
||||||
|
: rows.balance;
|
||||||
|
|
||||||
|
// Render the currency cell with currency-specific balance
|
||||||
|
return (
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-black whitespace-nowrap dark:text-black">
|
||||||
|
{currencyCode === "NGN" && (
|
||||||
|
<span className="font-semibold text-sm">₦</span>
|
||||||
|
)}
|
||||||
|
{currencyCode === "USD" && (
|
||||||
|
<span className="font-semibold text-sm">$</span>
|
||||||
|
)}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(currencyBalance)}
|
||||||
|
displayType="text"
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
};
|
343
src/components/Sidebar.js
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faFile,
|
||||||
|
faUserCircle,
|
||||||
|
faCog,
|
||||||
|
faSignOut,
|
||||||
|
faBars,
|
||||||
|
faTimes,
|
||||||
|
faRotate,
|
||||||
|
faMoneyBillTrendUp,
|
||||||
|
faCreditCard,
|
||||||
|
faBook,
|
||||||
|
faFileInvoice,
|
||||||
|
faLaptop,
|
||||||
|
faFileExcel
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import DurationDropdown from './DurationDropdown.js';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { logout } from '../redux/slices/auth';
|
||||||
|
import logo from '../assets/logo.png';
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
let activeStyle = {
|
||||||
|
color:'black',
|
||||||
|
fontSize: '17px',
|
||||||
|
borderRadius:'0.5rem',
|
||||||
|
backgroundColor:'#feecf0',
|
||||||
|
width:'100%',
|
||||||
|
border:'5px',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
};
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [collapseShow, setCollapseShow] = React.useState('hidden');
|
||||||
|
let user = useSelector((state) => state.auth.user);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav className="md:left-0 md:block md:fixed md:top-0 md:bottom-0 md:overflow-y-auto md:flex-row md:flex-nowrap md:overflow-hidden shadow-xl flex flex-wrap items-center justify-between relative md:w-64 z-10 py-4 px-6" style={{backgroundColor:'#671c2d'}}>
|
||||||
|
<div className="md:flex-col md:items-stretch md:min-h-full md:flex-nowrap px-0 flex flex-wrap items-center justify-between w-full mx-auto">
|
||||||
|
{/* Toggler */}
|
||||||
|
<button
|
||||||
|
className="cursor-pointer justify-center text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCollapseShow('bg-white m-2 py-3 px-6')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faBars}
|
||||||
|
style={{ fontSize: 15, color: 'red', paddingRight: 5 }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/* Brand Logo */}
|
||||||
|
<div className="top-6 text-sm py-3">
|
||||||
|
<a href="/">
|
||||||
|
<img
|
||||||
|
className="img"
|
||||||
|
alt="manifold logo"
|
||||||
|
src={logo}
|
||||||
|
width="100px"
|
||||||
|
height="40px"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* Duration */}
|
||||||
|
<ul className="md:hidden items-center flex flex-wrap list-none">
|
||||||
|
<li className="inline-block relative">
|
||||||
|
<DurationDropdown />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/* Collapse */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'md:flex md:flex-col md:items-stretch md:opacity-100 md:relative md:mt-4 md:shadow-none shadow absolute top-0 left-0 right-0 z-40 overflow-y-auto overflow-x-hidden h-auto items-center flex-1 rounded ' +
|
||||||
|
collapseShow
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Collapse header */}
|
||||||
|
<div className="md:min-w-full md:hidden block pb-4 mb-4 border-b border-solid border-blueGray-200">
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
<div className="w-6/12">
|
||||||
|
<a className="md:block text-left md:pb-2 text-blueGray-600 mr-0 inline-block whitespace-nowrap text-sm font-bold p-4 px-0">
|
||||||
|
Manifold Computers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="w-6/12 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="cursor-pointer text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent"
|
||||||
|
onClick={() => setCollapseShow('hidden')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTimes}
|
||||||
|
style={{ fontSize: 20, color: 'red', paddingRight: 5 }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<ul className="md:flex-col md:min-w-full flex flex-col list-none">
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white text-lg py-3 font-medium block"
|
||||||
|
to="/"
|
||||||
|
style={({ isActive }) => (isActive ? activeStyle : undefined)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFile}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color:'text-white',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative',
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Dashboard
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/user"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUserCircle}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
User Management
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/exchange-rates"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faRotate}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Rates
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/opening-balance"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faMoneyBillTrendUp}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Opening Balance
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/expense"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFileInvoice}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Accrued Expenses
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/prepaid"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faLaptop}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Prepaid Expenses
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/inventories"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faLaptop}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: 'text-blueGray-700',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Inventories
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{user.role && (
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-sm py-3 font-bold block"
|
||||||
|
to="/overdraft"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
isActive ? activeStyle : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCreditCard}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: ' text-white',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Overdraft
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/setting"
|
||||||
|
style={({ isActive }) => (isActive ? activeStyle : undefined)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCog}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: ' text-white',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Setting
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="items-center">
|
||||||
|
<NavLink
|
||||||
|
className=" text-white hover:text-black text-lg py-3 font-medium block"
|
||||||
|
to="/login"
|
||||||
|
onClick={() => dispatch(logout())}
|
||||||
|
style={({ isActive }) => (isActive ? activeStyle : undefined)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSignOut}
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: ' text-white',
|
||||||
|
paddingRight: 20,
|
||||||
|
left:12,
|
||||||
|
position:'relative'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Logout
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Sidebar;
|
41
src/components/TableHead.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
const TableHead = ({ columns, handleSorting }) => {
|
||||||
|
const [sortField, setSortField] = useState("");
|
||||||
|
const [order, setOrder] = useState("asc");
|
||||||
|
|
||||||
|
const handleSortingChange = (accessor) => {
|
||||||
|
const sortOrder =
|
||||||
|
accessor === sortField && order === "asc" ? "desc" : "asc";
|
||||||
|
setSortField(accessor);
|
||||||
|
setOrder(sortOrder);
|
||||||
|
handleSorting(accessor, sortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className="bg-gray-100 dark:bg-gray-400 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
{columns.map(({ label, accessor, sortable }) => {
|
||||||
|
const cl = sortable
|
||||||
|
? sortField === accessor && order === "asc"
|
||||||
|
? "up"
|
||||||
|
: sortField === accessor && order === "desc"
|
||||||
|
? "down"
|
||||||
|
: "default"
|
||||||
|
: "";
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
key={accessor}
|
||||||
|
onClick={sortable ? () => handleSortingChange(accessor) : null}
|
||||||
|
className={cl}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableHead;
|
13
src/hoc/withAuth.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export const withAuth = () => (WrappedComponent) => {
|
||||||
|
return (props) => {
|
||||||
|
let isAuthenticated = useSelector((state) => state.auth.isAuthenticated === false)
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [isAuthenticated])
|
||||||
|
return localStorage.getItem('accessToken') ? <WrappedComponent {...props} /> : <Navigate to="/login" replace={true} />
|
||||||
|
}
|
||||||
|
}
|
98
src/index.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.print-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
color: #383f4d;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
=================
|
||||||
|
Table
|
||||||
|
=====================
|
||||||
|
*/
|
||||||
|
|
||||||
|
.table_container {
|
||||||
|
max-width: 750px;
|
||||||
|
/* max-height: 500px; */
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 90%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #fff;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #1a1a1a;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th.up {
|
||||||
|
background-image: url("./assets/up_arrow.png");
|
||||||
|
}
|
||||||
|
.table th.down {
|
||||||
|
background-image: url("./assets/down_arrow.png");
|
||||||
|
}
|
||||||
|
.table th.default {
|
||||||
|
background-image: url("./assets/default.png");
|
||||||
|
}
|
||||||
|
th.up,
|
||||||
|
th.default,
|
||||||
|
th.down {
|
||||||
|
cursor: pointer;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:first-child td {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(n) td {
|
||||||
|
background: #eff0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(2n) td {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
@apply invisible absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-tooltip:hover .tooltip {
|
||||||
|
@apply visible z-50;
|
||||||
|
}
|
33
src/index.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { StrictMode } from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
|
||||||
|
import { store } from './redux/store'
|
||||||
|
import { Axios } from './api/instances'
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
import { config } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
// <StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
// </StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
reportWebVitals();
|
1669
src/pages/Dashboard.js
Normal file
500
src/pages/Expense.js
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
//import logo from '../assets/logo.png'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import {
|
||||||
|
faBell,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import {
|
||||||
|
faCaretDown
|
||||||
|
}from '@fortawesome/free-solid-svg-icons'
|
||||||
|
//import ReportTable from '../components/ReportTable';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { DataGrid,GridToolbar,GridToolbarContainer,
|
||||||
|
GridToolbarColumnsButton,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarDensitySelector,
|
||||||
|
GridExcelExportOptions,
|
||||||
|
GridFooterPlaceholder,
|
||||||
|
GridPrintExportOptions,
|
||||||
|
GridPrintExportMenuItem,
|
||||||
|
|
||||||
|
GridPanelFooter, GridPanelHeader} from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from '@mui/x-data-grid-generator';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Modal from '../components/Modal'
|
||||||
|
import ExpensesTable from "../components/ExpensesTable";
|
||||||
|
import { getNonbillableExpense } from '../redux/slices/nonbill';
|
||||||
|
import { InfinitySpin } from "react-loader-spinner"
|
||||||
|
import BillChart from '../components/BillChart';
|
||||||
|
import { COLORS } from '@mui/x-data-grid-generator/services/static-data';
|
||||||
|
import './expense.css'
|
||||||
|
import logo from "../assets/logo.png"
|
||||||
|
import { logout } from '../redux/slices/auth';
|
||||||
|
import PrintableTable from './PrintableTable';
|
||||||
|
function CustomToolbar() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridToolbarContainer>
|
||||||
|
|
||||||
|
<GridToolbarColumnsButton />
|
||||||
|
<GridToolbarFilterButton />
|
||||||
|
<GridToolbarDensitySelector
|
||||||
|
slotProps={{ tooltip: { title: 'Change density' } }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<GridToolbarExport
|
||||||
|
|
||||||
|
slotProps={{
|
||||||
|
tooltip: { title: 'Export data' },
|
||||||
|
button: { variant: 'outlined' },
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
printOptions={{ disableToolbarButton: false , includeCheckboxes: false, pageStyle: '.MuiDataGrid-root .MuiDataGrid-main { color: rgba(0, 0, 0, 0.87); background-color: gray} ', hideToolbar: true, fileName: 'yourFileName', bodyClassName: '.header', header: () => (
|
||||||
|
<div className="header">
|
||||||
|
{/* Your header content goes here */}
|
||||||
|
<h1>This is the header</h1>
|
||||||
|
</div>
|
||||||
|
),}}
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
</GridToolbarExport>
|
||||||
|
|
||||||
|
|
||||||
|
</GridToolbarContainer>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Expense = () => {
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||||
|
const [selectedRow,setSelectedRow] = React.useState(null)
|
||||||
|
const VISIBLE_FIELDS = ['Account ID', 'Tax Id', 'Payment Date','Description','Is Taxable','Amount','Currency ID','Reference Number','Customer Id','Non Billable'];
|
||||||
|
const { data } = useDemoData({
|
||||||
|
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: VISIBLE_FIELDS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const[isLoading,setIsLoading] = useState(true)
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExpensesLoading = useSelector(
|
||||||
|
(state) => state.nonbill.isExpensesLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
const expenses = useSelector((state) => state.nonbill.expenses);
|
||||||
|
console.log('trtrkkkkkkkkkkkkkkkkkkkkkkkkkkk')
|
||||||
|
|
||||||
|
const expensesWithId = expenses.map(expense => ({
|
||||||
|
...expense,
|
||||||
|
id: expense.expense_id
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ headerName: "Account ID", field: "account_id", sortable: true,sortbyOrder: "asc" },
|
||||||
|
{ headerName: "Tax Id", field: "tax_id", sortable: true },
|
||||||
|
{ headerName: "Payment Date", field: "payment_date", sortable: false },
|
||||||
|
{ headerName: "Description", field: "description", sortable: false },
|
||||||
|
{ headerName: "Is taxable ", field: "is_taxable", sortable: false },
|
||||||
|
{ headerName: "Amount ", field: "amount", sortable: false },
|
||||||
|
{ headerName: "Currency ID", field: "currency_id", sortable: false },
|
||||||
|
{ headerName: "Reference Number", field: "total_without_tax", sortable: false },
|
||||||
|
{ headerName: "Customer Id", field: "customer_id", sortable: false },
|
||||||
|
{ headerName: "Non Billable", field: "non_billable", sortable: false },
|
||||||
|
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const [isInclusiveTax, setIsInclusiveTax] = useState(false);
|
||||||
|
const [isBillable, setIsBillable] = useState(false);
|
||||||
|
console.log('newnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn')
|
||||||
|
const toggleButton = () =>{
|
||||||
|
setIsInclusiveTax(!isInclusiveTax)
|
||||||
|
}
|
||||||
|
const Toggle = () =>{
|
||||||
|
setIsBillable(!isBillable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getNonbillableExpense())
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result, "mmmmmmmmmmmm");
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(false)
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleRowClick = (params) => {
|
||||||
|
setSelectedRow(params.row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setSelectedRow(null);
|
||||||
|
};
|
||||||
|
const getCurrentDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
let month = today.getMonth() + 1;
|
||||||
|
let day = today.getDate();
|
||||||
|
|
||||||
|
// Add leading zero if month or day is less than 10
|
||||||
|
month = month < 10 ? '0' + month : month;
|
||||||
|
day = day < 10 ? '0' + day : day;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
console.log(data,'>>>>>>>>>>>>>>>>>>>...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:4000/v1/zoho/postAccrued', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to submit form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the form
|
||||||
|
event.target.reset();
|
||||||
|
|
||||||
|
// Reload the page
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
// Handle errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
|
||||||
|
<div className='relative md:ml-64 bg-blueGray-100'>
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Accrued Expenses
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<FontAwesomeIcon icon={faBell} className="border px-3 py-3 rounded-full" style={{fontSize:13}} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<div className="flex justify-center h-screen items-center ">
|
||||||
|
<InfinitySpin width="200" color="red"/>
|
||||||
|
</div>:
|
||||||
|
|
||||||
|
<>
|
||||||
|
<div className=' relative -top-8 float-end'>
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Expenses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className=" justify-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="max-w-md">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col bg-white outline-none focus:outline-none">
|
||||||
|
{/*header*/}
|
||||||
|
<div className="flex items-start justify-between mt-5 rounded-t">
|
||||||
|
<div className="flex-initial relative left-5 ">
|
||||||
|
<h3 className="text-black font-bold text-lg mb-1 " >
|
||||||
|
Add Expenses
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
type="button"
|
||||||
|
className=" rounded-full relative -top-6 p-1 inline-flex items-center justify-center text-black hover:text-black bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-red-500" style={{backgroundColor:'#e6e8ec'}}>
|
||||||
|
<span className="sr-only">Close menu</span>
|
||||||
|
|
||||||
|
<svg className="h-5 w-5 rounded-full" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="py-1 px-2">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
{/*body*/}
|
||||||
|
<form className='space-y-3 py-9' onSubmit={handleSubmit}>
|
||||||
|
<div className="relative px-2 flex-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="hidden sm:block" aria-hidden="true">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className=' grid grid-cols-2 space-x-3'>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='account_id'
|
||||||
|
placeholder="Account ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-2 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='date'
|
||||||
|
name='payment_date'
|
||||||
|
min={getCurrentDate()}
|
||||||
|
placeholder="Expected Payment Date"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
name='amount'
|
||||||
|
placeholder="Amount"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='customer_id'
|
||||||
|
placeholder="Customer ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='currency_id'
|
||||||
|
placeholder="Currency ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
type='string'
|
||||||
|
name='currency'
|
||||||
|
placeholder="Currency"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="NGN">Naira (NGN)</option>
|
||||||
|
<option value="USD">USD (United States Dollar)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='tax_id'
|
||||||
|
placeholder="Tax ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
|
||||||
|
type='button'
|
||||||
|
name='is_inclusive_tax'
|
||||||
|
onClick={toggleButton}
|
||||||
|
value={isInclusiveTax ? "true" : "false"}
|
||||||
|
placeholder="Enter true or false"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='reference_number'
|
||||||
|
placeholder="Reference Number"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='description'
|
||||||
|
placeholder="Description"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
|
||||||
|
type='button'
|
||||||
|
name='is_Billable'
|
||||||
|
onClick={Toggle}
|
||||||
|
value={isBillable ? "true" : "false"}
|
||||||
|
placeholder="Enter true or false"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-3 px-40 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
type="submit"
|
||||||
|
style={{backgroundColor:'#df3c62'}}
|
||||||
|
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className=' m-6' style={{ height: 400, width: '93%' }}>
|
||||||
|
|
||||||
|
{!isExpensesLoading && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbar,
|
||||||
|
}}
|
||||||
|
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
rows={expenses} // Use expensesWithId here
|
||||||
|
getRowId={(row) => row.id}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{
|
||||||
|
...data.initialState,
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedRow && (
|
||||||
|
<Modal isOpen={selectedRow !== null} onClose={handleCloseModal}>
|
||||||
|
<ul className='list-none'>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Account Id:</strong> {selectedRow.account_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> tax_id:</strong> {selectedRow.tax_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Payment date:</strong> {selectedRow.payment_date}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Description: </strong>{selectedRow.description}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>amount:</strong> {selectedRow.amount}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> currency id: </strong>{selectedRow.currency_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>total_without_tax:</strong> {selectedRow.total_without_tax}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> customer_id:</strong> {selectedRow.customer_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>non_billable:</strong> {selectedRow.non_billable}</li>
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<footer className="block py-4">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<hr className="mb-4 border-b-1 border-blueGray-200" />
|
||||||
|
<div className="flex flex-wrap items-center md:justify-between justify-center">
|
||||||
|
<div className="w-full md:w-4/12 px-4">
|
||||||
|
<div className="text-sm text-blueGray-500 font-semibold py-1">
|
||||||
|
Copyright © {new Date().getFullYear()}{" "}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-blueGray-500 hover:text-blueGray-700 text-sm font-semibold py-1"
|
||||||
|
>
|
||||||
|
Manifold Computers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Expense
|
151
src/pages/Expenses.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faBell,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import {
|
||||||
|
faCaretDown
|
||||||
|
}from '@fortawesome/free-solid-svg-icons'
|
||||||
|
//import ReportTable from '../components/ReportTable';
|
||||||
|
import { DataGrid,GridToolbar } from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from '@mui/x-data-grid-generator';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import ExpensesTable from "../components/ExpensesTable";
|
||||||
|
import { getNonbillableExpense } from '../redux/slices/nonbill';
|
||||||
|
import { InfinitySpin } from "react-loader-spinner"
|
||||||
|
|
||||||
|
const Expenses = () => {
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||||
|
const VISIBLE_FIELDS = ['Expenses ID', 'date', 'Account Name','Description','Account Paid','Currency Code','Currency ID','Total Tax','Status'];
|
||||||
|
const { data } = useDemoData({
|
||||||
|
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: VISIBLE_FIELDS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const[isLoading,setIsLoading] = useState(true)
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExpensesLoading = useSelector(
|
||||||
|
(state) => state.nonbill.isExpensesLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("wwwwwwwww.................................w")
|
||||||
|
const expenses = useSelector((state) => state.nonbill.expenses);
|
||||||
|
console.log(typeof expenses,'trtr')
|
||||||
|
|
||||||
|
const expensesWithId = expenses.map(expense => ({
|
||||||
|
...expense,
|
||||||
|
id: expense.expense_id
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ headerName: "Account ID", field: "account_id", sortable: true,sortbyOrder: "asc" },
|
||||||
|
{ headerName: "Tax Id", field: "tax_id", sortable: true },
|
||||||
|
{ headerName: "Payment Date", field: "payment_date", sortable: false },
|
||||||
|
{ headerName: "Description", field: "description", sortable: false },
|
||||||
|
{ headerName: "Is taxable ", field: "is_taxable", sortable: false },
|
||||||
|
{ headerName: "Amount ", field: "amount", sortable: false },
|
||||||
|
{ headerName: "Currency ID", field: "currency_id", sortable: false },
|
||||||
|
{ headerName: "Reference Number", field: "total_without_tax", sortable: false },
|
||||||
|
{ headerName: "Customer Id", field: "customer_id", sortable: false },
|
||||||
|
{ headerName: "Non Billable", field: "non_billable", sortable: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getNonbillableExpense())
|
||||||
|
.then((result) => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(false)
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<div className="flex justify-center h-screen items-center">
|
||||||
|
<InfinitySpin width="200" color="red"/>
|
||||||
|
</div>:
|
||||||
|
<div className='relative md:ml-64 bg-blueGray-100'>
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Cash flow forecast report
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<FontAwesomeIcon icon={faBell} className="border px-3 py-3 rounded-full" style={{fontSize:13}} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-4 gap-3 m-5'>
|
||||||
|
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Show report
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=' m-6 ' style={{ height: 400, width: '93%' }}>
|
||||||
|
{!isExpensesLoading && (
|
||||||
|
<DataGrid
|
||||||
|
slots={{
|
||||||
|
toolbar: GridToolbar,
|
||||||
|
// Use custom FilterPanel only for deep modification
|
||||||
|
// FilterPanel: MyCustomFilterPanel,
|
||||||
|
}}
|
||||||
|
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
rows={expenses} // Use expensesWithId here
|
||||||
|
getRowId={(row) => row.expense_id}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{
|
||||||
|
...data.initialState,
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Expenses
|
68
src/pages/ForgotPassword.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import logo from "../assets/logo.png";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { forgotPassword } from '../redux/slices/auth'
|
||||||
|
import pixel from "../assets/second-image.jpeg"
|
||||||
|
const ForgotPassword = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isForgotPasswordLoading = useSelector(state => state.auth.isForgotPasswordLoading)
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
dispatch(forgotPassword({ email: data.email })).then((res) => {
|
||||||
|
if (!res.payload) return
|
||||||
|
|
||||||
|
// TODO:: should redirect to login page
|
||||||
|
navigate('/login');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen">
|
||||||
|
<div className=" grid grid-cols-2">
|
||||||
|
<div className=' h-screen m-auto ' style={{backgroundColor:"#f8f9f9"}}>
|
||||||
|
<div className='relative top-12 m-16'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<h1 className='font-bold text-3xl '>Forgot Password</h1>
|
||||||
|
<p className=' text-lg font-normal' style={{color:'#7c7e8c'}}>Enter the email you used to sign up and we'll send you a temporary password.</p>
|
||||||
|
</div>
|
||||||
|
<div className=' relative top-10 space-y-4'>
|
||||||
|
<div className=''>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="Enter email address"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
|
||||||
|
className="w-full block bg-red-400 hover:bg-red-300 focus:bg-red-300 text-white font-semibold rounded-lg
|
||||||
|
px-4 py-3 mt-6"
|
||||||
|
style={{backgroundColor:'#f5426c'}}
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</button>
|
||||||
|
<div className=' text-center justify-center bg-white px-2 py-2'>
|
||||||
|
<p className='' style={{color:'#7c7e8c'}}>Didn't get a link ? <text className=' font-semibold' style={{color:'#f5426c'}}>Resend in 00:30</text></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className=" h-screen">
|
||||||
|
<img src={pixel} className=" bg-cover h-screen w-full " />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPassword;
|
219
src/pages/Inventories.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import { DataGrid,GridToolbar,GridToolbarContainer,
|
||||||
|
GridToolbarColumnsButton,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarDensitySelector,
|
||||||
|
GridExcelExportOptions,
|
||||||
|
GridFooterPlaceholder,
|
||||||
|
GridPrintExportOptions,
|
||||||
|
GridPrintExportMenuItem,
|
||||||
|
|
||||||
|
GridPanelFooter, GridPanelHeader} from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from "@mui/x-data-grid-generator";
|
||||||
|
import { getInventory } from "../redux/slices/inventory";
|
||||||
|
import { InfinitySpin } from "react-loader-spinner";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faBell } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import Modal from "../components/Modal";
|
||||||
|
import { COLORS } from "@mui/x-data-grid-generator/services/static-data";
|
||||||
|
import CurrencyFormat from "react-currency-format";
|
||||||
|
function CustomToolbar () {
|
||||||
|
|
||||||
|
return(
|
||||||
|
<GridToolbarContainer>
|
||||||
|
<GridToolbarFilterButton></GridToolbarFilterButton>
|
||||||
|
<GridToolbarColumnsButton></GridToolbarColumnsButton>
|
||||||
|
<GridToolbarDensitySelector
|
||||||
|
sx={{ color: 'green' }}
|
||||||
|
slotProps={{
|
||||||
|
tooltip:{title:'change density'},
|
||||||
|
|
||||||
|
}} />
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<GridToolbarExport
|
||||||
|
slotProps={{
|
||||||
|
tooltip: { title: 'Export data' },
|
||||||
|
button: { variant: 'outlined'},
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
printOptions={{ disableToolbarButton: false , includeCheckboxes: false, pageStyle: '.MuiDataGrid-root .MuiDataGrid-main { color: rgba(0, 0, 0, 0.87); background-color: gray} ', hideToolbar: true, fileName: 'yourFileName', bodyClassName: '.header', header: () => (
|
||||||
|
<div className="header">
|
||||||
|
{/* Your header content goes here */}
|
||||||
|
<nav>This is the header</nav>
|
||||||
|
</div>
|
||||||
|
),}}
|
||||||
|
/>
|
||||||
|
</GridToolbarContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Inventories = () => {
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = useState([]);
|
||||||
|
const [selectedRow, setSelectedRow] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isItemsLoading = useSelector((state) => state.inventory.isItemsLoading);
|
||||||
|
const items = useSelector((state) => state.inventory.items.Items);
|
||||||
|
const TotalinventoryCost = useSelector((state) => state.inventory.items.totalsumofItems);
|
||||||
|
const getCurrentDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
let month = today.getMonth() + 1;
|
||||||
|
let day = today.getDate();
|
||||||
|
|
||||||
|
// Add leading zero if month or day is less than 10
|
||||||
|
month = month < 10 ? '0' + month : month;
|
||||||
|
day = day < 10 ? '0' + day : day;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getInventory())
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result, "uuuuuuuuuuuuuuuuuuuu");
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleRowClick = (params) => {
|
||||||
|
setSelectedRow(params.row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setSelectedRow(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call useDemoData unconditionally
|
||||||
|
const { data } = useDemoData({
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: ["Item ID", "Item Name", "Available Stock"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{ color: "#671c2d" }}
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Inventories
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{ color: "#671c2d" }}
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span>Total Inventory Price : </span>
|
||||||
|
<CurrencyFormat
|
||||||
|
value={parseFloat(TotalinventoryCost?TotalinventoryCost:0)}
|
||||||
|
displayType={"text"}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
prefix={"₦"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<div className=' grid grid-cols-2 space-x-3'>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type='date'
|
||||||
|
name='payment_date'
|
||||||
|
min={getCurrentDate()}
|
||||||
|
placeholder="Expected Payment Date"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div></div>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faBell}
|
||||||
|
className="border px-3 py-3 rounded-full"
|
||||||
|
style={{ fontSize: 13 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center h-screen items-center">
|
||||||
|
<InfinitySpin width="200" color="red" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="m-6" style={{ height: 400, width: "95%" }}>
|
||||||
|
{!isItemsLoading && (
|
||||||
|
<>
|
||||||
|
<DataGrid
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbar,
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
url:('../assets/logo.png')
|
||||||
|
}}
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
rows={items}
|
||||||
|
getRowId={(row) => row.item_id}
|
||||||
|
columns={[
|
||||||
|
{ field: "item_id", headerName: "Item ID", sortable: true, width: 150 },
|
||||||
|
{ field: "name", headerName: "Item Name", sortable: true, width: 200 },
|
||||||
|
{ field: "available_stock", headerName: "Available Stock", sortable: true, width: 200 },
|
||||||
|
{ field: "account_name", headerName: "Account Name", sortable: true, width: 200 },
|
||||||
|
{ field: "description", headerName: "Description", sortable: true, width: 200 },
|
||||||
|
{ field: "manufacturer", headerName: "Manufacturer", sortable: true, width: 200 },
|
||||||
|
{ field: "rate", headerName: "Item Price", sortable: true, width: 200 },
|
||||||
|
{ field: "totalRate", headerName: "Total Item Price", sortable: true, width: 200 },
|
||||||
|
]}
|
||||||
|
initialState={{
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
{selectedRow && (
|
||||||
|
<Modal isOpen={selectedRow !== null} onClose={handleCloseModal}>
|
||||||
|
<ul className=" list-none">
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Item ID:</strong>{selectedRow.item_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Item Name:</strong> {selectedRow.name}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Available Stock:</strong> {selectedRow.available_stock}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Account Name:</strong> {selectedRow.account_name}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Description:</strong> {selectedRow.description}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Manufacturer:</strong> {selectedRow.manufacturer}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Item Price:</strong> {selectedRow.rate}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Total Item Price:</strong> {selectedRow.totalRate}</li>
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Inventories;
|
131
src/pages/Inventory.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import DataTable from "react-data-table-component"
|
||||||
|
//import { withAuth } from "../hoc/withAuth";
|
||||||
|
//import InventoryTable from "../components/InventoryTable";
|
||||||
|
//import moment from 'moment';
|
||||||
|
//import InventoriesTable from "../components/InventoriesTable";
|
||||||
|
import {getInventory} from "../redux/slices/inventory";
|
||||||
|
|
||||||
|
const Inventory = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isItemsLoading = useSelector((state)=>state.inventory.isItemsLoading)
|
||||||
|
|
||||||
|
console.log(isItemsLoading,"eeeeeeeeeeeeeeeee")
|
||||||
|
|
||||||
|
const items = useSelector((state)=> state.inventory.items)
|
||||||
|
|
||||||
|
console.log(typeof items,">>>>>>>>>>>>>>>>>>>>>>>>>>>>>llllllllll")
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ Name: "Item ID", accessor: "item_id", sortable: true,sortbyOrder: "asc" },
|
||||||
|
{ Name: "Item Name", accessor: "name", sortable: true },
|
||||||
|
{ Name: "Available Stock", accessor: "available_stock", sortable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
/*const columns = [
|
||||||
|
{ Name: "Item ID", selector:row=>row.item_id },
|
||||||
|
{ Name: "Item Name", selector: row=>row.name },
|
||||||
|
{ Name: "Available Stock", selector: row => row.available_stock},
|
||||||
|
];*/
|
||||||
|
|
||||||
|
console.log(columns,"eeeeer")
|
||||||
|
|
||||||
|
const item = [
|
||||||
|
{
|
||||||
|
"name": items.name,
|
||||||
|
"item_id":"1000000",
|
||||||
|
"available_stock":3,
|
||||||
|
"rate": 120,
|
||||||
|
"description": "500GB",
|
||||||
|
"tax_id": 982000000037049,
|
||||||
|
"purchase_tax_rule_id": 127919000000106780,
|
||||||
|
"sales_tax_rule_id": 127919000000106780,
|
||||||
|
"tax_percentage": "70%",
|
||||||
|
"hsn_or_sac": "string",
|
||||||
|
"sat_item_key_code": "string",
|
||||||
|
"unitkey_code": "string",
|
||||||
|
"sku": "s12345",
|
||||||
|
"product_type": "goods",
|
||||||
|
"is_taxable": true,
|
||||||
|
"tax_exemption_id": "string",
|
||||||
|
"account_id": " ",
|
||||||
|
"avatax_tax_code": 982000000037049,
|
||||||
|
"avatax_use_code": 982000000037049,
|
||||||
|
"item_type": " ",
|
||||||
|
"purchase_description": " ",
|
||||||
|
"purchase_rate": " ",
|
||||||
|
"purchase_account_id": " ",
|
||||||
|
"inventory_account_id": " ",
|
||||||
|
"vendor_id": " ",
|
||||||
|
"reorder_level": " ",
|
||||||
|
"initial_stock": " ",
|
||||||
|
"initial_stock_rate": " ",
|
||||||
|
"item_tax_preferences": [
|
||||||
|
{
|
||||||
|
"tax_id": 982000000037049,
|
||||||
|
"tax_specification": "intra"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"customfield_id": "46000000012845",
|
||||||
|
"value": "Normal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getInventory())
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result, "uuuuuuuuuuuuuuuuuuuu");
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
/* const InventoryHandler = (e) => {
|
||||||
|
dispatch(getInventory());
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
<div className="relative md:ml-64 ">
|
||||||
|
{/* Navbar */}
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="">
|
||||||
|
<div className="">
|
||||||
|
<div className="">
|
||||||
|
<div className=" ">
|
||||||
|
{!isItemsLoading && (
|
||||||
|
<DataTable
|
||||||
|
data={item}
|
||||||
|
columns={columns}
|
||||||
|
></DataTable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Inventory;
|
147
src/pages/Item.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faBell,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import {
|
||||||
|
faCaretDown
|
||||||
|
}from '@fortawesome/free-solid-svg-icons'
|
||||||
|
//import ReportTable from '../components/ReportTable';
|
||||||
|
import { DataGrid,GridToolbar } from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from '@mui/x-data-grid-generator';
|
||||||
|
|
||||||
|
const Item = () => {
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||||
|
const VISIBLE_FIELDS = ['Item ID', 'Item Name', 'Available Stock'];
|
||||||
|
const { data } = useDemoData({
|
||||||
|
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: VISIBLE_FIELDS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: "item_id", sortable: true,sortbyOrder: "asc" },
|
||||||
|
{field: "name", sortable: true },
|
||||||
|
{field: "available_stock", sortable: true },
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const item = [
|
||||||
|
{
|
||||||
|
"name":"rytuy",
|
||||||
|
"item_id":"1000000",
|
||||||
|
"available_stock":3,
|
||||||
|
"rate": 120,
|
||||||
|
"description": "500GB",
|
||||||
|
"tax_id": 982000000037049,
|
||||||
|
"purchase_tax_rule_id": 127919000000106780,
|
||||||
|
"sales_tax_rule_id": 127919000000106780,
|
||||||
|
"tax_percentage": "70%",
|
||||||
|
"hsn_or_sac": "string",
|
||||||
|
"sat_item_key_code": "string",
|
||||||
|
"unitkey_code": "string",
|
||||||
|
"sku": "s12345",
|
||||||
|
"product_type": "goods",
|
||||||
|
"is_taxable": true,
|
||||||
|
"tax_exemption_id": "string",
|
||||||
|
"account_id": " ",
|
||||||
|
"avatax_tax_code": 982000000037049,
|
||||||
|
"avatax_use_code": 982000000037049,
|
||||||
|
"item_type": " ",
|
||||||
|
"purchase_description": " ",
|
||||||
|
"purchase_rate": " ",
|
||||||
|
"purchase_account_id": " ",
|
||||||
|
"inventory_account_id": " ",
|
||||||
|
"vendor_id": " ",
|
||||||
|
"reorder_level": " ",
|
||||||
|
"initial_stock": " ",
|
||||||
|
"initial_stock_rate": " ",
|
||||||
|
"item_tax_preferences": [
|
||||||
|
{
|
||||||
|
"tax_id": 982000000037049,
|
||||||
|
"tax_specification": "intra"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom_fields": [
|
||||||
|
{
|
||||||
|
"customfield_id": "46000000012845",
|
||||||
|
"value": "Normal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className='relative md:ml-64 bg-blueGray-100'>
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Cash flow forecast report
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<FontAwesomeIcon icon={faBell} className="border px-3 py-3 rounded-full" style={{fontSize:13}} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-4 gap-3 m-5'>
|
||||||
|
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Show report
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=' m-6 ' style={{ height: 400, width: '93%' }}>
|
||||||
|
<DataGrid
|
||||||
|
slots={{
|
||||||
|
toolbar: GridToolbar,
|
||||||
|
// Use custom FilterPanel only for deep modification
|
||||||
|
// FilterPanel: MyCustomFilterPanel,
|
||||||
|
}}
|
||||||
|
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
data = {item}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{
|
||||||
|
...data.initialState,
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Item
|
113
src/pages/Login.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { InfinitySpin } from 'react-loader-spinner';
|
||||||
|
import logo from '../assets/logo.png';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faEye, faEyeSlash, faHands } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { login } from '../redux/slices/auth';
|
||||||
|
import { NavLink, Navigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import pixel from "../assets/first-image.jpeg"
|
||||||
|
|
||||||
|
const Login = (props) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const authLoading = useSelector((state) => state.auth.isAuthLoading);
|
||||||
|
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
|
||||||
|
const isZohoAuthenticated = useSelector(
|
||||||
|
(state) => state.auth.isZohoAuthenticated
|
||||||
|
);
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
let [searchParams, setSearchParams] = useSearchParams(props);
|
||||||
|
|
||||||
|
const changeEmailHandler = (e) => setEmail(e.target.value);
|
||||||
|
const changePasswordHandler = (e) => setPassword(e.target.value);
|
||||||
|
|
||||||
|
const loginHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(login({ email, password }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main">
|
||||||
|
<div className=' grid grid-cols-2'>
|
||||||
|
<div className=''>
|
||||||
|
<img src={pixel} className=" bg-cover h-screen w-full " />
|
||||||
|
</div>
|
||||||
|
<div className=' h-screen ' style={{backgroundColor:"#f8f9f9"}}>
|
||||||
|
<div className='relative top-12 m-16'>
|
||||||
|
<div className='flex space-x-2'>
|
||||||
|
<FontAwesomeIcon icon={faHands} className='' style={{fontSize:35,color:'#c49770'}}/>
|
||||||
|
<h1 className='font-bold text-3xl '> Welcome back</h1>
|
||||||
|
</div>
|
||||||
|
<p className=' text-lg' style={{color:'#7c7e8c'}}>Please input your login details.</p>
|
||||||
|
{isAuthenticated &&
|
||||||
|
!isZohoAuthenticated &&
|
||||||
|
!authLoading &&
|
||||||
|
window.location.replace(
|
||||||
|
`https://accounts.zoho.com/oauth/v2/auth?scope=ZohoBooks.settings.READ,ZohoBooks.invoices.READ,ZohoBooks.salesorders.READ,ZohoBooks.purchaseorders.READ,ZohoBooks.bills.READ,ZohoBooks.vendorpayments.READ,ZohoBooks.customerpayments.READ,ZohoBooks.banking.READ&client_id=${process.env.REACT_APP_ZOHO_CLIENT_ID}&state=testing&response_type=code&redirect_uri=${process.env.REACT_APP_BASE_URL}&access_type=offline&prompt=Consent`
|
||||||
|
)}
|
||||||
|
{isAuthenticated && isZohoAuthenticated && (
|
||||||
|
<Navigate to="/" replace={true} />
|
||||||
|
)}
|
||||||
|
<form className=' relative top-10 space-y-4' onSubmit={loginHandler}>
|
||||||
|
<div className='' >
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="Enter Email Address"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
onChange={changeEmailHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="Password"
|
||||||
|
placeholder="Enter Password"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
onChange={changePasswordHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-2'>
|
||||||
|
<div className='flex space-x-2 mt-2'>
|
||||||
|
<input type='checkbox' className='' size={20} style={{fontSize:12}}/>
|
||||||
|
<p className=' text-sm font-semibold '>Keep me logged in</p>
|
||||||
|
</div>
|
||||||
|
<div className=" text-right mt-2">
|
||||||
|
<NavLink
|
||||||
|
className="text-sm font-semibold text-red-700 hover:text-red-500 focus:text-red-500"
|
||||||
|
to="/forgot-password"
|
||||||
|
style={{color:'#f5426c'}}
|
||||||
|
>
|
||||||
|
Forgot Password?
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={authLoading}
|
||||||
|
className="w-full block bg-red-400 hover:bg-red-300 focus:bg-red-300 text-white font-semibold rounded-lg
|
||||||
|
px-4 py-3 mt-6"
|
||||||
|
style={{backgroundColor:'#f5426c'}}
|
||||||
|
>
|
||||||
|
{authLoading ? 'loading...' : 'Login'}{' '}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
101
src/pages/OpeningBalance.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import { getBankAccounts } from "../redux/slices/forecast";
|
||||||
|
import { withAuth } from "../hoc/withAuth";
|
||||||
|
import OpeningBalanceTable from "../components/OpeningBalanceTable";
|
||||||
|
import { downloadOpeningBalance } from '../redux/slices/zoho';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const OpeningBalance = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isBankAccountsLoading = useSelector(
|
||||||
|
(state) => state.forecast.isBankAccountsLoading
|
||||||
|
);
|
||||||
|
const bankAccounts = useSelector((state) => state.forecast.bankAccounts);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ label: "ID", accessor: "id", sortable: true, sortbyOrder: "asc" },
|
||||||
|
{ label: "Account Name", accessor: "accountName", sortable: true },
|
||||||
|
{ label: "Account Type", accessor: "accountType", sortable: false },
|
||||||
|
{ label: "Currency", accessor: "currency", sortable: true },
|
||||||
|
{ label: "Naira Balance", accessor: "nairaBalance", sortable: false },
|
||||||
|
{ label: "Dollar Balance", accessor: "dollarBalance", sortable: false },
|
||||||
|
{
|
||||||
|
label: "Balance",
|
||||||
|
accessor: "balance",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Overdraft",
|
||||||
|
accessor: "overdraftBalance",
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getBankAccounts());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const downloadOpeningBalanceHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let filename = 'rate' + moment().toISOString();
|
||||||
|
dispatch(downloadOpeningBalance());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className="text-black text-sm uppercase hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Opening Balance
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul className="flex-col md:flex-row list-none items-center hidden md:flex">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
<button
|
||||||
|
onClick={downloadOpeningBalanceHandler}
|
||||||
|
className="bg-red-500 text-white active:bg-red-600 text-xs font-bold px-3 py-1 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="w-full px-4 pt-12">
|
||||||
|
<div className="overflow-x-auto shadow-md sm:rounded-lg">
|
||||||
|
<div className="inline-block min-w-full align-middle">
|
||||||
|
<div className="overflow-hidden ">
|
||||||
|
{!isBankAccountsLoading && (
|
||||||
|
<OpeningBalanceTable
|
||||||
|
caption="Opening bank account with overdrafts"
|
||||||
|
data={bankAccounts}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withAuth(true)(OpeningBalance);
|
121
src/pages/Overdraft.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Sidebar from '../components/Sidebar';
|
||||||
|
import Form from '../components/Form';
|
||||||
|
import Edit from '../components/Edit';
|
||||||
|
import Delete from '../components/Delete';
|
||||||
|
import { getOverdrafts } from '../redux/slices/zoho';
|
||||||
|
import CurrencyFormat from 'react-currency-format';
|
||||||
|
|
||||||
|
const Overdraft = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isOverdraftLoading = useSelector(
|
||||||
|
(state) => state.zoho.isOverdraftLoading
|
||||||
|
);
|
||||||
|
const overdrafts = useSelector((state) => state.zoho.overdrafts);
|
||||||
|
console.log(overdrafts,"rrrrrrrrrrrrrrr")
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getOverdrafts());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className="text-black text-sm uppercase hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Overdraft
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<Form />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="w-full px-9 pt-12 ">
|
||||||
|
{/* Projects table */}
|
||||||
|
<table className="items-center w-full border-separate border ">
|
||||||
|
<thead className="bg-white border-blueGray-100 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
AccountName
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Account Type
|
||||||
|
</th>
|
||||||
|
{/* <th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Account Number
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Bank Name
|
||||||
|
</th> */}
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Currency
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Amount
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{!isOverdraftLoading && (
|
||||||
|
<tbody>
|
||||||
|
{overdrafts.map((overdraft, i) => (
|
||||||
|
<tr>
|
||||||
|
<th className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||||
|
{overdraft.accountName}
|
||||||
|
</th>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{overdraft.accountType}
|
||||||
|
</td>
|
||||||
|
{/* <td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{overdraft.accountNumber}
|
||||||
|
</td>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{overdraft.bankName}
|
||||||
|
</td> */}
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{overdraft.currency}
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-black">
|
||||||
|
{overdraft.currency != 'USD' ? (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
₦
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-semibold text-sm text-black">
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<CurrencyFormat
|
||||||
|
value={`${parseFloat(overdraft.amount)}`}
|
||||||
|
displayType={'text'}
|
||||||
|
thousandSeparator={true}
|
||||||
|
decimalScale={2}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className=" border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
<div className="flex justify-start space-x-5">
|
||||||
|
<Edit overdraft={overdraft} />
|
||||||
|
<Delete overdraft={overdraft} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Overdraft;
|
499
src/pages/Prepayed.js
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
//import logo from '../assets/logo.png'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import {
|
||||||
|
faBell,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import {
|
||||||
|
faCaretDown
|
||||||
|
}from '@fortawesome/free-solid-svg-icons'
|
||||||
|
//import ReportTable from '../components/ReportTable';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { DataGrid,GridToolbar,GridToolbarContainer,
|
||||||
|
GridToolbarColumnsButton,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarDensitySelector,
|
||||||
|
GridExcelExportOptions,
|
||||||
|
GridFooterPlaceholder,
|
||||||
|
GridPrintExportOptions,
|
||||||
|
GridPrintExportMenuItem,
|
||||||
|
|
||||||
|
GridPanelFooter, GridPanelHeader} from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from '@mui/x-data-grid-generator';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Modal from '../components/Modal'
|
||||||
|
import ExpensesTable from "../components/ExpensesTable";
|
||||||
|
import { getPrepaidExpense } from '../redux/slices/nonbill';
|
||||||
|
import { InfinitySpin } from "react-loader-spinner"
|
||||||
|
import BillChart from '../components/BillChart';
|
||||||
|
import { COLORS } from '@mui/x-data-grid-generator/services/static-data';
|
||||||
|
import './expense.css'
|
||||||
|
import logo from "../assets/logo.png"
|
||||||
|
import { logout } from '../redux/slices/auth';
|
||||||
|
import PrintableTable from './PrintableTable';
|
||||||
|
function CustomToolbar() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridToolbarContainer>
|
||||||
|
|
||||||
|
<GridToolbarColumnsButton />
|
||||||
|
<GridToolbarFilterButton />
|
||||||
|
<GridToolbarDensitySelector
|
||||||
|
slotProps={{ tooltip: { title: 'Change density' } }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<GridToolbarExport
|
||||||
|
|
||||||
|
slotProps={{
|
||||||
|
tooltip: { title: 'Export data' },
|
||||||
|
button: { variant: 'outlined' },
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
printOptions={{ disableToolbarButton: false , includeCheckboxes: false, pageStyle: '.MuiDataGrid-root .MuiDataGrid-main { color: rgba(0, 0, 0, 0.87); background-color: gray} ', hideToolbar: true, fileName: 'yourFileName', bodyClassName: '.header', header: () => (
|
||||||
|
<div className="header">
|
||||||
|
{/* Your header content goes here */}
|
||||||
|
<h1>This is the header</h1>
|
||||||
|
</div>
|
||||||
|
),}}
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
</GridToolbarExport>
|
||||||
|
|
||||||
|
|
||||||
|
</GridToolbarContainer>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Prepayed = () => {
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||||
|
const [selectedRow,setSelectedRow] = React.useState(null)
|
||||||
|
const VISIBLE_FIELDS = ['Account ID', 'Tax Id', 'Payment Date','Description','Is Taxable','Amount','Currency ID','Reference Number','Customer Id','Non Billable'];
|
||||||
|
const { data } = useDemoData({
|
||||||
|
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: VISIBLE_FIELDS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const[isLoading,setIsLoading] = useState(true)
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExpensesLoading = useSelector(
|
||||||
|
(state) => state.nonbill.isExpensesLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
const expenses = useSelector((state) => state.nonbill.expenses);
|
||||||
|
|
||||||
|
const expensesWithId = expenses.map(expense => ({
|
||||||
|
...expense,
|
||||||
|
id: expense.expense_id
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ headerName: "Account ID", field: "account_id", sortable: true,sortbyOrder: "asc" },
|
||||||
|
{ headerName: "Tax Id", field: "tax_id", sortable: true },
|
||||||
|
{ headerName: "Payment Date", field: "payment_date", sortable: false },
|
||||||
|
{ headerName: "Description", field: "description", sortable: false },
|
||||||
|
{ headerName: "Is taxable ", field: "is_taxable", sortable: false },
|
||||||
|
{ headerName: "Amount ", field: "amount", sortable: false },
|
||||||
|
{ headerName: "Currency ID", field: "currency_id", sortable: false },
|
||||||
|
{ headerName: "Reference Number", field: "total_without_tax", sortable: false },
|
||||||
|
{ headerName: "Customer Id", field: "customer_id", sortable: false },
|
||||||
|
{ headerName: "Non Billable", field: "non_billable", sortable: false },
|
||||||
|
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const [isInclusiveTax, setIsInclusiveTax] = useState(false);
|
||||||
|
const [isBillable, setIsBillable] = useState(false);
|
||||||
|
console.log('newnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn')
|
||||||
|
const toggleButton = () =>{
|
||||||
|
setIsInclusiveTax(!isInclusiveTax)
|
||||||
|
}
|
||||||
|
const Toggle = () =>{
|
||||||
|
setIsBillable(!isBillable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getPrepaidExpense())
|
||||||
|
.then((result) => {
|
||||||
|
console.log(result, "mmmmmmmmmmmm");
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(false)
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleRowClick = (params) => {
|
||||||
|
setSelectedRow(params.row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setSelectedRow(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
console.log(data,'>>>>>>>>>>>>>>>>>>>...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:4000/v1/zoho/postPrepaid', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to submit form');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the form
|
||||||
|
event.target.reset();
|
||||||
|
|
||||||
|
// Reload the page
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
// Handle errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getCurrentDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
let month = today.getMonth() + 1;
|
||||||
|
let day = today.getDate();
|
||||||
|
|
||||||
|
// Add leading zero if month or day is less than 10
|
||||||
|
month = month < 10 ? '0' + month : month;
|
||||||
|
day = day < 10 ? '0' + day : day;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
|
||||||
|
<div className='relative md:ml-64 bg-blueGray-100'>
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Prepaid Expenses
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<FontAwesomeIcon icon={faBell} className="border px-3 py-3 rounded-full" style={{fontSize:13}} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<div className="flex justify-center h-screen items-center ">
|
||||||
|
<InfinitySpin width="200" color="red"/>
|
||||||
|
</div>:
|
||||||
|
|
||||||
|
<>
|
||||||
|
<div className=' relative -top-8 float-end'>
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Expenses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className=" justify-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="max-w-md">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col bg-white outline-none focus:outline-none">
|
||||||
|
{/*header*/}
|
||||||
|
<div className="flex items-start justify-between mt-5 rounded-t">
|
||||||
|
<div className="flex-initial relative left-5 ">
|
||||||
|
<h3 className="text-black font-bold text-lg mb-1 " >
|
||||||
|
Add Expenses
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
type="button"
|
||||||
|
className=" rounded-full relative -top-6 p-1 inline-flex items-center justify-center text-black hover:text-black bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-red-500" style={{backgroundColor:'#e6e8ec'}}>
|
||||||
|
<span className="sr-only">Close menu</span>
|
||||||
|
|
||||||
|
<svg className="h-5 w-5 rounded-full" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="py-1 px-2">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
{/*body*/}
|
||||||
|
<form className='space-y-3 py-9' onSubmit={handleSubmit}>
|
||||||
|
<div className="relative px-2 flex-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="hidden sm:block" aria-hidden="true">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className=' grid grid-cols-2 space-x-3'>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='account_id'
|
||||||
|
placeholder="Account ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-2 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='date'
|
||||||
|
name='payment_date'
|
||||||
|
min={getCurrentDate()}
|
||||||
|
placeholder="Expected Payment Date"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
name='amount'
|
||||||
|
placeholder="Amount"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
required
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='customer_id'
|
||||||
|
placeholder="Customer ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='currency_id'
|
||||||
|
placeholder="Currency ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
type='string'
|
||||||
|
name='currency'
|
||||||
|
placeholder="Currency"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
>
|
||||||
|
<option value="NGN">Naira (NGN)</option>
|
||||||
|
<option value="USD">USD (United States Dollar)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='tax_id'
|
||||||
|
placeholder="Tax ID"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
|
||||||
|
type='button'
|
||||||
|
name='is_inclusive_tax'
|
||||||
|
onClick={toggleButton}
|
||||||
|
value={isInclusiveTax ? "true" : "false"}
|
||||||
|
placeholder="Enter true or false"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='reference_number'
|
||||||
|
placeholder="Reference Number"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='string'
|
||||||
|
name='description'
|
||||||
|
placeholder="Description"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
|
||||||
|
type='button'
|
||||||
|
name='is_Billable'
|
||||||
|
onClick={Toggle}
|
||||||
|
value={isBillable ? "true" : "false"}
|
||||||
|
placeholder="Enter true or false"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-3 px-40 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
type="submit"
|
||||||
|
style={{backgroundColor:'#df3c62'}}
|
||||||
|
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className=' m-6' style={{ height: 400, width: '93%' }}>
|
||||||
|
|
||||||
|
{!isExpensesLoading && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<DataGrid
|
||||||
|
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbar,
|
||||||
|
}}
|
||||||
|
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
rows={expenses} // Use expensesWithId here
|
||||||
|
getRowId={(row) => row.id}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{
|
||||||
|
...data.initialState,
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedRow && (
|
||||||
|
<Modal isOpen={selectedRow !== null} onClose={handleCloseModal}>
|
||||||
|
<ul className='list-none'>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Account Id:</strong> {selectedRow.account_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> tax_id:</strong> {selectedRow.tax_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Payment date:</strong> {selectedRow.payment_date}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>Description: </strong>{selectedRow.description}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>amount:</strong> {selectedRow.amount}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> currency id: </strong>{selectedRow.currency_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>total_without_tax:</strong> {selectedRow.total_without_tax}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong> customer_id:</strong> {selectedRow.customer_id}</li>
|
||||||
|
<li className=" border-b border-gray-200 py-2 text-lg"><strong>non_billable:</strong> {selectedRow.non_billable}</li>
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<footer className="block py-4">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<hr className="mb-4 border-b-1 border-blueGray-200" />
|
||||||
|
<div className="flex flex-wrap items-center md:justify-between justify-center">
|
||||||
|
<div className="w-full md:w-4/12 px-4">
|
||||||
|
<div className="text-sm text-blueGray-500 font-semibold py-1">
|
||||||
|
Copyright © {new Date().getFullYear()}{" "}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-blueGray-500 hover:text-blueGray-700 text-sm font-semibold py-1"
|
||||||
|
>
|
||||||
|
Manifold Computers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Prepayed
|
24
src/pages/PrintableTable.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
typography: {
|
||||||
|
h2: {
|
||||||
|
fontSize: "2rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
borderBottom: "2px solid black",
|
||||||
|
"@media print": {
|
||||||
|
fontSize: "8rem",
|
||||||
|
borderBottom: "20px solid red",
|
||||||
|
color: "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export default function PrintableTable() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Typography variant="h2">Hello CodeSandbox</Typography>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
117
src/pages/Rate.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Sidebar from "../components/Sidebar";
|
||||||
|
import { getExchangeRates } from "../redux/slices/forecast";
|
||||||
|
import { withAuth } from "../hoc/withAuth";
|
||||||
|
import { downloadExchangeRate } from '../redux/slices/zoho';
|
||||||
|
|
||||||
|
const Rate = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExchangeRateListLoading = useSelector(
|
||||||
|
(state) => state.forecast.isExchangeRateListLoading
|
||||||
|
);
|
||||||
|
const rates = useSelector((state) => state.forecast.rates);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getExchangeRates());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const downloadExchangeHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let filename = 'rate' + moment().toISOString();
|
||||||
|
dispatch(downloadExchangeRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className="text-black text-sm uppercase hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Exchange Rates
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul className="flex-col md:flex-row list-none items-center hidden md:flex">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
<button
|
||||||
|
onClick={downloadExchangeHandler}
|
||||||
|
className="bg-red-500 text-white active:bg-red-600 text-xs font-bold px-3 py-1 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="w-full px-4 pt-12">
|
||||||
|
<div className="overflow-x-auto shadow-md sm:rounded-lg">
|
||||||
|
<div className="inline-block min-w-full align-middle">
|
||||||
|
<div className="overflow-hidden ">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 table-fixed dark:gray-400">
|
||||||
|
<thead className="bg-gray-100 dark:bg-gray-400 sticky">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3 px-6 text-xs font-medium tracking-wider text-left text-black uppercase dark:text-black"
|
||||||
|
>
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3 px-6 text-xs font-medium tracking-wider text-left text-black uppercase dark:text-black"
|
||||||
|
>
|
||||||
|
Rate
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3 px-6 text-xs font-medium tracking-wider text-left text-black uppercase dark:text-black"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{!isExchangeRateListLoading && (
|
||||||
|
<tbody className="bg-white divide-y divide-gray-300 dark:bg-gray-300 dark:divide-gray-300">
|
||||||
|
{rates.map((rate, i) => (
|
||||||
|
<tr
|
||||||
|
key={rate.id}
|
||||||
|
className="hover:bg-gray-100 dark:hover:bg-white-400"
|
||||||
|
>
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-black">
|
||||||
|
{i + 1}
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-gray-500 whitespace-nowrap dark:text-black">
|
||||||
|
{rate.rate}
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-black">
|
||||||
|
{dayjs(rate.createdAt).format("DD-MMM-YYYY")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withAuth(true)(Rate);
|
157
src/pages/Register.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import logo from "../assets/logo.png";
|
||||||
|
import validator from 'validator'
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faEye,
|
||||||
|
faEyeSlash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { logout, signup } from '../redux/slices/auth'
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isSignupLoading = useSelector(state => state.auth.isSignupLoading)
|
||||||
|
const { id } = useParams()
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const { register, handleSubmit, reset, formState: { errors } } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerUser = (data) => {
|
||||||
|
|
||||||
|
if (!validator.isStrongPassword(data.password, {
|
||||||
|
minLength: 6, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1
|
||||||
|
})) {
|
||||||
|
toast.error("Password must be more than 5 characters including number, symbol, upper and lowercase", { autoClose: 5000 })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(signup({
|
||||||
|
data,
|
||||||
|
inviteToken: id
|
||||||
|
})).then((res) => {
|
||||||
|
if (!res.payload) return
|
||||||
|
|
||||||
|
localStorage.clear()
|
||||||
|
dispatch(logout())
|
||||||
|
navigate('/')
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}).catch(e => {
|
||||||
|
dispatch(logout())
|
||||||
|
navigate('/')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main">
|
||||||
|
<div className="h-screen m-auto">
|
||||||
|
<div className="bg-center inset-0 w-7/12 lg:block">
|
||||||
|
<div className="ml-auto left-6 top-6 text-sm px-5 py-3">
|
||||||
|
<a href="/login">
|
||||||
|
<img className="img" alt="manifold logo" src={logo} width="100px" height="40px" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div hidden role="hidden" className="fixed inset-0 w-6/12 ml-auto bg-white bg-opacity-70 backdrop-blur-xl lg:block">
|
||||||
|
</div>
|
||||||
|
<div className="relative h-screen m-auto lg:w-6/12">
|
||||||
|
<div className="m-auto py-12 px-6 sm:p-20 xl:w-10/12">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-red-700 font-medium text-2xl mb-1">Update Account Information</h1>
|
||||||
|
<h1 className="text-md font-normal text-gray-600 mb-7">Complete your registration by filling out the form below.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className='space-y-6 py-6' onSubmit={handleSubmit(registerUser)}>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">First Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="firstName"
|
||||||
|
placeholder="Enter First Name"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("firstName", { required: "first name is required *" })}
|
||||||
|
/>
|
||||||
|
{errors.firstName && <p className="text-xs mt-1 text-red-700">{errors.firstName.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">Last Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="lastName"
|
||||||
|
placeholder="Enter Last Name"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("lastName", { required: "last name is required *" })}
|
||||||
|
/>
|
||||||
|
{errors.lastName && <p className="text-xs mt-1 text-red-700">{errors.lastName.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder="Enter Email Address"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("email", { required: "email name is required *", pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/ })}
|
||||||
|
/>
|
||||||
|
{(errors.email && errors.email.type) === "pattern" && (
|
||||||
|
<p className="text-xs mt-1 text-red-700">enter valid email</p>
|
||||||
|
)}
|
||||||
|
{errors.email && <p className="text-xs mt-1 text-red-700">{errors.email.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="relative mb-5 mt-2">
|
||||||
|
<label className="block text-gray-700">Password</label>
|
||||||
|
<div className="absolute right-0 text-gray-600 flex items-center pr-5 pb-4 h-full cursor-pointer">
|
||||||
|
<button onClick={togglePasswordVisibility}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={showPassword ? faEye : faEyeSlash}
|
||||||
|
style={{ fontSize: 20, color: "grey" }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
id="password"
|
||||||
|
placeholder="Enter Password"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
{...register("password", { required: "password is required *" })}
|
||||||
|
/>
|
||||||
|
{errors.password && <p className="text-xs mt-1 text-red-700">{errors.password.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="text-right mt-2">
|
||||||
|
<br></br>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full block bg-red-400 hover:bg-red-300 focus:bg-red-300 text-white font-semibold rounded-lg
|
||||||
|
px-4 py-3 mt-6"
|
||||||
|
disabled={isSignupLoading}
|
||||||
|
>{isSignupLoading ? 'loading...' : 'Submit'}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
122
src/pages/Report.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Sidebar from '../components/Sidebar'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faBell,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import {
|
||||||
|
faCaretDown
|
||||||
|
}from '@fortawesome/free-solid-svg-icons'
|
||||||
|
//import ReportTable from '../components/ReportTable';
|
||||||
|
import { DataGrid,GridToolbar } from '@mui/x-data-grid';
|
||||||
|
import { useDemoData } from '@mui/x-data-grid-generator';
|
||||||
|
|
||||||
|
const Report = () => {
|
||||||
|
const [rowSelectionModel, setRowSelectionModel] = React.useState([]);
|
||||||
|
const VISIBLE_FIELDS = ['Invoice No.', 'Date', 'Status', 'Customer', 'Purchase'];
|
||||||
|
const { data } = useDemoData({
|
||||||
|
|
||||||
|
rowLength: 10,
|
||||||
|
maxColumns: 6,
|
||||||
|
visibleFields: VISIBLE_FIELDS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: "Invoice ", accessor: "invoiceNumber", headerName: 'Invoice', width: 150,sortable: true },
|
||||||
|
{field: "Date", accessor: "date", width: 150, sortable: true },
|
||||||
|
{field: "Status", accessor: "status", width: 150, sortable: true },
|
||||||
|
{ field: "Customer", accessor: "customer", width: 150, sortable: false },
|
||||||
|
{ field: "Purchase", accessor: "purchases", width: 200, sortable: false },
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const Items = [
|
||||||
|
{
|
||||||
|
"invoiceNumber":1,
|
||||||
|
"date":5/12/2022,
|
||||||
|
"status":"active",
|
||||||
|
"customer":"naza",
|
||||||
|
"purchase":"monthly subscription"
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className='relative md:ml-64 bg-blueGray-100'>
|
||||||
|
<nav className="absolute top-0 left-0 w-full">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Cash flow forecast report
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
<FontAwesomeIcon icon={faBell} className="border px-3 py-3 rounded-full" style={{fontSize:13}} />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="py-11">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-4 gap-3 m-5'>
|
||||||
|
<div className='flex border px-4 py-2 rounded-lg space-x-5'>
|
||||||
|
<p>Report from:<span className=' font-bold relative left-3'>Select Date</span></p>
|
||||||
|
<FontAwesomeIcon icon={faCaretDown} className=' m-1' />
|
||||||
|
</div>
|
||||||
|
<div className='border px-4 py-2 rounded-lg'>Forecast Period:<span className=' font-bold'>180days</span></div>
|
||||||
|
<div className='flex border px-4 py-2 rounded-lg space-x-12'>
|
||||||
|
<p className='relative space-x-3'>Select report:<span className=' font-bold relative left-3'>All</span></p>
|
||||||
|
<FontAwesomeIcon icon={faCaretDown} className=' m-1 items-end text-end' />
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Show report
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=' m-6 ' style={{ height: 400, width: '93%' }}>
|
||||||
|
<DataGrid
|
||||||
|
slots={{
|
||||||
|
toolbar: GridToolbar,
|
||||||
|
// Use custom FilterPanel only for deep modification
|
||||||
|
// FilterPanel: MyCustomFilterPanel,
|
||||||
|
}}
|
||||||
|
|
||||||
|
checkboxSelection
|
||||||
|
onRowSelectionModelChange={(newRowSelectionModel) => {
|
||||||
|
setRowSelectionModel(newRowSelectionModel);
|
||||||
|
}}
|
||||||
|
rowSelectionModel={rowSelectionModel}
|
||||||
|
{...data}
|
||||||
|
data = {Items}
|
||||||
|
columns={columns}
|
||||||
|
initialState={{
|
||||||
|
...data.initialState,
|
||||||
|
pagination: { paginationModel: { pageSize: 5 } },
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[5, 10, 25]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Report
|
151
src/pages/ResetPassword.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import validator from 'validator'
|
||||||
|
import logo from "../assets/logo.png";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faEye,
|
||||||
|
faEyeSlash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { reset } from '../redux/slices/auth'
|
||||||
|
|
||||||
|
const ResetPassword = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isResetPasswordLoading = useSelector(state => state.auth.isResetPasswordLoading)
|
||||||
|
const { id } = useParams()
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||||
|
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
newPassword: '',
|
||||||
|
confirmNewPassword: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const resetPassword = (data) => {
|
||||||
|
if (data.newPassword !== data.confirmNewPassword) {
|
||||||
|
toast.error("Password mismatch", { autoClose: 2000 })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isStrongPassword(data.newPassword, {
|
||||||
|
minLength: 6, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1
|
||||||
|
})) {
|
||||||
|
toast.error("Password must be more than 5 characters including number, symbol, upper and lowercase", { autoClose: 5000 })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(reset({
|
||||||
|
password: data.newPassword,
|
||||||
|
token: id
|
||||||
|
})).then((res) => {
|
||||||
|
if (!res.payload) return
|
||||||
|
|
||||||
|
// TODO:: should redirect to login page
|
||||||
|
navigate('/login');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleNewPasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowNewPassword(!showNewPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
|
||||||
|
// if (!isResetPasswordLoading) {
|
||||||
|
// navigate('/login');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }, [isResetPasswordLoading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main">
|
||||||
|
<div className="h-screen m-auto">
|
||||||
|
<div className="bg-center inset-0 w-7/12 lg:block">
|
||||||
|
<div className="ml-auto left-6 top-6 text-sm px-5 py-3">
|
||||||
|
<a href="/login">
|
||||||
|
<img className="img" alt="manifold logo" src={logo} width="100px" height="40px" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div hidden role="hidden" className="fixed inset-0 w-6/12 ml-auto bg-white bg-opacity-70 backdrop-blur-xl lg:block">
|
||||||
|
</div>
|
||||||
|
<div className="relative h-screen m-auto lg:w-6/12">
|
||||||
|
<div className="m-auto py-12 px-6 sm:p-20 xl:w-10/12">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h1 className="text-red-700 font-medium text-2xl mb-1">Reset Password</h1>
|
||||||
|
<h1 className="text-md font-normal text-gray-600 mb-7">Reset your passwod by filling out the form below.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className='space-y-6 py-6' onSubmit={handleSubmit(resetPassword)}>
|
||||||
|
|
||||||
|
<div className="relative mb-5 mt-2">
|
||||||
|
<label className="block text-gray-700">New Password</label>
|
||||||
|
<div className="absolute right-0 text-gray-600 flex items-center pr-5 pb-4 h-full cursor-pointer">
|
||||||
|
<button onClick={togglePasswordVisibility}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={showPassword ? faEye : faEyeSlash}
|
||||||
|
style={{ fontSize: 20, color: "grey" }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
id="newPassword"
|
||||||
|
placeholder="Enter New Password"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
{...register("newPassword", { required: "new password is required *" })}
|
||||||
|
/>
|
||||||
|
{errors.newPassword && <p className="text-xs mt-1 text-red-700">{errors.newPassword.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="relative mb-5 mt-2">
|
||||||
|
<label className="block text-gray-700">Confirm New Password</label>
|
||||||
|
<div className="absolute right-0 text-gray-600 flex items-center pr-5 pb-4 h-full cursor-pointer">
|
||||||
|
<button onClick={toggleNewPasswordVisibility}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={showNewPassword ? faEye : faEyeSlash}
|
||||||
|
style={{ fontSize: 20, color: "grey" }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showNewPassword ? 'text' : 'password'}
|
||||||
|
id="confirmNewPassword"
|
||||||
|
placeholder="Confirm New Password"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-200 mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
{...register("confirmNewPassword", { required: "confirm new password is required *" })}
|
||||||
|
/>
|
||||||
|
{errors.confirmNewPassword && <p className="text-xs mt-1 text-red-700">{errors.confirmNewPassword.message}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="text-right mt-2">
|
||||||
|
<br></br>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full block bg-red-400 hover:bg-red-300 focus:bg-red-300 text-white font-semibold rounded-lg px-4 py-3 mt-6"
|
||||||
|
disabled={isResetPasswordLoading} >
|
||||||
|
{isResetPasswordLoading ? 'loading...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPassword;
|
269
src/pages/Setting.js
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import validator from 'validator';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { InfinitySpin } from 'react-loader-spinner';
|
||||||
|
import Sidebar from '../components/Sidebar';
|
||||||
|
import { withAuth } from '../hoc/withAuth';
|
||||||
|
import { changePassword } from '../redux/slices/auth';
|
||||||
|
import {
|
||||||
|
exchangeRate,
|
||||||
|
updateExchangeRate,
|
||||||
|
generateReport,
|
||||||
|
} from '../redux/slices/forecast';
|
||||||
|
|
||||||
|
const Setting = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isChangePasswordLoading = useSelector(
|
||||||
|
(state) => state.auth.isChangePasswordLoading
|
||||||
|
);
|
||||||
|
const isUpdateExchangeRateLoading = useSelector(
|
||||||
|
(state) => state.forecast.isUpdateExchangeRateLoading
|
||||||
|
);
|
||||||
|
const { forecastNumber, forecastPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastInfo
|
||||||
|
);
|
||||||
|
const { latest, id } = useSelector((state) => state.forecast.rate);
|
||||||
|
const { selectedPeriod } = useSelector(
|
||||||
|
(state) => state.forecast.forecastDropdown
|
||||||
|
);
|
||||||
|
let isGeneratingReport = useSelector(
|
||||||
|
(state) => state.forecast.isGeneratingReport
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||||||
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||||
|
const [latestRate, setLatestRate] = useState(latest);
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
|
||||||
|
const changeCurrentPasswordHandler = (e) =>
|
||||||
|
setCurrentPassword(e.target.value);
|
||||||
|
|
||||||
|
const changeNewPasswordHandler = (e) => setNewPassword(e.target.value);
|
||||||
|
|
||||||
|
const exchangeRateHandler = (e) => setLatestRate(e.target.value);
|
||||||
|
|
||||||
|
const updateExchangeRateHandler = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(
|
||||||
|
updateExchangeRate({ id, latestRate, forecastNumber, forecastPeriod })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePasswordHandler = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validator.isStrongPassword(newPassword, {
|
||||||
|
minLength: 6,
|
||||||
|
minLowercase: 1,
|
||||||
|
minUppercase: 1,
|
||||||
|
minNumbers: 1,
|
||||||
|
minSymbols: 1,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
toast.error(
|
||||||
|
'Password must be more than 5 characters including number, symbol, upper and lowercase',
|
||||||
|
{ autoClose: 5000 }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(changePassword({ currentPassword, newPassword })).then((res) => {
|
||||||
|
if (!res.payload) return;
|
||||||
|
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
navigate('/setting');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCurrentPasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowCurrentPassword(!showCurrentPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNewPasswordVisibility = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowNewPassword(!showNewPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get exchange rate from backend
|
||||||
|
dispatch(exchangeRate({ forecastNumber, forecastPeriod }));
|
||||||
|
if (latestRate != latest) {
|
||||||
|
dispatch(
|
||||||
|
generateReport({ id: selectedPeriod, forecastNumber, forecastPeriod })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setLatestRate(latest);
|
||||||
|
}, [latest]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-xl hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{/* End Navbar */}
|
||||||
|
{/* Header */}
|
||||||
|
<div className="hidden sm:block" aria-hidden="true">
|
||||||
|
<div className="py-14">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isGeneratingReport && (
|
||||||
|
<>
|
||||||
|
<div className="grid h-screen place-items-center">
|
||||||
|
<div>
|
||||||
|
<h1>Please wait while we generate your report</h1>
|
||||||
|
<div className="text-center text-md font-bold text-red">
|
||||||
|
Do not leave this page
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<InfinitySpin width="200" color="red" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isGeneratingReport && (
|
||||||
|
<div className=' grid grid-cols-2 w-full relative left-14'>
|
||||||
|
<div className=''>
|
||||||
|
<div className="">
|
||||||
|
<h1 className="text-black font-bold text-lg mb-1">
|
||||||
|
Update Exchange rate
|
||||||
|
</h1>
|
||||||
|
<h1 className="text-md font-normal text-gray-600 mb-7">
|
||||||
|
Exchange rate
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<form
|
||||||
|
className=""
|
||||||
|
onSubmit={updateExchangeRateHandler}
|
||||||
|
>
|
||||||
|
<div className="">
|
||||||
|
<div className="">
|
||||||
|
<div className="">
|
||||||
|
|
||||||
|
<div className="relative w-9/12">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
placeholder="Update exchange rate"
|
||||||
|
style={{ transition: 'all .15s ease' }}
|
||||||
|
value={latestRate || ''}
|
||||||
|
onChange={exchangeRateHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="pt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{backgroundColor:'#df3c62'}}
|
||||||
|
disabled={isUpdateExchangeRateLoading}
|
||||||
|
className="inline-flex justify-center py-3 px-16 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-300"
|
||||||
|
>
|
||||||
|
{isUpdateExchangeRateLoading || isGeneratingReport
|
||||||
|
? 'loading...'
|
||||||
|
: 'Update'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isGeneratingReport && (
|
||||||
|
<>
|
||||||
|
<div className=' grid grid-cols-2 gap-28 m-14'>
|
||||||
|
<div className=''>
|
||||||
|
|
||||||
|
<h1 className="text-black font-bold text-lg mb-3">
|
||||||
|
Physical Metrics and Activity
|
||||||
|
</h1>
|
||||||
|
<h1 className="text-md font-normal text-gray-600 mb-7">
|
||||||
|
To change your passwod, please type in the current
|
||||||
|
password and your new password.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<form className="" onSubmit={changePasswordHandler}>
|
||||||
|
<div className="">
|
||||||
|
<div className="relative w-9/12">
|
||||||
|
<div className="text-gray-600 flex items-center pr-5 pb-4 h-full cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showCurrentPassword && 'text'}
|
||||||
|
id="currentPassword"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
style={{ transition: 'all .15s ease' }}
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={changeCurrentPasswordHandler}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-9/12 mb-5 mt-2">
|
||||||
|
<div className="absolute right-0 text-gray-600 flex items-center pr-5 pb-4 py-6 px-5 cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={showNewPassword && 'text'}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200
|
||||||
|
focus:bg-white focus:outline-none"
|
||||||
|
placeholder="Enter new password"
|
||||||
|
style={{ transition: 'all .15s ease' }}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={changeNewPasswordHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className=" py-6 ">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isChangePasswordLoading}
|
||||||
|
style={{backgroundColor:'#df3c62'}}
|
||||||
|
className="inline-flex py-3 px-16 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||||
|
>
|
||||||
|
{isChangePasswordLoading ? 'loading...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withAuth(true)(Setting);
|
481
src/pages/User.js
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faTrashAlt,
|
||||||
|
faRedo
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { inviteUser } from '../redux/slices/auth'
|
||||||
|
|
||||||
|
import Sidebar from "../components/Sidebar.js";
|
||||||
|
import { withAuth } from "../hoc/withAuth";
|
||||||
|
import { getUsers, updateAdminStatus, deleteUser } from '../redux/slices/user'
|
||||||
|
|
||||||
|
|
||||||
|
const User = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isInviteUserLoading = useSelector((state) => state.auth.inviteUserLoading);
|
||||||
|
const isUserLoading = useSelector((state) => state.user.isUserLoading);
|
||||||
|
const users = useSelector((state) => state.user.users);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
// const [toggle, setToggle] = useState(true);
|
||||||
|
const { register, handleSubmit, setValue, reset, formState: { errors } } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleClass = ' transform translate-x-5 bg-white';
|
||||||
|
|
||||||
|
const outterToggleClass = ' bg-red-600'
|
||||||
|
|
||||||
|
// Allows admin to invite user to the applicaiton and also re-invite a user
|
||||||
|
// if the email fails
|
||||||
|
|
||||||
|
|
||||||
|
const onDeleteUser = async () => {
|
||||||
|
await dispatch(deleteUser({ email }))
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
|
||||||
|
await dispatch(getUsers());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReInviteUser = async () => {
|
||||||
|
await dispatch(inviteUser({ email: email })).then((res) => {
|
||||||
|
if (!res.payload) return
|
||||||
|
|
||||||
|
// TODO:: should redirect to login page
|
||||||
|
reset({
|
||||||
|
email: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
setShowModal(false)
|
||||||
|
setShowInviteModal(false)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
await dispatch(getUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteModal = (user) => {
|
||||||
|
setShowDeleteModal(true)
|
||||||
|
setEmail(user.email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteModal = (user) => {
|
||||||
|
setShowInviteModal(true)
|
||||||
|
setEmail(user.email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserStaus = async (user) => {
|
||||||
|
await dispatch(updateAdminStatus({ user }))
|
||||||
|
|
||||||
|
dispatch(getUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
let email = (!data || !data.email) ? email : data.email
|
||||||
|
|
||||||
|
await dispatch(inviteUser({ email: email })).then((res) => {
|
||||||
|
if (!res.payload) return
|
||||||
|
|
||||||
|
// TODO:: should redirect to login page
|
||||||
|
reset({
|
||||||
|
email: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
setShowModal(false)
|
||||||
|
setShowInviteModal(false)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
await dispatch(getUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getUsers())
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="relative md:ml-64 bg-blueGray-100">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4">
|
||||||
|
<div className="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4">
|
||||||
|
{/* Brand */}
|
||||||
|
<a
|
||||||
|
className=" text-lg hidden lg:inline-block font-semibold"
|
||||||
|
href="#pablo"
|
||||||
|
style={{color:'#671c2d'}}
|
||||||
|
onClick={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
User Management
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* User */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{/* End Navbar */}
|
||||||
|
<div className="hidden sm:block" aria-hidden="true">
|
||||||
|
<div className="py-14">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-col md:flex-row list-none items-center hidden md:flex">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||||
|
<button onClick={() => setShowModal(true)}
|
||||||
|
className=" text-white active:bg-red-600 text-xs font-bold px-12 py-3 rounded outline-none focus:outline-none mr-1 mb-1"
|
||||||
|
type="button"
|
||||||
|
|
||||||
|
style={{ transition: "all .15s ease",backgroundColor:'#df3c62' }}
|
||||||
|
>
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className=" justify-end items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="max-w-md">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col bg-white outline-none focus:outline-none">
|
||||||
|
{/*header*/}
|
||||||
|
<div className="flex items-start justify-between mt-5 rounded-t">
|
||||||
|
<div className="flex-initial relative left-5 ">
|
||||||
|
<h3 className="text-black font-bold text-lg mb-1 " >
|
||||||
|
Add User
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
type="button"
|
||||||
|
className=" rounded-full p-1 inline-flex items-center justify-center text-black hover:text-black bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-red-500" style={{backgroundColor:'#e6e8ec'}}>
|
||||||
|
<span className="sr-only">Close menu</span>
|
||||||
|
|
||||||
|
<svg className="h-5 w-5 rounded-full" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="py-1 px-2">
|
||||||
|
<div className="border-t border-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
{/*body*/}
|
||||||
|
<form className=' space-y-3 py-9' onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="relative px-2 flex-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="hidden sm:block" aria-hidden="true">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='name'
|
||||||
|
placeholder="First name"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-2 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("name", { required: 'First name is required*', pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/ })}
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='name'
|
||||||
|
placeholder="Last name"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("name", { required: 'Last name is required*', pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/ })}
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='email'
|
||||||
|
placeholder="Enter email"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("email", { required: 'email address is required*', pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/ })}
|
||||||
|
|
||||||
|
/>
|
||||||
|
{(errors.email && errors.email.type) === "pattern" && (
|
||||||
|
<p className="text-xs mt-1 text-red-700">enter valid email</p>
|
||||||
|
)}
|
||||||
|
{errors.email && <p className="text-xs mt-1 text-red-700">{errors.email.message}</p>}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='password'
|
||||||
|
placeholder="Create password"
|
||||||
|
style={{ transition: "all .15s ease" }}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-white mt-2 border focus:border-grey-200 focus:bg-white focus:outline-none"
|
||||||
|
{...register("password", { required: 'password address is required*', pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/ })}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-3 px-40 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
type="submit"
|
||||||
|
style={{backgroundColor:'#df3c62'}}
|
||||||
|
disabled={isInviteUserLoading}
|
||||||
|
>
|
||||||
|
{isInviteUserLoading ? 'loading...' : 'Send invite'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showInviteModal ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-xl">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
|
||||||
|
{/*body*/}
|
||||||
|
{/* <form className='space-y-6 py-6' onSubmit={handleSubmit(onSubmit)}> */}
|
||||||
|
<div className="relative px-8 flex-auto">
|
||||||
|
<div className="relative w-12/12">
|
||||||
|
<p className="my-4 text-slate-500 text-lg leading-relaxed">
|
||||||
|
Do you want to reinvite user?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowInviteModal(false)}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 mb-1 mr-2"
|
||||||
|
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
onClick={onReInviteUser}
|
||||||
|
>
|
||||||
|
{isInviteUserLoading ? 're-inviting...' : 'Yes'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* </form> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
{showDeleteModal ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-xl">
|
||||||
|
{/*content*/}
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
|
||||||
|
{/*body*/}
|
||||||
|
{/* <form className='space-y-6 py-6' onSubmit={handleSubmit(onSubmit)}> */}
|
||||||
|
<div className="relative px-8 flex-auto">
|
||||||
|
<div className="relative w-12/12">
|
||||||
|
<p className="my-4 text-slate-500 text-lg leading-relaxed">
|
||||||
|
Are you sure you want to delete user?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/*footer*/}
|
||||||
|
<div className="flex items-center justify-center p-6 mt-5 rounded-b">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteModal(false)}
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 mb-1 mr-2"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 mb-1"
|
||||||
|
type="submit"
|
||||||
|
onClick={onDeleteUser}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* </form> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{/* End Modal */}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="w-full min-h-screen px-4 pt-12">
|
||||||
|
<div className="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||||
|
<div className="rounded-t mb-0 px-4 py-3 border-0">
|
||||||
|
<div className="flex flex-wrap items-center">
|
||||||
|
<div className="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||||
|
<h3 className="font-semibold text-base text-black">
|
||||||
|
Team
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="block w-full max-h-screen overflow-x-auto ">
|
||||||
|
{/* Projects table */}
|
||||||
|
<table className="items-center w-full border-separate border ">
|
||||||
|
<thead className="bg-white border-blueGray-100 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Admin
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Date
|
||||||
|
</th>
|
||||||
|
<th className="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{(!isUserLoading) && (
|
||||||
|
<tbody className=''>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<th className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||||
|
{(user.firstName ? user.firstName : '') + ' ' + (user.lastName ? user.lastName : '')}
|
||||||
|
</th>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{user.email}
|
||||||
|
</td>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
<div
|
||||||
|
className={"md:w-10 md:h-5 w-10 h-5 flex items-center rounded-full p-1 cursor-pointer" + (!user.role ? ' bg-rose-900 border-2 border-rose-900' : outterToggleClass)}
|
||||||
|
onClick={() => updateUserStaus({ role: !user.role, email: user.email })}
|
||||||
|
>
|
||||||
|
{/* Switch */}
|
||||||
|
<div
|
||||||
|
className={"md:w-3 md:h-3 h-5 w-5 rounded-full" + (!user.role ? ' bg-white' : toggleClass)} style={{color:''}}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{user.status ?
|
||||||
|
<div className=" flex rounded-lg bg-green-100 yellow-200 text-green-500 text-sm">
|
||||||
|
<div className=" text-center">
|
||||||
|
|
||||||
|
<p className="text-center relative left-3 border-dotted"><span className=" inline-block bg-green-500 w-2 h-2 rounded-full space-x-2"></span>completed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
:
|
||||||
|
<>
|
||||||
|
|
||||||
|
<div className=" flex rounded-lg bg-yellow-100 text-yellow-300 text-sm">
|
||||||
|
<div className="">
|
||||||
|
|
||||||
|
<p className="text-center relative left-3 border-dotted"><span className=" inline-block bg-yellow-300 w-2 h-2 rounded-full space-x-2"></span>invited</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
{dayjs(user.inviteDate).format('DD-MMM-YYYY')}
|
||||||
|
</td>
|
||||||
|
<td className=" border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4">
|
||||||
|
<div className="flex justify-start space-x-5">
|
||||||
|
|
||||||
|
<div className="has-tooltip" onClick={() => inviteModal({ email: user.email })}>
|
||||||
|
<span className="tooltip rounded shadow-lg p-2 bg-gray-100 text-black -mt-8">reinvite user</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faRedo}
|
||||||
|
style={{ fontSize: 17, color: "black" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="has-tooltip" onClick={() => deleteModal({ email: user.id })}>
|
||||||
|
<span className="tooltip rounded shadow-lg p-2 bg-gray-100 text-black -mt-8">delete user</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrashAlt}
|
||||||
|
style={{ fontSize: 17, color: "black" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<footer className="block py-4">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<hr className="mb-4 border-b-1 border-blueGray-200" />
|
||||||
|
<div className="flex flex-wrap items-center md:justify-between justify-center">
|
||||||
|
<div className="w-full md:w-4/12 px-4">
|
||||||
|
<div className="text-sm text-blueGray-500 font-semibold py-1">
|
||||||
|
Copyright © {new Date().getFullYear()}{" "}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-blueGray-500 hover:text-blueGray-700 text-sm font-semibold py-1"
|
||||||
|
>
|
||||||
|
Manifold Computers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuth(true)(User);
|
8
src/pages/expense.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@media print {
|
||||||
|
.print-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
245
src/redux/slices/auth.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Axios } from '../../api/instances';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
user: {},
|
||||||
|
isAuthenticated: localStorage.getItem('accessToken') ? true : false,
|
||||||
|
isZohoAuthenticated: false,
|
||||||
|
isAuthLoading: false,
|
||||||
|
error: null,
|
||||||
|
isChangePasswordLoading: false,
|
||||||
|
inviteUserLoading: false,
|
||||||
|
isUserLoading: false,
|
||||||
|
isResetPasswordLoading: false,
|
||||||
|
isForgotPasswordLoading: false,
|
||||||
|
isForgotPasswordSuccess: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = createAsyncThunk('login', async ({ email, password }) => {
|
||||||
|
try {
|
||||||
|
localStorage.clear()
|
||||||
|
|
||||||
|
const response = await Axios.post('login', {
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
console.log(login)
|
||||||
|
return response
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const changePassword = createAsyncThunk("/changePassword", async ({ currentPassword, newPassword }) => {
|
||||||
|
try {
|
||||||
|
const res = await Axios.post('/password/update', {
|
||||||
|
currentPassword, newPassword
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inviteUser = createAsyncThunk("/invite", async ({ email }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Axios.post('/invite', {
|
||||||
|
email
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUser = createAsyncThunk("/me", async () => {
|
||||||
|
let accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
try {
|
||||||
|
const res = await Axios.get('/me', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const signup = createAsyncThunk("/register", async ({ data,
|
||||||
|
inviteToken }) => {
|
||||||
|
try {
|
||||||
|
const res = await Axios.post('/register', {
|
||||||
|
...data,
|
||||||
|
inviteToken
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const forgotPassword = createAsyncThunk("/forgotPassword", async ({ email }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Axios.post('/password/forgot', {
|
||||||
|
email
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = createAsyncThunk("/reset", async ({ password, token }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Axios.post('/password/reset', {
|
||||||
|
password, token
|
||||||
|
}, {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginSlice = createSlice({
|
||||||
|
name: "login",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
logout(state) {
|
||||||
|
localStorage.clear()
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.isZohoAuthenticated = false;
|
||||||
|
state = state.initialState;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(login.pending, (state, action) => {
|
||||||
|
state.isAuthLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(login.fulfilled, (state, action) => {
|
||||||
|
localStorage.setItem('accessToken', JSON.stringify(action.payload.data.data.accessToken))
|
||||||
|
localStorage.setItem('refreshToken', JSON.stringify(action.payload.data.data.refreshToken))
|
||||||
|
// localStorage.setItem('forecastNumber', 3);
|
||||||
|
// localStorage.setItem('forecastPeriod', 'month');
|
||||||
|
|
||||||
|
state.isAuthLoading = false;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
state.isZohoAuthenticated = action.payload.data.data.isZohoAuthenticated;
|
||||||
|
})
|
||||||
|
.addCase(login.rejected, (state, action) => {
|
||||||
|
state.isAuthLoading = false;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.error = action.error;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(signup.pending, (state, action) => {
|
||||||
|
state.isSignupLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(signup.fulfilled, (state, action) => {
|
||||||
|
state.isSignupLoading = false;
|
||||||
|
toast.success('Registration successful', { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(signup.rejected, (state, action) => {
|
||||||
|
state.isSignupLoading = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(changePassword.pending, (state, action) => {
|
||||||
|
state.isChangePasswordLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(changePassword.fulfilled, (state, action) => {
|
||||||
|
state.isChangePasswordLoading = false;
|
||||||
|
toast.success('Password changed successfully', { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(changePassword.rejected, (state, action) => {
|
||||||
|
state.isChangePasswordLoading = false;
|
||||||
|
state.error = action.error;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(getUser.pending, (state, action) => {
|
||||||
|
state.isUserLoading = true
|
||||||
|
})
|
||||||
|
.addCase(getUser.fulfilled, (state, action) => {
|
||||||
|
state.user = action.payload
|
||||||
|
state.isUserLoading = false
|
||||||
|
})
|
||||||
|
.addCase(getUser.rejected, (state, action) => {
|
||||||
|
state.isUserLoading = false
|
||||||
|
localStorage.clear()
|
||||||
|
state.isAuthenticated = false
|
||||||
|
})
|
||||||
|
.addCase(forgotPassword.pending, (state, action) => {
|
||||||
|
state.isForgotPasswordLoading = true;
|
||||||
|
state.isForgotPasswordSuccess = false;
|
||||||
|
})
|
||||||
|
.addCase(forgotPassword.fulfilled, (state, action) => {
|
||||||
|
state.isForgotPasswordLoading = false;
|
||||||
|
state.isForgotPasswordSuccess = true;
|
||||||
|
|
||||||
|
toast.success('Check your email to reset your password', { autoClose: 5000 })
|
||||||
|
})
|
||||||
|
.addCase(forgotPassword.rejected, (state, action) => {
|
||||||
|
state.isForgotPasswordLoading = false;
|
||||||
|
state.isForgotPasswordSuccess = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(inviteUser.pending, (state, action) => {
|
||||||
|
state.inviteUserLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(inviteUser.fulfilled, (state, action) => {
|
||||||
|
toast.success('Invitation sent successfully')
|
||||||
|
state.inviteUserLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(inviteUser.rejected, (state, action) => {
|
||||||
|
toast.error(action.error.message)
|
||||||
|
state.inviteUserLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(reset.pending, (state, action) => {
|
||||||
|
state.isResetPasswordLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(reset.fulfilled, (state, action) => {
|
||||||
|
toast.success('Password reset successful', { autoClose: 2000 })
|
||||||
|
state.isResetPasswordLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(reset.rejected, (state, action) => {
|
||||||
|
toast.error(action.error.message)
|
||||||
|
state.isResetPasswordLoading = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { login, signup, getUser, forgotPassword, changePassword, reset, inviteUser }
|
||||||
|
|
||||||
|
export const { logout } = loginSlice.actions;
|
||||||
|
|
||||||
|
export default loginSlice.reducer;
|
380
src/redux/slices/forecast.js
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { Axios } from '../../api/instances';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
let fileDownload = require('js-file-download');
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isGeneratingReport: false,
|
||||||
|
isDownloadingReport: false,
|
||||||
|
isExchangeRateLoading: false,
|
||||||
|
isUpdateExchangeRateLoading: false,
|
||||||
|
isExchangeRateListLoading: false,
|
||||||
|
isBankAccountsLoading: false,
|
||||||
|
isSynchronizing: false,
|
||||||
|
rate: {
|
||||||
|
id: null,
|
||||||
|
latest: null,
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
forecastInfo: {
|
||||||
|
forecastNumber: 3,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
},
|
||||||
|
forecastDropdown: {
|
||||||
|
durations: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
forecastNumber: 1,
|
||||||
|
label: 'Current Month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
forecastNumber: 2,
|
||||||
|
label: 'Next Month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
forecastNumber: 3,
|
||||||
|
label: '3 Months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
forecastNumber: 6,
|
||||||
|
label: '6 Months',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
forecastPeriod: 'month',
|
||||||
|
forecastNumber: 12,
|
||||||
|
label: '12 Months',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedPeriod: 2,
|
||||||
|
},
|
||||||
|
report: {
|
||||||
|
openingBalance: {
|
||||||
|
naira: null,
|
||||||
|
dollar: null,
|
||||||
|
},
|
||||||
|
totalCashInflow: {
|
||||||
|
naira: null,
|
||||||
|
dollar: null,
|
||||||
|
},
|
||||||
|
totalCashOutflow: {
|
||||||
|
naira: null,
|
||||||
|
dollar: null,
|
||||||
|
},
|
||||||
|
closingBalance: {
|
||||||
|
naira: null,
|
||||||
|
dollar: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invoices: [],
|
||||||
|
bills: [],
|
||||||
|
sales: [],
|
||||||
|
purchases: [],
|
||||||
|
rates: [],
|
||||||
|
bankAccounts: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBankAccounts = createAsyncThunk('/bank-accounts', async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.get('zoho/bank/accounts', options);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getExchangeRates = createAsyncThunk('/rates', async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.get('zoho/exchange/rate', options);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const exchangeRate = createAsyncThunk(
|
||||||
|
'rate',
|
||||||
|
async ({ forecastNumber, forecastPeriod }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.get(
|
||||||
|
`/zoho/exchange/rate/${forecastNumber}/${forecastPeriod}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateExchangeRate = createAsyncThunk(
|
||||||
|
'updateRate',
|
||||||
|
async ({ id, latestRate, forecastNumber, forecastPeriod }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem('zohoAccessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('zohoAccessToken'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.put(
|
||||||
|
`/zoho/exchange/rate/${id}`,
|
||||||
|
{
|
||||||
|
forecastPeriod,
|
||||||
|
forecastNumber,
|
||||||
|
zohoAccessToken,
|
||||||
|
latest: latestRate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateReport = createAsyncThunk(
|
||||||
|
'generate',
|
||||||
|
async ({ id, forecastNumber, forecastPeriod }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem('zohoAccessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('zohoAccessToken'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.post(
|
||||||
|
'/zoho/generate/report',
|
||||||
|
{
|
||||||
|
forecastNumber,
|
||||||
|
forecastPeriod,
|
||||||
|
zohoAccessToken,
|
||||||
|
download: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = {
|
||||||
|
id,
|
||||||
|
forecastNumber,
|
||||||
|
forecastPeriod,
|
||||||
|
data: res.data.data,
|
||||||
|
};
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadReport = createAsyncThunk(
|
||||||
|
'download',
|
||||||
|
async ({ forecastNumber, forecastPeriod, filename }) => {
|
||||||
|
try {
|
||||||
|
let zohoAccessToken = localStorage.getItem('zohoAccessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('zohoAccessToken'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = Axios.post(
|
||||||
|
'/zoho/generate/report',
|
||||||
|
{
|
||||||
|
zohoAccessToken,
|
||||||
|
forecastNumber,
|
||||||
|
forecastPeriod,
|
||||||
|
download: true,
|
||||||
|
},
|
||||||
|
{ responseType: 'arraybuffer' }
|
||||||
|
).then((response) => {
|
||||||
|
const blob = new Blob([response.data], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
|
||||||
|
fileDownload(blob, `${filename}.xlsx`); //
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const resynApplication = createAsyncThunk('/resync', async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.delete('/zoho/forecast/resync', options);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const forecastSlice = createSlice({
|
||||||
|
name: 'forecast',
|
||||||
|
initialState,
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(generateReport.pending, (state, action) => {
|
||||||
|
state.isGeneratingReport = true;
|
||||||
|
})
|
||||||
|
.addCase(generateReport.fulfilled, (state, action) => {
|
||||||
|
state.invoices = action.payload.data.invoices;
|
||||||
|
state.sales = action.payload.data.sales;
|
||||||
|
state.bills = action.payload.data.bills;
|
||||||
|
state.purchases = action.payload.data.purchases;
|
||||||
|
state.forecastDropdown.selectedPeriod = action.payload.id;
|
||||||
|
state.forecastInfo.forecastPeriod = action.payload.forecastPeriod;
|
||||||
|
state.forecastInfo.forecastNumber = action.payload.forecastNumber;
|
||||||
|
state.report.openingBalance = action.payload.data.report.openingBalance;
|
||||||
|
state.report.totalCashInflow =
|
||||||
|
action.payload.data.report.totalCashInflow;
|
||||||
|
state.report.totalCashOutflow =
|
||||||
|
action.payload.data.report.totalCashOutflow;
|
||||||
|
state.report.closingBalance = action.payload.data.report.closingBalance;
|
||||||
|
state.isGeneratingReport = false;
|
||||||
|
})
|
||||||
|
.addCase(generateReport.rejected, (state, action) => {
|
||||||
|
toast.warning('An error occured while generating report', {
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
state.isGeneratingReport = false;
|
||||||
|
})
|
||||||
|
.addCase(downloadReport.pending, (state, action) => {
|
||||||
|
state.isDownloadingReport = true;
|
||||||
|
})
|
||||||
|
.addCase(downloadReport.fulfilled, (state, action) => {
|
||||||
|
state.isDownloadingReport = false;
|
||||||
|
toast.success('Report downloaded', { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(downloadReport.rejected, (state, action) => {
|
||||||
|
state.isDownloadingReport = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(exchangeRate.pending, (state, action) => {
|
||||||
|
state.isExchangeRateLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(exchangeRate.fulfilled, (state, action) => {
|
||||||
|
state.rate = action.payload;
|
||||||
|
state.isExchangeRateLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(exchangeRate.rejected, (state, action) => {
|
||||||
|
state.isExchangeRateLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(updateExchangeRate.pending, (state, action) => {
|
||||||
|
state.isUpdateExchangeRateLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(updateExchangeRate.fulfilled, (state, action) => {
|
||||||
|
state.rate = action.payload;
|
||||||
|
state.isUpdateExchangeRateLoading = false;
|
||||||
|
toast.success('Exchange rate updated', { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(updateExchangeRate.rejected, (state, action) => {
|
||||||
|
state.isUpdateExchangeRateLoading = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(getExchangeRates.pending, (state, action) => {
|
||||||
|
state.isExchangeRateListLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getExchangeRates.fulfilled, (state, action) => {
|
||||||
|
state.isExchangeRateListLoading = false;
|
||||||
|
state.rates = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(getExchangeRates.rejected, (state, action) => {
|
||||||
|
state.isExchangeRateListLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(getBankAccounts.pending, (state, action) => {
|
||||||
|
state.isBankAccountsLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getBankAccounts.fulfilled, (state, action) => {
|
||||||
|
state.isBankAccountsLoading = false;
|
||||||
|
state.bankAccounts = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(getBankAccounts.rejected, (state, action) => {
|
||||||
|
state.isBankAccountsLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(resynApplication.pending, (state, action) => {
|
||||||
|
state.isSynchronizing = true;
|
||||||
|
})
|
||||||
|
.addCase(resynApplication.fulfilled, (state, action) => {
|
||||||
|
state.isSynchronizing = false;
|
||||||
|
toast.success('resynced', { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(resynApplication.rejected, (state, action) => {
|
||||||
|
state.isSynchronizing = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
generateReport,
|
||||||
|
downloadReport,
|
||||||
|
exchangeRate,
|
||||||
|
updateExchangeRate,
|
||||||
|
getExchangeRates,
|
||||||
|
getBankAccounts,
|
||||||
|
resynApplication,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default forecastSlice.reducer;
|
75
src/redux/slices/inventory.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { Axios } from '../../api/instances';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isItemsLoading: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getInventory = createAsyncThunk('item', async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('accessToken'))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem('zohoAccessToken')
|
||||||
|
? JSON.parse(localStorage.getItem('zohoAccessToken'))
|
||||||
|
: null;
|
||||||
|
const res = await Axios.post(
|
||||||
|
"/zoho/getinventory",
|
||||||
|
{"accessToken":zohoAccessToken},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.clear()
|
||||||
|
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
||||||
|
console.log(res)
|
||||||
|
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const inventorySlice = createSlice({
|
||||||
|
name: 'inventory',
|
||||||
|
initialState,
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(getInventory.pending, (state, action) => {
|
||||||
|
state.isItemsLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getInventory.fulfilled, (state, action) => {
|
||||||
|
console.log(typeof action.payload,"problem")
|
||||||
|
state.items = action.payload;
|
||||||
|
state.isItemsLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(getInventory.rejected, (state, action) => {
|
||||||
|
toast.warning('An error occured ', {
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
state.isItemsLoading = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
getInventory
|
||||||
|
};
|
||||||
|
|
||||||
|
export default inventorySlice.reducer;
|
340
src/redux/slices/nonbill.js
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
|
import {Axios} from "../../api/instances";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
//import axios from "axios";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isExpensesLoading: false,
|
||||||
|
error: null,
|
||||||
|
expenses: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getNonbillableExpense = createAsyncThunk(
|
||||||
|
'expense',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem("zohoAccessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("zohoAccessToken"))
|
||||||
|
: null;
|
||||||
|
const response = await Axios.get(
|
||||||
|
"/zoho/bank/getallaccrued",
|
||||||
|
{ "accessToken": zohoAccessToken },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const getPrepaidExpense = createAsyncThunk(
|
||||||
|
'getPrepaidExpense',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem("zohoAccessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("zohoAccessToken"))
|
||||||
|
: null;
|
||||||
|
const response = await Axios.get(
|
||||||
|
"/zoho/bank/getallprepaidexp",
|
||||||
|
{ "accessToken": zohoAccessToken },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const postNonbillableExpense = createAsyncThunk(
|
||||||
|
'expense',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let zohoAccessToken = localStorage.getItem("zohoAccessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("zohoAccessToken"))
|
||||||
|
: null;
|
||||||
|
const response = await Axios.post(
|
||||||
|
"/zoho/bank/getallaccrued",
|
||||||
|
{ "accessToken": zohoAccessToken },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nonbillSlice = createSlice({
|
||||||
|
name: "nonbill",
|
||||||
|
initialState,
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(getNonbillableExpense.pending, (state, action) => {
|
||||||
|
state.isExpensesLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getNonbillableExpense.fulfilled, (state, action) => {
|
||||||
|
state.isExpensesLoading = false;
|
||||||
|
state.expenses = action.payload;
|
||||||
|
console.log("fulfilled", action.payload);
|
||||||
|
|
||||||
|
})
|
||||||
|
.addCase(getNonbillableExpense.rejected, (state, action) => {
|
||||||
|
toast.warning("An error occurred", {
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
state.isExpensesLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(getPrepaidExpense.pending, (state, action) => {
|
||||||
|
state.isExpensesLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getPrepaidExpense.fulfilled, (state, action) => {
|
||||||
|
state.isExpensesLoading = false;
|
||||||
|
state.expenses = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(getPrepaidExpense.rejected, (state, action) => {
|
||||||
|
toast.warning("An error occurred while fetching prepaid expenses", {
|
||||||
|
autoClose: 2000,
|
||||||
|
});
|
||||||
|
state.isExpensesLoading = false;
|
||||||
|
state.error = action.error.message || "Failed to fetch prepaid expenses";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { getNonbillableExpense,getPrepaidExpense };
|
||||||
|
|
||||||
|
export default nonbillSlice.reducer;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
121
src/redux/slices/user.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Axios } from '../../api/instances';
|
||||||
|
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
users: [],
|
||||||
|
isUsersLoading: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUsers = createAsyncThunk('/users', async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await Axios.get('/users', options);
|
||||||
|
|
||||||
|
return res.data.data
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAdminStatus = createAsyncThunk('/updateAdminStatus', async ({ user }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await Axios.patch('/users/role/update', {
|
||||||
|
role: user.role, email: user.email
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
return res.data.data
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteUser = createAsyncThunk('/deleteUser', async ({ email }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem('accessToken') ? JSON.parse(localStorage.getItem('accessToken')) : null
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Axios.delete(`/users/delete/${email}`, options);
|
||||||
|
|
||||||
|
// return res.data.data
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const userSlice = createSlice({
|
||||||
|
name: "user",
|
||||||
|
initialState,
|
||||||
|
// reducers: {
|
||||||
|
// deleteUser: (state) => {
|
||||||
|
// state.value += 1
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(getUsers.pending, (state, action) => {
|
||||||
|
state.isUsersLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getUsers.fulfilled, (state, action) => {
|
||||||
|
state.isUsersLoading = false;
|
||||||
|
state.users = action.payload
|
||||||
|
})
|
||||||
|
.addCase(getUsers.rejected, (state, action) => {
|
||||||
|
state.isUsersLoading = false;
|
||||||
|
state.error = action.error;
|
||||||
|
})
|
||||||
|
// .addCase(updateAdminStatus.pending, (state, action) => {
|
||||||
|
|
||||||
|
// })
|
||||||
|
.addCase(updateAdminStatus.fulfilled, (state, action) => {
|
||||||
|
toast.success('Successful', { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(updateAdminStatus.rejected, (state, action) => {
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
// .addCase(deleteUser.pending, (state, action) => {
|
||||||
|
|
||||||
|
// })
|
||||||
|
.addCase(deleteUser.fulfilled, (state, action) => {
|
||||||
|
toast.success('Successful', { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
.addCase(deleteUser.rejected, (state, action) => {
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// export const deleteUserAsync = (data) => async (dispatch) => {
|
||||||
|
// try{
|
||||||
|
// const res = await Axios.delete();
|
||||||
|
// dispatch(getUser())
|
||||||
|
// }catch(e){
|
||||||
|
// throw new Error(e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export { getUsers, updateAdminStatus, deleteUser }
|
||||||
|
|
||||||
|
|
||||||
|
export default userSlice.reducer;
|
348
src/redux/slices/zoho.js
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
|
import { Axios } from "../../api/instances";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
let fileDownload = require("js-file-download");
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
isLoading: false,
|
||||||
|
isOverdraftLoading: false,
|
||||||
|
isOverdraftUpdateLoading: false,
|
||||||
|
error: null,
|
||||||
|
zohoAccessToken: "",
|
||||||
|
zohoRefreshToken: "",
|
||||||
|
overdrafts: [],
|
||||||
|
charts: {
|
||||||
|
months: [],
|
||||||
|
forecastNairaInflow: [],
|
||||||
|
forecastNairaOutflow: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoho = createAsyncThunk("zoho", async ({ code }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.post(
|
||||||
|
"/zoho/token/generate",
|
||||||
|
{
|
||||||
|
code: code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const zohoRefresh = createAsyncThunk("zoho/refresh", async () => {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.get("/zoho/token/refresh", options);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadExchangeRate = createAsyncThunk(
|
||||||
|
"zoho/exchange/rate",
|
||||||
|
async () => {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await Axios.post("/zoho/exchange/rate/download", options, {
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
}).then((response) => {
|
||||||
|
const blob = new Blob([response.data], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
|
||||||
|
fileDownload(blob, `rate.xlsx`); //
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadOpeningBalance = createAsyncThunk(
|
||||||
|
"zoho/opening/balance",
|
||||||
|
async () => {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await Axios.post(
|
||||||
|
"zoho/bank/opening-balance/download",
|
||||||
|
options,
|
||||||
|
{
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
}
|
||||||
|
).then((response) => {
|
||||||
|
const blob = new Blob([response.data], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
|
||||||
|
fileDownload(blob, `opening-balance.xlsx`); //
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const createOverdraft = createAsyncThunk(
|
||||||
|
"/overdraft/create",
|
||||||
|
async ({ data }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.post(
|
||||||
|
"/zoho/overdraft",
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getOverdrafts = createAsyncThunk("/overdrafts", async () => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.get("zoho/overdraft", options);
|
||||||
|
console.log(res,">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCharts = createAsyncThunk("/charts", async ({ forecastNumber, forecastPeriod }) => {
|
||||||
|
try {
|
||||||
|
console.log('calling you');
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await Axios.post(
|
||||||
|
"/zoho/chart",
|
||||||
|
{
|
||||||
|
forecastNumber,
|
||||||
|
forecastPeriod,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("first the request", res.data.data);
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOverdraft = createAsyncThunk(
|
||||||
|
"/overdraft/update",
|
||||||
|
async ({ data }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await Axios.put(
|
||||||
|
`/zoho/overdraft/${data.accountId}`,
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteOverdraft = createAsyncThunk(
|
||||||
|
"/deleteOverdraft",
|
||||||
|
async ({ accountId }) => {
|
||||||
|
try {
|
||||||
|
let accessToken = localStorage.getItem("accessToken")
|
||||||
|
? JSON.parse(localStorage.getItem("accessToken"))
|
||||||
|
: null;
|
||||||
|
let options = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await Axios.delete(`/zoho/overdraft/${accountId}`, options);
|
||||||
|
|
||||||
|
// return res.data.data
|
||||||
|
} catch (error) {
|
||||||
|
throw error.response.data || error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const zohoSlice = createSlice({
|
||||||
|
name: "zoho",
|
||||||
|
initialState,
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(zoho.pending, (state, action) => {
|
||||||
|
state.isLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(zoho.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
localStorage.setItem("zohoAuthenticated", true);
|
||||||
|
localStorage.setItem(
|
||||||
|
"zohoAccessToken",
|
||||||
|
JSON.stringify(action.payload.zohoAccessToken)
|
||||||
|
);
|
||||||
|
localStorage.setItem(
|
||||||
|
"zohoTokenExpiry",
|
||||||
|
JSON.stringify(action.payload.zohoTokenExpiry)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.addCase(zoho.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.error;
|
||||||
|
})
|
||||||
|
.addCase(zohoRefresh.pending, (state, action) => {
|
||||||
|
state.isLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(zohoRefresh.fulfilled, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
localStorage.setItem("zohoAuthenticated", true);
|
||||||
|
localStorage.setItem(
|
||||||
|
"zohoAccessToken",
|
||||||
|
JSON.stringify(action.payload.zohoAccessToken)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.addCase(zohoRefresh.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.error;
|
||||||
|
})
|
||||||
|
.addCase(downloadExchangeRate.pending, (state, action) => {})
|
||||||
|
.addCase(downloadExchangeRate.fulfilled, (state, action) => {})
|
||||||
|
.addCase(downloadExchangeRate.rejected, (state, action) => {})
|
||||||
|
.addCase(createOverdraft.pending, (state, action) => {
|
||||||
|
state.isOverdraftLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(createOverdraft.fulfilled, (state, action) => {
|
||||||
|
state.isOverdraftLoading = false;
|
||||||
|
toast.success("Successful", { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(createOverdraft.rejected, (state, action) => {
|
||||||
|
state.isOverdraftLoading = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(getOverdrafts.pending, (state, action) => {
|
||||||
|
state.isOverdraftLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getOverdrafts.fulfilled, (state, action) => {
|
||||||
|
state.isOverdraftLoading = false;
|
||||||
|
state.overdrafts = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(getOverdrafts.rejected, (state, action) => {
|
||||||
|
state.isOverdraftLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(getCharts.pending, (state, action) => {
|
||||||
|
state.isChartLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(getCharts.fulfilled, (state, action) => {
|
||||||
|
state.isChartLoading = false;
|
||||||
|
state.charts = action.payload;
|
||||||
|
console.log('chating', state.charts);
|
||||||
|
})
|
||||||
|
.addCase(getCharts.rejected, (state, action) => {
|
||||||
|
state.isChartLoading = false;
|
||||||
|
})
|
||||||
|
.addCase(deleteOverdraft.fulfilled, (state, action) => {
|
||||||
|
toast.success("Deleted successfully", { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(deleteOverdraft.rejected, (state, action) => {
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(updateOverdraft.pending, (state, action) => {
|
||||||
|
state.isOverdraftUpdateLoading = true;
|
||||||
|
})
|
||||||
|
.addCase(updateOverdraft.fulfilled, (state, action) => {
|
||||||
|
state.isOverdraftUpdateLoading = false;
|
||||||
|
toast.success("Overdraft loan updated", { autoClose: 2000 });
|
||||||
|
})
|
||||||
|
.addCase(updateOverdraft.rejected, (state, action) => {
|
||||||
|
state.isOverdraftUpdateLoading = false;
|
||||||
|
toast.error(action.error.message, { autoClose: 2000 });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
zoho,
|
||||||
|
zohoRefresh,
|
||||||
|
downloadExchangeRate,
|
||||||
|
downloadOpeningBalance,
|
||||||
|
createOverdraft,
|
||||||
|
getOverdrafts,
|
||||||
|
updateOverdraft,
|
||||||
|
deleteOverdraft,
|
||||||
|
getCharts
|
||||||
|
};
|
||||||
|
|
||||||
|
export default zohoSlice.reducer;
|
24
src/redux/store.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
|
import authReducer from "./slices/auth"
|
||||||
|
import zohoReducer from "./slices/zoho"
|
||||||
|
import userReducer from "./slices/user"
|
||||||
|
import forecastReducer from "./slices/forecast"
|
||||||
|
import inventoryReducer from "./slices/inventory"
|
||||||
|
import nonbillReducer from "./slices/nonbill"
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
auth: authReducer,
|
||||||
|
zoho: zohoReducer,
|
||||||
|
user: userReducer,
|
||||||
|
forecast: forecastReducer,
|
||||||
|
inventory: inventoryReducer,
|
||||||
|
nonbill : nonbillReducer
|
||||||
|
},
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { store }
|
13
src/reportWebVitals.js
Normal 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;
|
95
src/routes/appRoutes.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import Login from '../pages/Login';
|
||||||
|
import Dashboard from '../pages/Dashboard';
|
||||||
|
import User from '../pages/User';
|
||||||
|
import Setting from '../pages/Setting';
|
||||||
|
import ForgotPassword from '../pages/ForgotPassword';
|
||||||
|
import ResetPassword from '../pages/ResetPassword';
|
||||||
|
import Register from '../pages/Register';
|
||||||
|
import Rate from '../pages/Rate';
|
||||||
|
import OpeningBalance from '../pages/OpeningBalance';
|
||||||
|
import Overdraft from '../pages/Overdraft';
|
||||||
|
import Inventories from '../pages/Inventories';
|
||||||
|
import Expenses from '../pages/Expenses';
|
||||||
|
import Report from '../pages/Report'
|
||||||
|
import InventoriesTable from '../components/InventoriesTable';
|
||||||
|
import Item from '../pages/Item';
|
||||||
|
import Expense from '../pages/Expense';
|
||||||
|
import Prepayed from '../pages/Prepayed';
|
||||||
|
export const appRoutes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
key: '/login',
|
||||||
|
component: Login,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register/:id',
|
||||||
|
key: '/register/:id',
|
||||||
|
component: Register,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/forgot-password',
|
||||||
|
key: '/forgot-password',
|
||||||
|
component: ForgotPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reset-password/:id',
|
||||||
|
key: '/reset-password/:id',
|
||||||
|
component: ResetPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
key: '/',
|
||||||
|
component: Dashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
key: '/user',
|
||||||
|
component: User,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/exchange-rates',
|
||||||
|
key: '/exchange-rates',
|
||||||
|
component: Rate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/opening-balance',
|
||||||
|
key: '/opening-balance',
|
||||||
|
component: OpeningBalance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/inventories',
|
||||||
|
key: '/inventories',
|
||||||
|
component: Inventories,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/expense',
|
||||||
|
key: '/expense',
|
||||||
|
component: Expense,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting',
|
||||||
|
key: '/setting',
|
||||||
|
component: Setting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/overdraft',
|
||||||
|
key: '/overdraft',
|
||||||
|
component: Overdraft,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/report',
|
||||||
|
key: '/report',
|
||||||
|
component: Report,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/item',
|
||||||
|
key: '/item',
|
||||||
|
component: Item,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/prepaid',
|
||||||
|
key: '/prepaid',
|
||||||
|
component: Prepayed,
|
||||||
|
},
|
||||||
|
];
|
48
src/useSortableTable.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function getDefaultSorting(defaultTableData, columns) {
|
||||||
|
const sorted = [...defaultTableData].sort((a, b) => {
|
||||||
|
const filterColumn = columns.filter((column) => column.sortbyOrder);
|
||||||
|
|
||||||
|
// Merge all array objects into single object and extract accessor and sortbyOrder keys
|
||||||
|
let { accessor = "id", sortbyOrder = "asc" } = Object.assign(
|
||||||
|
{},
|
||||||
|
...filterColumn
|
||||||
|
);
|
||||||
|
|
||||||
|
if (a[accessor] === null) return 1;
|
||||||
|
if (b[accessor] === null) return -1;
|
||||||
|
if (a[accessor] === null && b[accessor] === null) return 0;
|
||||||
|
|
||||||
|
const ascending = a[accessor]
|
||||||
|
.toString()
|
||||||
|
.localeCompare(b[accessor].toString(), "en", {
|
||||||
|
numeric: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortbyOrder === "asc" ? ascending : -ascending;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSortableTable = (data, columns) => {
|
||||||
|
const [tableData, setTableData] = useState(getDefaultSorting(data, columns));
|
||||||
|
|
||||||
|
const handleSorting = (sortField, sortOrder) => {
|
||||||
|
if (sortField) {
|
||||||
|
const sorted = [...tableData].sort((a, b) => {
|
||||||
|
if (a[sortField] === null) return 1;
|
||||||
|
if (b[sortField] === null) return -1;
|
||||||
|
if (a[sortField] === null && b[sortField] === null) return 0;
|
||||||
|
return (
|
||||||
|
a[sortField].toString().localeCompare(b[sortField].toString(), "en", {
|
||||||
|
numeric: true,
|
||||||
|
}) * (sortOrder === "asc" ? 1 : -1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setTableData(sorted);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [tableData, handleSorting];
|
||||||
|
};
|
5
src/utils/utils.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
let navigate;
|
||||||
|
|
||||||
|
const setNavigate = (navigateObj) => (navigate = navigateObj);
|
||||||
|
|
||||||
|
export { navigate, setNavigate };
|
10
tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: { 'DM Mono':"monospace"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|