First Commit

This commit is contained in:
Maxime Boulay
2024-12-23 08:05:29 +01:00
commit ba64ceb471
171 changed files with 654157 additions and 0 deletions

6
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
build
.dockerignore
**/.git
**/.DS_Store
**/node_modules

View File

@@ -0,0 +1 @@
REACT_APP_API_URL=https://localhost:7021

1
frontend/.env.docker Normal file
View File

@@ -0,0 +1 @@
DANGEROUSLY_DISABLE_HOST_CHECK=true

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

23
frontend/.vscode/launch.json vendored Normal file
View 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
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

52
frontend/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="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>

View File

@@ -0,0 +1,5 @@
{
"login" : "Login",
"register" : "Register",
"logout" : "Log out"
}

View File

@@ -0,0 +1,7 @@
{
"createNewPurchase" : "Create new purchase",
"createNewProduct" : "Create new product",
"date": "Date",
"CO2Cost": "CO2 cost",
"WaterCost": "Water cost"
}

View File

@@ -0,0 +1,5 @@
{
"product" : "Produit",
"quantity" : "Quantity",
"price" : "Price"
}

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

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

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

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

View File

@@ -0,0 +1,5 @@
{
"login" : "Connexion",
"register" : "Inscription",
"logout" : "Deconnexion"
}

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

View File

@@ -0,0 +1,5 @@
{
"product" : "Produit",
"quantity" : "Quantité",
"price" : "Prix"
}

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

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

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

43
frontend/src/App.js Normal file
View 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
View 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
},
});

View 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

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

View 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

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

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

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

View 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

View File

@@ -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>
</>
);
};

View File

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

View File

@@ -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>
</>
);
};

View File

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

View File

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

View File

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

View File

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

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

View 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

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

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

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

View File

@@ -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>
</>
);
};

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

View File

@@ -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)
}}
/>
</>
);
};

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

View File

@@ -0,0 +1,11 @@
import React from 'react';
function Spinner() {
return (
<div className='loadingSpinnerContainer'>
<div className='loadingSpinner'></div>
</div>
)
}
export default Spinner

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

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

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

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

View 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

View 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

View 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

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

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

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