First Commit
This commit is contained in:
6
frontend/.dockerignore
Normal file
6
frontend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
build
|
||||
.dockerignore
|
||||
**/.git
|
||||
**/.DS_Store
|
||||
**/node_modules
|
||||
1
frontend/.env.development
Normal file
1
frontend/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_API_URL=https://localhost:7021
|
||||
1
frontend/.env.docker
Normal file
1
frontend/.env.docker
Normal file
@@ -0,0 +1 @@
|
||||
DANGEROUSLY_DISABLE_HOST_CHECK=true
|
||||
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
23
frontend/.vscode/launch.json
vendored
Normal file
23
frontend/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"name": "Launch Firefox against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"breakOnLoad": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
frontend/Dockerfile
Normal file
16
frontend/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:14-alpine AS builder
|
||||
ENV NODE_ENV production
|
||||
# Add a work directory
|
||||
WORKDIR /src-frontend
|
||||
# Cache and Install dependencies
|
||||
COPY ./frontend/package.json .
|
||||
COPY ./frontend/package-lock.json .
|
||||
ARG REACT_APP_API_URL
|
||||
ENV REACT_APP_API_URL=$REACT_APP_API_URL
|
||||
RUN npm install
|
||||
# Copy app files
|
||||
COPY ./frontend/ .
|
||||
RUN mv ./public/locales/fr ./public/locales/FR
|
||||
RUN mv ./public/locales/en ./public/locales/EN
|
||||
# Build the app
|
||||
CMD ["npm", "run", "docker"]
|
||||
44
frontend/README.md
Normal file
44
frontend/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
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.<br />
|
||||
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.<br />
|
||||
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/).
|
||||
17521
frontend/package-lock.json
generated
Normal file
17521
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
frontend/package.json
Normal file
52
frontend/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"axios": "^1.3.2",
|
||||
"bootstrap": "5.2.3",
|
||||
"env-cmd": "^10.1.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"i18next": "22.4.9",
|
||||
"i18next-browser-languagedetector": "7.0.1",
|
||||
"i18next-xhr-backend": "3.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "2.7.0",
|
||||
"react-datepicker": "4.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "12.1.5",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "5.7.0",
|
||||
"react-toastify": "^9.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"docker": "env-cmd -f .env.docker react-scripts --max-http-header-size=2048 start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>GiecChallenge</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
5
frontend/public/locales/EN/Header.json
Normal file
5
frontend/public/locales/EN/Header.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"login" : "Login",
|
||||
"register" : "Register",
|
||||
"logout" : "Log out"
|
||||
}
|
||||
7
frontend/public/locales/EN/Home.json
Normal file
7
frontend/public/locales/EN/Home.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"createNewPurchase" : "Create new purchase",
|
||||
"createNewProduct" : "Create new product",
|
||||
"date": "Date",
|
||||
"CO2Cost": "CO2 cost",
|
||||
"WaterCost": "Water cost"
|
||||
}
|
||||
5
frontend/public/locales/EN/LineProduct.json
Normal file
5
frontend/public/locales/EN/LineProduct.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"product" : "Produit",
|
||||
"quantity" : "Quantity",
|
||||
"price" : "Price"
|
||||
}
|
||||
8
frontend/public/locales/EN/Login.json
Normal file
8
frontend/public/locales/EN/Login.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"login" : "Log in",
|
||||
"fillLogin" : "Please login to use the website",
|
||||
"logout" : "Log out",
|
||||
"emailPlaceholder" : "Enter your email",
|
||||
"passwordPlaceholder" : "Enter your password",
|
||||
"submit" : "Submit"
|
||||
}
|
||||
17
frontend/public/locales/EN/NewProduct.json
Normal file
17
frontend/public/locales/EN/NewProduct.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"addNewTranslation" : "Add another translation",
|
||||
"newGroup" : "New group",
|
||||
"group" : "Group",
|
||||
"groupSuccess" : "Group added",
|
||||
"newSubGroup" : "New sub group",
|
||||
"subGroup" : "Sub Group",
|
||||
"subGroupSuccess" : "Sub Group added",
|
||||
"newProduct" : "New product",
|
||||
"product" : "Product",
|
||||
"productSuccess" : "Product added",
|
||||
"language" : "Language",
|
||||
"CO2Emissions" : "CO2 Emissions",
|
||||
"waterEmissions" : "Water Emissions",
|
||||
"amortization" : "Amortization (in month)",
|
||||
"submit" : "Submit"
|
||||
}
|
||||
12
frontend/public/locales/EN/NewPurchase.json
Normal file
12
frontend/public/locales/EN/NewPurchase.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title" : "Create new purchase",
|
||||
"typeOfPurchase" : "Type of purchase",
|
||||
"manual" : "Manual",
|
||||
"command" : "Command",
|
||||
"product" : "Products",
|
||||
"addNewProduct" : "Add product",
|
||||
"datePurchase" : "Purchase date",
|
||||
"submit" : "Submit",
|
||||
"currency" : "Currency",
|
||||
"back" : "Back"
|
||||
}
|
||||
11
frontend/public/locales/EN/Purchase.json
Normal file
11
frontend/public/locales/EN/Purchase.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title" : "Purchase details",
|
||||
"addNewProduct" : "Add product",
|
||||
"product" : "Products",
|
||||
"CO2Cost" : "CO2 cost",
|
||||
"WaterCost" : "Water cost",
|
||||
"datePurchase" : "Purchase date",
|
||||
"submit" : "Submit",
|
||||
"currency" : "Currency",
|
||||
"back" : "Back"
|
||||
}
|
||||
5
frontend/public/locales/FR/Header.json
Normal file
5
frontend/public/locales/FR/Header.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"login" : "Connexion",
|
||||
"register" : "Inscription",
|
||||
"logout" : "Deconnexion"
|
||||
}
|
||||
7
frontend/public/locales/FR/Home.json
Normal file
7
frontend/public/locales/FR/Home.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"createNewPurchase" : "Créer une nouvelle liste d'achat",
|
||||
"createNewProduct" : "Créer un nouveau produit",
|
||||
"date": "Date",
|
||||
"CO2Cost": "Coût en CO2",
|
||||
"WaterCost": "Coût en eau"
|
||||
}
|
||||
5
frontend/public/locales/FR/LineProduct.json
Normal file
5
frontend/public/locales/FR/LineProduct.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"product" : "Produit",
|
||||
"quantity" : "Quantité",
|
||||
"price" : "Prix"
|
||||
}
|
||||
8
frontend/public/locales/FR/Login.json
Normal file
8
frontend/public/locales/FR/Login.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"login" : "Connexion",
|
||||
"fillLogin" : "Veuillez vous connecter pour accéder à l'application",
|
||||
"logout" : "Deconnexion",
|
||||
"emailPlaceholder" : "Saisissez votre email",
|
||||
"passwordPlaceholder" : "Saisissez votre mot de passe",
|
||||
"submit" : "Valider"
|
||||
}
|
||||
17
frontend/public/locales/FR/NewProduct.json
Normal file
17
frontend/public/locales/FR/NewProduct.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"newGroup" : "Nouveau groupe",
|
||||
"group" : "Groupe",
|
||||
"groupSuccess" : "Groupe ajouté",
|
||||
"addNewTranslation" : "Ajouter une traduction",
|
||||
"newSubGroup" : "Nouveau sous groupe",
|
||||
"subGroup" : "Sous groupe",
|
||||
"subGroupSuccess" : "Sous groupe ajouté",
|
||||
"newProduct" : "Nouveau produit",
|
||||
"product" : "Produit",
|
||||
"productSuccess" : "Produit ajouté",
|
||||
"language" : "Langue",
|
||||
"CO2Emissions" : "Emissions de CO2",
|
||||
"waterEmissions" : "Emissions d'eau",
|
||||
"amortization" : "Amortissement (en mois)",
|
||||
"submit" : "Valider"
|
||||
}
|
||||
12
frontend/public/locales/FR/NewPurchase.json
Normal file
12
frontend/public/locales/FR/NewPurchase.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title" : "Créer une nouvelle liste d'achat",
|
||||
"typeOfPurchase" : "Type de liste d'achat",
|
||||
"manual" : "Manuel",
|
||||
"command" : "Commande",
|
||||
"product" : "Produits",
|
||||
"addNewProduct" : "Ajouter un produit",
|
||||
"datePurchase" : "Date d'achat",
|
||||
"submit" : "Valider",
|
||||
"currency" : "Devise",
|
||||
"back" : "Retour"
|
||||
}
|
||||
11
frontend/public/locales/FR/Purchase.json
Normal file
11
frontend/public/locales/FR/Purchase.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title" : "Créer une nouvelle liste d'achat",
|
||||
"addNewProduct" : "Ajouter un produit",
|
||||
"product" : "Produits",
|
||||
"CO2Cost" : "Coût en CO2",
|
||||
"WaterCost" : "Coût en eau",
|
||||
"datePurchase" : "Date d'achat",
|
||||
"submit" : "Valider",
|
||||
"currency" : "Devise",
|
||||
"back" : "Retour"
|
||||
}
|
||||
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
2
frontend/public/robots.txt
Normal file
2
frontend/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
43
frontend/src/App.js
Normal file
43
frontend/src/App.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'
|
||||
import Header from './components/Header';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import NewPurchase from './pages/NewPurchase';
|
||||
import NewProduct from './pages/NewProduct';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import Purchase from './pages/Purchase';
|
||||
import {ToastContainer} from 'react-toastify'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Router>
|
||||
<div className='container'>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<PrivateRoute />}>
|
||||
<Route path="/" element={<Home />} />
|
||||
</Route>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/new-purchase" element={<PrivateRoute />}>
|
||||
<Route path="/new-purchase" element={<NewPurchase />} />
|
||||
</Route>
|
||||
<Route path="/new-product" element={<PrivateRoute />}>
|
||||
<Route path="/new-product" element={<NewProduct />} />
|
||||
</Route>
|
||||
<Route path="/purchase/:id" element={<PrivateRoute />}>
|
||||
<Route path="/purchase/:id" element={<Purchase />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
20
frontend/src/app/store.js
Normal file
20
frontend/src/app/store.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from '../features/auth/authSlice';
|
||||
import productReducer from '../features/products/productSlice';
|
||||
import purchaseReducer from '../features/purchases/purchaseSlice';
|
||||
import currencyReducer from '../features/currencies/currencySlice';
|
||||
import languageReducer from '../features/languages/languageSlice';
|
||||
import groupReducer from '../features/groups/groupSlice';
|
||||
import subGroupReducer from '../features/subgroups/subGroupSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
product: productReducer,
|
||||
purchase: purchaseReducer,
|
||||
currency: currencyReducer,
|
||||
language: languageReducer,
|
||||
group: groupReducer,
|
||||
subgroup: subGroupReducer
|
||||
},
|
||||
});
|
||||
16
frontend/src/components/BackButton.jsx
Normal file
16
frontend/src/components/BackButton.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import {FaArrowCircleLeft} from 'react-icons/fa'
|
||||
import {Link} from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BackButton = ({url}) => {
|
||||
const { t } = useTranslation(["Purchase"]);
|
||||
|
||||
return (
|
||||
<Link className="btn btn-reverse btn-black" to={url}>
|
||||
<FaArrowCircleLeft /> {t("back")}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackButton
|
||||
61
frontend/src/components/Header.jsx
Normal file
61
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import {FaSignInAlt, FaSignOutAlt, FaUser} from 'react-icons/fa'
|
||||
import {Link, useNavigate} from 'react-router-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {logout, reset} from '../features/auth/authSlice'
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function Header() {
|
||||
const {user} = useSelector(state => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation(["Header"]);
|
||||
|
||||
const onLogout = async() => {
|
||||
await dispatch(logout())
|
||||
dispatch(reset())
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="logo">
|
||||
<Link to='/'>GiecChallenge</Link>
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
!user ? (
|
||||
<>
|
||||
<li>
|
||||
<Link to='/login'>
|
||||
<FaSignInAlt /> {t("login")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to='/register'>
|
||||
<FaUser /> {t("register")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<LanguageSelector />
|
||||
</li>
|
||||
</>) : (
|
||||
<>
|
||||
<li>
|
||||
<a onClick={onLogout} href='/'>
|
||||
<FaSignOutAlt /> {t("logout")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<LanguageSelector />
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
77
frontend/src/components/Home/HomePurchase.jsx
Normal file
77
frontend/src/components/Home/HomePurchase.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import ProgressBar from 'react-bootstrap/ProgressBar';
|
||||
import { reset, getpurchasesbydate, deletepurchase, getCO2bydate} from '../../features/purchases/purchaseSlice'
|
||||
import {LinePurchase} from './LinePurchase'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const HomePurchase = () => {
|
||||
const { isLoading, purchases, CO2Emissions } = useSelector((state) => state.purchase)
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState(new Date());
|
||||
const [purchasesUser, setPurchasesUser] = useState([]);
|
||||
const [CO2EmissionsPurcentage, setCO2EmissionsPurcentage] = useState(0.0);
|
||||
const { t } = useTranslation(["Home"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setPurchasesUser(purchases);
|
||||
setCO2EmissionsPurcentage(CO2Emissions / (2500 / 365 * (endDate.getDate() - startDate.getDate() + 1)) * 100);
|
||||
}, [purchases, CO2Emissions])
|
||||
|
||||
useEffect(() => {
|
||||
changeDate();
|
||||
}, [startDate, endDate])
|
||||
|
||||
function changeDate() {
|
||||
dispatch(reset())
|
||||
dispatch(getpurchasesbydate({
|
||||
startDate: startDate.toLocaleDateString("es-CL"),
|
||||
endDate: endDate.toLocaleDateString("es-CL")
|
||||
}));
|
||||
dispatch(getCO2bydate({
|
||||
startDate: startDate.toLocaleDateString("es-CL"),
|
||||
endDate: endDate.toLocaleDateString("es-CL")
|
||||
}));
|
||||
}
|
||||
|
||||
const deletePurchase = (p) => {
|
||||
dispatch(reset());
|
||||
dispatch(deletepurchase(p)).then(
|
||||
changeDate()
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<div className='mbottom-10'>
|
||||
<ProgressBar now={CO2EmissionsPurcentage} label={`${CO2EmissionsPurcentage}%`} />
|
||||
</div>
|
||||
<div style={{display: "flex"}} className="form-group">
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={startDate} className="form-control" onChange={(date) => {setStartDate(date);}} />
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={endDate} className="form-control" onChange={(date) => {setEndDate(date);}} />
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='form-group width-20'>{t("date")}</div>
|
||||
<div className='form-group width-20'>{t("CO2Cost")}</div>
|
||||
<div className='form-group width-20'>{t("WaterCost")}</div>
|
||||
</div>
|
||||
{
|
||||
purchasesUser.map((purchaseUser, index) => {
|
||||
return (
|
||||
<LinePurchase key={index} purchaseUser={purchaseUser} onChange={(p) => deletePurchase(p)} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePurchase
|
||||
22
frontend/src/components/Home/LinePurchase.jsx
Normal file
22
frontend/src/components/Home/LinePurchase.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom'
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
export const LinePurchase = ({ purchaseUser, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div className='form-group width-20 inline-flex'>
|
||||
<Link to={`/purchase/${purchaseUser.id}`}>{new Date(Date.parse(purchaseUser.datePurchase)).toLocaleString("fr-FR", {month: '2-digit',day: '2-digit',year: 'numeric'})}</Link>
|
||||
</div>
|
||||
<div className='form-group width-20 inline-flex'>{parseFloat(purchaseUser.cO2Cost).toFixed(2)}</div>
|
||||
<div className='form-group width-20 inline-flex'>{parseFloat(purchaseUser.waterCost).toFixed(2)}</div>
|
||||
<div>
|
||||
<a onClick={() => onChange(purchaseUser.id)} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaTrash />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
22
frontend/src/components/LanguageSelector.jsx
Normal file
22
frontend/src/components/LanguageSelector.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const changeLanguage = (event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div onChange={changeLanguage}>
|
||||
<select name="language">
|
||||
<option value="FR">Français</option>
|
||||
<option value="EN">English</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
30
frontend/src/components/NewProduct/NewGroup/LineGroup.jsx
Normal file
30
frontend/src/components/NewProduct/NewGroup/LineGroup.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineGroup = ({ nameSelect, languageOptions, onChange }) => {
|
||||
const [group, setGroup] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
group,
|
||||
language,
|
||||
nameSelect
|
||||
});
|
||||
}, [onChange, group, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("group")}</label>
|
||||
<input type="language" name="group" id="group" className='form-control width-100' value={group} placeholder={t("group")} onChange={(e) => setGroup(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
92
frontend/src/components/NewProduct/NewGroup/NewGroup.jsx
Normal file
92
frontend/src/components/NewProduct/NewGroup/NewGroup.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/groups/groupSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineGroup } from './LineGroup'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NewGroup = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.group)
|
||||
const [groups, setGroups] = useState([{key: 0, name: "group0" }])
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
const [lineGroupsData, setLineGroupsData] = useState([])
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setGroups([{key: 0, name: "group0" }])
|
||||
setLineGroupsData([])
|
||||
toast.success(t("groupSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newGroup = {
|
||||
names: lineGroupsData.map((groupLine) => (
|
||||
{
|
||||
name: groupLine.group,
|
||||
language: groupLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newGroup))
|
||||
}
|
||||
|
||||
const addOrModifyLineLanguage = (lineLanguage) => {
|
||||
if (languagesOption[0] !== undefined && lineLanguage.language === '')
|
||||
lineLanguage.language = languagesOption[0].key;
|
||||
let existingLine = lineGroupsData.findIndex(lpd => lpd.nameSelect === lineLanguage.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineGroupsData(lineGroupsData.concat(lineLanguage))
|
||||
else {
|
||||
lineGroupsData[existingLine] = lineLanguage
|
||||
setLineGroupsData(lineGroupsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewGroup = () => {
|
||||
setGroups(groups.concat([
|
||||
{key: groups.length, name: "group" + groups.length }
|
||||
]));
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newGroup")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
{
|
||||
groups.map((item) => (
|
||||
<LineGroup key={item.key} languageOptions={languagesOption} nameSelect={item.key} onChange={(e) => addOrModifyLineLanguage(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewGroup()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewGroup
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineProductItem = ({ nameSelected, languageOptions, onChange }) => {
|
||||
const [productName, setProductName] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
nameSelected,
|
||||
productName,
|
||||
language
|
||||
});
|
||||
}, [onChange, productName, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="product">{t("product")}</label>
|
||||
<input type="text" name="productName" id="productName" className='form-control width-100' value={productName} placeholder={t("product")} onChange={(e) => setProductName(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/products/productSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineProductItem } from './LineProductItem'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {SubGroupSearchBox} from '../../SearchBox/SubGroupSearchBox'
|
||||
|
||||
export const NewProductItem = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.product)
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [lineProductsData, setLineProductsData] = useState([{ nameSelected: "product0",productName: "", language: "" }])
|
||||
const [CO2Emissions, setCO2Emission] = useState('0')
|
||||
const [waterEmissions, setWaterEmission] = useState('0')
|
||||
const [amortization, setAmortization] = useState('0')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setLineProductsData([{nameSelected: "product0", productName: "", language: "" }])
|
||||
toast.success(t("productSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newProduct = {
|
||||
group : selectedValue,
|
||||
CO2: CO2Emissions,
|
||||
water: waterEmissions,
|
||||
amortization: amortization,
|
||||
names: lineProductsData.map((productLine) => (
|
||||
{
|
||||
name: productLine.productName,
|
||||
language: productLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newProduct))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
if (languagesOption[0] !== undefined && lineProduct.language === '')
|
||||
lineProduct.language = languagesOption[0].key;
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.nameSelected === lineProduct.nameSelected);
|
||||
if (existingLine === -1) {
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
}
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
addOrModifyLineProduct(
|
||||
{ nameSelected: "product" + lineProductsData.length, productName: "", language: "" }
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newProduct")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<label htmlFor="product">{t("product")}</label>
|
||||
<SubGroupSearchBox key="subGroupSelected" className='mbottom-10 width-100' nameSelect="subGroupSelected" onChange={(e) => setSelectedValue(e)} />
|
||||
<label htmlFor="CO2Emissions">{t("CO2Emissions")}</label>
|
||||
<input type="text" key="CO2Emissions" className='mbottom-10 width-100' onChange={(e) => setCO2Emission(e.target.value)} />
|
||||
<label htmlFor="WaterEmissions">{t("waterEmissions")}</label>
|
||||
<input type="text" key="waterEmissions" className='mbottom-10 width-100'onChange={(e) => setWaterEmission(e.target.value)} />
|
||||
<label htmlFor="amortization">{t("amortization")}</label>
|
||||
<input type="text" key="amortization" className='mbottom-10 width-100'onChange={(e) => setAmortization(e.target.value)} />
|
||||
{
|
||||
lineProductsData.map((item) => (
|
||||
<LineProductItem key={item.nameSelected} languageOptions={languagesOption} nameSelected={item.nameSelected} onChange={(e) => addOrModifyLineProduct(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewProductItem
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LineSubGroup = ({ nameSelect, languageOptions, onChange }) => {
|
||||
const [subGroup, setSubGroup] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
subGroup,
|
||||
language,
|
||||
nameSelect
|
||||
});
|
||||
}, [onChange, subGroup, language]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("subGroup")}</label>
|
||||
<input type="text" name="subgroup" id="subgroup" className='form-control width-100' value={subGroup} placeholder={t("subGroup")} onChange={(e) => setSubGroup(e.target.value)} />
|
||||
<label htmlFor="language">{t("language")}</label>
|
||||
<select name="language" id="language" value={language} className="form-control width-100" onChange={(e) => setLanguage(e.target.value)}>
|
||||
{languageOptions}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {reset, create} from '../../../features/subgroups/subGroupSlice'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { LineSubGroup } from './LineSubGroup'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {GroupSearchBox} from '../../SearchBox/GroupSearchBox'
|
||||
|
||||
export const NewSubGroup = ({ languagesOption }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.group)
|
||||
const [subgroups, setSubGroups] = useState([{key: 0, name: "subgroup0" }])
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
const [lineSubGroupsData, setLineSubGroupsData] = useState([])
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
setSubGroups([{key: 0, name: "subgroup0" }])
|
||||
setLineSubGroupsData([])
|
||||
toast.success(t("subGroupSuccess"))
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const newSubGroup = {
|
||||
group : selectedValue,
|
||||
names: lineSubGroupsData.map((subGroupLine) => (
|
||||
{
|
||||
name: subGroupLine.subGroup,
|
||||
language: subGroupLine.language
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(newSubGroup))
|
||||
}
|
||||
|
||||
const addOrModifyLineLanguage = (lineLanguage) => {
|
||||
if (languagesOption[0] !== undefined && lineLanguage.language === '')
|
||||
lineLanguage.language = languagesOption[0].key;
|
||||
let existingLine = lineSubGroupsData.findIndex(lpd => lpd.nameSelect === lineLanguage.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineSubGroupsData(lineSubGroupsData.concat(lineLanguage))
|
||||
else {
|
||||
lineSubGroupsData[existingLine] = lineLanguage
|
||||
setLineSubGroupsData(lineSubGroupsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewGroup = () => {
|
||||
setSubGroups(subgroups.concat([
|
||||
{key: subgroups.length, name: "subgroup" + subgroups.length }
|
||||
]));
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("newSubGroup")}</legend>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<label htmlFor="group">{t("group")}</label>
|
||||
<GroupSearchBox key="groupSelected" className='mbottom-10' nameSelect="groupSelected" onChange={(e) => setSelectedValue(e)} />
|
||||
{
|
||||
subgroups.map((item) => (
|
||||
<LineSubGroup key={item.key} languageOptions={languagesOption} nameSelect={item.key} onChange={(e) => addOrModifyLineLanguage(e)}/>
|
||||
))
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewGroup()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewTranslation")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewSubGroup
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {createLaRuche, reset, resetIsSuccess} from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NewPurchaseLaRuche = ({ datePurchase, purchaseSubmittedForLaRuche }) => {
|
||||
const {isLoading, isError, isSuccess, message, purchasesToRename} = useSelector((state) => state.purchase)
|
||||
const [command, setCommand] = useState('')
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && !isLoading) {
|
||||
if (purchasesToRename.length === 0) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
else {
|
||||
dispatch(resetIsSuccess())
|
||||
purchaseSubmittedForLaRuche(purchasesToRename)
|
||||
}
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchase = {
|
||||
datePurchase: datePurchase,
|
||||
command: command
|
||||
}
|
||||
|
||||
dispatch(createLaRuche(purchase))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group">
|
||||
<label htmlFor="command">{t("command")}</label>
|
||||
<textarea name="command" id="command" className='form-control width-100' value={command} placeholder={t("command")} onChange={(e) => setCommand(e.target.value)}></textarea>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseLaRuche
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { reset, updatepurchase } from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LineProductUnknownPreFill } from '../SearchBox/LineProductUnknownPreFill';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const NewPurchaseLaRucheUnknownProduct = ({ listUnknowProduct }) => {
|
||||
const {isLoading, isError, isSuccess, message, purchase } = useSelector((state) => state.purchase)
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchaseToUpdate = {
|
||||
id: purchase.id,
|
||||
products: lineProductsData.flatMap((productLine) =>
|
||||
!Array.isArray(productLine.selectedValue) ?
|
||||
{
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: productLine.currencyIsoCode,
|
||||
translation: productLine.translation
|
||||
}
|
||||
: [])
|
||||
}
|
||||
|
||||
dispatch(updatepurchase(purchaseToUpdate))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === lineProduct.id);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group" id="formProducts">
|
||||
<label>{t("product")}</label>
|
||||
{
|
||||
listUnknowProduct.map((item) => (
|
||||
<LineProductUnknownPreFill key={item.id} unknowProduct={item} nameSelect={item.id} onChange={(e) => addOrModifyLineProduct(e)}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseLaRucheUnknownProduct
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {create, reset} from '../../features/purchases/purchaseSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../../components/Spinner'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LineProduct } from '../../components/SearchBox/LineProduct';
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
export const NewPurchaseManual = ({ datePurchase, currency }) => {
|
||||
const {isLoading, isError, isSuccess, message } = useSelector((state) => state.purchase)
|
||||
const [products, setProducts] = useState([{key: "0", name: "product0"}]);
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const [nbProduct, setNbProduct] = useState(1);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
}, [isError, isSuccess, message, products, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchase = {
|
||||
datePurchase: datePurchase,
|
||||
products: lineProductsData.map((productLine) => (
|
||||
{
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: currency
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
dispatch(create(purchase))
|
||||
}
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.nameSelect === lineProduct.nameSelect);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
setProducts(products.concat([
|
||||
{key: nbProduct, name: "product" + nbProduct }
|
||||
]));
|
||||
setNbProduct(nbProduct + 1)
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className="form-group" id="formProducts">
|
||||
<label>{t("product")}</label>
|
||||
{
|
||||
products.map((item) => (
|
||||
<LineProduct key={item.key} onChange={(e) => addOrModifyLineProduct(e)} nameSelect={item.name}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewProduct")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchaseManual
|
||||
19
frontend/src/components/NoteItem.jsx
Normal file
19
frontend/src/components/NoteItem.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
function NoteItem({id, note}) {
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
|
||||
return (
|
||||
<div className="note" style={{
|
||||
backgroundColor: note.isStaff ? 'rgba(0,0,0,0.7)' : '#fff',
|
||||
color: note.isStaff ? '#fff' : '#000',
|
||||
}} id={id}>
|
||||
<h4>Note from {note.isStaff ? <span>Staff</span> : <span>{user.name}</span>}</h4>
|
||||
<p>{note.text}</p>
|
||||
<div className="note-date">{new Date(note.createdAt).toLocaleString('fr-FR')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoteItem;
|
||||
15
frontend/src/components/PrivateRoute.jsx
Normal file
15
frontend/src/components/PrivateRoute.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuthStatus } from "../hooks/useAuthStatus";
|
||||
import Spinner from './Spinner'
|
||||
|
||||
export const PrivateRoute = () => {
|
||||
const {loggedIn, checkingStatus} = useAuthStatus()
|
||||
|
||||
if (checkingStatus)
|
||||
return <Spinner />
|
||||
|
||||
return loggedIn ? <Outlet /> : <Navigate to='/login' />;
|
||||
};
|
||||
|
||||
export default PrivateRoute
|
||||
60
frontend/src/components/SearchBox/GroupSearchBox.jsx
Normal file
60
frontend/src/components/SearchBox/GroupSearchBox.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset, getgroupsbyname } from '../../features/groups/groupSlice';
|
||||
|
||||
export const GroupSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { groups } = useSelector((state) => state.group)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getgroupsbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(groups.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name
|
||||
};
|
||||
}));
|
||||
}, [groups]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("group")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
35
frontend/src/components/SearchBox/LineProduct.jsx
Normal file
35
frontend/src/components/SearchBox/LineProduct.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBox } from './ProductSearchBox';
|
||||
|
||||
export const LineProduct = ({ nameSelect, onChange }) => {
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<ProductSearchBox key={nameSelect} className='mbottom-10' nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
48
frontend/src/components/SearchBox/LineProductPreFill.jsx
Normal file
48
frontend/src/components/SearchBox/LineProductPreFill.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBoxPreFill } from './ProductSearchBoxPreFill';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
|
||||
export const LineProductPreFill = ({ product, nameSelect, onChange, toDelete }) => {
|
||||
const [selectedValue, setSelectedValue] = useState(product.productId);
|
||||
const [quantity, setQuantity] = useState(product.quantity);
|
||||
const [price, setPrice] = useState(product.price);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
id: nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue,
|
||||
translation: product.product,
|
||||
currencyIsoCode: product.currencyIsoCode
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, product.currencyIsoCode, product.product, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<div>
|
||||
<fieldset key={`${nameSelect}fieldset`} className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<input type="hidden" key={`${nameSelect}currency`} name={`${nameSelect}currency`} className="width-100 inlineflex form-control" id={`${nameSelect}currency`} onChange={() => {}} value={product.currencyIsoCode} />
|
||||
<ProductSearchBoxPreFill key={`${nameSelect}searchbox`} className='mbottom-10' preSelectedValue={product.productId} preSelectedInputValue={product.translation} nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<a onClick={() => toDelete(product.id)} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaTrash />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProductSearchBox } from './ProductSearchBox';
|
||||
|
||||
export const LineProductUnknownPreFill = ({ unknowProduct, nameSelect, onChange }) => {
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const [quantity, setQuantity] = useState(unknowProduct.quantity);
|
||||
const [price, setPrice] = useState(unknowProduct.price);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
id: nameSelect,
|
||||
quantity,
|
||||
price,
|
||||
selectedValue,
|
||||
translation: unknowProduct.product,
|
||||
currencyIsoCode: unknowProduct.currencyIsoCode
|
||||
});
|
||||
}, [dispatch, onChange, quantity, selectedValue, price, unknowProduct.currencyIsoCode, unknowProduct.product, nameSelect]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset key={`${nameSelect}fieldset`} className='mtop-10'>
|
||||
<legend>{t("product")}</legend>
|
||||
<div>
|
||||
<input type="text" name={`${nameSelect}quantity`} className="width-40 inlineflex form-control" id={`${nameSelect}quantity`} value={quantity} placeholder={t("quantity")} onChange={(e) => { setQuantity(e.target.value); }} />
|
||||
<input type="text" name={`${nameSelect}price`} className="width-40 inlineflex form-control" id={`${nameSelect}price`} value={price} placeholder={t("price")} onChange={(e) => { setPrice(e.target.value); }} />
|
||||
</div>
|
||||
<input type="text" key={`${nameSelect}nameAlreadyPreFill`} name={`${nameSelect}nameAlreadyPreFill`} className="width-100 inlineflex form-control" id={`${nameSelect}nameAlreadyPreFill`} onChange={() => {}} value={unknowProduct.product} />
|
||||
<input type="hidden" key={`${nameSelect}currency`} name={`${nameSelect}currency`} className="width-100 inlineflex form-control" id={`${nameSelect}currency`} onChange={() => {}} value={unknowProduct.currencyIsoCode} />
|
||||
<ProductSearchBox key={`${nameSelect}searchbox`} className='mbottom-10' nameSelect={nameSelect} onChange={(e) => setSelectedValue(e)}/>
|
||||
</fieldset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
61
frontend/src/components/SearchBox/ProductSearchBox.jsx
Normal file
61
frontend/src/components/SearchBox/ProductSearchBox.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import {getproductbyname} from '../../features/products/productSlice'
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset } from '../../features/products/productSlice';
|
||||
|
||||
export const ProductSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { products } = useSelector((state) => state.product)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(products.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [products]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("product")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import {getproductbyname} from '../../features/products/productSlice'
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { reset } from '../../features/products/productSlice';
|
||||
|
||||
export const ProductSearchBoxPreFill = ({ preSelectedValue, preSelectedInputValue, nameSelect, onChange }) => {
|
||||
const { products } = useSelector((state) => state.product)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState(preSelectedInputValue);
|
||||
const [selectedValue, setSelectedValue] = useState(preSelectedValue);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["LineProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getproductbyname(inputValueChanged));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(products.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [products]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("product")}
|
||||
closeMenuOnSelect={true}
|
||||
defaultInputValue={preSelectedInputValue}
|
||||
defaultValue={preSelectedValue}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
60
frontend/src/components/SearchBox/SubGroupSearchBox.jsx
Normal file
60
frontend/src/components/SearchBox/SubGroupSearchBox.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {reset, getsubgroupsbyname} from '../../features/subgroups/subGroupSlice'
|
||||
|
||||
export const SubGroupSearchBox = ({ nameSelect, onChange }) => {
|
||||
const { subgroups } = useSelector((state) => state.subgroup)
|
||||
const [options, setOptions] = useState([]);
|
||||
const [inputValueChanged, setInputValueChanged] = useState('');
|
||||
const [selectedValue, setSelectedValue] = useState([]);
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation(["NewProduct"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValueChanged !== null && inputValueChanged.length > 2) {
|
||||
dispatch(getsubgroupsbyname(inputValueChanged));
|
||||
}
|
||||
}, [dispatch, inputValueChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedValue)
|
||||
dispatch(reset())
|
||||
}, [selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(subgroups.slice(0,10).map((p) => {
|
||||
return {
|
||||
value: p.id,
|
||||
label: p.names.find(function(name) { return name.language === localStorage.getItem('i18nextLng') }).name + ' - ' + p.group
|
||||
};
|
||||
}));
|
||||
}, [subgroups]);
|
||||
|
||||
const styles = {
|
||||
container: base => ({
|
||||
...base,
|
||||
flex: 1
|
||||
})
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
className="width-100"
|
||||
styles={styles}
|
||||
options={options}
|
||||
name={nameSelect}
|
||||
placeholder={t("subGroup")}
|
||||
closeMenuOnSelect={true}
|
||||
onChange={(e) => {
|
||||
setSelectedValue(e.value);
|
||||
}}
|
||||
onInputChange={(e) => {
|
||||
setInputValueChanged(e)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
11
frontend/src/components/Spinner.jsx
Normal file
11
frontend/src/components/Spinner.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<div className='loadingSpinnerContainer'>
|
||||
<div className='loadingSpinner'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
17
frontend/src/components/TicketItem.jsx
Normal file
17
frontend/src/components/TicketItem.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
function TicketItem({id, ticket}) {
|
||||
return (
|
||||
<div className="ticket" id={id}>
|
||||
<div>{new Date(ticket.createdAt).toLocaleString('fr-FR')}</div>
|
||||
<div>{ticket.product}</div>
|
||||
<div className={`status status-${ticket.status}`}>
|
||||
{ticket.status}
|
||||
</div>
|
||||
<Link to={`/ticket/${ticket._id}`} className="btn btn-reverse btn-sm">View</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TicketItem;
|
||||
25
frontend/src/features/auth/authService.js
Normal file
25
frontend/src/features/auth/authService.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/user`;
|
||||
|
||||
//Register user
|
||||
const register = async(userData) => {
|
||||
await axios.post(`${API_URL}/register`, userData);
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
//Login user
|
||||
const login = async(userData) => {
|
||||
const response = await axios.post(`${API_URL}/login`, userData)
|
||||
if (response.data) {
|
||||
localStorage.setItem('user', JSON.stringify(response.data))
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
|
||||
const authService = {register, logout, login}
|
||||
|
||||
export default authService
|
||||
94
frontend/src/features/auth/authSlice.js
Normal file
94
frontend/src/features/auth/authSlice.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import authService from "./authService";
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('user'))
|
||||
|
||||
const initialState = {
|
||||
user: user ?? null,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const register = createAsyncThunk('auth/register', async(user, thunkAPI) => {
|
||||
try {
|
||||
return await authService.register(user)
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const logout = createAsyncThunk('auth/logout', async(thunkAPI) => {
|
||||
try {
|
||||
return await authService.logout()
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const login = createAsyncThunk('user/login', async(user, thunkAPI) => {
|
||||
try {
|
||||
return await authService.login(user)
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(register.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(register.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.user = action.payload
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(register.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.user = null
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(logout.fulfilled, (state) => {
|
||||
state.user = null
|
||||
})
|
||||
.addCase(login.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(login.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.user = action.payload
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(login.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
state.user = null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const {reset} = authSlice.actions
|
||||
|
||||
export default authSlice.reducer
|
||||
38
frontend/src/features/currencies/currencyService.js
Normal file
38
frontend/src/features/currencies/currencyService.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/currency`;
|
||||
|
||||
//Create currency
|
||||
const create = async(currencyData, token) => {
|
||||
const response = await axios.post(API_URL, currencyData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all currencies
|
||||
const getcurrencies = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a currency
|
||||
const getcurrency = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a currency
|
||||
const closecurrency = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const currencyService = {create, getcurrencies, getcurrency, closecurrency}
|
||||
|
||||
export default currencyService
|
||||
132
frontend/src/features/currencies/currencySlice.js
Normal file
132
frontend/src/features/currencies/currencySlice.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import currencieservice from "./currencyService";
|
||||
|
||||
const initialState = {
|
||||
currency: {},
|
||||
currencies: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('currencies/create', async(currency, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.create(currency, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getcurrencies = createAsyncThunk('currencies/getcurrencies', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.getcurrencies(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getcurrency = createAsyncThunk('currencies/getcurrency', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.getcurrency(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const closecurrency = createAsyncThunk('currencies/closecurrency', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await currencieservice.closecurrency(thunkAPI.getState().currency.currency._id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const currencySlice = createSlice({
|
||||
name: 'currency',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.currency = {}
|
||||
state.currencies = []
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getcurrencies.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getcurrencies.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currencies = action.payload
|
||||
})
|
||||
.addCase(getcurrencies.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getcurrency.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getcurrency.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currency = action.payload
|
||||
})
|
||||
.addCase(getcurrency.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(closecurrency.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(closecurrency.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.currencies.map((currency) => currency._id === action.payload._id ? action.payload.status = 'closed' : currency)
|
||||
})
|
||||
.addCase(closecurrency.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset} = currencySlice.actions
|
||||
|
||||
export default currencySlice.reducer
|
||||
50
frontend/src/features/groups/groupService.js
Normal file
50
frontend/src/features/groups/groupService.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/group`;
|
||||
|
||||
//Create group
|
||||
const create = async(groupData, token) => {
|
||||
const response = await axios.post(API_URL, groupData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all groups
|
||||
const getgroups = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getgroupsbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a group
|
||||
const getgroup = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a group
|
||||
const updategroup = async(group, token) => {
|
||||
const response = await axios.put(API_URL, group, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a group
|
||||
const deletegroup = async(groupId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${groupId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupService = {create, getgroups, getgroup, getgroupsbyname, updategroup, deletegroup}
|
||||
|
||||
export default groupService
|
||||
164
frontend/src/features/groups/groupSlice.js
Normal file
164
frontend/src/features/groups/groupSlice.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import groupservice from "./groupService";
|
||||
|
||||
const initialState = {
|
||||
group: {},
|
||||
groups: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('groups/create', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.create(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroups = createAsyncThunk('groups/getgroups', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroups(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroupsbyname = createAsyncThunk('groups/getgroupsbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroupsbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getgroup = createAsyncThunk('groups/getgroup', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.getgroup(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updategroup = createAsyncThunk('groups/update', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.updategroup(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletegroup = createAsyncThunk('groups/delete', async(groupId, thunkAPI) => {
|
||||
try {
|
||||
return await groupservice.deletegroup(groupId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const groupslice = createSlice({
|
||||
name: 'group',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.group = {}
|
||||
state.CO2Emissions = 0
|
||||
state.groups = []
|
||||
state.groupsToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getgroups.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getgroups.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.groups = action.payload
|
||||
})
|
||||
.addCase(getgroups.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getgroupsbyname.fulfilled, (state, action) => {
|
||||
state.groups = action.payload
|
||||
})
|
||||
.addCase(getgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.group = action.payload
|
||||
})
|
||||
.addCase(getgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updategroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updategroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updategroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = groupslice.actions
|
||||
|
||||
export default groupslice.reducer
|
||||
44
frontend/src/features/languages/languageService.js
Normal file
44
frontend/src/features/languages/languageService.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/language`;
|
||||
|
||||
//Create language
|
||||
const create = async(languageData, token) => {
|
||||
const response = await axios.post(API_URL, languageData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all languages
|
||||
const getlanguages = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a language
|
||||
const getlanguageById = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a language
|
||||
const getlanguagebyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a language
|
||||
const closelanguage = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const languageService = {create, getlanguages, getlanguageById, getlanguagebyname, closelanguage}
|
||||
|
||||
export default languageService
|
||||
147
frontend/src/features/languages/languageSlice.js
Normal file
147
frontend/src/features/languages/languageSlice.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import languageservice from "./languageService";
|
||||
|
||||
const initialState = {
|
||||
language: {},
|
||||
languages: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('languages/create', async(language, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.create(language, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getlanguages = createAsyncThunk('languages/getlanguages', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.getlanguages(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getlanguage = createAsyncThunk('languages/getlanguage', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.getlanguage(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatelanguage = createAsyncThunk('languages/update', async(language, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.updatelanguage(language, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletelanguage = createAsyncThunk('languages/delete', async(languageId, thunkAPI) => {
|
||||
try {
|
||||
return await languageservice.deletelanguage(languageId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const languageslice = createSlice({
|
||||
name: 'language',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.language = {}
|
||||
state.languages = []
|
||||
state.groupsToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getlanguages.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getlanguages.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.languages = action.payload
|
||||
})
|
||||
.addCase(getlanguages.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getlanguage.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getlanguage.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.language = action.payload
|
||||
})
|
||||
.addCase(getlanguage.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatelanguage.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatelanguage.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatelanguage.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = languageslice.actions
|
||||
|
||||
export default languageslice.reducer
|
||||
44
frontend/src/features/products/productService.js
Normal file
44
frontend/src/features/products/productService.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/product`;
|
||||
|
||||
//Create product
|
||||
const create = async(productData, token) => {
|
||||
const response = await axios.post(API_URL, productData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all products
|
||||
const getproducts = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getproduct = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getproductbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a product
|
||||
const closeproduct = async(id, token) => {
|
||||
const response = await axios.put(`${API_URL}/${id}`, {status: 'close'}, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const productService = {create, getproducts, getproduct, getproductbyname, closeproduct}
|
||||
|
||||
export default productService
|
||||
160
frontend/src/features/products/productSlice.js
Normal file
160
frontend/src/features/products/productSlice.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import productService from "./productService";
|
||||
|
||||
const initialState = {
|
||||
product: {},
|
||||
products: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: '',
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('products/create', async(product, thunkAPI) => {
|
||||
try {
|
||||
return await productService.create(product, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproducts = createAsyncThunk('products/getproducts', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproducts(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproduct = createAsyncThunk('products/getproduct', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproduct(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getproductbyname = createAsyncThunk('products/getproductbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await productService.getproductbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const closeproduct = createAsyncThunk('products/closeproduct', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await productService.closeproduct(thunkAPI.getState().product.product._id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const productSlice = createSlice({
|
||||
name: 'product',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.product = {}
|
||||
state.products = []
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getproductbyname.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproductbyname.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products = action.payload
|
||||
})
|
||||
.addCase(getproductbyname.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getproducts.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproducts.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products = action.payload
|
||||
})
|
||||
.addCase(getproducts.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getproduct.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getproduct.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.product = action.payload
|
||||
})
|
||||
.addCase(getproduct.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(closeproduct.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(closeproduct.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.products.map((product) => product._id === action.payload._id ? action.payload.status = 'closed' : product)
|
||||
})
|
||||
.addCase(closeproduct.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset} = productSlice.actions
|
||||
|
||||
export default productSlice.reducer
|
||||
68
frontend/src/features/purchases/purchaseService.js
Normal file
68
frontend/src/features/purchases/purchaseService.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/purchase`;
|
||||
|
||||
//Create purchase
|
||||
const create = async(purchaseData, token) => {
|
||||
const response = await axios.post(API_URL, purchaseData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Create purchase La Ruche
|
||||
const createLaRuche = async(purchaseData, token) => {
|
||||
const response = await axios.post(`${API_URL}/laruche`, purchaseData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all purchases
|
||||
const getpurchases = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all purchases by date
|
||||
const getpurchasesbydate = async(dates, token) => {
|
||||
const response = await axios.get(`${API_URL}/${dates.startDate}/${dates.endDate}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get CO2 emissions between two dates
|
||||
const getCO2bydate = async(dates, token) => {
|
||||
const response = await axios.get(`${API_URL}/CO2/${dates.startDate}/${dates.endDate}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a purchase
|
||||
const getpurchase = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a purchase
|
||||
const updatepurchase = async(purchase, token) => {
|
||||
const response = await axios.put(API_URL, purchase, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a purchase
|
||||
const deletepurchase = async(purchaseId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${purchaseId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a purchase
|
||||
const deletelinepurchase = async(linePurchaseId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/line/${linePurchaseId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const purchaseService = {create, createLaRuche, getpurchases, getpurchasesbydate, getCO2bydate, getpurchase, updatepurchase, deletepurchase, deletelinepurchase}
|
||||
|
||||
export default purchaseService
|
||||
232
frontend/src/features/purchases/purchaseSlice.js
Normal file
232
frontend/src/features/purchases/purchaseSlice.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import purchaseService from "./purchaseService";
|
||||
|
||||
const initialState = {
|
||||
purchase: {},
|
||||
purchases: [],
|
||||
CO2Emissions: 0,
|
||||
purchasesToRename: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('purchases/create', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.create(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const createLaRuche = createAsyncThunk('purchases/createLaRuche', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.createLaRuche(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchases = createAsyncThunk('purchases/getpurchases', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchases(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchasesbydate = createAsyncThunk('purchases/getpurchasebydate', async(dates, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchasesbydate(dates, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getCO2bydate = createAsyncThunk('purchases/getCO2bydate', async(dates, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getCO2bydate(dates, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getpurchase = createAsyncThunk('purchases/getpurchase', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.getpurchase(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatepurchase = createAsyncThunk('purchases/update', async(purchase, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.updatepurchase(purchase, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletepurchase = createAsyncThunk('purchases/delete', async(purchaseId, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.deletepurchase(purchaseId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletelinepurchase = createAsyncThunk('purchases/deletelinepurchase', async(linePurchaseId, thunkAPI) => {
|
||||
try {
|
||||
return await purchaseService.deletelinepurchase(linePurchaseId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const purchaseSlice = createSlice({
|
||||
name: 'purchase',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.purchase = {}
|
||||
state.CO2Emissions = 0
|
||||
state.purchases = []
|
||||
state.purchasesToRename = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(createLaRuche.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(createLaRuche.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchasesToRename = action.payload.productsToTranslate
|
||||
state.purchase = action.payload
|
||||
})
|
||||
.addCase(createLaRuche.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getpurchases.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchases.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchases = action.payload
|
||||
})
|
||||
.addCase(getpurchases.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getpurchasesbydate.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchasesbydate.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchases = action.payload
|
||||
})
|
||||
.addCase(getpurchasesbydate.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getCO2bydate.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getCO2bydate.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.CO2Emissions = action.payload
|
||||
})
|
||||
.addCase(getCO2bydate.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getpurchase.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getpurchase.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.purchase = action.payload
|
||||
})
|
||||
.addCase(getpurchase.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatepurchase.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatepurchase.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatepurchase.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = purchaseSlice.actions
|
||||
|
||||
export default purchaseSlice.reducer
|
||||
50
frontend/src/features/subgroups/subGroupService.js
Normal file
50
frontend/src/features/subgroups/subGroupService.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_URL = `${process.env.REACT_APP_API_URL}/subGroup`;
|
||||
|
||||
//Create group
|
||||
const create = async(subGroupData, token) => {
|
||||
const response = await axios.post(API_URL, subGroupData, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get all groups
|
||||
const getsubgroups = async(token) => {
|
||||
const response = await axios.get(API_URL, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a product
|
||||
const getsubgroupsbyname = async(name, language, token) => {
|
||||
const response = await axios.get(`${API_URL}/name/${language}/${name}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Get a group
|
||||
const getsubgroup = async(id, token) => {
|
||||
const response = await axios.get(`${API_URL}/${id}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Update a group
|
||||
const updatesubgroup = async(subGroup, token) => {
|
||||
const response = await axios.put(API_URL, subGroup, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
//Delete a group
|
||||
const deletesubgroup = async(subGroupId, token) => {
|
||||
const response = await axios.delete(`${API_URL}/${subGroupId}`, getHeader(token))
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getHeader = (token) => {
|
||||
return { headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const groupService = {create, getsubgroups, getsubgroup, getsubgroupsbyname, updatesubgroup, deletesubgroup}
|
||||
|
||||
export default groupService
|
||||
162
frontend/src/features/subgroups/subGroupSlice.js
Normal file
162
frontend/src/features/subgroups/subGroupSlice.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import subGroupService from "./subGroupService";
|
||||
|
||||
const initialState = {
|
||||
subgroup: {},
|
||||
subgroups: [],
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
message: ''
|
||||
}
|
||||
|
||||
export const create = createAsyncThunk('subgroups/create', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.create(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroups = createAsyncThunk('subgroups/getsubgroups', async(_, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroups(getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroupsbyname = createAsyncThunk('subgroups/getsubgroupsbyname', async(name, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroupsbyname(name, getLanguage(thunkAPI), getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const getsubgroup = createAsyncThunk('subgroups/subgetgroup', async(id, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.getsubgroup(id, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const updatesubgroup = createAsyncThunk('subgroups/update', async(group, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.updatesubgroup(group, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const deletesubgroup = createAsyncThunk('subgroups/delete', async(groupId, thunkAPI) => {
|
||||
try {
|
||||
return await subGroupService.deletesubgroup(groupId, getToken(thunkAPI))
|
||||
} catch (error) {
|
||||
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
|
||||
return thunkAPI.rejectWithValue(message)
|
||||
}
|
||||
})
|
||||
|
||||
export const subgroupslice = createSlice({
|
||||
name: 'group',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
state.subgroup = {}
|
||||
state.subgroups = []
|
||||
},
|
||||
resetIsSuccess: (state) => {
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.isSuccess = false
|
||||
state.message = ''
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(create.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(create.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(create.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.message = action.payload
|
||||
state.isError = true
|
||||
})
|
||||
.addCase(getsubgroups.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getsubgroups.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.subgroups = action.payload
|
||||
})
|
||||
.addCase(getsubgroups.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(getsubgroupsbyname.fulfilled, (state, action) => {
|
||||
state.subgroups = action.payload
|
||||
})
|
||||
.addCase(getsubgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(getsubgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
state.subgroup = action.payload
|
||||
})
|
||||
.addCase(getsubgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
.addCase(updatesubgroup.pending, (state) => {
|
||||
state.isLoading = true
|
||||
})
|
||||
.addCase(updatesubgroup.fulfilled, (state, action) => {
|
||||
state.isSuccess = true
|
||||
state.isLoading = false
|
||||
state.isError = false
|
||||
})
|
||||
.addCase(updatesubgroup.rejected, (state, action) => {
|
||||
state.isSuccess = false
|
||||
state.isLoading = false
|
||||
state.isError = true
|
||||
state.message = action.payload
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getToken = (thunkAPI) => {
|
||||
return thunkAPI.getState().auth.user.token
|
||||
}
|
||||
|
||||
const getLanguage = () => {
|
||||
return localStorage.getItem('i18nextLng');
|
||||
}
|
||||
|
||||
export const {reset, resetIsSuccess} = subgroupslice.actions
|
||||
|
||||
export default subgroupslice.reducer
|
||||
11
frontend/src/hooks/setupProxy.js
Normal file
11
frontend/src/hooks/setupProxy.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
'/backendApi',
|
||||
createProxyMiddleware({
|
||||
target: process.env.REACT_APP_API_URL,
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
22
frontend/src/hooks/useAuthStatus.js
Normal file
22
frontend/src/hooks/useAuthStatus.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
|
||||
export const useAuthStatus = () => {
|
||||
const [loggedIn, setLoggedIn] = useState(false)
|
||||
const [checkingStatus, setCheckingStatus] = useState(true)
|
||||
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (user && new Date() < new Date(user.validTo)) {
|
||||
setLoggedIn(true)
|
||||
}
|
||||
else {
|
||||
setLoggedIn(false)
|
||||
}
|
||||
setCheckingStatus(false)
|
||||
}, [user])
|
||||
|
||||
return {loggedIn, checkingStatus}
|
||||
}
|
||||
30
frontend/src/i18n.js
Normal file
30
frontend/src/i18n.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import i18n from "i18next";
|
||||
import Backend from "i18next-xhr-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n
|
||||
// load translation using xhr -> see /public/locales
|
||||
// learn more: https://github.com/i18next/i18next-xhr-backend
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: "FR",
|
||||
lng: 'FR',
|
||||
debug: true,
|
||||
/* can have multiple namespace, in case you want to divide a huge translation into smaller pieces and load them on demand */
|
||||
ns: ["Header"],
|
||||
defaultNS: "Header",
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react as it escapes by default
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
317
frontend/src/index.css
Normal file
317
frontend/src/index.css
Normal file
@@ -0,0 +1,317 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.header ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header ul li {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.header ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header ul li a:hover {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.header ul li a svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 50px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.heading p {
|
||||
color: #828282;
|
||||
}
|
||||
|
||||
.boxes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.boxes div {
|
||||
padding: 30px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.boxes h2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.boxes a:hover {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
padding: 10px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.width-80 {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.width-70 {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.width-60 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.width-40 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.width-20 {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.width-10 {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.mtop-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mbottom-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inlineflex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
text-align: left;
|
||||
display: block;
|
||||
margin: 0 0 5px 3px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
appearance: button;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.btn-reverse {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: darkred;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
width: 150px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #000;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
color: red;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
p.status-in-progress {
|
||||
color: orangered;
|
||||
}
|
||||
|
||||
p.status-waiting {
|
||||
color: red;
|
||||
}
|
||||
|
||||
p.status-ready {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
p.status-complete {
|
||||
color: green;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: sticky;
|
||||
top: 95vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingSpinnerContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 8px solid;
|
||||
border-color: #000 transparent #555 transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.boxes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.ticket-created h2,
|
||||
.heading h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.heading p {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
26
frontend/src/index.js
Normal file
26
frontend/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { store } from './app/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import Spinner from './components/Spinner'
|
||||
import "./i18n";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
25
frontend/src/pages/Home.jsx
Normal file
25
frontend/src/pages/Home.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {FaShoppingCart} from 'react-icons/fa'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {HomePurchase} from '../components/Home/HomePurchase'
|
||||
import { reset} from '../features/purchases/purchaseSlice'
|
||||
|
||||
function Home() {
|
||||
const { t } = useTranslation(["Home"]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return <>
|
||||
<Link onClick={() => dispatch(reset())} to='/new-purchase'className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("createNewPurchase")}
|
||||
</Link>
|
||||
<Link onClick={() => dispatch(reset())} to='/new-product' className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("createNewProduct")}
|
||||
</Link>
|
||||
<HomePurchase />
|
||||
</>;
|
||||
}
|
||||
|
||||
export default Home;
|
||||
84
frontend/src/pages/Login.jsx
Normal file
84
frontend/src/pages/Login.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { FaSignInAlt } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import {login, reset } from '../features/auth/authSlice'
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Login() {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const {email, password} = formData
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {user, isLoading, isSuccess, isError, message} = useSelector((state) => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && user)
|
||||
navigate('/')
|
||||
dispatch(reset())
|
||||
}, [user, isLoading, isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = async(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const userData = {
|
||||
email,
|
||||
password
|
||||
}
|
||||
|
||||
await dispatch(login(userData))
|
||||
|
||||
if (!isLoading && isSuccess)
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id] : e.target.value,
|
||||
}))
|
||||
}
|
||||
|
||||
const { t } = useTranslation(["Login"]);
|
||||
|
||||
if (isLoading)
|
||||
return (<div>
|
||||
Loading...
|
||||
</div>)
|
||||
|
||||
return (<>
|
||||
<section className="heading">
|
||||
<h1>
|
||||
<FaSignInAlt /> {t("login")}
|
||||
</h1>
|
||||
<p>{t("fillLogin")}</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input type="email" name="email" id="email" value={email} className="form-control width-100" placeholder={t("emailPlaceholder")} onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password" id="password" value={password} className="form-control width-100" placeholder={t("passwordPlaceholder")} onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
44
frontend/src/pages/NewProduct.jsx
Normal file
44
frontend/src/pages/NewProduct.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import NewGroup from '../components/NewProduct/NewGroup/NewGroup';
|
||||
import NewSubGroup from '../components/NewProduct/NewSubGroup/NewSubGroup';
|
||||
import NewProductItem from '../components/NewProduct/NewProductItem/NewProductItem';
|
||||
import { getlanguages } from '../features/languages/languageSlice';
|
||||
|
||||
function NewProduct() {
|
||||
const { languages, isLoading } = useSelector((state) => state.language)
|
||||
const [languagesOption, setLanguageOptions] = useState([])
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setLanguageOptions(languages.map((languageOption) => {
|
||||
if (languageOption.names.length > 0) {
|
||||
return <option value={languageOption.id} key={languageOption.id}>{languageOption.names.findIndex(function(name) { return name.language === i18n.language }) === -1 ? languageOption.names[0].name : languageOption.names.find(function(name) { return name.language === i18n.language }).name}</option>;
|
||||
}
|
||||
}));
|
||||
}, [languages, i18n.language])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getlanguages())
|
||||
}, [dispatch])
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<NewGroup languagesOption={languagesOption} />
|
||||
<NewSubGroup languagesOption={languagesOption} />
|
||||
<NewProductItem languagesOption={languagesOption} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewProduct
|
||||
97
frontend/src/pages/NewPurchase.jsx
Normal file
97
frontend/src/pages/NewPurchase.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { reset} from '../features/purchases/purchaseSlice'
|
||||
import {getcurrencies} from '../features/currencies/currencySlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NewPurchaseManual } from '../components/NewPurchaseTypes/NewPurchaseManual';
|
||||
import { NewPurchaseLaRuche } from '../components/NewPurchaseTypes/NewPurchaseLaRuche';
|
||||
import { NewPurchaseLaRucheUnknownProduct } from '../components/NewPurchaseTypes/NewPurchaseLaRucheUnknownProduct';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
function NewPurchase() {
|
||||
const { currencies, isLoading, isError, message } = useSelector((state) => state.currency)
|
||||
const [typeOfPurchase, setTypeOfPurchase] = useState('manual')
|
||||
const [datePurchase, setDatePurchase] = useState(new Date())
|
||||
const [currency, setCurrency] = useState('EUR');
|
||||
const [currencyOptions, setCurrencyOptions] = useState([]);
|
||||
const [laRucheUnknownProduct, setLaRucheUnknownProduct] = useState([]);
|
||||
const { t } = useTranslation(["NewPurchase"]);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
setCurrencyOptions(currencies.map((currencyOption) => {
|
||||
if (currencyOption.isoCode === 'EUR')
|
||||
setCurrency(currencyOption.id)
|
||||
return <option value={currencyOption.id} key={currencyOption.id}>{currencyOption.names.find(function(name) { return name.language === i18n.language }).name}</option>;
|
||||
}));
|
||||
}, [currencies])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reset())
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
}, [isError])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getcurrencies())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [isLoading])
|
||||
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<section className="heading">
|
||||
<h1>{t("title")}</h1>
|
||||
</section>
|
||||
<section className="form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">{t("typeOfPurchase")}</label>
|
||||
<select name="typeOfPurchase" id="typeOfPurchase" value={typeOfPurchase} className="form-control width-100" onChange={(e) => setTypeOfPurchase(e.target.value)}>
|
||||
<option value="manual">{t("manual")}</option>
|
||||
<option value="LaRuche">La Ruche Qui Dit Oui</option>
|
||||
</select>
|
||||
<label htmlFor="name">{t("datePurchase")}</label>
|
||||
<DatePicker dateFormat="dd/MM/yyyy" selected={datePurchase} className="form-control width-100" onChange={(date) => setDatePurchase(date)} />
|
||||
{ typeOfPurchase === "manual" ?
|
||||
(
|
||||
<>
|
||||
<label htmlFor="currency">{t("currency")}</label>
|
||||
<select name="currency" id="currency" value={currency} className="form-control width-100" onChange={(e) => setCurrency(e.target.value)}>
|
||||
{currencyOptions}
|
||||
</select>
|
||||
</>
|
||||
) :
|
||||
<></>
|
||||
}
|
||||
</div>
|
||||
{ typeOfPurchase === "manual" ?
|
||||
(
|
||||
<NewPurchaseManual datePurchase={datePurchase} currency={currency} />
|
||||
) :
|
||||
laRucheUnknownProduct.length === 0 ?
|
||||
(
|
||||
<NewPurchaseLaRuche datePurchase={datePurchase} purchaseSubmittedForLaRuche={(e) => setLaRucheUnknownProduct(e)} />
|
||||
) :
|
||||
(
|
||||
<NewPurchaseLaRucheUnknownProduct listUnknowProduct={laRucheUnknownProduct} />
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPurchase
|
||||
86
frontend/src/pages/NewTicket.jsx
Normal file
86
frontend/src/pages/NewTicket.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {create, reset} from '../features/tickets/ticketSlice'
|
||||
import { toast } from 'react-toastify'
|
||||
import Spinner from '../components/Spinner'
|
||||
import BackButton from '../components/BackButton'
|
||||
|
||||
function NewTicket() {
|
||||
const {user} = useSelector((state) => state.auth)
|
||||
const {isLoading, isError, isSuccess, message} = useSelector((state) => state.ticket)
|
||||
const [name] = useState(user.name)
|
||||
const [email] = useState(user.email)
|
||||
const [product, setProduct] = useState('iPhone')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
dispatch(reset())
|
||||
}, [isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const ticket = {
|
||||
user: user.id,
|
||||
product,
|
||||
description
|
||||
}
|
||||
dispatch(create(ticket))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackButton url='/' />
|
||||
<section className="heading">
|
||||
<h1>Create new ticket</h1>
|
||||
<p>Please fill out the form below</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Customer name</label>
|
||||
<input type="text" className="form-control" value={name} disabled />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Customer email</label>
|
||||
<input type="text" className="form-control" value={email} disabled />
|
||||
</div>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="product">Product</label>
|
||||
<select name="product" id="product" value={product} className="form-control" onChange={(e) => setProduct(e.target.value)}>
|
||||
<option value="iPhone">iPhone</option>
|
||||
<option value="MacBook Pro">MacBook Pro</option>
|
||||
<option value="iMac">iMac</option>
|
||||
<option value="iPad">iPad</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description of the issue</label>
|
||||
<textarea name="description" id="description" className='form-control' value={description} placeholder="Description" onChange={(e) => setDescription(e.target.value)}></textarea>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewTicket
|
||||
163
frontend/src/pages/Purchase.jsx
Normal file
163
frontend/src/pages/Purchase.jsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {useDispatch, useSelector} from 'react-redux'
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {toast} from 'react-toastify'
|
||||
import Modal from 'react-modal'
|
||||
import Spinner from "../components/Spinner";
|
||||
import { LineProductPreFill } from "../components/SearchBox/LineProductPreFill";
|
||||
import { getpurchase, reset, updatepurchase, deletelinepurchase} from '../features/purchases/purchaseSlice'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { FaShoppingCart } from 'react-icons/fa';
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
function Purchase() {
|
||||
const {purchase, isLoading, isError, message} = useSelector((state) => state.purchase)
|
||||
const dispatch = useDispatch()
|
||||
const params = useParams()
|
||||
const [purchaseUser, setPurchaseUser] = useState()
|
||||
const [datePurchase, setDatePurchase] = useState()
|
||||
const [lineProductsData, setLineProductsData] = useState([]);
|
||||
const { t } = useTranslation(["Purchase"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
toast.error(message)
|
||||
}
|
||||
}, [dispatch, isError, message])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getpurchase(params.id))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setPurchaseUser(purchase)
|
||||
if (Object.keys(purchase).length > 0) {
|
||||
if (lineProductsData.length == 0) {
|
||||
setDatePurchase(new Date(Date.parse(purchase.datePurchase)))
|
||||
setLineProductsData(purchase.products.map((p) => {
|
||||
return {
|
||||
id : p.id,
|
||||
currencyIsoCode : p.currencyIsoCode,
|
||||
productId : p.productId,
|
||||
price : p.price,
|
||||
quantity : p.quantity,
|
||||
translation : p.product,
|
||||
selectedValue : p.id
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}, [purchase])
|
||||
|
||||
const addOrModifyLineProduct = (lineProduct) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === lineProduct.id);
|
||||
if (existingLine === -1)
|
||||
setLineProductsData(lineProductsData.concat(lineProduct))
|
||||
else {
|
||||
lineProductsData[existingLine] = lineProduct
|
||||
setLineProductsData(lineProductsData)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteLineProduct = (p) => {
|
||||
let existingLine = lineProductsData.findIndex(lpd => lpd.id === p);
|
||||
dispatch(reset());
|
||||
if (isNaN(p)) {
|
||||
dispatch(deletelinepurchase(p))
|
||||
}
|
||||
lineProductsData.splice(existingLine, 1)
|
||||
}
|
||||
|
||||
const addNewProduct = () => {
|
||||
setLineProductsData(lineProductsData.concat([
|
||||
{
|
||||
id : lineProductsData.length,
|
||||
currencyIsoCode : lineProductsData[0].currencyIsoCode,
|
||||
productId : '',
|
||||
price : 0,
|
||||
quantity : 0,
|
||||
translation : '',
|
||||
selectedValue : ''
|
||||
}
|
||||
]));
|
||||
}
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const purchaseToUpdate = {
|
||||
id: purchase.id,
|
||||
products: lineProductsData.flatMap((productLine) =>
|
||||
!Array.isArray(productLine.selectedValue) ?
|
||||
{
|
||||
id: productLine.id,
|
||||
product: productLine.selectedValue,
|
||||
quantity: productLine.quantity,
|
||||
price: productLine.price,
|
||||
currencyIsoCode: productLine.currencyIsoCode,
|
||||
translation: productLine.translation
|
||||
}
|
||||
: [])
|
||||
}
|
||||
|
||||
dispatch(updatepurchase(purchaseToUpdate))
|
||||
}
|
||||
|
||||
if (isLoading)
|
||||
return <Spinner />
|
||||
|
||||
if (isError)
|
||||
return <h3>An error occured</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
Object.keys(purchase).length > 0 ? (
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="form-group">
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-40'>{t("datePurchase")}</div>
|
||||
<div className='width-60'><DatePicker dateFormat="dd/MM/yyyy" selected={datePurchase} className="form-control width-100" onChange={(date) => setDatePurchase(date)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-80'>{t("CO2Cost")}</div>
|
||||
<div className='width-20'>{parseFloat(purchaseUser.cO2Cost).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='width-100'>
|
||||
<div className='inlineflex'>
|
||||
<div className='width-80'>{t("WaterCost")}</div>
|
||||
<div className='width-20'>{parseFloat(purchaseUser.waterCost).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
lineProductsData.length > 0 ?
|
||||
lineProductsData.map((item) => (
|
||||
<LineProductPreFill key={item.id} product={item} nameSelect={item.id} onChange={(e) => addOrModifyLineProduct(e)} toDelete={(e) => deleteLineProduct(e)}/>
|
||||
)) :
|
||||
<></>
|
||||
}
|
||||
<div>
|
||||
<a onClick={() => addNewProduct()} href="#/" className='btn btn-reverse btn-block'>
|
||||
<FaShoppingCart /> {t("addNewProduct")}
|
||||
</a>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : <></>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Purchase;
|
||||
88
frontend/src/pages/Register.jsx
Normal file
88
frontend/src/pages/Register.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import {useNavigate} from 'react-router-dom'
|
||||
import { useState, useEffect } from "react";
|
||||
import { FaUser } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import {useSelector, useDispatch } from 'react-redux'
|
||||
import {register, reset} from '../features/auth/authSlice'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Register() {
|
||||
const { i18n } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
const {email, password, password2} = formData
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {user, isLoading, isError, isSuccess, message} = useSelector(state => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (isError)
|
||||
toast.error(message)
|
||||
if (isSuccess && user) {
|
||||
dispatch(reset())
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
}, [user, isLoading, isError, isSuccess, message, navigate, dispatch])
|
||||
|
||||
const onSubmit = async(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (password !== password2){
|
||||
toast.error('Passwords must match')
|
||||
}
|
||||
else {
|
||||
const userData = {
|
||||
email,
|
||||
password,
|
||||
language: i18n.language
|
||||
}
|
||||
dispatch(register(userData));
|
||||
if (!isLoading && isSuccess)
|
||||
navigate('/')
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.id] : e.target.value,
|
||||
}))
|
||||
}
|
||||
|
||||
return (<>
|
||||
<section className="heading">
|
||||
<h1>
|
||||
<FaUser /> Register {user}
|
||||
</h1>
|
||||
<p>Please create an account</p>
|
||||
</section>
|
||||
<section className="form">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group">
|
||||
<input type="email" name="email" id="email" value={email} className="form-control width-100" placeholder="Enter your email" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password" id="password" value={password} className="form-control width-100" placeholder="Enter your password" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input type="password" name="password2" id="password2" value={password2} className="form-control width-100" placeholder="Re-enter your password" onChange={onChange} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button className="btn btn-block">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</>);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
137
frontend/src/serviceWorker.js
Normal file
137
frontend/src/serviceWorker.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user