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

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