initial commit

This commit is contained in:
Augustine Rapheal 2024-07-13 13:54:55 +01:00
commit 451e3468bd
87 changed files with 32771 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_BASE_URL=
REACT_APP_MANIFOLD_API_URL=

20
.eslintrc.json Normal file
View 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
View 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
View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@ -0,0 +1,5 @@
module.exports = {
"fontawesome-svg-core": {
license: "free",
},
};

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache.forever();
return {
plugins: ["macros"],
};
};

22569
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

78
package.json Normal file
View 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
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

23
public/index.html Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

BIN
src/assets/down_arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

BIN
src/assets/first-image.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

BIN
src/assets/up_arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

141
src/components/BarChart.js Normal file
View 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;

View 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
View 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;

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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
View 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;

View 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
View 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);

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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
View 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);

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</span>
)}{" "}
<CurrencyFormat
value={parseFloat(data.overdraftBalance)}
displayType={"text"}
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
}
return <td key={accessor}>{tData}</td>;
})}
</tr>
);
})}
</tbody>
);
};
export default InventoriesTableBody;

View File

@ -0,0 +1,12 @@
import React from 'react'
const InventoryTable = () => {
return (
<div>
</div>
)
}
export default InventoryTable

View 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;

View 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">
&#8358;
</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</span>
)}{" "}
<CurrencyFormat
value={parseFloat(data.balance)}
displayType={"text"}
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
}
return <td key={accessor}>{tData}</td>;
})}
</tr>
);
})}
</tbody>
</>
);
};
export default InvoiceTableBody;

View File

159
src/components/LineChart.js Normal file
View 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
View 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">&#8203;</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
View 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;

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</span>
)}{" "}
<CurrencyFormat
value={parseFloat(data.overdraftBalance)}
displayType={"text"}
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
}
return <td key={accessor}>{tData}</td>;
})}
</tr>
);
})}
</tbody>
);
};
export default OpeningBalanceTableBody;

View File

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</span>
)}{" "}
<CurrencyFormat
value={parseFloat(data.balance)}
displayType={"text"}
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
}
return <td key={accessor}>{tData}</td>;
})}
</tr>
);
})}
</tbody>
</>
);
};
export default PurchaseTableBody;

View 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;

View 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">&#8358;</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</span>
)}{" "}
<CurrencyFormat
value={parseFloat(data.overdraftBalance)}
displayType={"text"}
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
}
return <td key={accessor}>{tData}</td>;
})}
</tr>
);
})}
</tbody>
);
};
export default ReportTableBody;

View 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;

View 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">
&#8358;
</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">&#36;</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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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
View 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">&#8358;</span>
)}
{currencyCode === "USD" && (
<span className="font-semibold text-sm">&#36;</span>
)}
<CurrencyFormat
value={parseFloat(currencyBalance)}
displayType="text"
thousandSeparator={true}
decimalScale={2}
/>
</td>
);
};

343
src/components/Sidebar.js Normal file
View 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;

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

500
src/pages/Expense.js Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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">
&#8358;
</span>
) : (
<span className="font-semibold text-sm text-black">
&#36;
</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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

95
src/routes/appRoutes.js Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
let navigate;
const setNavigate = (navigateObj) => (navigate = navigateObj);
export { navigate, setNavigate };

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
fontFamily: { 'DM Mono':"monospace"},
},
},
plugins: [],
};